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 85798bec2..469976b86 100644 --- a/.gitignore.orig +++ b/.gitignore.orig @@ -98,24 +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 -<<<<<<< HEAD BahaTurret/BDArmory.sln.DotSettings.user -BDArmory.Core/bin/Debug/UnityEngine.dll -BDArmory.Core/bin/Debug/KSPAssets.dll -BDArmory.Core/bin/Debug/BDArmory.Core.dll -BDArmory.Core/bin/Debug/Assembly-CSharp.dll -BDArmory.Core/bin/Debug/UnityEngine.UI.dll BahaTurret/BDArmory.csproj.user -======= -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 ->>>>>>> 311105e5c29cb0a842f90aa18021dbbdf54390a8 diff --git a/BDArmory.Core/BDAPersistantSettingsField.cs b/BDArmory.Core/BDAPersistantSettingsField.cs deleted file mode 100644 index 9004a4806..000000000 --- a/BDArmory.Core/BDAPersistantSettingsField.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Core -{ - [AttributeUsage(AttributeTargets.Field)] - public class BDAPersistantSettingsField : Attribute - { - public BDAPersistantSettingsField() - { - } - - 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(BDAPersistantSettingsField), false)) continue; - - var fieldValue = field.Current.GetValue(null); - if (fieldValue.GetType() == typeof(Vector2d)) - settings.SetValue(field.Current.Name, ((Vector2d)fieldValue).ToString("G"), true); - else - settings.SetValue(field.Current.Name, field.Current.GetValue(null).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(BDAPersistantSettingsField), 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) - { - 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); - } - - Debug.LogError("[BDArmory]: 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 73edc0750..000000000 --- a/BDArmory.Core/BDArmory.Core.csproj +++ /dev/null @@ -1,121 +0,0 @@ - - - - - Debug - AnyCPU - {A6F1753E-9570-4C40-AF72-A179890582E5} - Library - Properties - BDArmory.Core - BDArmory.Core - v4.7.2 - 512 - - - - true - portable - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - 6 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - 6 - 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/BDArmorySettings.cs b/BDArmory.Core/BDArmorySettings.cs deleted file mode 100644 index cf4cba7a9..000000000 --- a/BDArmory.Core/BDArmorySettings.cs +++ /dev/null @@ -1,144 +0,0 @@ -using UnityEngine; - -using System; - -namespace BDArmory.Core -{ - public class BDArmorySettings - { - public static string settingsConfigURL = "GameData/BDArmory/settings.cfg"; - - // Settings section toggles - [BDAPersistantSettingsField] public static bool GENERAL_SETTINGS_TOGGLE = true; - [BDAPersistantSettingsField] public static bool RADAR_SETTINGS_TOGGLE = true; - [BDAPersistantSettingsField] public static bool SPAWN_SETTINGS_TOGGLE = true; - [BDAPersistantSettingsField] public static bool SLIDER_SETTINGS_TOGGLE = true; - [BDAPersistantSettingsField] public static bool OTHER_SETTINGS_TOGGLE = true; - - // Window settings - [BDAPersistantSettingsField] public static bool STRICT_WINDOW_BOUNDARIES = true; - [BDAPersistantSettingsField] public static float REMOTE_ORCHESTRATION_WINDOW_WIDTH = 225f; - [BDAPersistantSettingsField] public static float VESSEL_SWITCHER_WINDOW_WIDTH = 500f; - [BDAPersistantSettingsField] public static bool VESSEL_SWITCHER_WINDOW_SORTING = false; - [BDAPersistantSettingsField] public static float VESSEL_SPAWNER_WINDOW_WIDTH = 450f; - - // General toggle settings - [BDAPersistantSettingsField] public static bool INSTAKILL = false; - [BDAPersistantSettingsField] public static bool INFINITE_AMMO = false; - [BDAPersistantSettingsField] public static bool BULLET_HITS = true; - [BDAPersistantSettingsField] public static bool EJECT_SHELLS = true; - [BDAPersistantSettingsField] public static bool AIM_ASSIST = true; - [BDAPersistantSettingsField] public static bool DRAW_AIMERS = true; - [BDAPersistantSettingsField] public static bool DRAW_DEBUG_LINES = false; - [BDAPersistantSettingsField] public static bool DRAW_DEBUG_LABELS = false; - [BDAPersistantSettingsField] public static bool REMOTE_SHOOTING = false; - [BDAPersistantSettingsField] public static bool BOMB_CLEARANCE_CHECK = true; - [BDAPersistantSettingsField] public static bool SHOW_AMMO_GAUGES = false; - [BDAPersistantSettingsField] public static bool SHELL_COLLISIONS = true; - [BDAPersistantSettingsField] public static bool BULLET_DECALS = true; - [BDAPersistantSettingsField] public static bool DISABLE_RAMMING = true; // Prevent craft from going into ramming mode when out of ammo. - [BDAPersistantSettingsField] public static bool DEFAULT_FFA_TARGETING = false; // Free-for-all combat style instead of teams (changes target selection behaviour) - [BDAPersistantSettingsField] public static bool DEBUG_RAMMING_LOGGING = false; // Controls whether ramming logging debug information is printed to the Debug.Log - [BDAPersistantSettingsField] public static bool PERFORMANCE_LOGGING = false; - [BDAPersistantSettingsField] public static bool RUNWAY_PROJECT = false; // Enable/disable Runway Project specific enhancements. - [BDAPersistantSettingsField] public static bool DISABLE_KILL_TIMER = true; //disables the kill timers. - [BDAPersistantSettingsField] public static bool AUTO_ENABLE_VESSEL_SWITCHING = false; // Automatically enables vessel switching on competition start. - [BDAPersistantSettingsField] public static bool AUTONOMOUS_COMBAT_SEATS = false; // Enable/disable seats without kerbals. - [BDAPersistantSettingsField] public static bool DESTROY_UNCONTROLLED_WMS = false; // Automatically destroy the WM if there's no kerbal or drone core controlling it. - [BDAPersistantSettingsField] public static bool DUMB_IR_SEEKERS = false; // IR missiles will go after hottest thing they can see - [BDAPersistantSettingsField] public static bool AUTOCATEGORIZE_PARTS = true; - [BDAPersistantSettingsField] public static bool SHOW_CATEGORIES = true; - [BDAPersistantSettingsField] public static bool IGNORE_TERRAIN_CHECK = false; - [BDAPersistantSettingsField] public static bool DISPLAY_PATHING_GRID = false; //laggy when the grid gets large - [BDAPersistantSettingsField] public static bool ADVANCED_EDIT = true; //Used for debug fields not nomrally shown to regular users - - // General slider settings - [BDAPersistantSettingsField] public static int COMPETITION_DURATION = 5; // Competition duration in minutes - [BDAPersistantSettingsField] public static float COMPETITION_INITIAL_GRACE_PERIOD = 60; // Competition initial grace period in seconds. - [BDAPersistantSettingsField] public static float COMPETITION_FINAL_GRACE_PERIOD = 10; // Competition final grace period in seconds. - [BDAPersistantSettingsField] public static float COMPETITION_KILL_TIMER = 15; // Competition kill timer in seconds. - [BDAPersistantSettingsField] public static float COMPETITION_KILLER_GM_FREQUENCY = 60; // Competition killer GM timer in seconds. - [BDAPersistantSettingsField] public static float COMPETITION_KILLER_GM_GRACE_PERIOD = 150; // Competition killer GM grace period in seconds. - [BDAPersistantSettingsField] public static float COMPETITION_KILLER_GM_MAX_ALTITUDE = 30; // Altitude in km at which to kill off craft. - [BDAPersistantSettingsField] public static float COMPETITION_NONCOMPETITOR_REMOVAL_DELAY = 30; // Competition non-competitor removal delay in seconds. - [BDAPersistantSettingsField] public static float COMPETITION_DISTANCE = 1000; // Competition distance. - [BDAPersistantSettingsField] public static float DEBRIS_CLEANUP_DELAY = 15f; // Clean up debris after 30s. - [BDAPersistantSettingsField] public static int MAX_NUM_BULLET_DECALS = 200; - [BDAPersistantSettingsField] public static int TERRAIN_ALERT_FREQUENCY = 1; // Controls how often terrain avoidance checks are made (gets scaled by 1+(radarAltitude/500)^2) - [BDAPersistantSettingsField] public static int CAMERA_SWITCH_FREQUENCY = 3; // Controls the minimum time between automated camera switches - [BDAPersistantSettingsField] public static float MAX_BULLET_RANGE = 8000f; //TODO: remove all references to this so it can be deprecated! all ranges should be supplied in part config! - [BDAPersistantSettingsField] public static float TRIGGER_HOLD_TIME = 0.2f; - [BDAPersistantSettingsField] public static float BDARMORY_UI_VOLUME = 0.35f; - [BDAPersistantSettingsField] public static float BDARMORY_WEAPONS_VOLUME = 0.45f; - [BDAPersistantSettingsField] public static float MAX_GUARD_VISUAL_RANGE = 200000f; - [BDAPersistantSettingsField] public static float MAX_ACTIVE_RADAR_RANGE = 200000f; //NOTE: used ONLY for display range of radar windows! Actual radar range provided by part configs! - [BDAPersistantSettingsField] public static float MAX_ENGAGEMENT_RANGE = 200000f; //NOTE: used ONLY for missile dlz parameters! - [BDAPersistantSettingsField] public static float IVA_LOWPASS_FREQ = 2500f; - [BDAPersistantSettingsField] public static float SMOKE_DEFLECTION_FACTOR = 10f; - - // Physics constants - [BDAPersistantSettingsField] public static float GLOBAL_LIFT_MULTIPLIER = 0.25f; - [BDAPersistantSettingsField] public static float GLOBAL_DRAG_MULTIPLIER = 6f; - [BDAPersistantSettingsField] public static float RECOIL_FACTOR = 0.75f; - [BDAPersistantSettingsField] public static float DMG_MULTIPLIER = 100f; - [BDAPersistantSettingsField] public static float BALLISTIC_DMG_FACTOR = 1.55f; - [BDAPersistantSettingsField] public static float HITPOINT_MULTIPLIER = 3.0f; - [BDAPersistantSettingsField] public static float EXP_DMG_MOD_BALLISTIC_NEW = 0.65f; - [BDAPersistantSettingsField] public static float EXP_DMG_MOD_MISSILE = 6.75f; - [BDAPersistantSettingsField] public static float EXP_IMP_MOD = 0.25f; - [BDAPersistantSettingsField] public static bool EXTRA_DAMAGE_SLIDERS = false; - - // FX - [BDAPersistantSettingsField] public static bool FIRE_FX_IN_FLIGHT = false; - [BDAPersistantSettingsField] public static int MAX_FIRES_PER_VESSEL = 10; //controls fx for penetration only for landed or splashed - [BDAPersistantSettingsField] public static float FIRELIFETIME_IN_SECONDS = 90f; //controls fx for penetration only for landed or splashed - - // Radar settings - [BDAPersistantSettingsField] public static float RWR_WINDOW_SCALE_MIN = 0.50f; - [BDAPersistantSettingsField] public static float RWR_WINDOW_SCALE = 1f; - [BDAPersistantSettingsField] public static float RWR_WINDOW_SCALE_MAX = 1.50f; - [BDAPersistantSettingsField] public static float RADAR_WINDOW_SCALE_MIN = 0.50f; - [BDAPersistantSettingsField] public static float RADAR_WINDOW_SCALE = 1f; - [BDAPersistantSettingsField] public static float RADAR_WINDOW_SCALE_MAX = 1.50f; - [BDAPersistantSettingsField] public static float TARGET_WINDOW_SCALE_MIN = 0.50f; - [BDAPersistantSettingsField] public static float TARGET_WINDOW_SCALE = 1f; - [BDAPersistantSettingsField] public static float TARGET_WINDOW_SCALE_MAX = 2f; - [BDAPersistantSettingsField] public static float TARGET_CAM_RESOLUTION = 1024f; - [BDAPersistantSettingsField] public static bool BW_TARGET_CAM = true; - - // Game modes - [BDAPersistantSettingsField] public static bool PEACE_MODE = false; - [BDAPersistantSettingsField] public static bool TAG_MODE = false; - [BDAPersistantSettingsField] public static bool PAINTBALL_MODE = false; - [BDAPersistantSettingsField] public static bool GRAVITY_HACKS = false; - [BDAPersistantSettingsField] public static bool BATTLEDAMAGE = false; - - // Remote logging - [BDAPersistantSettingsField] public static bool REMOTE_LOGGING_VISIBLE = false; // Show/hide the remote orchestration toggle - [BDAPersistantSettingsField] public static bool REMOTE_LOGGING_ENABLED = false; // Enable/disable remote orchestration - [BDAPersistantSettingsField] public static string REMOTE_CLIENT_SECRET = ""; // Token used to authorize remote orchestration client - [BDAPersistantSettingsField] public static string COMPETITION_HASH = ""; // Competition hash used for orchestration - - // Spawner settings - [BDAPersistantSettingsField] public static bool SHOW_SPAWN_OPTIONS = true; // Show spawn options. - [BDAPersistantSettingsField] public static Vector2d VESSEL_SPAWN_GEOCOORDS = new Vector2d(0.05096, -74.8016); // Spawning coordinates on a planetary body. - [BDAPersistantSettingsField] public static float VESSEL_SPAWN_ALTITUDE = 5f; // Spawning altitude above the surface. - [BDAPersistantSettingsField] public static float VESSEL_SPAWN_DISTANCE_FACTOR = 20f; // Scale factor for the size of the spawning circle. - [BDAPersistantSettingsField] public static float VESSEL_SPAWN_DISTANCE = 10f; // Radius of the size of the spawning circle. - [BDAPersistantSettingsField] public static bool VESSEL_SPAWN_DISTANCE_TOGGLE = false; // Toggle between scaling factor and absolute distance. - [BDAPersistantSettingsField] public static float VESSEL_SPAWN_EASE_IN_SPEED = 1f; // Rate to limit "falling" during spawning. - [BDAPersistantSettingsField] public static int VESSEL_SPAWN_CONCURRENT_VESSELS = 0; // Maximum number of vessels to spawn in concurrently (continuous spawning mode). - [BDAPersistantSettingsField] public static int VESSEL_SPAWN_LIVES_PER_VESSEL = 0; // Maximum number of times to spawn a vessel (continuous spawning mode). - [BDAPersistantSettingsField] public static float OUT_OF_AMMO_KILL_TIME = -1f; // Out of ammo kill timer for continuous spawn mode. - [BDAPersistantSettingsField] public static bool VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING = false; // Spawn craft again after single spawn competition finishes. - [BDAPersistantSettingsField] public static bool VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN = false; // Dump competition scores every time a vessel spawns. - [BDAPersistantSettingsField] public static bool SHOW_SPAWN_LOCATIONS = false; // Show the interesting spawn locations. - - // Tournament settings - [BDAPersistantSettingsField] public static bool SHOW_TOURNAMENT_OPTIONS = false; // Show tournament options. - [BDAPersistantSettingsField] public static string TOURNAMENT_FILES_LOCATION = ""; // Tournament files location (under AutoSpawn). - [BDAPersistantSettingsField] public static float TOURNAMENT_DELAY_BETWEEN_HEATS = 10; // Delay between heats - [BDAPersistantSettingsField] public static int TOURNAMENT_ROUNDS = 1; // Rounds - [BDAPersistantSettingsField] public static int TOURNAMENT_VESSELS_PER_HEAT = 8; // Vessels Per Heat - } -} diff --git a/BDArmory.Core/BuildingDamage.cs b/BDArmory.Core/BuildingDamage.cs deleted file mode 100644 index 175d9c6f3..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]: 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/PartExtensions.cs b/BDArmory.Core/Extension/PartExtensions.cs deleted file mode 100644 index da8e046d0..000000000 --- a/BDArmory.Core/Extension/PartExtensions.cs +++ /dev/null @@ -1,523 +0,0 @@ -using System; -using System.Collections.Generic; -using BDArmory.Core.Services; -using BDArmory.Core.Utils; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Core.Extension -{ - public enum ExplosionSourceType { Other, Missile, Bullet }; - 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 - - ////////////////////////////////////////////////////////// - // Basic Add Hitpoints for compatibility (only used by lasers) - ////////////////////////////////////////////////////////// - damage = (float)Math.Round(damage, 2); - - if (p.GetComponent() != null) - { - ApplyHitPoints(p.GetComponent(), damage); - } - else - { - Dependencies.Get().AddDamageToPart_svc(p, damage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Standard Hitpoints Applied : " + damage); - } - } - - public static float AddExplosiveDamage(this Part p, - float explosiveDamage, - float caliber, - ExplosionSourceType sourceType) - { - if (BDArmorySettings.PAINTBALL_MODE) return 0f; // Don't add damage when paintball mode is enabled - - float damage_ = 0f; - - ////////////////////////////////////////////////////////// - // Explosive Hitpoints - ////////////////////////////////////////////////////////// - - switch (sourceType) - { - case ExplosionSourceType.Missile: - damage_ = (BDArmorySettings.DMG_MULTIPLIER / 100) * BDArmorySettings.EXP_DMG_MOD_MISSILE * explosiveDamage; - break; - default: - damage_ = (BDArmorySettings.DMG_MULTIPLIER / 100) * BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW * explosiveDamage; - break; - } - - var damage_before = damage_; - ////////////////////////////////////////////////////////// - // Armor Reduction factors - ////////////////////////////////////////////////////////// - - if (p.HasArmor()) - { - float armorMass_ = p.GetArmorThickness(); - float damageReduction = DamageReduction(armorMass_, damage_, sourceType, caliber); - - damage_ = damageReduction; - } - - ////////////////////////////////////////////////////////// - // Apply Hitpoints - ////////////////////////////////////////////////////////// - - if (p.GetComponent() != null) - { - ApplyHitPoints(p.GetComponent(), (float)damage_); - } - else - { - ApplyHitPoints(p, damage_); - } - return damage_; - } - - public static float AddBallisticDamage(this Part p, - float mass, - float caliber, - float multiplier, - float penetrationfactor, - float bulletDmgMult, - float impactVelocity) - { - if (BDArmorySettings.PAINTBALL_MODE) return 0f; // Don't add damage when paintball mode is enabled - - ////////////////////////////////////////////////////////// - // Basic Kinetic Formula - ////////////////////////////////////////////////////////// - //Hitpoints mult for scaling in settings - //1e-4 constant for adjusting MegaJoules for gameplay - - float damage_ = ((0.5f * (mass * Mathf.Pow(impactVelocity, 2))) - * (BDArmorySettings.DMG_MULTIPLIER / 100) * bulletDmgMult - * 1e-4f * BDArmorySettings.BALLISTIC_DMG_FACTOR); - - var damage_before = damage_; - ////////////////////////////////////////////////////////// - // Armor Reduction factors - ////////////////////////////////////////////////////////// - - if (p.HasArmor()) - { - float armorMass_ = p.GetArmorThickness(); - float damageReduction = DamageReduction(armorMass_, damage_, ExplosionSourceType.Bullet, caliber, penetrationfactor); - - damage_ = damageReduction; - } - - ////////////////////////////////////////////////////////// - // Apply Hitpoints - ////////////////////////////////////////////////////////// - - if (p.GetComponent() != null) - { - ApplyHitPoints(p.GetComponent(), (float)damage_); - } - else - { - ApplyHitPoints(p, damage_, caliber, mass, mass, impactVelocity, penetrationfactor); - } - return damage_; - } - - /// - /// Ballistic Hitpoint Damage - /// - public static void ApplyHitPoints(Part p, float damage_, float caliber, float mass, float multiplier, float impactVelocity, float penetrationfactor) - { - ////////////////////////////////////////////////////////// - // Apply HitPoints Ballistic - ////////////////////////////////////////////////////////// - Dependencies.Get().AddDamageToPart_svc(p, damage_); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: mass: " + mass + " caliber: " + caliber + " multiplier: " + multiplier + " velocity: " + impactVelocity + " penetrationfactor: " + penetrationfactor); - Debug.Log("[BDArmory]: Ballistic Hitpoints Applied : " + Math.Round(damage_, 2)); - } - - if (BDArmorySettings.BATTLEDAMAGE && !BDArmorySettings.PAINTBALL_MODE) - { - CheckDamageFX(p, caliber); - } - } - - /// - /// Explosive Hitpoint Damage - /// - public static void ApplyHitPoints(Part p, float damage) - { - ////////////////////////////////////////////////////////// - // Apply Hitpoints / Explosive - ////////////////////////////////////////////////////////// - - Dependencies.Get().AddDamageToPart_svc(p, damage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Explosive Hitpoints Applied to " + p.name + ": " + Math.Round(damage, 2)); - - if (BDArmorySettings.BATTLEDAMAGE && !BDArmorySettings.PAINTBALL_MODE) - { - CheckDamageFX(p, 50); - } - } - - /// - /// Kerbal Hitpoint Damage - /// - public static void ApplyHitPoints(KerbalEVA kerbal, float damage) - { - ////////////////////////////////////////////////////////// - // Apply Hitpoints / Kerbal - ////////////////////////////////////////////////////////// - - Dependencies.Get().AddDamageToKerbal_svc(kerbal, damage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Hitpoints Applied to " + kerbal.name + ": " + Math.Round(damage, 2)); - } - - public static void AddForceToPart(Rigidbody rb, Vector3 force, Vector3 position, ForceMode mode) - { - ////////////////////////////////////////////////////////// - // Add The force to part - ////////////////////////////////////////////////////////// - - rb.AddForceAtPosition(force, position, mode); - Debug.Log("[BDArmory]: Force Applied : " + Math.Round(force.magnitude, 2)); - } - - public static void Destroy(this Part p) - { - Dependencies.Get().SetDamageToPart_svc(p, -1); - } - - public static bool HasArmor(this Part p) - { - return p.GetArmorThickness() > 15f; - } - - public static bool GetFireFX(this Part p) - { - return Dependencies.Get().HasFireFX_svc(p); - } - - public static float GetFireFXTimeOut(this Part p) - { - return Dependencies.Get().GetFireFXTimeOut(p); - } - - public static float Damage(this Part p) - { - return Dependencies.Get().GetPartDamage_svc(p); - } - - public static float MaxDamage(this Part p) - { - return Dependencies.Get().GetMaxPartDamage_svc(p); - } - - public static void ReduceArmor(this Part p, double massToReduce) - { - if (!p.HasArmor()) return; - massToReduce = Math.Max(0.10, Math.Round(massToReduce, 2)); - Dependencies.Get().ReduceArmor_svc(p, (float)massToReduce); - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Armor Removed : " + massToReduce); - } - } - - public static float GetArmorThickness(this Part p) - { - if (p == null) return 0f; - return Dependencies.Get().GetPartArmor_svc(p); - } - - public static float GetArmorPercentage(this Part p) - { - if (p == null) return 0; - float armor_ = Dependencies.Get().GetPartArmor_svc(p); - float maxArmor_ = Dependencies.Get().GetMaxArmor_svc(p); - - return armor_ / maxArmor_; - } - - public static float GetDamagePercentatge(this Part p) - { - if (p == null) return 0; - - float damage_ = p.Damage(); - float maxDamage_ = p.MaxDamage(); - - return damage_ / maxDamage_; - } - - public static void RefreshAssociatedWindows(this Part part) - { - //Thanks FlowerChild - //refreshes part action window - - //IEnumerator window = UnityEngine.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(); - - MonoUtilities.RefreshContextWindows(part); - } - - public static bool IsMissile(this Part part) - { - return part.Modules.Contains("MissileBase") || part.Modules.Contains("MissileLauncher") || - part.Modules.Contains("BDModularGuidance"); - } - - public static float GetArea(this Part part, bool isprefab = false, Part prefab = null) - { - var size = part.GetSize(); - float sfcAreaCalc = 2f * (size.x * size.y) + 2f * (size.y * size.z) + 2f * (size.x * size.z); - - return sfcAreaCalc; - } - - public static float GetAverageBoundSize(this Part part) - { - var size = part.GetSize(); - - return (size.x + size.y + size.z) / 3f; - } - - public static float GetVolume(this Part part) - { - var size = part.GetSize(); - var volume = size.x * size.y * size.z; - return volume; - } - - public static Vector3 GetSize(this Part part) - { - var size = part.GetComponentInChildren().mesh.bounds.size; - - // if (part.name.Contains("B9.Aero.Wing.Procedural")) // Covered by SuicidalInsanity's patch. - // { - // size = size * 0.1f; - // } - - float scaleMultiplier = 1f; - if (part.Modules.Contains("TweakScale")) - { - var tweakScaleModule = part.Modules["TweakScale"]; - scaleMultiplier = tweakScaleModule.Fields["currentScale"].GetValue(tweakScaleModule) / - tweakScaleModule.Fields["defaultScale"].GetValue(tweakScaleModule); - } - - return size * scaleMultiplier; - } - - public static bool IsAero(this Part part) - { - return part.Modules.Contains("ModuleControlSurface") || - part.Modules.Contains("ModuleLiftingSurface"); - } - - public static string GetExplodeMode(this Part part) - { - return Dependencies.Get().GetExplodeMode_svc(part); - } - - public static bool IgnoreDecal(this Part part) - { - if ( - part.Modules.Contains("FSplanePropellerSpinner") || - part.Modules.Contains("ModuleWheelBase") || - part.Modules.Contains("KSPWheelBase") || - part.gameObject.GetComponentUpwards() || - part.Modules.Contains("ModuleDCKShields") || - part.Modules.Contains("ModuleShieldGenerator") - ) - { - return true; - } - else - { - return false; - } - } - - public static bool HasFuel(this Part part) - { - bool hasFuel = false; - using (IEnumerator resources = part.Resources.GetEnumerator()) - while (resources.MoveNext()) - { - if (resources.Current == null) continue; - switch (resources.Current.resourceName) - { - case "LiquidFuel": - if (resources.Current.amount > 1d) hasFuel = true; - break; - } - } - return hasFuel; - } - - public static float DamageReduction(float armor, float damage, ExplosionSourceType sourceType, float caliber = 0, float penetrationfactor = 0) - { - float _damageReduction; - - switch (sourceType) - { - case ExplosionSourceType.Missile: - if (BDAMath.Between(armor, 100f, 200f)) - { - damage *= 0.95f; - } - else if (BDAMath.Between(armor, 200f, 400f)) - { - damage *= 0.875f; - } - else if (BDAMath.Between(armor, 400f, 500f)) - { - damage *= 0.80f; - } - break; - default: - if (!(penetrationfactor >= 1f)) - { - //if (BDAMath.Between(armor, 100f, 200f)) - //{ - // damage *= 0.300f; - //} - //else if (BDAMath.Between(armor, 200f, 400f)) - //{ - // damage *= 0.250f; - //} - //else if (BDAMath.Between(armor, 400f, 500f)) - //{ - // damage *= 0.200f; - //} - - //y=(98.34817*x)/(97.85935+x) - - _damageReduction = (113 * armor) / (154 + armor); - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Damage Before Reduction : " + Math.Round(damage, 2) / 100); - Debug.Log("[BDArmory]: Damage Reduction : " + Math.Round(_damageReduction, 2) / 100); - Debug.Log("[BDArmory]: Damage After Armor : " + Math.Round(damage *= (_damageReduction / 100f))); - } - - damage *= (_damageReduction / 100f); - } - break; - } - - return damage; - } - - public static void CheckDamageFX(Part part, float caliber) - { - //what can get damaged? engines, wings, SAS, cockpits (past a certain dmg%, kill kerbals?), weapons(would be far easier to just have these have low hp), radars - - if ((part.GetComponent() != null || part.GetComponent() != null) && part.GetDamagePercentatge() < 0.95f) //first hit's free - { - ModuleEngines engine; - engine = part.GetComponent(); - if (part.GetDamagePercentatge() >= 0.50f) - { - if (engine.thrustPercentage > 0) - { - //engine.maxThrust -= ((engine.maxThrust * 0.125f) / 100); // doesn't seem to adjust thrust; investigate - engine.thrustPercentage -= ((engine.maxThrust * 0.125f) / 100); //workaround hack - Mathf.Clamp(engine.thrustPercentage, 0, 1); - } - } - if (part.GetDamagePercentatge() < 0.50f) - { - if (engine.EngineIgnited) - { - engine.PlayFlameoutFX(true); - engine.Shutdown(); //kill a badly damaged engine and don't allow restart - engine.allowRestart = false; - } - } - } - if (part.GetComponent() != null && part.GetDamagePercentatge() > 0.125f) //ensure wings can still generate some lift - { - ModuleLiftingSurface wing; - wing = part.GetComponent(); - if (wing.deflectionLiftCoeff > (caliber * caliber / 20000))//2x4m wing board = 2 Lift, 0.25 Lift/m2. 20mm round = 20*20=400/20000= 0.02 Lift reduced per hit - { - wing.deflectionLiftCoeff -= (caliber * caliber / 20000); //.50 would be .008 Lift, and 30mm would be .045 Lift per hit - } - } - if (part.GetComponent() != null && part.GetDamagePercentatge() > 0.125f) - { - ModuleControlSurface aileron; - aileron = part.GetComponent(); - aileron.deflectionLiftCoeff -= (caliber * caliber / 20000); - if (part.GetDamagePercentatge() < 0.75f) - { - if (aileron.ctrlSurfaceRange >= 0.5) - { - aileron.ctrlSurfaceRange -= 0.5f; - } - } - } - if (part.GetComponent() != null && part.GetDamagePercentatge() < 0.75f) - { - ModuleReactionWheel SAS; - SAS = part.GetComponent(); - if (SAS.PitchTorque > 1) - { - SAS.PitchTorque -= (1 - part.GetDamagePercentatge()); - } - if (SAS.YawTorque > 1) - { - SAS.YawTorque -= (1 - part.GetDamagePercentatge()); - } - if (SAS.RollTorque > 1) - { - SAS.RollTorque -= (1 - part.GetDamagePercentatge()); - } - } - if (part.protoModuleCrew.Count > 0 && part.GetDamagePercentatge() < 0.50f) //really, the way to go would be via PooledBullet and have it check when calculating penetration depth - { //if A) the bullet goes through, and B) part's kerballed - ProtoCrewMember crewMember = part.protoModuleCrew.FirstOrDefault(x => x != null); - if (crewMember != null) - { - crewMember.UnregisterExperienceTraits(part); - crewMember.Die(); - part.RemoveCrewmember(crewMember); // sadly, I wasn't able to get the K.I.A. portrait working - //Vessel.CrewWasModified(part.vessel); - Debug.Log(crewMember.name + " was killed by damage to cabin!"); - if (HighLogic.CurrentGame.Parameters.Difficulty.MissingCrewsRespawn) - { - crewMember.StartRespawnPeriod(); - } - //ScreenMessages.PostScreenMessage(crewMember.name + " killed by damage to " + part.vessel.name + part.partName + ".", 5.0f, ScreenMessageStyle.UPPER_LEFT); - } - } - } - - public static Vector3 GetBoundsSize(Part part) - { - return PartGeometryUtil.MergeBounds(part.GetRendererBounds(), part.transform).size; - } - } -} diff --git a/BDArmory.Core/Extension/VesselExtensions.cs b/BDArmory.Core/Extension/VesselExtensions.cs deleted file mode 100644 index 3eea61531..000000000 --- a/BDArmory.Core/Extension/VesselExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using UnityEngine; - -namespace BDArmory.Core.Extension -{ - public static class VesselExtensions - { - public static bool InOrbit(this Vessel v) - { - try - { - return !v.LandedOrSplashed && - (v.situation == Vessel.Situations.ORBITING || - v.situation == Vessel.Situations.SUB_ORBITAL || - v.situation == Vessel.Situations.ESCAPING); - } - catch - { - return false; - } - } - - public static bool InVacuum(this Vessel v) - { - return v.atmDensity <= 0.001f; - } - - public static Vector3d Velocity(this Vessel v) - { - try - { - if (!v.InOrbit()) - { - return v.srf_velocity; - } - else - { - return v.obt_velocity; - } - } - catch - { - //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; - } - } -} diff --git a/BDArmory.Core/Module/HitpointTracker.cs b/BDArmory.Core/Module/HitpointTracker.cs deleted file mode 100644 index 089ffd725..000000000 --- a/BDArmory.Core/Module/HitpointTracker.cs +++ /dev/null @@ -1,293 +0,0 @@ -using BDArmory.Core.Extension; -using UnityEngine; - -namespace BDArmory.Core.Module -{ - public class HitpointTracker : PartModule - { - #region KSP Fields - - [KSPField(isPersistant = false, 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 = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorThickness"),//Armor Thickness - UI_FloatRange(minValue = 0f, maxValue = 1500f, stepIncrement = 5f, scene = UI_Scene.All)] - public float Armor = 10f; - - [KSPField(isPersistant = true)] - public float maxHitPoints = 0f; - - [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; - - #endregion KSP Fields - - private readonly float hitpointMultiplier = BDArmorySettings.HITPOINT_MULTIPLIER; - - private float previousHitpoints; - private bool _updateHitpoints = false; - private bool _forceUpdateHitpointsUI = false; - private const int HpRounding = 100; - - 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; - } - else - enabled = false; - } - } - - public void SetupPrefab() - { - if (part != null) - { - var maxHitPoints_ = CalculateTotalHitpoints(); - - if (!_forceUpdateHitpointsUI && previousHitpoints == maxHitPoints_) return; - - //Add Hitpoints - 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; - - Hitpoints = maxHitPoints_; - - //Add Armor - UI_FloatRange armorFieldFlight = (UI_FloatRange)Fields["Armor"].uiControlFlight; - armorFieldFlight.maxValue = 1500f; - armorFieldFlight.minValue = 0f; - - UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; - armorFieldEditor.maxValue = 1500f; - armorFieldEditor.minValue = 0f; - part.RefreshAssociatedWindows(); - - if (!ArmorSet) overrideArmorSetFromConfig(); - - previousHitpoints = maxHitPoints_; - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[HitpointTracker]: OnStart part is null"); - } - } - - public override void OnStart(StartState state) - { - isEnabled = true; - - if (part != null) _updateHitpoints = true; - - if (HighLogic.LoadedSceneIsFlight) - { - UI_FloatRange armorField = (UI_FloatRange)Fields["Armor"].uiControlFlight; - //Once started the max value of the field should be the initial one - armorField.maxValue = Armor; - part.RefreshAssociatedWindows(); - } - GameEvents.onEditorShipModified.Add(ShipModified); - } - - private void OnDestroy() - { - GameEvents.onEditorShipModified.Remove(ShipModified); - } - - public void ShipModified(ShipConstruct data) - { - _updateHitpoints = true; - } - - public override void OnUpdate() - { - RefreshHitPoints(); - } - - public void Update() - { - RefreshHitPoints(); - } - - private void RefreshHitPoints() - { - if (_updateHitpoints) - { - SetupPrefab(); - _updateHitpoints = false; - _forceUpdateHitpointsUI = false; - } - } - - #region Hitpoints Functions - - public float CalculateTotalHitpoints() - { - float hitpoints; - - if (!part.IsMissile()) - { - var averageSize = part.GetAverageBoundSize(); - var sphereRadius = averageSize * 0.5f; - var sphereSurface = 4 * Mathf.PI * sphereRadius * sphereRadius; - var structuralVolume = sphereSurface * 0.1f; - - var density = (part.mass * 1000f) / structuralVolume; - density = Mathf.Clamp(density, 1000, 10000); - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[HitpointTracker]: Hitpoint Calc" + part.name + " | structuralVolume : " + structuralVolume); - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[HitpointTracker]: Hitpoint Calc" + part.name + " | Density : " + density); - - var structuralMass = density * structuralVolume; - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[HitpointTracker]: Hitpoint Calc" + part.name + " | structuralMass : " + structuralMass); - //3. final calculations - hitpoints = structuralMass * hitpointMultiplier * 0.33f; - - if (hitpoints > 10 * part.mass * 1000f || hitpoints < 0.1f * part.mass * 1000f) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[HitpointTracker]: Clamping hitpoints for part {part.name}"); - hitpoints = hitpointMultiplier * part.mass * 333f; - } - - // SuicidalInsanity B9 patch - if (part.name.Contains("B9.Aero.Wing.Procedural")) - { - if (part.Modules.Contains("FARWingAerodynamicModel") || part.Modules.Contains("FARControllableSurface")) - { - hitpoints = (part.mass * 1000f) * 3.5f * hitpointMultiplier * 0.33f; //To account for FAR's Strength-mass Scalar. - } - else - { - hitpoints = (part.mass * 1000f) * 7f; // since wings are basically a 2d object, lets have mass be our scalar - afterall, 2x the mass will ~= 2x the surfce area - } - } - - hitpoints = Mathf.Round(hitpoints / HpRounding) * HpRounding; - if (hitpoints <= 0) hitpoints = HpRounding; - } - else - { - hitpoints = 5; - Armor = 2; - } - - //override based on part configuration for custom parts - if (maxHitPoints != 0) - { - hitpoints = maxHitPoints; - } - - if (hitpoints <= 0) hitpoints = HpRounding; - return hitpoints; - } - - public void DestroyPart() - { - if (part.mass <= 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; - - if (Hitpoints <= 0) - { - DestroyPart(); - } - } - - public void AddDamage(float partdamage) - { - if (part.name == "Weapon Manager" || part.name == "BDModulePilotAI") return; - - partdamage = Mathf.Max(partdamage, 0.01f) * -1; - Hitpoints += partdamage; - - if (Hitpoints <= 0) - { - DestroyPart(); - } - } - - public void AddDamageToKerbal(KerbalEVA kerbal, float damage) - { - damage = Mathf.Max(damage, 0.01f) * -1; - Hitpoints += damage; - - if (Hitpoints <= 0) - { - // oh the humanity! - PartExploderSystem.AddPartToExplode(kerbal.part); - } - } - - public void ReduceArmor(float massToReduce) - { - Armor -= massToReduce; - if (Armor < 0) - { - Armor = 0; - } - } - - public void overrideArmorSetFromConfig(float thickness = 0) - { - ArmorSet = true; - if (ArmorThickness != 0) - { - Armor = ArmorThickness; - } - } - - #endregion Hitpoints Functions - } -} diff --git a/BDArmory.Core/PartExploderSystem.cs b/BDArmory.Core/PartExploderSystem.cs deleted file mode 100644 index a47b3868e..000000000 --- a/BDArmory.Core/PartExploderSystem.cs +++ /dev/null @@ -1,40 +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(); - // part.Die(); // DEBUG check whether part.explode() actually removes the part. - } - } while (ExplodingPartsQueue.Count > 0); - } - } -} diff --git a/BDArmory.Core/Properties/AssemblyInfo.cs b/BDArmory.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index 4d784f1ec..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.0.3")] -[assembly: AssemblyFileVersion("1.4.0.3")] -[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.Core/Utils/BlastPhysicsUtils.cs b/BDArmory.Core/Utils/BlastPhysicsUtils.cs deleted file mode 100644 index 3e38746b4..000000000 --- a/BDArmory.Core/Utils/BlastPhysicsUtils.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using BDArmory.Core.Extension; -using UnityEngine; - -namespace BDArmory.Core.Utils -{ - public static class BlastPhysicsUtils - { - // This values represent percentage of the blast radius where we consider that the damage happens. - - public static BlastInfo CalculatePartBlastEffects(Part part, float distanceToHit, double vesselMass, float explosiveMass, float range) - { - float clampedMinDistanceToHit = ClampRange(explosiveMass, distanceToHit); - - var minPressureDistance = distanceToHit + part.GetAverageBoundSize(); - - double minPressurePerMs = 0; - - if (minPressureDistance <= range) - { - float clampedMaxDistanceToHit = ClampRange(explosiveMass, minPressureDistance); - double maxScaledDistance = CalculateScaledDistance(explosiveMass, clampedMaxDistanceToHit); - minPressurePerMs = CalculateIncidentImpulse(maxScaledDistance, explosiveMass); - } - - double minScaledDistance = CalculateScaledDistance(explosiveMass, clampedMinDistanceToHit); - double maxPressurePerMs = CalculateIncidentImpulse(minScaledDistance, explosiveMass); - - double totalDamage = (maxPressurePerMs + minPressurePerMs);// * 2 / 2 ; - - float effectivePartArea = CalculateEffectiveBlastAreaToPart(range, part); - - float positivePhase = 5; - - double maxforce = CalculateForce(maxPressurePerMs, effectivePartArea, positivePhase); - double minforce = CalculateForce(minPressurePerMs, effectivePartArea, positivePhase); - - double force = (maxforce + minforce) / 2f; - - float acceleration = (float)(force / vesselMass); - - // Calculation of damage - - float finalDamage = (float)totalDamage; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log( - "[BDArmory]: Blast Debug data: {" + part.name + "}, " + - " clampedMinDistanceToHit: {" + clampedMinDistanceToHit + "}," + - " minPressureDistance: {" + minPressureDistance + "}," + - " minScaledDistance: {" + minScaledDistance + "}," + - " minPressurePerMs: {" + minPressurePerMs + "}," + - " maxPressurePerMs: {" + maxPressurePerMs + "}," + - " totalDamage: {" + totalDamage + "}," + - " finalDamage: {" + finalDamage + "},"); - } - - return new BlastInfo() { TotalPressure = maxPressurePerMs, EffectivePartArea = effectivePartArea, PositivePhaseDuration = positivePhase, VelocityChange = acceleration, Damage = finalDamage }; - } - - private static float CalculateEffectiveBlastAreaToPart(float range, Part part) - { - float circularArea = Mathf.PI * range * range; - - return Mathf.Clamp(circularArea, 0f, part.GetArea() * 0.40f); - } - - private static double CalculateScaledDistance(float explosiveCharge, float distanceToHit) - { - return (distanceToHit / Math.Pow(explosiveCharge, 1f / 3f)); - } - - private static float ClampRange(float explosiveCharge, float distanceToHit) - { - float cubeRootOfChargeWeight = (float)Math.Pow(explosiveCharge, 1f / 3f); - - return Mathf.Clamp(distanceToHit, 0.0674f * cubeRootOfChargeWeight, 40f * cubeRootOfChargeWeight); - } - - private static double CalculateIncidentImpulse(double scaledDistance, float explosiveCharge) - { - double t = Math.Log(scaledDistance) / Math.Log(10); - double cubeRootOfChargeWeight = Math.Pow(explosiveCharge, 0.3333333); - double ii = 0; - if (scaledDistance <= 0.955) - { //NATO version - double U = 2.06761908721 + 3.0760329666 * t; - ii = 2.52455620925 - 0.502992763686 * U + - 0.171335645235 * Math.Pow(U, 2) + - 0.0450176963051 * Math.Pow(U, 3) - - 0.0118964626402 * Math.Pow(U, 4); - } - else if (scaledDistance > 0.955) - { //version from ??? - var U = -1.94708846747 + 2.40697745406 * t; - 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); - } - - ii = Math.Pow(10, ii); - ii = ii * cubeRootOfChargeWeight; - return ii; - } - - /// - /// Calculate newtons from the pressure in kPa and the surface on Square meters - /// - /// kPa - /// m2 - /// - private static double CalculateForce(double pressure, float surface, double timeInMs) - { - return pressure * 1000f * surface * (timeInMs / 1000f); - } - - /// - /// Method based on Hopkinson-Cranz Scaling Law - /// Z value of 14.8 - /// - /// tnt equivales mass in kg - /// explosive range in meters - public static float CalculateBlastRange(double tntMass) - { - return (float)(14.8f * Math.Pow(tntMass, 1 / 3f)); - } - - /// - /// Method based on Hopkinson-Cranz Scaling Law - /// Z value of 14.8 - /// - /// expected range in meters - /// explosive range in meters - public static float CalculateExplosiveMass(float range) - { - return (float)Math.Pow((range / 14.8f), 3); - } - } - - public struct BlastInfo - { - public float VelocityChange { get; set; } - public float EffectivePartArea { get; set; } - public float Damage { get; set; } - public double TotalPressure { get; set; } - public double PositivePhaseDuration { get; set; } - } -} 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/Ammo/BulletInfo.cs b/BDArmory/Ammo/BulletInfo.cs new file mode 100644 index 000000000..ece1ff749 --- /dev/null +++ b/BDArmory/Ammo/BulletInfo.cs @@ -0,0 +1,378 @@ +using BDArmory.Utils; +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using static BDArmory.Bullets.PooledBullet; + +namespace BDArmory.Bullets +{ + public class BulletInfo + { + public string name { get; private set; } + public string DisplayName { get; private set; } + public float caliber { get; private set; } + public float bulletMass { get; private set; } + public float bulletVelocity { get; private set; } + 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 + public bool EMP { get; private set; } + public bool nuclear { get; private set; } + public bool beehive { get; private set; } + public string subMunitionType { get; private set; } + public float tntMass { get; private set; } + public float massMod { get; private set; } + public float impulse { get; private set; } + public string fuzeType { get; private set; } + public float guidanceDPS { get; private set; } + public float guidanceRange { get; private set; } + public int projectileCount { get; private set; } + public float subProjectileDispersion { get; private set; } + public float projectileTTL { get; private set; } + public float apBulletMod { get; private set; } + public string bulletDragTypeName { get; private set; } + public Color projectileColorC { get; private set; } + public string projectileColor { get; private set; } + public Color startColorC { get; private set; } + public string startColor { get; private set; } + public bool fadeColor { get; private set; } + // Parsed types + public PooledBulletTypes eHEType { get; private set; } + public BulletFuzeTypes eFuzeType { get; private set; } + public float fuzeSensitivity { get; private set; } = -1f; + public float fuzeDelay { get; private set; } = -1f; + public BulletDragTypes bulletDragType { get; private set; } + // Calculated Values + public float bulletBallisticCoefficient { get; private set; } + public bool sabot { get; private set; } + + public static BulletInfos bullets; + public static HashSet bulletNames; + public static BulletInfo defaultBullet; + + // Fixes for old configs + private static readonly List<(string, string)> oldSubmunitionConfigs = []; + + public BulletInfo(string name, string DisplayName, float caliber, float bulletVelocity, float bulletMass, + string explosive, bool incendiary, float tntMass, bool EMP, bool nuclear, bool beehive, string subMunitionType, float massMod, float impulse, string fuzeType, float guidanceDPS, float guidanceRange, + float apBulletDmg, int projectileCount, float subProjectileDispersion, float projectileTTL, string bulletDragTypeName, string projectileColor, string startColor, bool fadeColor) + { + this.name = name; + this.DisplayName = DisplayName; + this.caliber = caliber; + this.bulletVelocity = bulletVelocity; + this.bulletMass = bulletMass; + this.explosive = explosive; + this.incendiary = incendiary; + this.tntMass = tntMass; + this.EMP = EMP; + this.nuclear = nuclear; + this.beehive = beehive; + this.subMunitionType = subMunitionType; + this.massMod = massMod; + this.impulse = impulse; + this.fuzeType = fuzeType; + this.guidanceDPS = guidanceDPS; + this.guidanceRange = guidanceRange; + this.apBulletMod = apBulletDmg; + this.projectileCount = projectileCount; + this.subProjectileDispersion = subProjectileDispersion; + this.projectileTTL = projectileTTL; + this.bulletDragTypeName = bulletDragTypeName; + this.projectileColor = projectileColor; + this.projectileColorC = GUIUtils.ParseColor255(projectileColor); + this.startColor = startColor; + this.startColorC = GUIUtils.ParseColor255(startColor); + this.fadeColor = fadeColor; + } + + public static void Load() + { + if (bullets != null) return; // Only load the bullet defs once on startup. + bullets = new BulletInfos(); + if (bulletNames == null) bulletNames = new HashSet(); + UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("BULLET"); + ConfigNode node; + + // First locate BDA's default bullet definition so we can fill in missing fields. + if (defaultBullet == null) + for (int i = 0; i < nodes.Length; ++i) + { + if (nodes[i].parent.name != "BD_Bullets") 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.BulletInfo]: Parsing default bullet definition from " + nodes[i].parent.name); + //tagsList = Misc.BDAcTools.ParseNames((string)ParseField(node, "attributeTags", typeof(string))); //would prefer not to do a rocketInfo and have separate node fields for every attribute + defaultBullet = new BulletInfo( + "def", + (string)ParseField(node, "DisplayName", typeof(string)), + (float)ParseField(node, "caliber", typeof(float)), + (float)ParseField(node, "bulletVelocity", typeof(float)), + (float)ParseField(node, "bulletMass", typeof(float)), + (string)ParseField(node, "explosive", typeof(string)), + (bool)ParseField(node, "incendiary", typeof(bool)), + (float)ParseField(node, "tntMass", typeof(float)), + (bool)ParseField(node, "EMP", typeof(bool)), + (bool)ParseField(node, "nuclear", typeof(bool)), + (bool)ParseField(node, "beehive", typeof(bool)), + (string)ParseField(node, "subMunitionType", typeof(string)), + (float)ParseField(node, "massMod", typeof(float)), + (float)ParseField(node, "impulse", typeof(float)), + (string)ParseField(node, "fuzeType", typeof(string)), + (float)ParseField(node, "guidanceDPS", typeof(float)), + (float)ParseField(node, "guidanceRange", typeof(float)), + (float)ParseField(node, "apBulletMod", typeof(float)), + Math.Max((int)ParseField(node, "projectileCount", typeof(int)), 1), + -1, + (float)ParseField(node, "projectileTTL", typeof(float)), + (string)ParseField(node, "bulletDragTypeName", typeof(string)), + (string)ParseField(node, "projectileColor", typeof(string)), + (string)ParseField(node, "startColor", typeof(string)), + (bool)ParseField(node, "fadeColor", typeof(bool)) + ); + defaultBullet.ParseTypes(); + defaultBullet.PreCalcData(); + bullets.Add(defaultBullet); + bulletNames.Add("def"); + break; + } + if (defaultBullet == null) throw new ArgumentException("Failed to find BDArmory's default bullet definition.", "defaultBullet"); + + // Now add in the rest of the bullets. + for (int i = 0; i < nodes.Length; i++) + { + string name_ = ""; + try + { + node = nodes[i].config; + name_ = (string)ParseField(node, "name", typeof(string)); + string parentName = nodes[i].parent.name != "part" ? nodes[i].parent.name : nodes[i].parent.parent.name; + if (bulletNames.Contains(name_)) // Avoid duplicates. + { + if (parentName != "BD_Bullets" || name_ != "def") // Don't report the default bullet definition as a duplicate. + Debug.LogError("[BDArmory.BulletInfo]: Bullet definition " + name_ + " from " + parentName + " already exists, skipping."); + continue; + } + Debug.Log("[BDArmory.BulletInfo]: Parsing definition of bullet " + name_ + " from " + parentName); + BulletInfo tempBullet = new BulletInfo( + name_, + (string)ParseField(node, "DisplayName", typeof(string)), + (float)ParseField(node, "caliber", typeof(float)), + (float)ParseField(node, "bulletVelocity", typeof(float)), + (float)ParseField(node, "bulletMass", typeof(float)), + (string)ParseField(node, "explosive", typeof(string)), + (bool)ParseField(node, "incendiary", typeof(bool)), + (float)ParseField(node, "tntMass", typeof(float)), + (bool)ParseField(node, "EMP", typeof(bool)), + (bool)ParseField(node, "nuclear", typeof(bool)), + (bool)ParseField(node, "beehive", typeof(bool)), + (string)ParseField(node, "subMunitionType", typeof(string)), + (float)ParseField(node, "massMod", typeof(float)), + (float)ParseField(node, "impulse", typeof(float)), + (string)ParseField(node, "fuzeType", typeof(string)), + (float)ParseField(node, "guidanceDPS", typeof(float)), + (float)ParseField(node, "guidanceRange", typeof(float)), + (float)ParseField(node, "apBulletMod", typeof(float)), + (int)ParseField(node, "projectileCount", typeof(int)), + (float)ParseField(node, "subProjectileDispersion", typeof(float)), + (float)ParseField(node, "projectileTTL", typeof(float)), + (string)ParseField(node, "bulletDragTypeName", typeof(string)), + (string)ParseField(node, "projectileColor", typeof(string)), + (string)ParseField(node, "startColor", typeof(string)), + (bool)ParseField(node, "fadeColor", typeof(bool)) + ); + tempBullet.ParseTypes(); + tempBullet.PreCalcData(); + bullets.Add(tempBullet); + bulletNames.Add(name_); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.BulletInfo]: Error Loading Bullet Config '" + name_ + "' | " + e.ToString()); + } + } + PostProcessOldSubmunitionConfigs(); + } + 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}' | {e.Message}", field); } + } + catch (Exception e) + { + if (field == "name") throw; // Sanity check for field "name" to avoid potential stack overflow. + if (defaultBullet != null) + { + // 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 returning the def bullet name, not current bullet name + if (field == "EMP" || field == "nuclear" || field == "beehive" || field == "subMunitionType" || field == "massMod" || field == "impulse" || field == "subProjectileDispersion" || field == "guidanceDPS" ||field == "projectileTTL" || (field == "projectileCount" && node.HasValue("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 + } + else if (field == "projectileCount" && node.HasValue("subProjectileCount")) // Old projectile/subprojectile bullet def + { + try + { + string name = (string)ParseField(node, "name", typeof(string)); + int projectileCount = (int)ParseField(node, "subProjectileCount", type); // Treat the subProjectileCount as projectileCount. + Debug.LogWarning($"[BDArmory.BulletInfo]: Old bullet def detected for {name}, using subProjectileCount ({projectileCount}) for projectileCount. Please upgrade your mod's bullet defs."); + if (node.HasValue("subMunitionType")) oldSubmunitionConfigs.Add((name, (string)ParseField(node, "subMunitionType", typeof(string)))); + return projectileCount; + } + catch (Exception e2) + { + Debug.LogError($"[BDArmory.BulletInfo]: Old bullet def detected, but failed to parse subProjectileCount. Using default value of {defaultValue} for projectileCount.\n{e2}"); + return defaultValue; + } + } + else + { + string name = "unknown"; + try { name = (string)ParseField(node, "name", typeof(string)); } catch { } + Debug.LogWarning($"[BDArmory.BulletInfo]: Using default value of {defaultValue} for {field} of {name} | {e.Message}"); + } + return defaultValue; + } + else + throw; + } + } + + private static void PostProcessOldSubmunitionConfigs() + { + if (oldSubmunitionConfigs.Count == 0) return; + Debug.LogWarning($"[BDArmory.BulletInfo]: Attempting to correct bullet definitions with old submunition configs. This may cause irregularities or failures in weapons using these bullet definitions. Please upgrade your configs ASAP."); + try + { + foreach (var pair in oldSubmunitionConfigs) + { + if (!bullets.Exists(b => b.name == pair.Item1) || !bullets.Exists(b => b.name == pair.Item2)) + { + Debug.LogWarning($"[BDArmory.BulletInfo]: One or more of {pair.Item1} and {pair.Item2} is missing from the bullet definitions, unable to correct the config."); + continue; + } + var bullet = bullets[pair.Item1]; + var submunition = bullets[pair.Item2]; + if (bullet.projectileCount == 1 && submunition.projectileCount > 1) + { + bullet.subMunitionType += $"; {submunition.projectileCount}"; + Debug.LogWarning($"[BDArmory.BulletInfo]: Updating {bullet.name} to have {submunition.projectileCount} sub-projectiles of type {submunition.name}"); + } + if (submunition.projectileCount != 1) + { + submunition.projectileCount = 1; // Submunitions shouldn't contain multiple projectiles (no recursion). + Debug.LogWarning($"[BDArmory.BulletInfo]: Updating {submunition.name} to be a single projectile."); + } + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.BulletInfo]: Failed to post-process old submunition configs, expect irregularities or failures: {e}"); + } + oldSubmunitionConfigs.Clear(); + } + + private void ParseTypes() + { + if (tntMass > 0) + eHEType = explosive.ToLower() switch + { + "standard" or "true" => PooledBulletTypes.Explosive, + "shaped" => PooledBulletTypes.Shaped, + _ => PooledBulletTypes.Slug + }; + else + eHEType = PooledBulletTypes.Slug; + + bulletDragType = bulletDragTypeName.ToLower() switch + { + "none" => BulletDragTypes.None, + "numericalintegration" => BulletDragTypes.NumericalIntegration, + "analyticestimate" => BulletDragTypes.AnalyticEstimate, + _ => BulletDragTypes.AnalyticEstimate + }; + + if (tntMass > 0 || beehive) + { + string[] fuzeStrings = fuzeType.Split([',']); + if (fuzeStrings.Length > 0) + { + eFuzeType = fuzeStrings[0].ToLower() switch + { + //Anti-Air fuzes + "timed" => BulletFuzeTypes.Timed, + "proximity" => BulletFuzeTypes.Proximity, + "flak" => BulletFuzeTypes.Flak, + //Anti-Armor fuzes + "delay" => BulletFuzeTypes.Delay, + "penetrating" => BulletFuzeTypes.Penetrating, + "impact" => BulletFuzeTypes.Impact, + "none" => beehive ? BulletFuzeTypes.Timed : BulletFuzeTypes.Impact, + _ => beehive ? BulletFuzeTypes.Timed : BulletFuzeTypes.Impact + }; + } + else + { + eFuzeType = beehive ? BulletFuzeTypes.Timed : BulletFuzeTypes.Impact; + } + + + if (eFuzeType == BulletFuzeTypes.Delay && fuzeStrings.Length > 1) + { + if (float.TryParse(fuzeStrings[1], out float temp)) + fuzeDelay = temp; + } + else if (eFuzeType == BulletFuzeTypes.Penetrating && fuzeStrings.Length > 1) + { + if (float.TryParse(fuzeStrings[1], out float temp)) + fuzeDelay = temp; + if (fuzeStrings.Length > 2 && float.TryParse(fuzeStrings[2], out temp)) + fuzeSensitivity = temp; + } + } + else + { + eFuzeType = BulletFuzeTypes.None; + } + } + + private void PreCalcData() + { + bulletBallisticCoefficient = PooledBullet.calcBulletBallisticCoefficient(caliber, bulletMass); + + sabot = (eHEType == PooledBulletTypes.Slug && PooledBullet.isSabot(bulletMass, caliber)); + } + } + + public class BulletInfos : List + { + public BulletInfo this[string name] + { + get { return Find((value) => { return value.name == name; }); } + } + } +} diff --git a/BDArmory/Modules/ModuleAmmoSwitch.cs b/BDArmory/Ammo/ModuleAmmoSwitch.cs similarity index 89% rename from BDArmory/Modules/ModuleAmmoSwitch.cs rename to BDArmory/Ammo/ModuleAmmoSwitch.cs index 06f0bbb6e..afa67381d 100644 --- a/BDArmory/Modules/ModuleAmmoSwitch.cs +++ b/BDArmory/Ammo/ModuleAmmoSwitch.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +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 { @@ -73,28 +75,28 @@ public override void OnStart(PartModule.StartState state) public override void OnAwake() { - //Debug.Log("FS AWAKE "+initialized+" "+configLoaded+" "+resourceAmounts); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: FS AWAKE "+initialized+" "+configLoaded+" "+resourceAmounts); if (configLoaded) { initializeData(); } - //Debug.Log("FS AWAKE DONE " + (configLoaded ? tankList.Count.ToString() : "NO CONFIG")); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: FS AWAKE DONE " + (configLoaded ? tankList.Count.ToString() : "NO CONFIG")); } public override void OnLoad(ConfigNode node) { base.OnLoad(node); - //Debug.Log("FS LOAD " + initialized + " " + resourceAmounts+configLoaded); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: FS LOAD " + initialized + " " + resourceAmounts+configLoaded); if (!configLoaded) { initializeData(); } if (basePartMass != part.mass) { - Debug.LogError("Error: BDAcAmmoSwitch Mass Discrepancy detected in part '" + part.name + "'.", part); + Debug.LogError("[BDArmory.ModuleAmmoSwitch]: Mass Discrepancy detected in part '" + part.name + "'.", part); } configLoaded = true; - //Debug.Log("FS LOAD DONE " + tankList.Count); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: FS LOAD DONE " + tankList.Count); } private void initializeData() @@ -178,7 +180,7 @@ private void assignResourcesToPart(bool calledByPlayer) } } - //Debug.Log("refreshing UI"); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: refreshing UI"); if (tweakableUI == null) { @@ -190,7 +192,7 @@ private void assignResourcesToPart(bool calledByPlayer) } else { - Debug.Log("no UI to refresh"); + Debug.Log("[BDArmory.ModuleAmmoSwitch]: no UI to refresh"); } } @@ -211,7 +213,7 @@ private void setupTankInPart(Part currentPart, bool calledByPlayer) { if (tankList[tankCount].resources[resourceCount].name != "Structural") { - //Debug.Log("new node: " + tankList[i].resources[j].name); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: new node: " + tankList[i].resources[j].name); ConfigNode newResourceNode = new ConfigNode("RESOURCE"); newResourceNode.AddValue("name", tankList[tankCount].resources[resourceCount].name); newResourceNode.AddValue("maxAmount", tankList[tankCount].resources[resourceCount].maxAmount); @@ -224,12 +226,12 @@ private void setupTankInPart(Part currentPart, bool calledByPlayer) newResourceNode.AddValue("amount", tankList[tankCount].resources[resourceCount].amount); } - //Debug.Log("add node to part"); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: add node to part"); currentPart.AddResource(newResourceNode); } else { - //Debug.Log("Skipping structural fuel type"); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: Skipping structural fuel type"); } } } @@ -290,7 +292,7 @@ private void setupTankList(bool calledByPlayer) { initialResourceTankArray = resourceTankArray; } - //Debug.Log("FSDEBUGRES: " + resourceTankArray.Length+" "+resourceAmounts); + //Debug.Log("[BDArmory.ModuleAmmoSwitch]: FSDEBUGRES: " + resourceTankArray.Length+" "+resourceAmounts); for (int tankCount = 0; tankCount < resourceTankArray.Length; tankCount++) { resourceList.Add(new List()); @@ -309,9 +311,9 @@ private void setupTankList(bool calledByPlayer) resourceList[tankCount].Add(double.Parse(resourceAmountArray[amountCount].Trim())); initialResourceList[tankCount].Add(double.Parse(initialResourceAmountArray[amountCount].Trim())); } - catch + catch (Exception e) { - Debug.Log("BDAcAmmoSwitch: error parsing resource amount " + tankCount + "/" + amountCount + ": '" + resourceTankArray[amountCount] + "': '" + resourceAmountArray[amountCount].Trim() + "'"); + Debug.Log("[BDArmory.ModuleAmmoSwitch]: error parsing resource amount " + tankCount + "/" + amountCount + ": '" + resourceTankArray[amountCount] + "': '" + resourceAmountArray[amountCount].Trim() + "'. Exception: " + e.Message); } } } diff --git a/BDArmory/Ammo/ModuleCASE.cs b/BDArmory/Ammo/ModuleCASE.cs new file mode 100644 index 000000000..c86d599c4 --- /dev/null +++ b/BDArmory/Ammo/ModuleCASE.cs @@ -0,0 +1,554 @@ +using System; +using System.Text; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons; +using BDArmory.UI; + +namespace BDArmory.Ammo +{ + class ModuleCASE : PartModule, IPartMassModifier, IPartCostModifier + { + public static Dictionary detSpheres = new Dictionary(); + GameObject visSphere; + Renderer r_sphere; + GameObject visDome; + Renderer r_dome; + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => CASEmass; + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.FIXED; + public float GetModuleCost(float baseCost, ModifierStagingSituation situation) => CASEcost; + public ModifierChangeWhen GetModuleCostChangeWhen() => ModifierChangeWhen.FIXED; + + private double ammoMass = 0; + private double ammoQuantity = 0; + private double ammoExplosionYield = 0; + + private string explModelPath = "BDArmory/Models/explosion/explosion"; + private string explSoundPath = "BDArmory/Sounds/explode1"; + + private string limitEdexploModelPath = "BDArmory/Models/explosion/30mmExplosion"; + private string shuntExploModelPath = "BDArmory/Models/explosion/CASEexplosion"; + private string detDomeModelpath = "BDArmory/Models/explosion/detHemisphere"; + public string SourceVessel = ""; + public bool hasDetonated = false; + private float blastRadius = -1; + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); + + public bool externallyCalled = false; + + public override void OnStart(StartState state) + { + if (HighLogic.LoadedSceneIsFlight) + { + part.explosionPotential = 1.0f; + part.force_activate(); + } + } + [KSPField(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_AddedMass")]//CASE mass + + public float CASEmass = 0f; + + private float CASEcost = 0f; + // private float origCost = 0; + private float origMass = 0f; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_CASE"),//Cellular Ammo Storage Equipment Tier + UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = 1f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float CASELevel = 0; //tier of ammo storage. 0 = nothing, ammosplosion; 1 = base, ammosplosion contained(barely), 2 = blast safely shunted outside, minimal damage to surrounding parts + + [KSPField(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_CASE_Sim"),//Detonation Sim +UI_FloatRange(minValue = 0f, maxValue = 100, stepIncrement = 0.5f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float blastSim = 0; + + [KSPField(isPersistant = true)] + 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) + { + var internalmag = part.FindModuleImplementing(); + if (internalmag != null) + { + Fields["CASELevel"].guiActiveEditor = false; + Fields["CASEmass"].guiActiveEditor = false; + } + else + { + using (IEnumerator resource = part.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resourceAmount.Add(resource.Current.maxAmount); + } + UI_FloatRange ATrangeEditor = (UI_FloatRange)Fields["CASELevel"].uiControlEditor; + ATrangeEditor.onFieldChanged = CASESetup; + origMass = part.mass; + //origScale = part.rescaleFactor; + CASESetup(null, null); + } + + var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); + var collider = sphere.GetComponent(); + if (collider) + { + collider.enabled = false; + Destroy(collider); + } + Renderer r = sphere.GetComponent(); + var shader = Shader.Find("KSP/Alpha/Unlit Transparent"); + r.material = new Material(shader); + r.receiveShadows = false; + r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + r.material.color = new Color(Color.red.r, 0, 0, 0.35f); + r.enabled = true; + sphere.SetActive(false); + detSpheres[0] = ObjectPool.CreateObjectPool(sphere, 10, true, true); + + var dome = GameDatabase.Instance.GetModel(detDomeModelpath); + if (dome == null) + { + Debug.LogError("[BDArmory.ModuleCase]: model '" + detDomeModelpath + "' not found."); + dome = GameObject.CreatePrimitive(PrimitiveType.Sphere); + var dc = dome.GetComponent(); + if (dc) + { + dc.enabled = false; + Destroy(dc); + } + } + Renderer d = dome.GetComponentInChildren(); + if (d != null) + { + d.material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + d.material.SetColor("_TintColor", Color.blue); + } + + dome.SetActive(false); + detSpheres[1] = ObjectPool.CreateObjectPool(dome, 10, true, true); + if (detSpheres[0] != null) + { + visSphere = detSpheres[0].GetPooledObject(); + visSphere.transform.SetPositionAndRotation(transform.position, transform.rotation); + visSphere.transform.localScale = Vector3.zero; + r_sphere = visSphere.GetComponent(); + } + if (detSpheres[1] != null) + { + visDome = detSpheres[1].GetPooledObject(); + visDome.transform.SetPositionAndRotation(transform.position, transform.rotation); + visDome.transform.localScale = Vector3.zero; + r_dome = visDome.GetComponentInChildren(); + } + } + if (HighLogic.LoadedSceneIsFlight) + { + SourceVessel = part.vessel.GetName(); //set default to vesselname for cases where no attacker, i.e. Ammo exploding on destruction cooking off adjacent boxes + GameEvents.onGameSceneSwitchRequested.Add(HandleSceneChange); + } + } + + public void HandleSceneChange(GameEvents.FromToAction fromTo) + { + if (fromTo.from == GameScenes.FLIGHT) + { hasDetonated = true; } // Don't trigger explosions on scene changes. + } + + void CASESetup(BaseField field, object obj) + { + if (externallyCalled) return; + //CASEmass = ((origMass / 2) * CASELevel); + CASEmass = (0.05f * CASELevel); //+50kg per level + //part.mass = CASEmass; + CASEcost = (CASELevel * 1000); + //part.transform.localScale = (Vector3.one * (origScale + (CASELevel/10))); + //Debug.Log("[BDArmory.ModuleCASE] part.mass = " + part.mass + "; CASElevel = " + CASELevel + "; CASEMass = " + CASEmass + "; Scale = " + part.transform.localScale); + + if (Case2 && CASELevel != 2) + { + int i = 0; + using (IEnumerator resource = part.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + //if (resource.Current.maxAmount < 80) //original value < 100, at risk of fractional amount + { + resource.Current.maxAmount = resourceAmount[i]; + } + //else resource.Current.maxAmount = Math.Floor(resource.Current.maxAmount * 1.25); + i++; + } + } + if (!Case2 && CASELevel == 2) + { + using (IEnumerator resource = part.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount *= 0.8; + resource.Current.maxAmount = Math.Floor(resource.Current.maxAmount); + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + + var CASE = pSym.Current.FindModuleImplementing(); + if (CASE == null) continue; + CASE.externallyCalled = true; + CASE.CASELevel = CASELevel; + CASE.CASEmass = CASEmass; + CASE.CASEcost = CASEcost; + + if (CASE.Case2 && CASE.CASELevel != 2) + { + using (IEnumerator resource = pSym.Current.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount = Math.Floor(resource.Current.maxAmount * 1.25); + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + if (!CASE.Case2 && CASE.CASELevel == 2) + { + using (IEnumerator resource = pSym.Current.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount *= 0.8; + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + CASE.Case2 = CASE.CASELevel == 2 ? true : false; + CASE.externallyCalled = false; + GUIUtils.RefreshAssociatedWindows(pSym.Current); + } + Case2 = CASELevel == 2 ? true : false; + GUIUtils.RefreshAssociatedWindows(part); + } + public override void OnLoad(ConfigNode node) + { + base.OnLoad(node); + + if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) return; + var internalmag = part.FindModuleImplementing(); + if (internalmag == null) + { + CASESetup(null, null); //don't apply mass/cost to weapons with integral ammo protection, assume it's baked into weapon mass/cost + } + } + + private List GetResources() + { + List resources = new List(); + + foreach (PartResource resource in part.Resources) + { + if (!resources.Contains(resource)) { resources.Add(resource); } + } + return resources; + } + private void CalculateBlast() + { + ammoMass = 0; + ammoQuantity = 0; + ammoExplosionYield = 0; + blastRadius = 0; + foreach (PartResource resource in GetResources()) + { + var resources = part.Resources.ToList(); + using (IEnumerator ammo = resources.GetEnumerator()) + while (ammo.MoveNext()) + { + if (ammo.Current == null) continue; + if (ammo.Current.resourceName == resource.resourceName) + { + ammoMass = ammo.Current.info.density; + ammoQuantity = ammo.Current.amount; + ammoExplosionYield += (((ammoMass * 1000) * ammoQuantity) / 20); + } + } + } + if (ammoExplosionYield > 0) + { + switch (CASELevel) + { + case 1: + ammoExplosionYield /= 2; + break; + case 2: + ammoExplosionYield /= 4; + break; + default: + break; + } + blastRadius = BlastPhysicsUtils.CalculateBlastRange(ammoExplosionYield * BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE); + } + } + public float GetBlastRadius() + { + //if (blastRadius >= 0 && HighLogic.LoadedSceneIsEditor) return blastRadius; //only calc blast radius once in Editor if F2 weapon alignment/blast visualization enabeld + CalculateBlast(); + return blastRadius; + } + public void DetonateIfPossible() + { + if (hasDetonated || part == null || part.vessel == null || !part.vessel.loaded || part.vessel.packed) return; + hasDetonated = true; // Set hasDetonated here to avoid recursive calls due to ammo boxes exploding each other. + var vesselName = vessel != null ? vessel.vesselName : null; + Vector3 direction = default(Vector3); + GetBlastRadius(); + if (ammoExplosionYield <= 0) return; + if (CASELevel != 2) //a considerable quantity of explosives and propellants just detonated inside your ship + { + if (CASELevel == 0) + { + ExplosionFx.CreateExplosion(part.transform.position, (float)ammoExplosionYield, explModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 120, part, SourceVessel, null, $"{part.partInfo.title} ({Math.Floor(ammoQuantity)} rounds)(CASE-0)", direction, -1, false, part.mass + ((float)ammoExplosionYield * 10f), 1200 * BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE); + 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), limitEdexploModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 60, part, SourceVessel, null, $"{part.partInfo.title} ({Math.Floor(ammoQuantity)} rounds)(CASE-I)", direction, -1, false, part.mass + ((float)ammoExplosionYield * 10f), 600 * BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE); + 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, shuntExploModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 30, part, SourceVessel, null, $"{part.partInfo.title} ({Math.Floor(ammoQuantity)} rounds)(CASE-II)", direction, -1, true); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleCASE]: CASE II explosion, tntMassEquivilent: " + ammoExplosionYield); + Ray BlastRay = new Ray(part.transform.position, part.transform.up); + 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. + { + 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()) + { + RaycastHit hit = hitsEnu.Current; + Part hitPart = null; + KerbalEVA hitEVA = null; + + if (FlightGlobals.currentMainBody == null || hit.collider.gameObject != FlightGlobals.currentMainBody.gameObject) + { + try + { + hitPart = hit.collider.gameObject.GetComponentInParent(); + hitEVA = hit.collider.gameObject.GetComponentUpwards(); + } + catch (NullReferenceException e) + { + Debug.LogError("[BDArmory.ModuleCASE]: NullReferenceException for AmmoExplosion Hit: " + e.Message + "\n" + e.StackTrace); + continue; + } + + if (hitPart == null || hitPart == part) continue; + if (ProjectileUtils.IsIgnoredPart(hitPart)) continue; // Ignore ignored parts. + + + if (hitEVA != null) + { + hitPart = hitEVA.part; + if (hitPart.rb != null) + ApplyDamage(hitPart, hit); + break; + } + + if (hitPart.vessel != part.vessel) + { + 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, explosionLayerMask)) + { + if (FlightGlobals.currentMainBody == null || LOShit.collider.gameObject != FlightGlobals.currentMainBody.gameObject) + { + KerbalEVA eva = LOShit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : LOShit.collider.gameObject.GetComponentInParent(); + if (p == hitPart) + { + ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, 200, (float)ammoExplosionYield, dist, this.part.vessel.GetName(), ExplosionSourceType.BattleDamage, part.mass); + } + } + } + } + else + { + ApplyDamage(hitPart, hit); + } + } + } + } + } + } + if (part.vessel != null) // Already in the process of being destroyed. + part.Destroy(); + } + private void ApplyDamage(Part hitPart, RaycastHit hit) + { + //hitting a vessel Part + //No struts, they cause weird bugs :) -BahamutoD + if (hitPart == null) return; + if (hitPart.partInfo.name.Contains("Strut")) return; + float explDamage; + if (BDArmorySettings.BULLET_HITS) + { + BulletHitFX.CreateBulletHit(hitPart, hit.point, hit, hit.normal, false, 200, 3, null); + } + + explDamage = 100; + explDamage = Mathf.Clamp(explDamage, 0, ((float)ammoExplosionYield * 10)); + explDamage *= BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE; + hitPart.AddDamage(explDamage); + float armorToReduce = hitPart.GetArmorThickness() * 0.25f; + hitPart.ReduceArmor(armorToReduce); + + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleCASE]: " + hitPart.name + " damaged, armor reduced by " + armorToReduce); + + BDACompetitionMode.Instance.Scores.RegisterBattleDamage(SourceVessel, hitPart.vessel, explDamage); + } + + void OnDestroy() + { + if (BDArmorySettings.BATTLEDAMAGE && BDArmorySettings.BD_AMMOBINS && BDArmorySettings.BD_VOLATILE_AMMO && HighLogic.LoadedSceneIsFlight && !VesselSpawnerStatus.vesselsSpawning) + { + if (!hasDetonated) DetonateIfPossible(); + } + GameEvents.onGameSceneSwitchRequested.Remove(HandleSceneChange); + if (visSphere != null) visSphere.SetActive(false); + if (visDome != null) visDome.SetActive(false); + } + + void OnGUI() + { + if (HighLogic.LoadedSceneIsEditor) + { + bool disableCASESimulation = false; + if (BDArmorySettings.BD_AMMOBINS) //having this on showWeaponAlignment could get really annoying if lots of ammo boxes on a craft and merely wanting to calibrate guns + { + if (BDArmorySetup.showCASESimulation || blastSim >= 1) + DrawDetonationVisualization(); //though perhaps a per-box visualizer toggle would be smarter than a global one? + else if (blastTimeline > 0) disableCASESimulation = true; + } + else if (blastTimeline > 0) disableCASESimulation = true; + if (disableCASESimulation) + { + visSphere.SetActive(false); + visDome.SetActive(false); + simStartTime = 0; + } + } + } + + float simStartTime = 0; + float blastTimeline = 0; + float simTimer => Time.time - simStartTime; + Color blastColor = Color.red; + public static FloatCurve blastCurve = new([ + new(0, 1640, -2922.85f, -2922.85f), + new(1, 128, -81.1f, -81.1f), + new(5, 20, 7.24f, 7.24f), + new(10, 10), + new(20, 7), + new(40, 1)]); //'close enough' approximation for the rather more complex geometry of the actual blast dmg equations + + void DrawDetonationVisualization() + { + Vector2 guiPos; + GetBlastRadius(); + if (!BDArmorySetup.showCASESimulation) simStartTime = 0; + else if (simTimer > 5) simStartTime = Time.time; //another possible improvement would have a 'sim blast range' slider that would allow seeing damage at specific range instead of cycling the anim + blastTimeline = BDArmorySetup.showCASESimulation ? Mathf.Clamp01(simTimer / 2) : blastSim / 100; + float blastDmg = Mathf.Clamp(blastCurve.Evaluate(blastRadius * blastTimeline) + (11 - (blastRadius * blastTimeline * 0.4f)) * (float)ammoExplosionYield, 0, 1200) / (CASELevel == 1 ? 2 : 1); //CASE I clamps to 600, so mult CAS 0 dmg to maintian color per x dmg value + blastColor = Color.HSVToRGB((((CASELevel == 1 ? 600 : 1200) - (float)blastDmg) / (CASELevel == 1 ? 600 : 1200)) / 4, 1, 1); //yellow = 200dmg, green, less, orange-> , more + + switch (CASELevel) + { + case 0: + visDome.SetActive(false); + visSphere.SetActive(true); + visSphere.transform.position = transform.position; + visSphere.transform.localScale = Vector3.one * Mathf.Lerp(0, blastRadius, blastTimeline); + r_sphere.material.color = new Color(blastColor.r, blastColor.g, blastColor.b, (1.3f - blastTimeline) / 2); + break; + case 1: + visSphere.SetActive(false); + visDome.SetActive(true); + r_dome.material.SetColor("_TintColor", new Color(blastColor.r, blastColor.g, blastColor.b, 0.15f - (blastTimeline / 10))); + visDome.transform.position = transform.position; + visDome.transform.localScale = Vector3.one * Mathf.Lerp(0, blastRadius, blastTimeline); + visDome.transform.rotation = transform.rotation; + break; + case 2: + Vector3 fwdPos = transform.position + (blastTimeline * blastRadius * transform.up); + GUIUtils.DrawLineBetweenWorldPositions(transform.position, fwdPos, 4, Color.red); + visSphere.SetActive(false); + visDome.SetActive(false); + blastDmg = Mathf.Clamp(blastDmg, 0, 100); + break; + } + if (GUIUtils.WorldToGUIPos(transform.position, out guiPos)) + { + Rect labelRect = new Rect(guiPos.x + 64, guiPos.y + 32, 200, 100); + string label = $"{Mathf.Round(blastDmg)} damage at {Math.Round(blastTimeline * blastRadius, 2)}m"; + GUI.Label(labelRect, label); + } + } + + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + output.Append(Environment.NewLine); + var internalmag = part.FindModuleImplementing(); + if (internalmag != null) + { + output.AppendLine($" Has Intrinsic C.A.S.E. Type {CASELevel}"); + } + else + { + output.AppendLine($"Can add Cellular Ammo Storage Equipment to reduce ammo explosion damage"); + } + + output.AppendLine(""); + + return output.ToString(); + } + 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(); + } + } + } + } + } +} diff --git a/BDArmory/Ammo/PooledBullet.cs b/BDArmory/Ammo/PooledBullet.cs new file mode 100644 index 000000000..fadca59df --- /dev/null +++ b/BDArmory/Ammo/PooledBullet.cs @@ -0,0 +1,2475 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Armor; +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Shaders; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Bullets +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class PooledBulletManager : MonoBehaviour + { + public static PooledBulletManager Instance; + readonly HashSet activeBullets = []; + + public static void AddBullet(PooledBullet bullet) { Instance.activeBullets.Add(bullet); } + public static void RemoveBullet(PooledBullet bullet) { Instance.activeBullets.Remove(bullet); } + + void Awake() + { + if (Instance != null) Destroy(Instance); + Instance = this; + activeBullets.Clear(); + } + + void FixedUpdate() + { + if (activeBullets.Count == 0) return; + var autoSync = Physics.autoSyncTransforms; + try + { + // Perform the various stages that pooled bullets go through in blocks to hopefully reduce physics sync delays. + // Bullets should get deactivated and removed from activeBullets if they die. + var bullets = activeBullets.ToList(); // Pre-convert to a list and skip null bullets. This avoids moving subprojectiles from flak rounds. + foreach (var bullet in bullets) if (bullet != null && bullet.isActiveAndEnabled) bullet.PreCollisions(); + Physics.SyncTransforms(); Physics.autoSyncTransforms = false; // Sync the physics, then prevent any auto-syncing while we run our collision checks. + foreach (var bullet in bullets) if (bullet != null && bullet.isActiveAndEnabled) bullet.DoCollisions(); // All the Physics calls occur here. + Physics.autoSyncTransforms = autoSync; // Re-enable auto-syncing. + foreach (var bullet in bullets) if (bullet != null && bullet.isActiveAndEnabled) bullet.PostCollisions(); + } + catch (Exception e) + { // This shouldn't happen, but if it does, some active bullets may get out of sync. + Debug.LogError($"[BDArmory.PooledBulletManager]: DEBUG {e.Message}\n{e.StackTrace}"); + Physics.autoSyncTransforms = autoSync; + } + } + } + + public class PooledBullet : MonoBehaviour + { + #region Declarations + + 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 + { + Slug, + Explosive, + Shaped + } + public enum BulletFuzeTypes + { + None, + Impact, + Timed, + Proximity, + Flak, + Delay, + Penetrating + } + public enum BulletDragTypes + { + None, + AnalyticEstimate, + NumericalIntegration + } + + //public PooledBulletTypes bulletType; + public BulletFuzeTypes fuzeType; + public float fuzeDelay = -1f; + public float fuzeSensitivity = -1f; + public PooledBulletTypes HEType; + public BulletDragTypes dragType; + + public Vessel sourceVessel; + public string sourceVesselName; + public Part sourceWeapon; + public string team; + public Color lightColor = GUIUtils.ParseColor255("255, 235, 145, 255"); + public Color projectileColor; + public string bulletTexturePath; + public string smokeTexturePath; + public bool fadeColor; + Color smokeColor = Color.white; + public Color startColor; + Color currentColor; + public bool bulletDrop = true; + public float tracerStartWidth = 1; + public float tracerEndWidth = 1; + public float tracerLength = 0; + public float tracerDeltaFactor = 1.35f; + public float tracerLuminance = 1; + public Vector3 currentPosition { get { return _currentPosition; } set { _currentPosition = value; transform.position = value; } } // Local alias for transform.position speeding up access by around 100x. + Vector3 _currentPosition = default; + public Vector3 previousPosition { get; private set; } // Previous position, adjusted for the current Krakensbane. (Used for APS targeting.) + + //explosive parameters + public float radius = 30; + public float tntMass = 0; + public float blastPower = 8; + public float blastHeat = -1; + public float bulletDmgMult = 1; + public string explModelPath; + public string explSoundPath; + + //general params + public bool incendiary; + public float apBulletMod = 0; + public bool nuclear = false; + public string flashModelPath; + public string shockModelPath; + public string blastModelPath; + public string plumeModelPath; + public string debrisModelPath; + public string blastSoundPath; + //public bool homing = false; + public bool beehive = false; + public string subMunitionType; + public bool EMP = false; + + //gravitic parameters + public float impulse = 0; + public float massMod = 0; + + //mutator Param + public bool stealResources; + public float dmgMult = 1; + + Vector3 startPosition; + public float detonationRange = 5f; + public float timeToDetonation; + float armingTime; + float randomWidthScale = 1; + LineRenderer[] bulletTrail; + public float timeAlive = 0; + public float timeToLiveUntil; + Light lightFlash; + bool wasInitiated; + public Vector3 currentVelocity; // Current real velocity w/o offloading + public float bulletMass; + public float caliber = 1; + public float bulletVelocity; //muzzle velocity + public float guidanceDPS = 0; + public float guidanceRange = -1f; + public bool sabot = false; + private float HERatio = 0.06f; + public float ballisticCoefficient; + float currentSpeed; // Current speed of the bullet, for drag purposes. + public float timeElapsedSinceCurrentSpeedWasAdjusted; // Time since the current speed was adjusted, to allow tracking speed changes of the bullet in air and water. + bool underwater = false; + bool startsUnderwater = false; + public static Shader bulletShader; + public static bool shaderInitialized; + private float impactSpeed; + private float dragVelocityFactor; + + public bool hasPenetrated = false; + public bool hasDetonated = false; + public bool hasRicocheted = false; + public bool fuzeTriggered = false; + private Part CurrentPart = null; + private bool previousWasReverseHit = false; + + public bool isAPSprojectile = false; + public bool isSubProjectile = false; + public PooledRocket tgtRocket = null; + public PooledBullet tgtShell = null; + + public Vessel targetVessel; + float atmosphereDensity; + public int penTicker = 0; + + Ray bulletRay; + + #endregion Declarations + + static RaycastHit[] hits; + static RaycastHit[] reverseHits; + static BulletHit[] orderedHits; + static Collider[] overlapSphereColliders; + static List allHits; + static Dictionary rayLength; + static List vesselsInRange; + private Vector3[] linePositions = new Vector3[2]; + private Vector3[] smokePositions = new Vector3[5]; + + private List partsHit = new List(); + + private double distanceTraveled = 0; + private double distanceLastHit = double.PositiveInfinity; + private bool hypervelocityImpact = false; + private float deltaMass = -1f; + private float relaxationTime = 0f; + private double initialHitDistance = 0; + private float kDist = 1; + private float iTime = 0; // Consistent naming with ModuleWeapon: TimeWarp.fixedDeltaTime - timeToCPA of proxy detonation. + private Coroutine frameDelayedRoutine = null; + + public double DistanceTraveled { get { return distanceTraveled; } set { distanceTraveled = value; } } + + void Awake() + { + hits ??= new RaycastHit[100]; + reverseHits ??= new RaycastHit[100]; + orderedHits ??= new BulletHit[200]; + FillOrderedHits(orderedHits); + overlapSphereColliders ??= new Collider[1000]; + allHits ??= []; + vesselsInRange ??= []; + rayLength ??= []; + } + + void OnEnable() + { + currentPosition = transform.position; // In case something sets transform.position instead of currentPosition. + previousPosition = currentPosition; + startPosition = currentPosition; + currentSpeed = currentVelocity.magnitude; // this is the velocity used for drag estimations (only), use total velocity, not muzzle velocity + timeAlive = 0; + armingTime = isSubProjectile ? 0 : 1.5f * ((beehive ? BlastPhysicsUtils.CalculateBlastRange(tntMass) : detonationRange) / bulletVelocity); //beehive rounds have artifically large detDists; only need explosive radius arming check + fuzeTriggered = false; + if (HEType != PooledBulletTypes.Slug) + { + HERatio = Mathf.Clamp(tntMass / (bulletMass < tntMass ? tntMass * 1.25f : bulletMass), 0.01f, 0.95f); + } + else + { + HERatio = 0; + } + bool hasNuclearSubMunitions = false; + if (beehive) + { + try + { + string projType = subMunitionType.Split([';'])[0]; + if (BulletInfo.bulletNames.Contains(projType)) + hasNuclearSubMunitions = BulletInfo.bullets[projType].nuclear; + } + catch { } // Ignored, non-nuclear submunitions. + } + if (nuclear && !isSubProjectile || hasNuclearSubMunitions) // Sub-projectiles get these set from the parent shell during beehive detonation. + { + 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; + } + } + 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. + hypervelocityImpact = false; + initialHitDistance = 0; + deltaMass = -1f; + kDist = 1; + dragVelocityFactor = 1; + relaxationTime = 0.001f * 30f * (0.5f * caliber) / (sabot ? 3850f : 4500f); + + startsUnderwater = FlightGlobals.currentMainBody.ocean && FlightGlobals.getAltitudeAtPos(currentPosition) < 0; + underwater = startsUnderwater; + + projectileColor.a = Mathf.Clamp(projectileColor.a, 0.25f, 1f); + startColor.a = Mathf.Clamp(startColor.a, 0.25f, 1f); + currentColor = projectileColor; + if (fadeColor) + { + currentColor = startColor; + } + + if (lightFlash == null || !gameObject.GetComponent()) + { + lightFlash = gameObject.AddOrGetComponent(); + lightFlash.type = LightType.Point; + lightFlash.range = 8; + lightFlash.intensity = 1; + lightFlash.color = lightColor; + lightFlash.enabled = true; + } + + //tracer setup + if (bulletTrail == null || !gameObject.GetComponent()) + { + bulletTrail = new LineRenderer[2]; + bulletTrail[0] = gameObject.AddOrGetComponent(); + + GameObject bulletFX = new GameObject("bulletTrail"); + bulletFX.transform.SetParent(gameObject.transform); + bulletTrail[1] = bulletFX.AddOrGetComponent(); + } + + if (!shaderInitialized) + { + shaderInitialized = true; + bulletShader = BDAShaderLoader.BulletShader; + } + + // Note: call SetTracerPosition() after enabling the bullet and making adjustments to it's position. + if (!wasInitiated) + { + bulletTrail[0].positionCount = linePositions.Length; + bulletTrail[1].positionCount = smokePositions.Length; + bulletTrail[0].material = new Material(bulletShader); + bulletTrail[1].material = new Material(bulletShader); + randomWidthScale = UnityEngine.Random.Range(0.5f, 1f); + gameObject.layer = 15; + } + smokeColor.r = 0.85f; + smokeColor.g = 0.85f; + smokeColor.b = 0.85f; + smokeColor.a = 0.75f; + bulletTrail[0].material.mainTexture = GameDatabase.Instance.GetTexture(bulletTexturePath, false); + bulletTrail[0].material.SetColor("_TintColor", currentColor); + bulletTrail[0].material.SetFloat("_Lum", tracerLuminance > 0 ? tracerLuminance : 0.5f); + if (!string.IsNullOrEmpty(smokeTexturePath)) + { + bulletTrail[1].material.mainTexture = GameDatabase.Instance.GetTexture(smokeTexturePath, false); + bulletTrail[1].material.SetColor("_TintColor", smokeColor); + bulletTrail[1].material.SetFloat("_Lum", 0.5f); + bulletTrail[1].textureMode = LineTextureMode.Tile; + bulletTrail[1].material.SetTextureScale("_MainTex", new Vector2(0.1f, 1)); + bulletTrail[1].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + bulletTrail[1].receiveShadows = false; + bulletTrail[1].enabled = true; + } + else + { + bulletTrail[1].enabled = false; + } + + tracerStartWidth *= 2f; + tracerEndWidth *= 2f; + + //leftPenetration = 1; + penTicker = 0; + wasInitiated = true; + frameDelayedRoutine = StartCoroutine(FrameDelayedRoutine()); + + // Log shots fired. + if (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. + } + else + { + sourceVesselName = null; + } + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) + { + BDATargetManager.FiredBullets.Add(this); + } + PooledBulletManager.AddBullet(this); + } + + void OnDisable() + { + PooledBulletManager.RemoveBullet(this); + sourceVessel = null; + sourceWeapon = null; + CurrentPart = null; + previousWasReverseHit = false; + sabot = false; + partsHit.Clear(); + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) + { + BDATargetManager.FiredBullets.Remove(this); + } + isAPSprojectile = false; + tgtRocket = null; + tgtShell = null; + smokeTexturePath = ""; + } + + void OnDestroy() + { + StopCoroutine(frameDelayedRoutine); + } + + IEnumerator FrameDelayedRoutine() + { + yield return new WaitForFixedUpdate(); + lightFlash.enabled = false; + } + + void OnWillRenderObject() + { + if (!gameObject.activeInHierarchy) + { + return; + } + Camera currentCam = Camera.current; + if (TargetingCamera.IsTGPCamera(currentCam)) + { + UpdateWidth(currentCam, 4); + } + else + { + UpdateWidth(currentCam, 1); + } + } + + void Update() + { + if (!gameObject.activeInHierarchy) return; + + if (fadeColor) + { + FadeColor(); + bulletTrail[0].material.SetColor("_TintColor", currentColor * (tracerLuminance > 0 ? tracerLuminance : 0.5f)); + } + if (tracerLuminance > 1 && bulletTrail[1].enabled) + { + float fade = Mathf.Lerp(0.75f, 0.05f, 0.07f); + smokeColor.a = fade; + bulletTrail[1].material.SetColor("_TintColor", smokeColor); + bulletTrail[1].material.SetTextureOffset("_MainTex", new Vector2(-timeAlive / 3, 0)); + if (fade <= 0.05f) bulletTrail[1].enabled = false; + } + SetTracerPosition(); + } + + /// + /// These functions replace FixedUpdate and are called by PooledBulletManager to try to group all the Physics calls together to reduce the need for physics sync calls. + /// It seems to be slightly faster for large numbers of bullets. + /// + public void PreCollisions() + { + //floating origin and velocity offloading corrections + if (BDKrakensbane.IsActive) + { + currentPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + startPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + previousPosition = currentPosition; + timeAlive += TimeWarp.fixedDeltaTime; + + if (Time.time > timeToLiveUntil) //kill bullet when TTL ends + { + if (isAPSprojectile) + { + if (HEType != PooledBulletTypes.Explosive && tntMass > 0) + ExplosionFx.CreateExplosion(currentPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, default, -1, true, sourceVelocity: currentVelocity); + } + KillBullet(); + return; + } + /* + if (fuzeTriggered) + { + if (!hasDetonated) + { + ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, false, bulletMass, -1, dmgMult); + hasDetonated = true; + KillBullet(); + return; + } + } + */ + + if (ProximityAirDetonation(true)) // Pre-move proximity detonation check. + { + //detonate + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currentPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, HEType == PooledBulletTypes.Explosive ? default : currentVelocity, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? ExplosionFx.WarheadTypes.ShapedCharge : ExplosionFx.WarheadTypes.Standard, null, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1, sourceVelocity: currentVelocity, bulletHitRegistered: false); + if (nuclear) + NukeFX.CreateExplosion(currentPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", sourceVelocity: currentVelocity, bulletHitRegistered: false); + if (beehive) + BeehiveDetonation(); + hasDetonated = true; + KillBullet(); + return; + } + } + public void DoCollisions() + { + CheckBulletCollisions(TimeWarp.fixedDeltaTime); + } + public void PostCollisions() + { + if (!hasRicocheted) MoveBullet(TimeWarp.fixedDeltaTime); // Ricochets perform movement internally. + + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if (startsUnderwater && !underwater) // Bullets that start underwater can exit the water if fired close enough to the surface. + { + startsUnderwater = false; + } + if (!startsUnderwater && underwater) // Bullets entering water from air either disintegrate or don't penetrate far enough to bother about. Except large caliber naval shells. + { + if (caliber < 75f) + { + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currentPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, default, -1, false, bulletMass, -1, dmgMult); + if (nuclear) + NukeFX.CreateExplosion(currentPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + hasDetonated = true; + + KillBullet(); + return; + } + else + { + if (HEType != PooledBulletTypes.Slug) + { + if (fuzeType == BulletFuzeTypes.Delay || fuzeType == BulletFuzeTypes.Penetrating) + { + fuzeTriggered = true; + delayedDetonationRoutine = StartCoroutine(DelayedDetonationRoutine()); + } + else //if (fuzeType != BulletFuzeTypes.None) + { + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currentPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, default, -1, false, bulletMass, -1, dmgMult); + if (nuclear) + NukeFX.CreateExplosion(currentPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + hasDetonated = true; + if (BDArmorySettings.waterHitEffect && FlightGlobals.currentMainBody.ocean) FXMonger.Splash(currentPosition, caliber / 2); + KillBullet(); + return; + } + } + } + } + } + ////////////////////////////////////////////////// + //Flak Explosion (air detonation/proximity fuse) + ////////////////////////////////////////////////// + + if (ProximityAirDetonation(false)) // Post-move proximity (end-of-life) detonation check + { + //detonate + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currentPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, HEType == PooledBulletTypes.Explosive ? default : currentVelocity, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? ExplosionFx.WarheadTypes.ShapedCharge : ExplosionFx.WarheadTypes.Standard, null, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1, sourceVelocity: currentVelocity, bulletHitRegistered: false); + if (nuclear) + NukeFX.CreateExplosion(currentPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", sourceVelocity: currentVelocity, bulletHitRegistered: false); + if (beehive) + BeehiveDetonation(); + hasDetonated = true; + KillBullet(); + return; + } + } + + /// + /// Move the bullet for the period of time, tracking distance traveled and accounting for drag and gravity. + /// This is now done using the second order symplectic leapfrog method. + /// Note: water drag on bullets breaks the symplectic nature of the integrator (since it's modifying the Hamiltonian), which isn't accounted for during aiming. + /// + /// Period to consider, typically TimeWarp.fixedDeltaTime + public void MoveBullet(float period) + { + atmosphereDensity = (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currentPosition), FlightGlobals.getExternalTemperature(currentPosition)); + // Initial half-timestep velocity change (leapfrog integrator) + LeapfrogVelocityHalfStep(0.5f * period); + + if (targetVessel != null && atmosphereDensity > 0.05f && guidanceDPS > 0) + { + Vector3 targetVec = targetVessel.CoM - currentPosition; + + if (penTicker == 0 && Vector3.Dot(targetVec, currentVelocity) > 0 && (guidanceRange < 0 || targetVec.sqrMagnitude < guidanceRange * guidanceRange)) //don't circle around if it misses, or after it hits something + { + Vector3 leadTargetOffset = targetVessel.CoM + Vector3.Distance(targetVessel.CoM, currentPosition) / bulletVelocity * targetVessel.Velocity(); + //if (VectorUtils.Angle(currentVelocity, leadTargetOffset) > 1) currentVelocity *= 2f * ballisticCoefficient / (TimeWarp.fixedDeltaTime * currentVelocity.magnitude * atmosphereDensity + 2f * ballisticCoefficient); needs bulletdrop gravity accel factored in as well + //apply some drag to projectile if it's turning. Will mess up initial CPA aim calculations, true; on the other hand, its a guided homing bullet. + currentVelocity = Vector3.RotateTowards(currentVelocity, leadTargetOffset - currentPosition, period * guidanceDPS * atmosphereDensity * Mathf.Deg2Rad, 0); //adapt to rockets for homing rockets? + } + } + + // Full-timestep position change (leapfrog integrator) + currentPosition += period * currentVelocity; //move bullet + distanceTraveled += period * currentVelocity.magnitude; // calculate flight distance for achievement purposes + + if (!underwater && FlightGlobals.currentMainBody.ocean && FlightGlobals.getAltitudeAtPos(currentPosition) <= 0) // Check if the bullet is now underwater. + { + float hitAngle = VectorUtils.Angle(GetDragAdjustedVelocity(), -VectorUtils.GetUpDirection(currentPosition)); + if (RicochetScenery(hitAngle)) + { + tracerStartWidth /= 2; + tracerEndWidth /= 2; + + currentVelocity = Vector3.Reflect(currentVelocity, VectorUtils.GetUpDirection(currentPosition)); + currentVelocity = (hitAngle / 150) * 0.65f * currentVelocity; + + Vector3 randomDirection = UnityEngine.Random.rotation * Vector3.one; + + currentVelocity = Vector3.RotateTowards(currentVelocity, randomDirection, + UnityEngine.Random.Range(0f, 5f) * Mathf.Deg2Rad, 0); + } + else + { + underwater = true; + } + if (BDArmorySettings.waterHitEffect && FlightGlobals.currentMainBody.ocean) FXMonger.Splash(currentPosition, caliber / 2); + } + // Second half-timestep velocity change (leapfrog integrator) (should be identical code-wise to the initial half-step) + LeapfrogVelocityHalfStep(0.5f * period); + } + + private void LeapfrogVelocityHalfStep(float period) + { + timeElapsedSinceCurrentSpeedWasAdjusted += period; // Track flight time for drag purposes + if (bulletDrop) + currentVelocity += period * FlightGlobals.getGeeForceAtPosition(currentPosition); + if (underwater) + { + UpdateDragEstimate(); //update the drag estimate, accounting for water/air environment changes. Note: changes due to bulletDrop aren't being applied to the drop. + currentVelocity *= dragVelocityFactor; // Note: If applied to aerial flight, this screws up targeting, because the weapon's aim code doesn't know how to account for drag. Only have it apply when underwater for now. Review later? + currentSpeed = currentVelocity.magnitude; + timeElapsedSinceCurrentSpeedWasAdjusted = 0; + } + } + + /// + /// Get the current velocity, adjusted for drag if necessary. + /// + /// + Vector3 GetDragAdjustedVelocity() + { + UpdateDragEstimate(); + if (timeElapsedSinceCurrentSpeedWasAdjusted > 0) + { + return currentVelocity * dragVelocityFactor; + } + return currentVelocity; + } + + public bool CheckBulletCollisions(float period) + { + //reset our hit variables to default state + hasPenetrated = true; + hasDetonated = false; + hasRicocheted = false; + //CurrentPart = null; //this needs to persist, at least while the bullet is Enabled + //penTicker = 0; + + if (BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS) + { + allHits.Clear(); + rayLength.Clear(); + CheckBulletCollisionWithVessels(period); + CheckBulletCollisionWithScenery(period); + using var hitsEnu = allHits.OrderBy(x => x.hit.distance).GetEnumerator(); // Check all hits in order of distance. + while (hitsEnu.MoveNext()) if (BulletHitAnalysis(hitsEnu.Current, period)) return true; + return false; + } + else + return CheckBulletCollision(period); + } + + /// + /// This performs checks using the relative velocity to each vessel within the range of the movement of the bullet. + /// This is particularly relevant at high velocities (e.g., in orbit) where the sideways velocity of co-moving objects causes a complete miss. + /// + /// + /// + public void CheckBulletCollisionWithVessels(float period) + { + if (!BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS) return; + + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels); + var overlapSphereRadius = GetOverlapSphereRadius(period); // OverlapSphere of sufficient size to catch all potential craft of <100m radius. + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(currentPosition, overlapSphereRadius, overlapSphereColliders, layerMask); + if (overlapSphereColliderCount == overlapSphereColliders.Length) + { + overlapSphereColliders = Physics.OverlapSphere(currentPosition, overlapSphereRadius, layerMask); + overlapSphereColliderCount = overlapSphereColliders.Length; + } + + vesselsInRange.Clear(); + using (var hitsEnu = overlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) + { + while (hitsEnu.MoveNext()) + { + if (hitsEnu.Current == null) continue; + try + { + Part partHit = hitsEnu.Current.GetComponentInParent(); + if (partHit == null) continue; + if (partHit.vessel == sourceVessel) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.vessel != null && !vesselsInRange.Contains(partHit.vessel)) vesselsInRange.Add(partHit.vessel); + } + catch (Exception e) // ignored + { + Debug.LogWarning("[BDArmory.PooledBullet]: Exception thrown in CheckBulletCollisionWithVessels: " + e.Message + "\n" + e.StackTrace); + } + } + } + foreach (var vessel in vesselsInRange.OrderBy(v => (v.CoM - currentPosition).sqrMagnitude)) + { + CheckBulletCollisionWithVessel(period, vessel); + } + } + + /// + /// Calculate the required radius of the overlap sphere such that a craft <100m in radius could potentially collide with the bullet. + /// + /// The period of motion (TimeWarp.fixedDeltaTime). + /// The required radius. + float GetOverlapSphereRadius(float period) + { + float maxRelSpeedSqr = 0, relVelSqr; + Vector3 relativeVelocity; + using var v = FlightGlobals.Vessels.GetEnumerator(); + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded) continue; // Ignore invalid craft. + relativeVelocity = v.Current.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity; + if (Vector3.Dot(relativeVelocity, v.Current.CoM - currentPosition) >= 0) continue; // Ignore craft that aren't approaching. + relVelSqr = relativeVelocity.sqrMagnitude; + if (relVelSqr > maxRelSpeedSqr) maxRelSpeedSqr = relVelSqr; + } + return 100f + period * BDAMath.Sqrt(maxRelSpeedSqr); // Craft of radius <100m that could collide within the period. + } + + public void CheckBulletCollisionWithVessel(float period, Vessel vessel) + { + var relativeVelocity = currentVelocity - (Vector3)vessel.Velocity(); + float dist = period * relativeVelocity.magnitude; + bulletRay = new Ray(currentPosition, relativeVelocity + 0.5f * period * FlightGlobals.getGeeForceAtPosition(currentPosition)); + 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. + { + hits = Physics.RaycastAll(bulletRay, dist, layerMask); + hitCount = hits.Length; + } + + 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(reverseRay, dist, layerMask); + reverseHitCount = reverseHits.Length; + } + for (int i = 0; i < reverseHitCount; ++i) + { + reverseHits[i].distance = dist - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } + + if (hitCount + reverseHitCount > 0) + { + bool hitFound = false; + Part hitPart; + using (var hit = hits.Take(hitCount).AsEnumerable().GetEnumerator()) + while (hit.MoveNext()) + { + hitPart = hit.Current.collider.gameObject.GetComponentInParent(); + if (hitPart == null) continue; + if (hitPart.vessel == vessel) allHits.Add(new BulletHit { hit = hit.Current }); + hitFound = true; + } + using (var hit = reverseHits.Take(reverseHitCount).AsEnumerable().GetEnumerator()) + while (hit.MoveNext()) + { + hitPart = hit.Current.collider.gameObject.GetComponentInParent(); + if (hitPart == null) continue; + if (hitPart.vessel == vessel) allHits.Add(new BulletHit { hit = hit.Current, isReverseHit = true }); + hitFound = true; + } + if (hitFound) rayLength[vessel] = dist; + } + } + + public void CheckBulletCollisionWithScenery(float period) + { + float dist = period * currentVelocity.magnitude; + bulletRay = new Ray(currentPosition, currentVelocity + 0.5f * period * FlightGlobals.getGeeForceAtPosition(currentPosition)); + var layerMask = (int)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. + { + hits = Physics.RaycastAll(bulletRay, dist, layerMask); + hitCount = hits.Length; + } + allHits.AddRange(hits.Take(hitCount).Select(hit => new BulletHit { hit = hit })); + //allHits.AddRange(hits.Take(hitCount)); + /* + 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(reverseRay, dist, layerMask); + reverseHitCount = reverseHits.Length; + } + for (int i = 0; i < reverseHitCount; ++i) + { + reverseHits[i].distance = dist - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } + allHits.AddRange(reverseHits.Take(reverseHitCount)); + */ + } + + /// + /// Check for bullet collision in the upcoming period. + /// This also performs a raycast in reverse to detect collisions from rays starting within an object. + /// + /// Period to consider, typically TimeWarp.fixedDeltaTime + /// true if a collision is detected, false otherwise. + public bool CheckBulletCollision(float period) + { + float dist = period * currentVelocity.magnitude; + bulletRay = new Ray(currentPosition, currentVelocity + 0.5f * period * FlightGlobals.getGeeForceAtPosition(currentPosition)); + 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. + { + hits = Physics.RaycastAll(bulletRay, dist, layerMask); + hitCount = hits.Length; + } + + 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(reverseRay, dist, layerMask); + reverseHitCount = reverseHits.Length; + } + for (int i = 0; i < reverseHitCount; ++i) + { + 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; + int totalHits = CollateHits(ref hits, hitCount, ref reverseHits, reverseHitCount); + for (int i = 0; i < totalHits; ++i) + if (BulletHitAnalysis(orderedHits[i], period)) return true; + } + return false; + } + + int CollateHits(ref RaycastHit[] forwardHits, int forwardHitCount, ref RaycastHit[] reverseHits, int reverseHitCount) + { + var totalHitCount = forwardHitCount + reverseHitCount; + if (orderedHits.Length < totalHitCount) + { + Array.Resize(ref orderedHits, totalHitCount); + FillOrderedHits(orderedHits); + } + for (int i = 0; i < forwardHitCount; ++i) + { + orderedHits[i].hit = forwardHits[i]; + orderedHits[i].isReverseHit = false; + } + for (int i = 0; i < reverseHitCount; ++i) + { + orderedHits[i + forwardHitCount].hit = reverseHits[i]; + orderedHits[i + forwardHitCount].isReverseHit = true; + } + Array.Sort(orderedHits, 0, totalHitCount, BulletHitComparer.bulletHitComparer); // This generates garbage, but less than other methods using Linq or Lists. + return totalHitCount; + } + + void FillOrderedHits(BulletHit[] array) + { + for (int i = 0; i < array.Length; ++i) + array[i] = new BulletHit(); + } + + /// + /// Internals of the bullet collision hits loop in CheckBulletCollision so it can also be called from CheckBulletCollisionWithVessel. + /// + /// The raycast hit. + /// Whether the hit is a vessel hit or not. + /// The distance the bullet moved in the current reference frame. + /// The period the bullet moved for. + /// true if the bullet hits and dies, false otherwise. + bool BulletHitAnalysis(BulletHit bulletHit, float period) + { + if (!hasPenetrated || hasRicocheted || hasDetonated) + { + return true; + } + Part hitPart; + KerbalEVA hitEVA; + try + { + hitPart = bulletHit.hit.collider.gameObject.GetComponentInParent(); + hitEVA = bulletHit.hit.collider.gameObject.GetComponentUpwards(); + } + catch (NullReferenceException e) + { + Debug.Log("[BDArmory.PooledBullet]:NullReferenceException for Ballistic Hit: " + e.Message); + return true; + } + + if (hitPart != null) + { + if (ProjectileUtils.IsIgnoredPart(hitPart)) return false; // Ignore ignored parts. + if (hitPart == sourceWeapon) return false; // Ignore weapon that fired the bullet. + if (bulletHit.isReverseHit && ProjectileUtils.IsArmorPart(hitPart)) + { + CurrentPart = hitPart; + previousWasReverseHit = bulletHit.isReverseHit; + return false; //only have bullet hit armor panels once - no back armor to hit if penetration + } + if (CurrentPart && CurrentPart.persistentId == hitPart.persistentId && (bulletHit.isReverseHit == previousWasReverseHit)) + return false; // If we're dealing with a part that has more than 1 collider, and we're hitting different + // colliders while still going in the same direction, then ignore the part. Entry wound must + // match with an exit wound. This doesn't catch cases of this where there's an intervening part + } + + previousWasReverseHit = bulletHit.isReverseHit; + 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 + BDKrakensbane.FrameVelocityV3f)).magnitude; + else + impactSpeed = GetDragAdjustedVelocity().magnitude; + distanceTraveled += bulletHit.hit.distance; + if (dmgMult < 0) + { + hitPart.AddInstagibDamage(); + } + else + { + ProjectileUtils.ApplyDamage(hitPart, bulletHit.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, bulletHit.hit, bulletRay); + ResourceUtils.StealResources(hitPart, sourceVessel, stealResources); + if (BDArmorySettings.KERBAL_ERA) + { + KillBullet(); // Kerbals are too thick-headed for penetration... + } + return BDArmorySettings.KERBAL_ERA; + } + + if (hitPart != null && hitPart.vessel == sourceVessel) return false; //avoid autohit; + + Vector3 impactVelocity = GetDragAdjustedVelocity(); + Vector3 hitPartVelocity = (hitPart != null && hitPart.rb != null) ? hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f : Vector3.zero; + impactVelocity -= hitPartVelocity; + + 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 (!double.IsPositiveInfinity(distanceLastHit)) + { + if (impactSpeed > 2500 || hypervelocityImpact) + { + // 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 + bulletHit.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 + bulletHit.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 + bulletHit.hit.distance - distanceLastHit) + " m."); + } + } + } + } + else if (deltaMass > 0f) + { + float factor = (bulletMass - deltaMass * Mathf.Clamp01((float)(distanceTraveled + bulletHit.hit.distance - distanceLastHit) / (currentSpeed * dragVelocityFactor * relaxationTime))) / bulletMass; + factor = Mathf.Max(factor, (1.1f * caliber - 10f) / (length - 10f)); + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.PooledBullet] factor: {factor}. Distance Since Last Hit: {distanceTraveled + bulletHit.hit.distance - distanceLastHit} m. Unadjusted Mass: {bulletMass} kg. Adjusted Mass: {bulletMass * factor} kg. deltaMass: {deltaMass}. relaxationTime: {relaxationTime}s. currentSpeed: {currentSpeed * dragVelocityFactor}."); + bulletMass *= factor; + length = (length - 10) * factor + 10; + deltaMass = -1f; + } + } + else + { + distanceLastHit = distanceTraveled + bulletHit.hit.distance; + } + + float hitAngle = VectorUtils.Angle(impactVelocity, -bulletHit.hit.normal); + float dist = hitPart != null && hitPart.vessel != null && rayLength.ContainsKey(hitPart.vessel) ? rayLength[hitPart.vessel] : currentVelocity.magnitude * period; + + if (ProjectileUtils.CheckGroundHit(hitPart, bulletHit.hit, caliber)) + { + if (!BDArmorySettings.PAINTBALL_MODE) ProjectileUtils.CheckBuildingHit(bulletHit.hit, bulletMass, impactVelocity, bulletDmgMult); + if (!RicochetScenery(hitAngle)) + { + ExplosiveDetonation(hitPart, bulletHit.hit, bulletRay); + KillBullet(); + distanceTraveled += bulletHit.hit.distance; + return true; + } + else + { + if (fuzeType == BulletFuzeTypes.Impact) + { + ExplosiveDetonation(hitPart, bulletHit.hit, bulletRay); + } + DoRicochet(hitPart, bulletHit.hit, hitAngle, bulletHit.hit.distance / dist, period); + return true; + } + } + if (hitPart == null) return false; // Hits below here are part hits. + + //Standard Pipeline Hitpoints, Armor and Explosives + //impactSpeed = impactVelocity.magnitude; //Moved up for the projectile weardown calculation + if (initialHitDistance == 0) initialHitDistance = distanceTraveled + bulletHit.hit.distance; + if (massMod != 0) + { + var ME = hitPart.FindModuleImplementing(); + if (ME == null) + { + ME = (ModuleMassAdjust)hitPart.AddModule("ModuleMassAdjust"); + } + ME.massMod += massMod; + ME.duration += BDArmorySettings.WEAPON_FX_DURATION; + } + if (EMP && !VesselModuleRegistry.IgnoredVesselTypes.Contains(hitPart.vesselType)) + { + var emp = hitPart.vessel.rootPart.FindModuleImplementing(); + if (emp == null) + { + emp = (ModuleDrainEC)hitPart.vessel.rootPart.AddModule("ModuleDrainEC"); + var MB = hitPart.vessel.rootPart.FindModuleImplementing(); + if (MB != null) emp.isMissile = true; + } + emp.incomingDamage += (caliber * Mathf.Clamp(bulletMass - tntMass, 0.1f, 101)) * BDArmorySettings.DMG_MULTIPLIER; //soft EMP caps at 100; can always add a EMP amount value to bulletcfg later, but this should work for now + emp.softEMP = true; + } + if (impulse != 0 && hitPart.rb != null) + { + distanceTraveled += bulletHit.hit.distance; + if (!BDArmorySettings.PAINTBALL_MODE) + { hitPart.rb.AddForceAtPosition(impactVelocity.normalized * impulse, bulletHit.hit.point, ForceMode.Impulse); } + // comment this out and allow impulse rounds to do damage? + ProjectileUtils.ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, 0, bullet.DisplayName, ExplosionSourceType.Bullet, true); + if (BDArmorySettings.BULLET_HITS) + { + BulletHitFX.CreateBulletHit(hitPart, bulletHit.hit.point, bulletHit.hit, bulletHit.hit.normal, false, caliber, 0, team); + } + KillBullet(); + return true; //impulse rounds shouldn't penetrate/do damage + // + } + float anglemultiplier = Mathf.Cos(hitAngle * Mathf.Deg2Rad); + //calculate armor thickness + float thickness = ProjectileUtils.CalculateThickness(hitPart, anglemultiplier); + //calculate armor strength + float penetration = 0; + float penetrationFactor = 0; + int armorType = 1; + //float length = 0; //Moved up for the new bullet wear over distance system + + if (!double.IsPositiveInfinity(distanceLastHit)) + { + // Add the thickness of the armor to the distanceLastHit + distanceLastHit += 0.001f * thickness; + } + + bool ERAhit = false; + + var Armor = hitPart.FindModuleImplementing(); + if (Armor != null) + { + float Ductility = Armor.Ductility; + float hardness = Armor.Hardness; + 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? + } + } + + armorType = (int)Armor.ArmorTypeNum; + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log($"[BDArmory.PooledBullet]: ArmorVars found: Strength : {Strength}; Ductility: {Ductility}; Hardness: {hardness}; MaxTemp: {safeTemp}; Density: {Density}; thickness: {thickness}; hit angle: {hitAngle}"); + } + + //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) + { + // 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); + } + */ + + caliber = newCaliber; //update bullet with new caliber post-deformation(if any) + penetrationFactor = ProjectileUtils.CalculateArmorPenetration(hitPart, penetration, thickness); + //Reactive Armor calcs + //Round has managed to punch through front plate of RA, triggering RA + //if NXRA, will activate on anything that can pen front plate + + var RA = hitPart.FindModuleImplementing(); + if (RA != null) + { + if (penetrationFactor > 1) + { + float thicknessModifier = 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; + } + else + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: Hit Transform: {bulletHit.hit.collider.transform.name}"); + if (bulletHit.hit.collider.transform.name.Substring(0, 8) == "section_") + { + Vector3 ERAnormal = bulletHit.hit.collider.transform.forward; + + float normalDot = Vector3.Dot(ERAnormal, bulletHit.hit.normal); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: Normal Dot: {normalDot}, ERAnormal: {ERAnormal}, hit.normal: {bulletHit.hit.normal}."); + if (Mathf.Abs(normalDot) > 0.943969f) // ERA and hit normal have to be within 20° of each other + { + if (sabot) + { + float flyerThickness = thickness * anglemultiplier; + float flyerPlateL = RA.ERAflyerPlateHalfDimension; + float flyerMass = 4 * flyerPlateL * flyerPlateL * (flyerThickness * 0.001f) * Armor.Density; + float flyerVelocity; + if (RA.ERAbackingPlate) + { + flyerVelocity = RA.ERAgurneyConstant / BDAMath.Sqrt(2f * flyerMass / RA.ERAexplosiveMass + 0.33333333f); // Gurney Equation for sandwich with identical plates + } + else + { + float mRatio = flyerMass / RA.ERAexplosiveMass; + float temp = 1f + 2f * mRatio; + flyerVelocity = RA.ERAgurneyConstant / BDAMath.Sqrt((1f + temp * temp * temp) / (6f * (1f + mRatio)) + mRatio); // Gurney Equation for open faced sandwich + } + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: Flyer Velocity: {flyerVelocity} m/s."); + float ERAdelay = 0.001f * RA.ERAdetonationDelay; + + float sinAngle = Mathf.Sin(hitAngle * Mathf.Deg2Rad); + float tanAngle = sinAngle / anglemultiplier; + + float tInit, tFinal, tFinal1, tFinal2; + + float ERAthickness = 0f; + + float apparentThickness = 1000f * anglemultiplier / (2f * caliber); + + float additionalT = 0.5f; + + if (RA.ERAbackingPlate || normalDot > 0) + { + tInit = 0f; + if (ERAdelay < 0) + { + tInit = -impactSpeed * anglemultiplier * ERAdelay / (anglemultiplier * impactSpeed + flyerVelocity); + } + // Time for the flyer plate lower end to reach the projectile path + tFinal1 = 1000f * flyerPlateL / (tanAngle * flyerVelocity); + // Time for flyer plate to reach the tail of the projectile + tFinal2 = (length - impactSpeed * ERAdelay) / (impactSpeed + flyerVelocity / anglemultiplier); + + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: FRONT PLATE. tFinal1: {tFinal1}, tFinal2: {tFinal2}, ERAdelay: {ERAdelay}."); + + // If we're hitting the back of the plate + if (normalDot < 0) + // Then clamp the max time to when the flyer plate stops + tFinal1 = Mathf.Min(tFinal1, 1000f * RA.ERAspacing / flyerVelocity); + + if (tFinal1 < tFinal2) + tFinal = tFinal1; + else + { + tFinal = tFinal2; + additionalT = 1f; + } + + // If ERAdelay is < -tFinal1, I.E. if ERA is detonated early enough, flyer plate completely misses the projectile + // this is here for counter-ERA projectiles + if (ERAdelay > -tFinal1) + { + // If tFinal2 < 0, I.E. if projectile clears the plate before the ERA detonates then no impact on the projectile aside from thickness + if (tFinal2 > 0) + { + // If the plate is so inclined it's apparent height is < 2 * caliber we linearly decrease the thickness + ERAthickness = flyerThickness * (tanAngle * (tFinal - tInit) * flyerVelocity / caliber + additionalT) * Mathf.Clamp01(apparentThickness * flyerPlateL); + ERAhit = true; + } + else + ERAthickness = thickness; + } + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: tInit: {tInit}, tFinal: {tFinal}, ERAdelay: {ERAdelay}, ERAthickness {ERAthickness}."); + } + + if (RA.ERAbackingPlate || normalDot < 0) + { + float plateSpacing = flyerThickness + RA.ERAexplosiveThickness; + float flyerPlateL2 = flyerPlateL - 0.001f * plateSpacing * tanAngle; + + // Time for flyer plate upper end to reach the projectile path + tFinal1 = 1000f * flyerPlateL2 / (tanAngle * flyerVelocity); + // Time for flyer plate to reach the tail of the projectile + tFinal2 = (anglemultiplier * (impactSpeed * ERAdelay - length) - plateSpacing) / (flyerVelocity - anglemultiplier * impactSpeed); + + // Time of intersection of the projectile tip and the flyer plate + float tIntermediateContact = (plateSpacing - impactSpeed * anglemultiplier * ERAdelay) / (anglemultiplier * impactSpeed - flyerVelocity); + // Delay must be greater than this for projectile to reach the end of the plate + float tMaxDelTip = 1000f * flyerPlateL / (sinAngle * impactSpeed) - tFinal1; + + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: BACK PLATE. tFinal1: {tFinal1}, tFinal2: {tFinal2}, tIntermediateContact: {tIntermediateContact}, tMaxDelTip: {tMaxDelTip}, ERAdelay: {ERAdelay}."); + + tInit = 0f; + tFinal = 0f; + additionalT = 0.5f; + + if (ERAdelay < plateSpacing / (anglemultiplier * impactSpeed)) + { + // Projectile starts in-front of the plate + // And if the ERAdelay > tMaxDelTip + if (ERAdelay > tMaxDelTip) + { + // Then the projectile can catch up to the plate + // Projectile first contacts at tIntermediateContact + tInit = tIntermediateContact; + + // And the plate can be completely fed into the projectile + // limited by the projectile length + if (tFinal1 < tFinal2) + tFinal = tFinal1; + else + { + tFinal = tFinal2; + additionalT = 1.0f; + } + } + // If ERAdelay < tMaxDelTip then the plate misses the projectile + } + else if (ERAdelay < (plateSpacing / anglemultiplier + length) / impactSpeed) + { + // Projectile starts touching the plate but not past the plate + // And if ERAdelay < tMaxDelTip + if (ERAdelay < tMaxDelTip) + { + // Then the plate does not completely feed into the projectile + tFinal = tIntermediateContact; + } + else + { + // The plate can completely feed into the projectile, limited + // by the projectile length + if (tFinal1 < tFinal2) + tFinal = tFinal1; + else + { + tFinal = tFinal2; + additionalT = 1.0f; + } + } + } + else + { + // Projectile starts past the plate + // And if ERAdelay < tMaxDelTip + length/impactSpeed + if (ERAdelay < tMaxDelTip + length / impactSpeed) + { + // Then the plate catches up with the projectile + tInit = tFinal2; + // And interaction is limited by projectile length + if (tFinal1 < tIntermediateContact) + tFinal = tFinal1; + else + { + tFinal = tIntermediateContact; + additionalT = 1.0f; + } + } + // Otherwise, the plate cannot catch up to the projectile + } + + // If hit on plate limited by space + if (normalDot > 0) + { + float tFlyerImpact = 1000f * RA.ERAspacing / flyerVelocity; + if (tInit > tFlyerImpact) + { + // If the plate impacts before the projectile hits then the plate provides + // only its thickness as a contribution + tInit = 0f; + tFinal = 0f; + ERAthickness += thickness; + } + else + { + // If the plate impacts after the projectile hits it, but before tFinal, then + // clamp to the impact time + tFinal = Mathf.Min(tFlyerImpact, tFinal); + } + } + + if (tFinal > 0) + ERAhit = true; + else + additionalT = 0f; + + // If the plate is so inclined it's apparent height is < 2 * caliber we linearly decrease the thickness + ERAthickness += flyerThickness * (tanAngle * (tFinal - tInit) * flyerVelocity / caliber + additionalT) * Mathf.Clamp01(apparentThickness * flyerPlateL2); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: tInit: {tInit}, tFinal: {tFinal}, ERAdelay: {ERAdelay}, ERAthickness: {ERAthickness}."); + } + + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.PooledBullet]: thickness was {thickness} mm, is now {ERAthickness} mm."); + + thickness = ERAthickness; + } + else //standard rounds + { + if (caliber >= RA.sensitivity) //big enough round to trigger RA + { + thickness *= thicknessModifier; + } + } + } + if (sabot || fuzeType == BulletFuzeTypes.Delay || fuzeType == BulletFuzeTypes.Penetrating || fuzeType == BulletFuzeTypes.None) //non-explosive impact + { + if (int.TryParse(bulletHit.hit.collider.transform.name.Substring(8), out int result)) + RA.UpdateSectionScales(result - 1, true, ERAnormal); //detonate RA section + //explosive impacts handled in ExplosionFX + //if explosive and contact fuze, kill bullet? + else + Debug.LogWarning($"[BDArmory.PooledBullet]: Hit on ERA: {hitPart.name} has hit an improperly named section: {bulletHit.hit.collider.transform.name}. Please ensure that these are named \"section_[number]\" and that your \"sections\" transform does not have colliders."); + } + } + } + } + penetrationFactor = ProjectileUtils.CalculateArmorPenetration(hitPart, penetration, thickness); //RA stop round? + } + else ProjectileUtils.CalculateArmorDamage(hitPart, penetrationFactor, caliber, hardness, Ductility, Density, impactSpeed, sourceVesselName, ExplosionSourceType.Bullet, armorType); + } + else + { + Debug.Log("[PooledBUllet].ArmorVars not found; hitPart null"); + } + //determine what happens to bullet + //pen < 1: bullet stopped by armor + //pen > 1 && <2: bullet makes it into part, but can't punch through other side + //pen > 2: bullet goes stragiht through part and out other side + if (penetrationFactor < 1) //stopped by armor + { + if (RicochetOnPart(hitPart, bulletHit.hit, hitAngle, impactSpeed, bulletHit.hit.distance / dist, period)) + { + bool viableBullet = ProjectileUtils.CalculateBulletStatus(bulletMass, caliber, sabot); + if (!viableBullet) + { + distanceTraveled += bulletHit.hit.distance; + KillBullet(); + return true; + } + else + { + //rounds w/ contact fuzes are going to detoante anyway + if (fuzeType == BulletFuzeTypes.Impact || fuzeType == BulletFuzeTypes.Timed) + { + ExplosiveDetonation(hitPart, bulletHit.hit, bulletRay); + ProjectileUtils.CalculateShrapnelDamage(hitPart, bulletHit.hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc daamge from bullet exploding + } + if (fuzeType == BulletFuzeTypes.Delay) + { + fuzeTriggered = true; + } + } + } + if (!hasRicocheted) // explosive bullets that get stopped by armor will explode + { + if (hitPart.rb != null && hitPart.rb.mass > 0) + { + float forceAverageMagnitude = impactSpeed * impactSpeed * (1f / bulletHit.hit.distance) * bulletMass; + + float accelerationMagnitude = forceAverageMagnitude / (hitPart.vessel.GetTotalMass() * 1000); + + hitPart.rb.AddForceAtPosition(impactVelocity.normalized * accelerationMagnitude, bulletHit.hit.point, ForceMode.Acceleration); + + 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 += bulletHit.hit.distance; + hasPenetrated = false; + if (dmgMult < 0) + { + hitPart.AddInstagibDamage(); + } + if (fuzeTriggered) + { + //Debug.Log("[BDArmory.PooledBullet]: Active Delay Fuze failed to penetrate, detonating"); + fuzeTriggered = false; + StopCoroutine(delayedDetonationRoutine); + } + ExplosiveDetonation(hitPart, bulletHit.hit, bulletRay); + ProjectileUtils.CalculateShrapnelDamage(hitPart, bulletHit.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; + } + } + else //penetration >= 1 + { + // 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 penRatio = 1 - thickness / penetration; + float velocityRatio = penRatio; + //const float spacedFactor = 1f; + float oldBulletMass = bulletMass; + // 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 = penRatio; + + if (impactSpeed > 2500f) + { + hypervelocityImpact = true; + // 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)) * penRatio; + } + + // We cap the minimum L/D to be 1.1 to avoid that edge case in the pen formula + if ((massRatio * (length - 10f) + 10f) < (1.1f * caliber)) + { + if (caliber < 10f || length < 10.05f) + { + massRatio = 1.1f * caliber / length; + } + else + { + massRatio = (1.1f * caliber - 10f) / (length - 10f); + } + + if (massRatio > 1) + { + Debug.LogError($"DEBUG Bullet Ratio: {massRatio} is greater than 1! Length: {length} Caliber: {caliber}."); + massRatio = 1; + } + + bulletMass *= massRatio; + + // In the case we are reaching that cap we decrease the velocity by + // the penRatio minus the portion that went into erosion + velocityRatio /= massRatio; + } + else + { + float adjustedPenRatio = 1f - BDAMath.Sqrt(1f - penRatio); + if (!ERAhit) + deltaMass = bulletMass * (massRatio - adjustedPenRatio); //spacedFactor * adjustedPenRatio); + 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 1 - (thickness/penetration)^2 + velocityRatio = 1f - penRatio; // thickness/penetration + velocityRatio = 0.05f * (1f - velocityRatio * velocityRatio) + 0.95f; // 1 - (thickness/penetration)^2 + } + ExplosionFx.CreateExplosion(bulletHit.hit.point, oldBulletMass - bulletMass, "BDArmory/Models/explosion/30mmExplosion", explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, currentVelocity, 70, false, bulletMass, -1, dmgMult, ExplosionFx.WarheadTypes.Standard, null, 1f, -1, currentVelocity); //explosion simming ablated material flashing into plasma, HE amount = bullet mass lost on hit + } + else + { + // If the projectile has already been eroded away we just decrease the + // velocity by the penRatio + } + } + else + { + // Low velocity impacts similarly have velocity decreased by penRatio + } + + + ballisticCoefficient = calcBulletBallisticCoefficient(caliber, bulletMass); // if bullet not killed by impact, + // possbily deformed from impact; grab new ballistic coeff for drag + + //fully penetrated continue ballistic damage + hasPenetrated = true; + bool viableBullet = ProjectileUtils.CalculateBulletStatus(bulletMass, caliber, sabot); + + ResourceUtils.StealResources(hitPart, sourceVessel, stealResources); + //ProjectileUtils.CheckPartForExplosion(hitPart); + + if (dmgMult < 0) + { + hitPart.AddInstagibDamage(); + ProjectileUtils.ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, 0, bullet.DisplayName, ExplosionSourceType.Bullet, true); + } + else + { + 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, bulletHit.hit, dmgMult, penetrationFactor, caliber, bulletMass, (impactVelocity * (armorType == 1 ? 1 : velocityRatio)).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 + } + + impactVelocity = impactVelocity * velocityRatio; //moving this here so unarmored parts take proper damage from the full impact speed and energy delivery of the round, vs everything else properly recieving reduced damage from a round that has to punch through armor first + currentVelocity = hitPartVelocity + impactVelocity; + + currentSpeed = currentVelocity.magnitude; + timeElapsedSinceCurrentSpeedWasAdjusted = 0; + + //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 (HEType != PooledBulletTypes.Slug) + { + if (fuzeType == BulletFuzeTypes.Delay) + { + //currentPosition += (currentVelocity * period) / 3; //when using post-penetration currentVelocity, this yields distances pretty close to distance a Delay fuze would travel before detonation + //commented out, since this could cause explosions to phase through armor/parts between hit point and detonation point + //distanceTraveled += hit.distance; + if (!fuzeTriggered) + { + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Delay Fuze Tripped at t: " + Time.time); + fuzeTriggered = true; + delayedDetonationRoutine = StartCoroutine(DelayedDetonationRoutine()); + } + } + else if (fuzeType == BulletFuzeTypes.Penetrating) //should look into having this be a set depth. For now, assume fancy inertial/electrical mechanism for detecting armor thickness based on time spent passing through + { + if (!fuzeTriggered) + { + if (fuzeSensitivity > 0 ? (thickness > fuzeSensitivity) : penetrationFactor < 1.5f) + { + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Penetrating Fuze Tripped at t: " + Time.time); + fuzeTriggered = true; + delayedDetonationRoutine = StartCoroutine(DelayedDetonationRoutine()); + } + } + } + else //impact by impact, Timed, Prox and Flak, if for whatever reason those last two have 0 proxi range + { + //if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: impact Fuze detonation"); + var calculateShrapnel = !ProjectileUtils.IsArmorPart(CurrentPart); //HE round that's punched through an armor panel would be exploding on the wrong side of it for shrapnel damage to be relevant. ExplosiveDetonation disables the bullet setting CurrentPart to null + ExplosiveDetonation(hitPart, bulletHit.hit, bulletRay, true); + if (calculateShrapnel) + ProjectileUtils.CalculateShrapnelDamage(hitPart, bulletHit.hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc daamge from bullet exploding + hasDetonated = true; + KillBullet(); + distanceTraveled += bulletHit.hit.distance; + return true; + } + if (!viableBullet) + { + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: !viable bullet, removing"); + var calculateShrapnel = !ProjectileUtils.IsArmorPart(CurrentPart); //HE round that's punched through an armor panel would be exploding on the wrong side of it for shrapnel damage to be relevant. ExplosiveDetonation disables the bullet setting CurrentPart to null + ExplosiveDetonation(hitPart, bulletHit.hit, bulletRay, true); + if (calculateShrapnel) + ProjectileUtils.CalculateShrapnelDamage(hitPart, bulletHit.hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc daamge from bullet exploding + hasDetonated = true; + KillBullet(); + distanceTraveled += bulletHit.hit.distance; + return true; + } + } + penTicker += 1; + } + if (!partsHit.Contains(hitPart)) partsHit.Add(hitPart); + //bullet should not go any further if moving too slowly after hit + //smaller caliber rounds would be too deformed to do any further damage + if (hasPenetrated && (currentVelocity - hitPartVelocity).sqrMagnitude < 1e4f) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.PooledBullet]: Bullet Velocity too low, stopping"); + } + if (!fuzeTriggered) KillBullet(); + distanceTraveled += bulletHit.hit.distance; + return true; + } + return false; + } + + Coroutine delayedDetonationRoutine = null; + IEnumerator DelayedDetonationRoutine() + { + double currDist = distanceTraveled; + if (fuzeDelay > 0) + { + float sqrSpeed = currentVelocity.sqrMagnitude; + yield return new WaitForSecondsFixed(fuzeDelay); + float elapsedDist = (float)(distanceTraveled - currDist); + if (elapsedDist * elapsedDist < sqrSpeed * fuzeDelay * fuzeDelay) + { + sqrSpeed = BDAMath.Sqrt(sqrSpeed); + elapsedDist /= sqrSpeed; + distanceTraveled += sqrSpeed * (fuzeDelay - elapsedDist); + currentPosition += currentVelocity * (fuzeDelay - elapsedDist); + } + } + else + { + var wait = new WaitForEndOfFrame(); + yield return wait; + yield return wait; + } + fuzeTriggered = false; + if (!hasDetonated) + { + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currentPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, HEType == PooledBulletTypes.Explosive ? default : currentVelocity, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? ExplosionFx.WarheadTypes.ShapedCharge : ExplosionFx.WarheadTypes.Standard, CurrentPart, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); + if (nuclear) + NukeFX.CreateExplosion(currentPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", hitPart: CurrentPart); + hasDetonated = true; + + // Underwater splash now taken care of in ExplosionFX + /*if (tntMass > 1 && BDArmorySettings.waterHitEffect && FlightGlobals.currentMainBody.ocean) + { + Vector3 up = VectorUtils.GetUpDirection(currentPosition, out double alt); + if ((alt <= 0) && (alt > -detonationRange)) + { + //double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(currentPosition); + //double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(currentPosition); + //FXMonger.Splash(FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitudeAtPos, longitudeAtPos, 0), tntMass * 20); + + FXMonger.Splash(currentPosition - up * (float)alt, tntMass * 20f); + } + }*/ + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Delayed Detonation at: " + Time.time); + KillBullet(); + } + } + + public class SourceInfo + { + public Vessel vessel; + public string team; + public Part weapon; + public Vector3 position; + + public SourceInfo(Vessel vesselIn, string teamIn, Part weaponIn, Vector3 positionIn) + { + vessel = vesselIn; + team = teamIn; + weapon = weaponIn; + position = positionIn; + } + } + + public class NukeInfo + { + public string flashModelPath; + public string shockModelPath; + public string blastModelPath; + public string plumeModelPath; + public string debrisModelPath; + public string blastSoundPath; + + public NukeInfo() + { + flashModelPath = BDModuleNuke.defaultflashModelPath; + shockModelPath = BDModuleNuke.defaultShockModelPath; + blastModelPath = BDModuleNuke.defaultBlastModelPath; + plumeModelPath = BDModuleNuke.defaultPlumeModelPath; + debrisModelPath = BDModuleNuke.defaultDebrisModelPath; + blastSoundPath = BDModuleNuke.defaultBlastSoundPath; + } + + public NukeInfo(string flashModelPathIn, string shockModelPathIn, string blastModelPathIn, + string plumeModelPathIn, string debrisModelPathIn, string blastSoundPathIn) + { + flashModelPath = flashModelPathIn; + shockModelPath = shockModelPathIn; + blastModelPath = blastModelPathIn; + plumeModelPath = plumeModelPathIn; + debrisModelPath = debrisModelPathIn; + blastSoundPath = blastSoundPathIn; + } + } + + public class GraphicsInfo + { + public string bulletTexturePath; + public Color projectileColor; + public Color startColor; + public float tracerStartWidth; + public float tracerEndWidth; + public float tracerLength; + public float tracerLuminance; + public float tracerDeltaFactor; + public string smokeTexturePath; + public string explModelPath; + public string explSoundPath; + + public GraphicsInfo(string bulletTexturePathIn, Color projectileColorIn, Color startColorIn, + float tracerStartWidthIn, float tracerEndWidthIn, float tracerLengthIn, float tracerLuminescanceIn, + float tracerDeltaFactorIn, string smokeTexturePathIn, string explModelPathIn, string explSoundPathIn) + { + bulletTexturePath = bulletTexturePathIn; + projectileColor = projectileColorIn; + startColor = startColorIn; + tracerStartWidth = tracerStartWidthIn; + tracerEndWidth = tracerEndWidthIn; + tracerLength = tracerLengthIn; + tracerLuminance = tracerLuminescanceIn; + tracerDeltaFactor = tracerDeltaFactorIn; + smokeTexturePath = smokeTexturePathIn; + explModelPath = explModelPathIn; + explSoundPath = explSoundPathIn; + } + } + + public static bool isSabot(float bulletMass, float caliber) + { + return ((((bulletMass * 1000f) / ((caliber * caliber * Mathf.PI / 400f) * 19f) + 1f) * 10f) > caliber * 4f); + } + + public static float calcBulletBallisticCoefficient(float caliber, float bulletMass) + { + float bulletDragArea = Mathf.PI * (caliber * caliber / 4f); //if bullet not killed by impact, possbily deformed from impact; grab new ballistic coeff for drag + return bulletMass / ((bulletDragArea / 1000000f) * 0.295f); // mm^2 to m^2 + } + + public static void FireBullet(BulletInfo bulletType, int projectileCount, + SourceInfo sourceInfo, GraphicsInfo graphicsInfo, NukeInfo nukeInfo, + bool drop, float TTL, float timestep, float detRange, float detTime, + bool isAPSP = false, PooledRocket targetRocket = null, PooledBullet targetShell = null, + bool steal = false, float damageMult = 1f, float bulletDmgMult = 1f, + bool addSourcePartVel = true, float additionalVel = 0f, float additionalPhysicsVel = 0f, + Vector3 subPVelorDir = default, bool isSubP = false, + float maxDeviation = -1f, + Vessel tgtVessel = null, float guidance = 0f, + bool registerShot = false) + { + if (ModuleWeapon.bulletPool == null) + { + GameObject templateBullet = new GameObject("Bullet"); + templateBullet.AddComponent(); + templateBullet.SetActive(false); + ModuleWeapon.bulletPool = ObjectPool.CreateObjectPool(templateBullet, 100, true, true); + } + + Vector3 firedVelocity = default; + float dispersionVelocityforAngle = 0f; + + if (isSubP && maxDeviation < 0) + { + float incrementVelocity = 1000 / (additionalPhysicsVel + bulletType.bulletVelocity); //using 1km/s as a reference Unit + float dispersionAngle = bulletType.subProjectileDispersion > 0 ? bulletType.subProjectileDispersion : BDAMath.Sqrt(projectileCount) / 2; //fewer fragments/pellets are going to be larger-> move slower, less dispersion + dispersionVelocityforAngle = 1000 / incrementVelocity * Mathf.Sin(dispersionAngle * Mathf.Deg2Rad); // convert m/s dispersion to angle, accounting for vel of round + } + + for (int i = 0; i < projectileCount; i++) + { + GameObject firedBullet = ModuleWeapon.bulletPool.GetPooledObject(); + PooledBullet pBullet = firedBullet.GetComponent(); + + pBullet.currentPosition = sourceInfo.position; + + pBullet.caliber = bulletType.caliber; + pBullet.bulletVelocity = bulletType.bulletVelocity + additionalVel; + pBullet.bulletMass = bulletType.bulletMass; + pBullet.incendiary = bulletType.incendiary; + pBullet.apBulletMod = bulletType.apBulletMod; + pBullet.bulletDmgMult = bulletDmgMult; + pBullet.fuzeDelay = bulletType.fuzeDelay; + pBullet.fuzeSensitivity = bulletType.fuzeSensitivity; + + pBullet.ballisticCoefficient = bulletType.bulletBallisticCoefficient; + + pBullet.timeElapsedSinceCurrentSpeedWasAdjusted = isSubP ? 0f : timestep; + // measure bullet lifetime in time rather than in distance, because distances get very relative in orbit + pBullet.timeToLiveUntil = Time.time + TTL; + + if (isSubP && maxDeviation < 0) + { + pBullet.currentVelocity = subPVelorDir + UnityEngine.Random.onUnitSphere * dispersionVelocityforAngle; + } + else + { + pBullet.currentVelocity = VectorUtils.GaussianDirectionDeviation(subPVelorDir, (maxDeviation / 2)) * bulletType.bulletVelocity; + } + + if (addSourcePartVel) + { + firedVelocity = pBullet.currentVelocity; + pBullet.currentVelocity += BDKrakensbane.FrameVelocityV3f + sourceInfo.weapon.rb.velocity; // use the real vessel velocity, w/o offloading + } + + pBullet.sourceWeapon = sourceInfo.weapon; + pBullet.sourceVessel = sourceInfo.vessel; + pBullet.team = sourceInfo.team; + pBullet.bulletTexturePath = graphicsInfo.bulletTexturePath; + pBullet.projectileColor = graphicsInfo.projectileColor; + pBullet.startColor = graphicsInfo.startColor; + pBullet.fadeColor = bulletType.fadeColor; + pBullet.tracerStartWidth = graphicsInfo.tracerStartWidth; + pBullet.tracerEndWidth = graphicsInfo.tracerEndWidth; + pBullet.tracerLength = graphicsInfo.tracerLength; + pBullet.tracerLuminance = graphicsInfo.tracerLuminance; + pBullet.tracerDeltaFactor = graphicsInfo.tracerDeltaFactor; + if (!string.IsNullOrEmpty(graphicsInfo.smokeTexturePath)) pBullet.smokeTexturePath = graphicsInfo.smokeTexturePath; + pBullet.bulletDrop = drop; + + if (bulletType.tntMass > 0) + { + pBullet.HEType = bulletType.eHEType; + } + else + { + pBullet.HEType = PooledBulletTypes.Slug; + } + + if (bulletType.tntMass > 0 || (!isSubP && bulletType.beehive)) + { + pBullet.tntMass = bulletType.tntMass; + pBullet.explModelPath = graphicsInfo.explModelPath; + pBullet.explSoundPath = graphicsInfo.explSoundPath; + pBullet.detonationRange = detRange; + pBullet.timeToDetonation = detTime; + pBullet.fuzeType = bulletType.eFuzeType; + } + else + { + pBullet.fuzeType = BulletFuzeTypes.None; + pBullet.sabot = bulletType.sabot; + } + + pBullet.EMP = bulletType.EMP; + pBullet.nuclear = bulletType.nuclear; + if (pBullet.nuclear) // Inherit the parent shell's nuke models. + { + pBullet.flashModelPath = nukeInfo.flashModelPath; + pBullet.shockModelPath = nukeInfo.shockModelPath; + pBullet.blastModelPath = nukeInfo.blastModelPath; + pBullet.plumeModelPath = nukeInfo.plumeModelPath; + pBullet.debrisModelPath = nukeInfo.debrisModelPath; + pBullet.blastSoundPath = nukeInfo.blastSoundPath; + } + + // No sub-sub projectiles! + pBullet.beehive = bulletType.beehive && !isSubP; + if (bulletType.beehive && !isSubP) + { + pBullet.subMunitionType = bulletType.subMunitionType; + } + + pBullet.impulse = bulletType.impulse; + pBullet.massMod = bulletType.massMod; + + //pBullet.homing = BulletInfo.homing; + pBullet.dragType = bulletType.bulletDragType; + + pBullet.tgtShell = targetShell; + pBullet.tgtRocket = targetRocket; + + pBullet.bullet = bulletType; + pBullet.stealResources = steal; + pBullet.dmgMult = damageMult; + pBullet.targetVessel = tgtVessel; + pBullet.guidanceDPS = guidance; + pBullet.guidanceRange = bulletType.guidanceRange; + pBullet.isSubProjectile = isSubP; + pBullet.isAPSprojectile = isAPSP; + pBullet.gameObject.SetActive(true); + + if (registerShot) + BDACompetitionMode.Instance.Scores.RegisterShot(sourceInfo.vessel.GetName()); + + if (!addSourcePartVel) + { + pBullet.SetTracerPosition(); + if (pBullet.CheckBulletCollisions(timestep)) continue; // Bullet immediately hit something and died. + if (!pBullet.hasRicocheted) pBullet.MoveBullet(timestep); // Move the bullet the remaining part of the frame. + pBullet.currentPosition += (TimeWarp.fixedDeltaTime - timestep) * BDKrakensbane.FrameVelocityV3f; // Re-adjust for Krakensbane. + pBullet.timeAlive = timestep; + } + else if (!pBullet.CheckBulletCollisions(timestep)) // Check that the bullet won't immediately hit anything and die. + { + // The following gets bullet tracers to line up properly when at orbital velocities. + // It should be consistent with how it's done in Aim(). + // Technically, there could be a small gap between the collision check and the start position, but this should be insignificant. + if (!pBullet.hasRicocheted) // Movement is handled internally for ricochets. + { + Vector3 gravity = drop ? (Vector3)FlightGlobals.getGeeForceAtPosition(pBullet.currentPosition) : Vector3.zero; + pBullet.currentPosition = AIUtils.PredictPosition(pBullet.currentPosition, firedVelocity, gravity, timestep); + pBullet.currentVelocity += timestep * gravity; // Adjusting the velocity here mostly eliminates bullet deviation due to iTime. + pBullet.DistanceTraveled += timestep * pBullet.currentVelocity.magnitude; // Adjust the distance traveled to account for iTime. + } + pBullet.timeAlive = timestep; + pBullet.SetTracerPosition(); + pBullet.currentPosition += TimeWarp.fixedDeltaTime * (sourceInfo.weapon.rb.velocity + BDKrakensbane.FrameVelocityV3f); // Account for velocity off-loading after visuals are done. + } + } + } + + public void BeehiveDetonation() + { + if (subMunitionType == null) + { + Debug.Log("[BDArmory.PooledBullet] Beehive round not configured with subMunitionType!"); + return; + } + string[] subMunitionData = subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + if (subMunitionData.Length < 2 || !int.TryParse(subMunitionData[1], out int count)) count = 1; + if (BulletInfo.bulletNames.Contains(projType)) + { + BulletInfo sBullet = BulletInfo.bullets[projType]; + + + + float subDetonationRange = 0; + if (sBullet.tntMass > 0) + { + subDetonationRange = sBullet.nuclear ? Mathf.Pow(sBullet.tntMass, 0.33333f) * 10 * (10 * atmosphereDensity) : BlastPhysicsUtils.CalculateBlastRange(sBullet.tntMass) * 0.666f; + } + + SourceInfo sourceInfo = new SourceInfo(sourceVessel, team, sourceWeapon, currentPosition); + GraphicsInfo graphicsInfo = new GraphicsInfo(bulletTexturePath, sBullet.projectileColorC, sBullet.startColorC, + sBullet.caliber / 300, sBullet.caliber / 750, tracerLength, tracerLuminance, tracerDeltaFactor, "", + sBullet.tntMass > 0.5f ? explModelPath : "BDArmory/Models/explosion/30mmExplosion", explSoundPath); + NukeInfo nukeInfo = sBullet.nuclear ? new NukeInfo(flashModelPath, shockModelPath, blastModelPath, + plumeModelPath, debrisModelPath, blastSoundPath) : new NukeInfo(); + + float dragAdjSpeed = GetDragAdjustedVelocity().magnitude; + float subProjectileSpeed = dragAdjSpeed + sBullet.bulletVelocity; + float subTTL = Mathf.Max(sBullet.projectileTTL, 1.1f * detonationRange / subProjectileSpeed); + float subDetonationTime = sBullet.eFuzeType switch + { + BulletFuzeTypes.Timed => detonationRange / subProjectileSpeed, //because beehive TimedFuze for the parent shell is timeToDetonation - detonationRange / bulletVelocity + BulletFuzeTypes.Flak => detonationRange / subProjectileSpeed + Time.fixedDeltaTime, // Detonate at expected impact time for flak (plus 1 frame to allow proximity detection). + _ => subTTL // Otherwise detonate at the TTL. + }; + + FireBullet(sBullet, count * sBullet.projectileCount, sourceInfo, graphicsInfo, nukeInfo, + bulletDrop, subTTL, iTime, subDetonationRange, subDetonationTime, + isAPSprojectile, tgtRocket, tgtShell, stealResources, dmgMult, bulletDmgMult, + false, dragAdjSpeed, bulletVelocity, currentVelocity, true); + } + } + /// + /// Proximity detection prior to and after moving + /// The proximity check prior to moving needs to be done first in case moving the bullet would collide with a target, which would trigger that first. + /// + /// + /// + private bool ProximityAirDetonation(bool preMove) + { + if (!preMove && isAPSprojectile && (tgtShell != null || tgtRocket != null)) // APS can detonate at close range. + { + if (currentPosition.CloserToThan(tgtShell != null ? tgtShell.transform.position : tgtRocket.transform.position, detonationRange / 2)) + { + 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 true; + } + } + + if (timeAlive < armingTime && (fuzeType == BulletFuzeTypes.Proximity || fuzeType == BulletFuzeTypes.Timed)) return false; // Not yet armed. + + if (preMove) // For proximity detonation. + { + if (fuzeType != BulletFuzeTypes.Proximity && fuzeType != BulletFuzeTypes.Flak) return false; // Invalid type. + + Vector3 bulletAcceleration = bulletDrop ? FlightGlobals.getGeeForceAtPosition(currentPosition) : Vector3.zero; + using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) + { + while (loadedVessels.MoveNext()) + { + if (loadedVessels.Current == null || !loadedVessels.Current.loaded) continue; + if (loadedVessels.Current == sourceVessel) continue; + Vector3 relativeVelocity = loadedVessels.Current.Velocity() - currentVelocity; + if (Vector3.Dot(relativeVelocity, loadedVessels.Current.CoM - currentPosition) >= 0) continue; // Ignore craft that aren't approaching. + float localDetonationRange = detonationRange + loadedVessels.Current.GetRadius(average: true); // Detonate when the (average) outermost part of the vessel is within the detonateRange. + float detRangeTime = TimeWarp.fixedDeltaTime + 2 * localDetonationRange / Mathf.Max(1f, relativeVelocity.magnitude); // Time for this frame's movement plus the relative separation to change by twice the detonation range + the vessel's radius (within reason). This is more than the worst-case time needed for the bullet to reach the CPA (ignoring relative acceleration, technically we should be solving x=v*t+1/2*a*t^2 for t). + var timeToCPA = loadedVessels.Current.TimeToCPA(currentPosition, currentVelocity, bulletAcceleration, detRangeTime); + if (timeToCPA > 0 && timeToCPA < detRangeTime) // Going to reach the CPA within the detRangeTime + { + Vector3 adjustedTgtPos = loadedVessels.Current.PredictPosition(timeToCPA); + Vector3 CPA = AIUtils.PredictPosition(currentPosition, currentVelocity, bulletAcceleration, timeToCPA); + float minSepSqr = (CPA - adjustedTgtPos).sqrMagnitude; + float localDetonationRangeSqr = localDetonationRange * localDetonationRange; + if (minSepSqr < localDetonationRangeSqr) + { + timeToCPA = Mathf.Max(0, timeToCPA - BDAMath.Sqrt((localDetonationRangeSqr - minSepSqr) / relativeVelocity.sqrMagnitude)); // Move the detonation time back to the point where it came within the detonation range, but not before the current time. + if (timeToCPA < TimeWarp.fixedDeltaTime) // Detonate if timeToCPA is this frame. + { + currentPosition = AIUtils.PredictPosition(currentPosition, currentVelocity, bulletAcceleration, timeToCPA); // Adjust the bullet position back to the detonation position. + iTime = TimeWarp.fixedDeltaTime - timeToCPA; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.PooledBullet]: Detonating proxy round with detonation range {detonationRange}m at {currentPosition} at distance {(currentPosition - loadedVessels.Current.PredictPosition(timeToCPA)).magnitude}m from {loadedVessels.Current.vesselName} of radius {loadedVessels.Current.GetRadius(average: true)}m"); + currentPosition -= timeToCPA * BDKrakensbane.FrameVelocityV3f; // Adjust for Krakensbane. + return true; + } + } + } + } + } + return false; + } + else // For end-of-life detonation. + { + if (!(((HEType != PooledBulletTypes.Slug || nuclear) && tntMass > 0) || beehive)) return false; + if (!(fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak)) return false; + if (timeAlive > (beehive ? timeToDetonation - detonationRange / bulletVelocity : timeToDetonation)) + { + iTime = 0; + currentPosition -= TimeWarp.fixedDeltaTime * BDKrakensbane.FrameVelocityV3f; // Adjust for Krakensbane. + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.PooledBullet]: Proximity detonation from reaching max time {timeToDetonation}s"); + return true; + } + return false; + } + } + + private void UpdateDragEstimate() + { + switch (dragType) + { + case BulletDragTypes.None: // Don't do anything else + return; + + case BulletDragTypes.AnalyticEstimate: + CalculateDragAnalyticEstimate(currentSpeed, timeElapsedSinceCurrentSpeedWasAdjusted); + break; + + case BulletDragTypes.NumericalIntegration: // Numerical Integration is currently Broken + CalculateDragNumericalIntegration(); + break; + } + } + + private void CalculateDragNumericalIntegration() + { + Vector3 dragAcc = currentVelocity * currentVelocity.magnitude * + (float) + FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currentPosition), + FlightGlobals.getExternalTemperature(currentPosition)); + dragAcc *= 0.5f; + dragAcc /= ballisticCoefficient; + + currentVelocity -= dragAcc * TimeWarp.deltaTime; + //numerical integration; using Euler is silly, but let's go with it anyway + } + + private void CalculateDragAnalyticEstimate(float initialSpeed, float timeElapsed) + { + float atmDensity; + if (underwater) + atmDensity = 1030f; // Sea water (3% salt) has a density of 1030kg/m^3 at 4°C at sea level. https://en.wikipedia.org/wiki/Density#Various_materials + else + atmDensity = atmosphereDensity; + + dragVelocityFactor = 2f * ballisticCoefficient / (timeElapsed * initialSpeed * atmDensity + 2f * ballisticCoefficient); + + // Force Drag = 1/2 atmdensity*velocity^2 * drag coeff * area + // Derivation: + // F = 1/2 * ρ * v^2 * Cd * A + // Cb = m / (Cd * A) + // dv/dt = F / m = -1/2 * ρ v^2 m / Cb (minus due to direction being opposite velocity) + // => ∫ 1/v^2 dv = -1/2 * ∫ ρ/Cb dt + // => -1/v = -1/2*t*ρ/Cb + a + // => v(t) = 2*Cb / (t*ρ + 2*Cb*a) + // v(0) = v0 => a = 1/v0 + // => v(t) = 2*Cb*v0 / (t*v0*ρ + 2*Cb) + // => drag factor at time t is 2*Cb / (t*v0*ρ + 2*Cb) + + } + + private bool ExplosiveDetonation(Part hitPart, RaycastHit hit, Ray ray, bool penetratingHit = false) + { + /////////////////////////////////////////////////////////////////////// + // High Explosive Detonation + /////////////////////////////////////////////////////////////////////// + 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."); // This is getting called regardless of fuzeType, so don't give a warning. + // } + return false; + } + if (hitPart == null || hitPart.vessel != sourceVessel) + { + //if bullet hits and is HE, detonate and kill bullet + if ((HEType != PooledBulletTypes.Slug || nuclear) && tntMass > 0) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.PooledBullet]: Detonation Triggered | penetration: {hasPenetrated} penTick: {penTicker}; airDet: {(fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak)} {(fuzeType == BulletFuzeTypes.Timed ? "detRange: " + distanceTraveled : "")}"); + } + if ((fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak) || HEType == PooledBulletTypes.Shaped) + { + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(hit.point, GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, HEType == PooledBulletTypes.Explosive ? default : ray.direction, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? ExplosionFx.WarheadTypes.ShapedCharge : ExplosionFx.WarheadTypes.Standard, hitPart, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); + if (nuclear) + NukeFX.CreateExplosion(hit.point, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", hitPart: hitPart); + } + else + { + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(hit.point - (ray.direction * 0.1f), GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, null, HEType == PooledBulletTypes.Explosive ? default : ray.direction, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? ExplosionFx.WarheadTypes.ShapedCharge : ExplosionFx.WarheadTypes.Standard, hitPart, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); + if (nuclear) + NukeFX.CreateExplosion(hit.point - (ray.direction * 0.1f), ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", hitPart: hitPart); + } + 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; + } + + public void UpdateWidth(Camera c, float resizeFactor) + { + if (c == null) + { + return; + } + if (bulletTrail == null) + { + return; + } + if (!gameObject.activeInHierarchy) + { + return; + } + + float fov = c.fieldOfView; + float factor = (fov / 60) * resizeFactor * Mathf.Clamp(Vector3.Distance(currentPosition, c.transform.position), 0, 3000) / 50; + bulletTrail[0].startWidth = tracerStartWidth * factor * randomWidthScale; + bulletTrail[0].endWidth = tracerEndWidth * factor * randomWidthScale; + + if (bulletTrail[1].enabled) + { + bulletTrail[1].startWidth = (tracerStartWidth / 2) * factor * 0.5f; + bulletTrail[1].endWidth = (tracerEndWidth / 2) * factor * 0.5f; + } + } + + 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); + } + + static Vector3 ViewerVelocity + { + get + { + if (Time.time != _viewerVelocity.Item1) + { + if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.gameObject.activeInHierarchy) // Missiles don't become null on being killed. + { + _viewerVelocity = (Time.time, FlightGlobals.ActiveVessel.Velocity()); + } + else + { + _viewerVelocity = (Time.time, _viewerVelocity.Item2); // Maintain the last velocity. + } + } + return _viewerVelocity.Item2; + } + } + static (float, Vector3) _viewerVelocity = new(0, default); + public void SetTracerPosition() + { + // visual tracer velocity is relative to the observer (which uses srf_vel when below 100km (f*&king KSP!), not orb_vel) + var tracerDirection = currentVelocity - ViewerVelocity; + if (tracerLength == 0 || timeAlive < tracerLength / bulletVelocity) //while timeAlive < the time it would take to move tracerlength from the muzzle, reduce tracer length so it doesnt draw rear of tracer behind gun + { + linePositions[1] = currentPosition - Mathf.Min(tracerDeltaFactor * 0.45f * TimeWarp.fixedDeltaTime, timeAlive) * tracerDirection; + } + else + { + linePositions[1] = currentPosition - tracerLength * tracerDirection.normalized; + } + linePositions[0] = currentPosition; + smokePositions[0] = startPosition; + for (int i = 0; i < smokePositions.Length - 1; i++) + { + if (timeAlive < i) + { + smokePositions[i] = currentPosition; + } + } + if (timeAlive > smokePositions.Length) + { + //smokePositions[0] = smokePositions[1]; + startPosition = smokePositions[1]; //Start position isn't used for anything else, so modifying shouldn't be an issue. Vestigial value from some deprecated legacy function? + for (int i = 0; i < smokePositions.Length - 1; i++) + { + smokePositions[i] = smokePositions[i + 1]; + //have it so each sec interval after timeAlive > smokePositions.length, have smokePositions[i] = smokePosition[i+1]if i < = smokePosition.length - 1; + } + timeAlive -= 1; + } + smokePositions[4] = currentPosition; + if (BDKrakensbane.IsActive) + { + Vector3 offset = BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + for (int i = 0; i < linePositions.Length; ++i) linePositions[i] -= offset; + for (int i = 0; i < smokePositions.Length; ++i) smokePositions[i] -= offset; + } + //if (Vector3.Distance(startPosition, currPosition) > 1000) smokePositions[0] = currPosition - ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()).normalized * 1000); + bulletTrail[0].SetPositions(linePositions); + if (bulletTrail[1].enabled) bulletTrail[1].SetPositions(smokePositions); + } + + void FadeColor() + { + Vector4 endColorV = new Vector4(projectileColor.r, projectileColor.g, projectileColor.b, projectileColor.a); + float delta = TimeWarp.deltaTime; + Vector4 finalColorV = Vector4.MoveTowards(currentColor, endColorV, delta); + currentColor = new Color(finalColorV.x, finalColorV.y, finalColorV.z, Mathf.Clamp(finalColorV.w, 0.25f, 1f)); + } + + bool RicochetOnPart(Part p, RaycastHit hit, float angleFromNormal, float impactVel, float fractionOfDistance, float period) + { + float hitTolerance = p.crashTolerance; + //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.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Ricochet chance: " + chance); + if (random < chance) + { + DoRicochet(p, hit, angleFromNormal, fractionOfDistance, period); + return true; + } + else + { + return false; + } + } + + bool RicochetScenery(float hitAngle) + { + float reflectRandom = UnityEngine.Random.Range(-75f, 90f); + if (reflectRandom > 90 - hitAngle && caliber <= 30f) + { + return true; + } + + return false; + } + + public void DoRicochet(Part p, RaycastHit hit, float hitAngle, float fractionOfDistance, float period) + { + //ricochet + if (BDArmorySettings.BULLET_HITS) + { + BulletHitFX.CreateBulletHit(p, hit.point, hit, hit.normal, true, caliber, 0, null); + } + + tracerStartWidth /= 2; + tracerEndWidth /= 2; + + MoveBullet(fractionOfDistance * period); // Move the bullet up to the impact point (including velocity and tracking updates). + var hitPoint = p != null ? AIUtils.PredictPosition(hit.point, p.vessel.Velocity(), p.vessel.acceleration_immediate, fractionOfDistance * period) : hit.point; // Adjust the hit point for the movement of the part. + currentPosition = hitPoint; // This is usually very accurate (<1mm), but is sometimes off by a couple of metres for some reason. + Vector3 hitPartVelocity = p != null ? p.vessel.Velocity() : Vector3.zero; + Vector3 relativeVelocity = currentVelocity - hitPartVelocity; + relativeVelocity = Vector3.Reflect(relativeVelocity, hit.normal); // Change angle. + relativeVelocity = Vector3.RotateTowards(relativeVelocity, UnityEngine.Random.onUnitSphere, UnityEngine.Random.Range(0f, 5f) * Mathf.Deg2Rad, 0); // Add some randomness to the new direction. + relativeVelocity *= hitAngle / 150 * 0.65f; // Reduce speed. + currentVelocity = hitPartVelocity + relativeVelocity; // Update the new current velocity. + MoveBullet((1f - fractionOfDistance) * period); // Move the bullet the remaining distance in the new direction. + bulletTrail[1].enabled = false; + hasRicocheted = true; + } + + private float GetExplosivePower() + { + return tntMass > 0 ? tntMass : blastPower; + } + } + + public class BulletHit + { + public RaycastHit hit { get; set; } + public bool isReverseHit { get; set; } = false; + } + + /// + /// Comparer for bullet hit sorting. + /// + internal class BulletHitComparer : IComparer + { + int IComparer.Compare(BulletHit left, BulletHit right) + { + return left.hit.distance.CompareTo(right.hit.distance); + } + public static BulletHitComparer bulletHitComparer = new BulletHitComparer(); + } +} diff --git a/BDArmory/Ammo/PooledRocket.cs b/BDArmory/Ammo/PooledRocket.cs new file mode 100644 index 000000000..b6f932817 --- /dev/null +++ b/BDArmory/Ammo/PooledRocket.cs @@ -0,0 +1,1207 @@ +using System; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; + +using BDArmory.Armor; +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Weapons; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using static BDArmory.Bullets.PooledBullet; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Bullets +{ + public class PooledRocket : MonoBehaviour + { + public RocketInfo rocket; //get tracers, expFX urls moved to BulletInfo + //Seeker/homing rocket code (Image-Recognition tracking?) + + 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; + public float thrustTime; + public bool shaped; + public float timeToDetonation; + float armingTime; + public bool flak; + public bool detonateAtMinimumDistance = false; // Detonate flak rockets when they reach min distance instead of when they enter the proximity range. + public bool concussion; + public bool gravitic; + public bool EMP; + public bool choker; + public bool thief; + public float massMod = 0; + public float impulse = 0; + 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; + public float blastRadius = 0; + public float randomThrustDeviation = 0.05f; + public float massScalar = 0.012f; + private float HERatio = 0.1f; + 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; + public float lifeTime; + + public Vector3 currentPosition { get { return _currentPosition; } set { _currentPosition = value; transform.position = value; } } // Local alias for transform.position speeding up access by around 100x. Only use during FixedUpdates, as it may not be up-to-date otherwise. + Vector3 _currentPosition = default; + Vector3 startPosition; + bool startUnderwater = false; + Ray RocketRay; + private float impactVelocity; + public Vector3 currentVelocity = Vector3.zero; // Current real velocity w/o offloading + Vector3 currentAcceleration = default; + + public bool hasPenetrated = false; + public bool hasDetonated = false; + public int penTicker = 0; + private Part CurrentPart = null; + private const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Wheels); + + private float distanceFromStart = 0; + + //bool isThrusting = true; + public bool isAPSprojectile = false; + public bool isSubProjectile = false; + public PooledRocket tgtRocket = null; + public PooledBullet tgtShell = null; + + Rigidbody rb; + public Rigidbody parentRB; + + KSPParticleEmitter[] pEmitters; + BDAGaplessParticleEmitter[] gpEmitters; + + float randThrustSeed; + + public AudioSource audioSource; + + static RaycastHit[] hits = new RaycastHit[10]; + static Collider[] detonateOverlapSphereColliders = new Collider[10]; + static List allHits; + static Collider[] overlapSphereColliders; + + void Awake() + { + if (allHits == null) allHits = []; + if (overlapSphereColliders == null) { overlapSphereColliders = new Collider[1000]; } + } + + void OnEnable() + { + BDArmorySetup.numberOfParticleEmitters++; + currentPosition = transform.position; // In case something sets transform.position instead of currentPosition. + ApplyKrakensbane(true); // Preemptively undo the krakensbane for the initial frame so that it can be applied in the BetterLateThanNever timing phase. + hasDetonated = false; + + rb = gameObject.AddOrGetComponent(); + + pEmitters = gameObject.GetComponentsInChildren(); + + using (var pe = pEmitters.AsEnumerable().GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + if (FlightGlobals.getStaticPressure(currentPosition) == 0 && pe.Current.useWorldSpace) + { + pe.Current.emit = false; + } + + else if (pe.Current.useWorldSpace) + { + BDAGaplessParticleEmitter gpe = pe.Current.gameObject.AddComponent(); + gpe.rb = rb; + gpe.emit = true; + } + + else + { + pe.Current.emit = true; + EffectBehaviour.AddParticleEmitter(pe.Current); + } + } + gpEmitters = gameObject.GetComponentsInChildren(); + + startPosition = currentPosition; + transform.rotation = transform.parent.rotation; + startTime = Time.time; + armingTime = isSubProjectile ? 0 : BDAMath.Sqrt(4 * blastRadius * rocketMass / thrust); // d = a/2 * t^2 for initial 0 relative velocity + if (FlightGlobals.currentMainBody.ocean && FlightGlobals.getAltitudeAtPos(currentPosition) < 0) + { + startUnderwater = true; + } + else + startUnderwater = false; + massScalar = 0.012f / rocketMass; + + rb.mass = rocketMass; + rb.isKinematic = false; + rb.useGravity = false; + rb.velocity = parentRB ? parentRB.velocity : Vector3.zero; // Use rb.velocity in the velocity frame reference. Use currentVelocity for absolute velocity. + currentVelocity = rb.velocity + BDKrakensbane.FrameVelocityV3f; + transform.parent = null; // Clear the parent transform so the rocket is now independent. + + randThrustSeed = UnityEngine.Random.Range(0f, 100f); + thrustVector = new Vector3(0, 0, thrust); + dragVector = new Vector3(); + + SetupAudio(); + + // Log rockets fired. + if (sourceVessel) + { + sourceVesselName = sourceVessel.GetName(); // Set the source vessel name as the vessel might have changed its name or died by the time the rocket hits. + BDACompetitionMode.Instance.Scores.RegisterRocketFired(sourceVesselName); + } + else + { + sourceVesselName = null; + } + if (tntMass <= 0) + { + explosive = false; + } + if (explosive) + { + HERatio = Mathf.Clamp(tntMass / ((rocketMass * 1000) < tntMass ? tntMass * 1.25f : (rocketMass * 1000)), 0.01f, 0.95f); + } + else + { + HERatio = 0; + } + 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; + } + } + + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.BetterLateThanNever, BetterLateThanNever); + } + + void OnDisable() + { + BDArmorySetup.OnVolumeChange -= UpdateVolume; + BDArmorySetup.numberOfParticleEmitters--; + foreach (var gpe in gpEmitters) + if (gpe != null) + { + gpe.emit = false; + } + foreach (var pe in pEmitters) + if (pe != null) + { + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + sourceVessel = null; + sourceVesselName = null; + spawnTransform = null; + CurrentPart = null; + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) + { + BDATargetManager.FiredRockets.Remove(this); + } + isAPSprojectile = false; + tgtRocket = null; + tgtShell = null; + rb.isKinematic = true; + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.BetterLateThanNever, BetterLateThanNever); + } + + void FixedUpdate() + { + if (!gameObject.activeInHierarchy) + { + return; + } + currentPosition = transform.position; // Adjust our local copy for any adjustments that the physics engine has made. + distanceFromStart = Vector3.Distance(currentPosition, startPosition); + + if (rb && !rb.isKinematic) + { + UpdateKinematics(); // Update forces and get current velocity. + + //guidance and attitude stabilisation scales to atmospheric density. + float atmosMultiplier = Mathf.Clamp01(2.5f * (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currentPosition), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody)); + if (atmosMultiplier > 0) + { + //model transform. always points prograde + var atmosFactor = atmosMultiplier * 0.5f * 0.012f * currentVelocity.sqrMagnitude * TimeWarp.fixedDeltaTime; // aero-stabilize + transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(currentVelocity, transform.up), atmosFactor); + } + } + + if (Time.time - startTime > thrustTime) + { + using (var pe = pEmitters.AsEnumerable().GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + pe.Current.emit = false; + } + using (var gpe = gpEmitters.AsEnumerable().GetEnumerator()) + while (gpe.MoveNext()) + { + if (gpe.Current == null) continue; + gpe.Current.emit = false; + } + if (audioSource) + { + audioSource.loop = false; + audioSource.Stop(); + } + } + + if (ProximityAirDetonation()) // Proximity detection should happen before collision detection. + { + Detonate(currentPosition, false, airDetonation: true); + return; + } + if (CheckCollisions()) return; // Collided and detonated. + + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if (FlightGlobals.currentMainBody.ocean && FlightGlobals.getAltitudeAtPos(currentPosition) > 0 && startUnderwater) + { + startUnderwater = false; + if (BDArmorySettings.waterHitEffect) FXMonger.Splash(currentPosition, caliber); + } + if (FlightGlobals.currentMainBody.ocean && FlightGlobals.getAltitudeAtPos(currentPosition) <= 0 && !startUnderwater) + { + if (tntMass > 0) //look into fuze options similar to bullets? + { + Detonate(currentPosition, false); + } + if (BDArmorySettings.waterHitEffect) FXMonger.Splash(currentPosition, caliber); + } + } + + if (Time.time - startTime > lifeTime) + { + Detonate(currentPosition, true, airDetonation: true); + return; + } + if (beehive && Time.time - startTime >= timeToDetonation - 1) + { + Detonate(currentPosition, false, airDetonation: true); + return; + } + } + + void BetterLateThanNever() => ApplyKrakensbane(); // This makes sure the KB corrections are applied to the correct frame in case of vessel changes. + void ApplyKrakensbane(bool reverse = false) + { + if (BDKrakensbane.IsActive) + { + var offset = BDKrakensbane.FloatingOriginOffset; // Working with the RB in the velocity frame means we apply the KB offset instead of the nonKB one. + if (reverse) + { + currentPosition += offset; + startPosition += offset; + } + else + { + currentPosition -= offset; + startPosition -= offset; + } + } + } + + void UpdateKinematics() + { + var gravity = Vector3.zero; + if (FlightGlobals.RefFrameIsRotating) + { + gravity = FlightGlobals.getGeeForceAtPosition(currentPosition); + rb.AddForce(gravity, ForceMode.Acceleration); + } + currentAcceleration = gravity; + + if (Time.time - startTime <= thrustTime) + { + thrustVector.x = randomThrustDeviation * (1 - (Mathf.PerlinNoise(4 * Time.time, randThrustSeed) * 2)) / massScalar;//this needs to scale w/ rocket mass, or light projectiles will be + thrustVector.y = randomThrustDeviation * (1 - (Mathf.PerlinNoise(randThrustSeed, 4 * Time.time) * 2)) / massScalar;//far more affected than heavier ones + rb.AddRelativeForce(thrustVector); + currentAcceleration += Quaternion.FromToRotation(Vector3.forward, rb.transform.forward) * thrustVector / rb.mass; + }//0.012/rocketmass - use .012 as baseline, it's the mass of the hydra, which the randomTurstdeviation was originally calibrated for + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if (FlightGlobals.getAltitudeAtPos(currentPosition) < 0) + { + //atmosMultiplier *= 83.33f; + dragVector.z = -(0.5f * 1 * currentVelocity.sqrMagnitude * 0.5f * (Mathf.PI * caliber * caliber * 0.25f / 1000000)); + rb.AddRelativeForce(dragVector); //this is going to throw off aiming code, but you aren't going to hit anything with rockets underwater anyway + currentAcceleration += Quaternion.FromToRotation(Vector3.forward, rb.transform.forward) * dragVector / rb.mass; + } + //dragVector.z = -(0.5f * (atmosMultiplier * 0.012f) * currentVelocity.sqrMagnitude * 0.5f * ((Mathf.PI * caliber * caliber * 0.25f) / 1000000)); + //rb.AddRelativeForce(dragVector); + //Debug.Log("[ROCKETDRAG] current vel: " + currentVelocity.ToString("0.0") + "; current dragforce: " + dragVector.magnitude + "; current atm density: " + atmosMultiplier.ToString("0.00")); + } + currentVelocity = rb.velocity + BDKrakensbane.FrameVelocityV3f;// + 0.5f * TimeWarp.fixedDeltaTime * currentAcceleration; // Approximation to the average velocity throughout the coming physics. + } + + /// + /// 2nd-order approximation to the position on the next frame. + /// (Close enough that phasing isn't an issue.) + /// + /// TimeWarp.fixedDeltaTime + /// + Vector3 PredictPosition(float duration, Vector3 referenceVelocity = default) => + AIUtils.PredictPosition(currentPosition, currentVelocity - referenceVelocity, currentAcceleration, duration); + + /// + /// Collision detection within the next frame. + /// + /// true the rocket detonates + bool CheckCollisions() + { + hasPenetrated = true; + penTicker = 0; + + if (BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS) + { + allHits.Clear(); + CheckCollisionWithVessels(); + CheckCollisionWithScenery(); + using var hitsEnu = allHits.OrderBy(x => x.distance).GetEnumerator(); // Check all hits in order of distance. + while (hitsEnu.MoveNext()) if (HitAnalysis(hitsEnu.Current)) return true; + return false; + } + else + { + return CheckCollision(); + } + } + + /// + /// Collision detection between two points (for non-orbital speeds). + /// Note: unlike for bullets, this is performing collision detection for the previous frame. + /// + /// true if the rocket has detonated + bool CheckCollision() + { + var expectedPosition = PredictPosition(TimeWarp.fixedDeltaTime); + float dist = (currentPosition - expectedPosition).magnitude; + RocketRay = new Ray(currentPosition, expectedPosition - currentPosition); + var hitCount = Physics.RaycastNonAlloc(RocketRay, 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. + { + hits = Physics.RaycastAll(RocketRay, dist, layerMask); + hitCount = hits.Length; + } + if (hitCount > 0) + { + var orderedHits = hits.Take(hitCount).OrderBy(x => x.distance); + using var hitsEnu = orderedHits.GetEnumerator(); + while (hitsEnu.MoveNext()) + if (HitAnalysis(hitsEnu.Current)) return true; + } + return false; + } + + void CheckCollisionWithVessels() + { + List nearbyVessels = []; + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels); + var overlapSphereRadius = GetOverlapSphereRadius(); // OverlapSphere of sufficient size to catch all potential craft of <100m radius. + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(currentPosition, overlapSphereRadius, overlapSphereColliders, layerMask); + if (overlapSphereColliderCount == overlapSphereColliders.Length) + { + overlapSphereColliders = Physics.OverlapSphere(currentPosition, overlapSphereRadius, layerMask); + overlapSphereColliderCount = overlapSphereColliders.Length; + } + + using var hitsEnu = overlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator(); + while (hitsEnu.MoveNext()) + { + if (hitsEnu.Current == null) continue; + try + { + Part partHit = hitsEnu.Current.GetComponentInParent(); + if (partHit == null) continue; + if (partHit.vessel == sourceVessel) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.vessel != null && !nearbyVessels.Contains(partHit.vessel)) nearbyVessels.Add(partHit.vessel); + } + catch (Exception e) // ignored + { + Debug.LogWarning("[BDArmory.PooledRocket]: Exception thrown in CheckCollisionWithVessels: " + e.Message + "\n" + e.StackTrace); + } + } + foreach (var vessel in nearbyVessels.OrderBy(v => (v.CoM - currentPosition).sqrMagnitude)) + { + CheckCollisionWithVessel(vessel); // FIXME Convert this to use RaycastCommand to do all the raycasts in parallel. + } + + } + + /// + /// Calculate the required radius of the overlap sphere such that a craft <100m in radius could potentially have collided with the rocket. + /// + /// The required radius. + float GetOverlapSphereRadius() + { + float maxRelSpeedSqr = 0, relVelSqr; + Vector3 relativeVelocity; + using var v = FlightGlobals.Vessels.GetEnumerator(); + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded) continue; // Ignore invalid craft. + relativeVelocity = v.Current.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity; + if (Vector3.Dot(relativeVelocity, v.Current.CoM - currentPosition) >= 0) continue; // Ignore craft that aren't approaching. + relVelSqr = relativeVelocity.sqrMagnitude; + if (relVelSqr > maxRelSpeedSqr) maxRelSpeedSqr = relVelSqr; + } + return 100f + TimeWarp.fixedDeltaTime * BDAMath.Sqrt(maxRelSpeedSqr); // Craft of radius <100m that could have collided within the period. + } + + /// + /// Check for having collided with a vessel in the last frame in a vessel-relative reference frame. + /// + /// + void CheckCollisionWithVessel(Vessel vessel) + { + var expectedPosition = PredictPosition(TimeWarp.fixedDeltaTime, vessel.rb_velocity + BDKrakensbane.FrameVelocityV3f); + float dist = (expectedPosition - currentPosition).magnitude; + RocketRay = new Ray(currentPosition, expectedPosition - currentPosition); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels); + + var hitCount = Physics.RaycastNonAlloc(RocketRay, 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. + { + hits = Physics.RaycastAll(RocketRay, dist, layerMask); + hitCount = hits.Length; + } + + if (hitCount > 0) + { + Part hitPart; + using var hit = hits.Take(hitCount).AsEnumerable().GetEnumerator(); + while (hit.MoveNext()) + { + hitPart = hit.Current.collider.gameObject.GetComponentInParent(); + if (hitPart == null) continue; + if (hitPart.vessel == vessel) allHits.Add(hit.Current); + } + } + } + + void CheckCollisionWithScenery() + { + var expectedPosition = PredictPosition(TimeWarp.fixedDeltaTime); + float dist = (currentPosition - expectedPosition).magnitude; + RocketRay = new Ray(currentPosition, expectedPosition - currentPosition); + const int layerMask = (int)LayerMasks.Scenery; + var hitCount = Physics.RaycastNonAlloc(RocketRay, 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. + { + hits = Physics.RaycastAll(RocketRay, dist, layerMask); + hitCount = hits.Length; + } + allHits.AddRange(hits.Take(hitCount)); + } + + /// + /// Internals of the rocket collision hits loop in CheckCollision so it can also be called from CheckCollisionWithVessel. + /// + /// The raycast hit + /// true if the rocket detonates, false otherwise + bool HitAnalysis(RaycastHit hit) + { + if (!hasPenetrated || hasDetonated) return true; + + Part hitPart; + KerbalEVA hitEVA; + try + { + hitPart = hit.collider.gameObject.GetComponentInParent(); + hitEVA = hit.collider.gameObject.GetComponentUpwards(); + } + catch (NullReferenceException e) + { + Debug.LogWarning("[BDArmory.PooledRocket]:NullReferenceException for Kinetic Hit: " + e.Message); + return false; + } + + if (hitPart != null) + { + if (ProjectileUtils.IsIgnoredPart(hitPart)) return false; // Ignore ignored parts. + if (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) + impactVelocity = (currentVelocity - (hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f)).magnitude; + else + impactVelocity = currentVelocity.magnitude; + if (dmgMult < 0) + { + hitPart.AddInstagibDamage(); + } + else + { + ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, 1, caliber, rocketMass * 1000, impactVelocity, bulletDmgMult, distanceFromStart, explosive, incendiary, false, sourceVessel, rocketName, team, ExplosionSourceType.Rocket, true, true, true); + } + ResourceUtils.StealResources(hitPart, sourceVessel, thief); + Detonate(hit.point, false, hitPart); + return true; + } + + if (hitPart != null && hitPart.vessel == sourceVessel) return false; //avoid autohit; + + Vector3 impactVector = currentVelocity; + 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 = currentVelocity - (hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f); + + float hitAngle = VectorUtils.Angle(impactVector, -hit.normal); + + if (ProjectileUtils.CheckGroundHit(hitPart, hit, caliber)) + { + if (!BDArmorySettings.PAINTBALL_MODE) ProjectileUtils.CheckBuildingHit(hit, rocketMass * 1000, currentVelocity, bulletDmgMult); + Detonate(hit.point, false); + return true; + } + + impactVelocity = impactVector.magnitude; + if (gravitic) + { + var ME = hitPart.FindModuleImplementing(); + if (ME == null) + { + ME = (ModuleMassAdjust)hitPart.AddModule("ModuleMassAdjust"); + } + ME.massMod += massMod; + ME.duration += BDArmorySettings.WEAPON_FX_DURATION; + } + if (concussion && hitPart.rb != null || BDArmorySettings.PAINTBALL_MODE) + { + if (concussion && hitPart.rb != null) + { + hitPart.rb.AddForceAtPosition(impactVector.normalized * impulse, hit.point, ForceMode.Acceleration); + } + BDACompetitionMode.Instance.Scores.RegisterRocketStrike(sourceVesselName, hitPart.vessel.GetName()); + Detonate(hit.point, false, hitPart); + return true; //impulse rounds shouldn't penetrate/do damage + } + float anglemultiplier = (float)Math.Cos(Math.PI * hitAngle / 180.0); + + float thickness = ProjectileUtils.CalculateThickness(hitPart, anglemultiplier); + float penetration = 0; + float penetrationFactor = 0; + var Armor = hitPart.FindModuleImplementing(); + if (Armor != null) + { + float Ductility = Armor.Ductility; + float hardness = Armor.Hardness; + 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.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.PooledRocket]: 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, false); + //calculate penetration + /*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); + + var RA = hitPart.FindModuleImplementing(); + if (RA != null) + { + if (penetrationFactor > 1) + { + float thicknessModifier = RA.armorModifier; + { + if (RA.NXRA) //non-explosive RA, always active + { + thickness *= thicknessModifier; + } + else + { + if (caliber >= RA.sensitivity && hit.collider.transform.name.Substring(0, 8) == "section_") //big enough round to trigger RA and hit an ERA section + { + thickness *= thicknessModifier; + if (tntMass <= 0) //non-explosive impact + { + if (int.TryParse(hit.collider.transform.name.Substring(8), out int result)) + RA.UpdateSectionScales(result - 1); //detonate RA section + //explosive impacts handled in ExplosionFX + else + Debug.LogWarning($"[BDArmory.PooledBullet]: Hit on ERA: {hitPart.name} has hit an improperly named section: {hit.collider.transform.name}. Please ensure that these are named \"section_[number]\" and that your \"sections\" transform does not have colliders."); + } + } + } + } + } + penetrationFactor = ProjectileUtils.CalculateArmorPenetration(hitPart, penetration, thickness); //RA stop round? + } + else ProjectileUtils.CalculateArmorDamage(hitPart, penetrationFactor, caliber, hardness, Ductility, Density, impactVelocity, sourceVessel.GetName(), ExplosionSourceType.Rocket, armorType); + + //calculate return bullet post-pen vel + //calculate armor damage + //FIXME later - if doing bullet style armor penetrtion, then immplement armor penetration, and let AP/kinetic warhead rockets (over?)penetrate parts + } + else + { + Debug.Log("[BDArmory.PooledRocket]: ArmorVars not found; hitPart null"); + } + if (penetration > thickness) + { + currentVelocity *= BDAMath.Sqrt(thickness / penetration); + if (penTicker > 0) currentVelocity *= 0.55f; + rb.velocity = currentVelocity - BDKrakensbane.FrameVelocityV3f; // In case the rocket survives and has further physics updates. + } + + if (penetrationFactor > 1) + { + hasPenetrated = true; + + bool viableBullet = ProjectileUtils.CalculateBulletStatus(rocketMass * 1000, caliber); + if (dmgMult < 0) + { + hitPart.AddInstagibDamage(); + } + else + { + 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) + { + BDACompetitionMode.Instance.Scores.RegisterRocketStrike(sourceVesselName, hitPart.vessel.GetName()); //if non-explosive hit, add rocketstrike, else ExplosionFX adds rocketstrike from HE detonation + } + } + ResourceUtils.StealResources(hitPart, sourceVessel, thief); + + penTicker += 1; + //ProjectileUtils.CheckPartForExplosion(hitPart); + + if (explosive || !viableBullet) + { + currentPosition += currentVelocity * TimeWarp.fixedDeltaTime / 3; + + Detonate(currentPosition, false, hitPart); //explode inside part + return true; + } + } + else // stopped by armor + { + if (hitPart.rb != null && hitPart.rb.mass > 0) + { + float forceAverageMagnitude = impactVelocity * impactVelocity * + (1f / hit.distance) * (rocketMass * 1000); + + float accelerationMagnitude = + forceAverageMagnitude / (hitPart.vessel.GetTotalMass() * 1000); + + hitPart.rb.AddForceAtPosition(impactVector.normalized * accelerationMagnitude, hit.point, ForceMode.Acceleration); + + 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); + } + + hasPenetrated = false; + //ProjectileUtils.ApplyDamage(hitPart, hit, 1, penetrationFactor, caliber, rocketMass * 1000, impactVelocity, bulletDmgMult, distanceFromStart, explosive, incendiary, false, sourceVessel, rocketName, team); + //not going to do ballistic damage if stopped by armor + ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Rocket, (rocketMass * 1000), penetrationFactor); + //the warhead exploding, on the other hand... + Detonate(hit.point, false, hitPart); + return true; + } + + if (penTicker >= 2) + { + Detonate(hit.point, false, hitPart); + return true; + } + + if (currentVelocity.sqrMagnitude <= 10000 && hasPenetrated && (Time.time - startTime > thrustTime)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.PooledRocket]: Rocket ballistic velocity too low, stopping"); + } + Detonate(hit.point, false, hitPart); + return true; + } + return false; + } + + private bool ProximityAirDetonation() + { + if (isAPSprojectile && (tgtShell != null || tgtRocket != null)) + { + if (currentPosition.CloserToThan(tgtShell != null ? tgtShell.currentPosition : tgtRocket.currentPosition, 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 true; + } + } + + if (Time.time - startTime < armingTime) return false; + if (!(((explosive || nuclear) && tntMass > 0) || beehive)) return false; + if (!flak) return false; // Invalid type. + + using var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator(); + while (loadedVessels.MoveNext()) + { + if (loadedVessels.Current == null || !loadedVessels.Current.loaded) continue; + if (loadedVessels.Current == sourceVessel) continue; + Vector3 relativeVelocity = loadedVessels.Current.Velocity() - currentVelocity; + float relativeSpeed = relativeVelocity.magnitude; + if (Vector3.Dot(relativeVelocity, loadedVessels.Current.CoM - currentPosition) >= 0) continue; // Ignore craft that aren't approaching. + float localDetonationRange = detonationRange + loadedVessels.Current.GetRadius(average: true); // Detonate when the (average) outermost part of the vessel is within the detonateRange. + float detRangeTime = TimeWarp.fixedDeltaTime + 2 * localDetonationRange / Mathf.Max(1f, relativeSpeed); // Time for this frame's movement plus the relative separation to change by twice the detonation range + the vessel's radius (within reason). This is more than the worst-case time needed for the rocket to reach the CPA (ignoring relative acceleration, technically we should be solving x=v*t+1/2*a*t^2 for t). + var timeToCPA = loadedVessels.Current.TimeToCPA(currentPosition, currentVelocity, currentAcceleration, detRangeTime); + if (timeToCPA > 0 && timeToCPA < detRangeTime) // Going to reach the CPA within the detRangeTime + { + Vector3 adjustedTgtPos = loadedVessels.Current.PredictPosition(timeToCPA); + Vector3 CPA = AIUtils.PredictPosition(currentPosition, currentVelocity, currentAcceleration, timeToCPA); + float minSepSqr = (CPA - adjustedTgtPos).sqrMagnitude; + float localDetonationRangeSqr = localDetonationRange * localDetonationRange; + if (minSepSqr < localDetonationRangeSqr) + { + if (!detonateAtMinimumDistance) + { + // Move the detonation time back to the point where it came within the detonation range, but not before the current time. + float correctionDistance = BDAMath.Sqrt(localDetonationRangeSqr - minSepSqr); + if (Time.time - startTime > thrustTime) + { + timeToCPA = Mathf.Max(0, timeToCPA - correctionDistance / relativeSpeed); + } + else + { + float acceleration = currentAcceleration.magnitude; + relativeSpeed += timeToCPA * acceleration; // Get the relative speed at the CPA for the correction. + float determinant = relativeSpeed * relativeSpeed - 2 * acceleration * correctionDistance; + timeToCPA = determinant > 0 ? Mathf.Max(0, timeToCPA - (relativeSpeed - BDAMath.Sqrt(determinant)) / acceleration) : 0; + } + } + if (timeToCPA < TimeWarp.fixedDeltaTime) // Detonate if timeToCPA is this frame. + { + currentPosition = AIUtils.PredictPosition(currentPosition, currentVelocity, currentAcceleration, timeToCPA); // Adjust the rocket position to the detonation position. + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.PooledRocket]: Detonating proxy rocket with detonation range {detonationRange}m ({localDetonationRange}m) at distance {(currentPosition - loadedVessels.Current.PredictPosition(timeToCPA)).magnitude}m ({timeToCPA}s) from {loadedVessels.Current.vesselName} of radius {loadedVessels.Current.GetRadius(average: true)}m"); + currentPosition -= timeToCPA * BDKrakensbane.FrameVelocityV3f; // Adjust for Krakensbane. + return true; + } + } + } + } + return false; + } + + void Update() + { + if (!gameObject.activeInHierarchy) + { + return; + } + if (HighLogic.LoadedSceneIsFlight) + { + if (BDArmorySetup.GameIsPaused || (Time.time - startTime > thrustTime)) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + } + else + { + if (!audioSource.isPlaying) + { + audioSource.Play(); + } + } + } + } + + void Detonate(Vector3 pos, bool missed, Part hitPart = null, bool airDetonation = false) + { + hasDetonated = true; + if (!missed) + { + if (beehive) + { + BeehiveDetonation(); + } + else + { + if (tntMass > 0) + { + Vector3 direction = default(Vector3); + if (shaped) + { + direction = currentVelocity.normalized; + //direction = transform.forward //ideal, but no guarantee that mod rockets have correct transform orientation + } + if (gravitic) + { + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(currentPosition, blastRadius, detonateOverlapSphereColliders, layerMask); + if (overlapSphereColliderCount == detonateOverlapSphereColliders.Length) + { + detonateOverlapSphereColliders = Physics.OverlapSphere(currentPosition, blastRadius, layerMask); + overlapSphereColliderCount = detonateOverlapSphereColliders.Length; + } + using (var hitsEnu = detonateOverlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) + { + while (hitsEnu.MoveNext()) + { + 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(currentPosition, partHit.transform.position); + if (gravitic) + { + if (partHit.mass > 0) + { + 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 + } + } + } + } + } + if (incendiary) + { // throw 20 random raytraces out in a cone and see what gets tagged + var RaycastCommands = new Unity.Collections.NativeArray(20, Unity.Collections.Allocator.TempJob); + var RaycastHits = new Unity.Collections.NativeArray(20, Unity.Collections.Allocator.TempJob); // Note: RaycastCommands only return the first hit until Unity 2022.2. + + for (int j = 0; j < 20; ++j) + RaycastCommands[j] = new RaycastCommand(currentPosition, VectorUtils.GaussianDirectionDeviation(currentVelocity, 80), blastRadius * 1.2f, (int)LayerMasks.Parts); + var job = RaycastCommand.ScheduleBatch(RaycastCommands, RaycastHits, 1, default); + job.Complete(); // Wait for the job to complete. + foreach (var hit in RaycastHits) + { + if (hit.collider != null) + { + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (p != null) + { + float distance = Vector3.Distance(currentPosition, hit.point); + 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.PooledRocket]: Applying fire to " + p.name + " at distance " + distance + "m, for " + BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius)) + " seconds"); ; + } + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.PooledRocket] incendiary raytrace: " + hit.point.x + "; " + hit.point.y + "; " + hit.point.z); + } + } + /* + for (int f = 0; f < 20; f++) //throw 20 random raytraces out in a sphere and see what gets tagged + { + Ray LoSRay = new Ray(prevPosition, VectorUtils.GaussianDirectionDeviation(currentVelocity, 80)); + RaycastHit hit; + if (Physics.Raycast(LoSRay, out hit, blastRadius * 1.2f, layerMask)) // only add fires to parts in LoS of blast + { + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (p != null) + { + float distance = Vector3.Distance(currentPosition, hit.point); + 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.PooledRocket]: Applying fire to " + p.name + " at distance " + distance + "m, for " + BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius)) + " seconds"); ; + } + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.PooledRocket] incendiary raytrace: " + hit.point.x + "; " + hit.point.y + "; " + hit.point.z); + } + } + */ + } + if (concussion || EMP || choker) + { + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(currentPosition, 25, detonateOverlapSphereColliders, layerMask); + if (overlapSphereColliderCount == detonateOverlapSphereColliders.Length) + { + detonateOverlapSphereColliders = Physics.OverlapSphere(currentPosition, 25, layerMask); + overlapSphereColliderCount = detonateOverlapSphereColliders.Length; + } + using (var hitsEnu = detonateOverlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) + { + var craftHit = new HashSet(); + while (hitsEnu.MoveNext()) + { + 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); + + if (partHit != null) + { + float distance = Vector3.Distance(partHit.transform.position, currentPosition); + if (concussion && partHit.mass > 0) + { + partHit.rb.AddForceAtPosition((partHit.transform.position - currentPosition).normalized * impulse, partHit.transform.position, ForceMode.Acceleration); + } + if (EMP && !VesselModuleRegistry.IgnoredVesselTypes.Contains(partHit.vesselType)) + { + var MDEC = partHit.vessel.rootPart.FindModuleImplementing(); + if (MDEC == null) + { + MDEC = (ModuleDrainEC)partHit.vessel.rootPart.AddModule("ModuleDrainEC"); + var MB = partHit.vessel.rootPart.FindModuleImplementing(); + if (MB != null) MDEC.isMissile = true; + } + MDEC.incomingDamage = (25 - distance) * 5 * BDArmorySettings.DMG_MULTIPLIER; //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 + } + } + } + } + ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Rocket, caliber, null, sourceVesselName, null, null, direction, -1, true, Hitpart: hitPart, sourceVelocity: airDetonation ? currentVelocity : default); + } + else + { + if (nuclear) + NukeFX.CreateExplosion(pos, ExplosionSourceType.Rocket, sourceVesselName, rocket.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", hitPart: hitPart, sourceVelocity: airDetonation ? currentVelocity : default); + else + ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Rocket, caliber, null, sourceVesselName, null, null, direction, -1, false, rocketMass * 1000, -1, dmgMult, shaped ? ExplosionFx.WarheadTypes.ShapedCharge : ExplosionFx.WarheadTypes.Standard, hitPart, apMod, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)distanceFromStart : -1, sourceVelocity: airDetonation ? currentVelocity : default); + } + } + } + } + gameObject.SetActive(false); + } + + public void BeehiveDetonation() + { + if (subMunitionType == null) + { + Debug.Log("[BDArmory.PooledRocket] Beehive round not configured with subMunitionType!"); + return; + } + string[] subMunitionData = subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + if (subMunitionData.Length < 2 || !int.TryParse(subMunitionData[1], out int count)) count = 1; + if (BulletInfo.bulletNames.Contains(projType)) + { + BulletInfo sBullet = BulletInfo.bullets[projType]; + + float relVelocity = (thrust / rocketMass) * Mathf.Clamp(Time.time - startTime, 0, thrustTime); //currVel is rocketVel + orbitalvel, if in orbit, which will dramatically increase dispersion cone angle, so using accel * time instad + + SourceInfo sourceInfo = new SourceInfo(sourceVessel, team, sourceWeapon, currentPosition); + GraphicsInfo graphicsInfo = new GraphicsInfo("BDArmory/Textures/bullet", sBullet.projectileColorC, sBullet.startColorC, + sBullet.caliber / 300, sBullet.caliber / 750, 0, 1.75f, 2.65f, "", + sBullet.tntMass > 0.5f ? explModelPath : "BDArmory/Models/explosion/30mmExplosion", explSoundPath); + NukeInfo nukeInfo = sBullet.nuclear ? new NukeInfo(flashModelPath, shockModelPath, blastModelPath, + plumeModelPath, debrisModelPath, blastSoundPath) : new NukeInfo(); + + float currSpeed = currentVelocity.magnitude; + + float subTTL = Mathf.Max(sBullet.projectileTTL, 1.1f * detonationRange / (sBullet.bulletVelocity + currSpeed)); + + FireBullet(sBullet, count * sBullet.projectileCount, sourceInfo, graphicsInfo, nukeInfo, + true, subTTL, TimeWarp.fixedDeltaTime, detonationRange, detonationRange / Mathf.Max(1, currSpeed), + isAPSprojectile, tgtRocket, tgtShell, thief, dmgMult, bulletDmgMult, + false, currSpeed, relVelocity, currentVelocity, true); + } + else + { + RocketInfo sRocket = RocketInfo.rockets[projType]; + for (int s = 0; s < count; s++) + { + GameObject rocketObj = ModuleWeapon.rocketPool[sRocket.name].GetPooledObject(); + rocketObj.transform.position = currentPosition; + //rocketObj.transform.rotation = currentRocketTfm.rotation; + rocketObj.transform.rotation = transform.rotation; + rocketObj.transform.localScale = 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.detonateAtMinimumDistance = detonateAtMinimumDistance; + rocket.detonationRange = detonationRange; + rocket.timeToDetonation = detonationRange / Mathf.Max(1, currentVelocity.magnitude); // Only a short time remaining to the target. + 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) //no submunitions of submunitions, not while detoantionRange remains the sasme (sub-submunitions would instantly spawn) + //{ + // 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; + } + rocket.isSubProjectile = true; + rocketObj.SetActive(true); + } + } + } + + void SetupAudio() + { + audioSource = gameObject.GetComponent(); + if (audioSource == null) { audioSource = gameObject.AddComponent(); } + audioSource.loop = true; + audioSource.minDistance = 1; + audioSource.maxDistance = 2000; + audioSource.dopplerLevel = 0.5f; + audioSource.volume = 0.9f * BDArmorySettings.BDARMORY_WEAPONS_VOLUME; + audioSource.pitch = 1f; + audioSource.priority = 255; + audioSource.spatialBlend = 1; + audioSource.clip = SoundUtils.GetAudioClip(rocketSoundPath); + + UpdateVolume(); + BDArmorySetup.OnVolumeChange += UpdateVolume; + } + + void UpdateVolume() + { + if (audioSource) + { + audioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; + } + } + void OnGUI() + { + if (((HighLogic.LoadedSceneIsFlight && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDTISettings.TEAMICONS) || HighLogic.LoadedSceneIsFlight && !BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDTISettings.TEAMICONS && BDTISettings.PERSISTANT) && BDTISettings.MISSILES) + { + if (distanceFromStart > 100) + { + GUIUtils.DrawTextureOnWorldPos(transform.position, BDTISetup.Instance.TextureIconRocket, new Vector2(20, 20), 0); + } + } + } + } +} diff --git a/BDArmory/Ammo/RocketInfo.cs b/BDArmory/Ammo/RocketInfo.cs new file mode 100644 index 000000000..a9135c707 --- /dev/null +++ b/BDArmory/Ammo/RocketInfo.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +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 projectileCount { get; private set; } + public float thrustDeviation { get; private set; } + public string rocketModelPath { get; private set; } + + public static RocketInfos rockets; + public static HashSet rocketNames; + public static RocketInfo defaultRocket; + + 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 projectileCount, 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; + this.flak = flak; + this.EMP = EMP; + 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.projectileCount = projectileCount; + this.thrustDeviation = thrustDeviation; + this.rocketModelPath = rocketModelPath; + } + + public static void Load() + { + if (rockets != null) return; // Only load them once on startup. + rockets = new RocketInfos(); + if (rocketNames == null) rocketNames = new HashSet(); + UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("ROCKET"); + ConfigNode node; + + // First locate BDA's default rocket definition so we can fill in missing fields. + if (defaultRocket == null) + for (int i = 0; i < nodes.Length; ++i) + { + if (nodes[i].parent.name != "BD_Rockets") 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.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)), + (bool)ParseField(node, "flak", typeof(bool)), + (bool)ParseField(node, "EMP", typeof(bool)), + (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, "projectileCount", typeof(int)), 1), + (float)ParseField(node, "thrustDeviation", typeof(float)), + (string)ParseField(node, "rocketModelPath", typeof(string)) + ); + rockets.Add(defaultRocket); + rocketNames.Add("def"); + break; + } + if (defaultRocket == null) throw new ArgumentException("Failed to find BDArmory's default rocket definition.", "defaultRocket"); + + // Now add in the rest of the rockets. + for (int i = 0; i < nodes.Length; i++) + { + string name_ = ""; + try + { + node = nodes[i].config; + name_ = (string)ParseField(node, "name", typeof(string)); + if (rocketNames.Contains(name_)) // Avoid duplicates. + { + if (nodes[i].parent.name != "BD_Rockets" || name_ != "def") // Don't report the default bullet definition as a duplicate. + Debug.LogError("[BDArmory.RocketInfo]: Rocket definition " + name_ + " from " + nodes[i].parent.name + " already exists, skipping."); + continue; + } + Debug.Log("[BDArmory.RocketInfo]: Parsing definition of rocket " + name_ + " from " + nodes[i].parent.name); + 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)), + (bool)ParseField(node, "flak", typeof(bool)), + (bool)ParseField(node, "EMP", typeof(bool)), + (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, "projectileCount", typeof(int)), + (float)ParseField(node, "thrustDeviation", typeof(float)), + (string)ParseField(node, "rocketModelPath", typeof(string)) + ) + ); + rocketNames.Add(name_); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.RocketInfo]: Error Loading Rocket Config '" + name_ + "' | " + e.ToString()); + } + } + } + + 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}' | {e.Message}", field); } + } + catch (Exception e) + { + if (field == "name") throw; // Sanity check for field "name" to avoid potential stack overflow. + if (defaultRocket != null) + { + // 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(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 + { + string name = "unknown"; + try { name = (string)ParseField(node, "name", typeof(string)); } catch { } + Debug.LogError($"[BDArmory.BulletInfo]: Using default value of {defaultValue} for {field} of {name} | {e.Message}"); + } + return defaultValue; + } + else + throw; + } + } + } + + public class RocketInfos : List + { + public RocketInfo this[string name] + { + get { return Find((value) => { return value.name == name; }); } + } + } +} diff --git a/BDArmory/Ammo/_description b/BDArmory/Ammo/_description new file mode 100644 index 000000000..a2d311871 --- /dev/null +++ b/BDArmory/Ammo/_description @@ -0,0 +1,3 @@ +Projectiles: +- bullets +- rockets \ No newline at end of file diff --git a/BDArmory/Armor/ArmorInfo.cs b/BDArmory/Armor/ArmorInfo.cs new file mode 100644 index 000000000..6ecbe46a2 --- /dev/null +++ b/BDArmory/Armor/ArmorInfo.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +using BDArmory.Utils; + +namespace BDArmory.Armor +{ + public class ArmorInfo + { + public string name { get; private set; } + 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 radarReflectivity { get; private set; } //radar stealthiness + 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 yield, float youngModulus, float Ductility, float Diffusivity, float SafeUseTemp, float Stealth, 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.radarReflectivity = Stealth; + 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() + { + if (armors != null) return; // Only load the armor defs once on startup. + armors = new ArmorInfos(); + if (armorNames == null) armorNames = new List(); + 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) + { + if (nodes[i].parent.name != "BD_Armors") 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.ArmorInfo]: Parsing default armor definition from " + nodes[i].parent.name); + defaultArmor = new ArmorInfo( + "def", + (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, "radarReflectivity", typeof(float)), + (float)ParseField(node, "Cost", typeof(float)), + defaultPenShrapnel, + defaultPenHEAT + ); + armors.Add(defaultArmor); + armorNames.Add("def"); + break; + } + if (defaultArmor == null) throw new ArgumentException("Failed to find BDArmory's default armor definition.", "defaultArmor"); + + // 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 (armorNames.Contains(name_)) // Avoid duplicates. + { + if (nodes[i].parent.name != "BD_Armors" || name_ != "def") // Don't report the default bullet definition as a duplicate. + Debug.LogError("[BDArmory.ArmorInfo]: Armor definition " + name_ + " from " + nodes[i].parent.name + " already exists, skipping."); + continue; + } + Debug.Log("[BDArmory.ArmorInfo]: Parsing definition of armor " + name_ + " from " + nodes[i].parent.name); + armors.Add( + new ArmorInfo( + name_, + (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, "radarReflectivity", typeof(float)), + (float)ParseField(node, "Cost", typeof(float)), + defaultPenShrapnel, + defaultPenHEAT + ) + ); + armorNames.Add(name_); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.ArmorInfo]: Error Loading Armor 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 + armors.Remove(defaultArmor); + armorNames.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 (defaultArmor != null) + { + // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. + var defaultValue = typeof(ArmorInfo).GetProperty(field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultArmor); + Debug.LogError("[BDArmory.ArmorInfo]: Using default value of " + defaultValue.ToString() + " for " + field + " | " + e.ToString()); + return defaultValue; + } + else + throw; + } + } + } + + public class ArmorInfos : List + { + public ArmorInfo this[string name] + { + get { return Find((value) => { return value.name == name; }); } + } + } +} \ No newline at end of file diff --git a/BDArmory/Armor/BDAdjustableArmor.cs b/BDArmory/Armor/BDAdjustableArmor.cs new file mode 100644 index 000000000..3c9bdd4fc --- /dev/null +++ b/BDArmory/Armor/BDAdjustableArmor.cs @@ -0,0 +1,453 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Damage; +using KSP.Localization; +using BDArmory.Utils; +using BDArmory.Settings; + +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 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, scaleneTri ? clamped ? maxScale / 2 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y / 2 : clamped ? maxScale : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y); + + 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_AI_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 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, scaleneTri ? clamped ? maxScale / 2 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y / 2 : clamped ? maxScale : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y); + UI_FloatSemiLogRange ALength = (UI_FloatSemiLogRange)Fields["Length"].uiControlEditor; + ALength.UpdateLimits(clamped ? 0.1f : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, clamped ? maxScale : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y); + UI_FloatSemiLogRange SWidth = (UI_FloatSemiLogRange)Fields["scaleneWidth"].uiControlEditor; + SWidth.UpdateLimits(clamped ? 0.1f : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, clamped ? maxScale / 2 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y / 2); + + if (!clamped) + { + Events["ToggleScaleClamp"].guiName = StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_enabledText"); + } + else + { + Events["ToggleScaleClamp"].guiName = StringUtils.Localize("#LOC_BDArmory_AI_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; + + float origBreakingForce; + float origBreakingTorque; + + public override void OnStart(StartState state) + { + armorTransforms = part.FindModelTransforms(ArmorTransformName); + ThicknessTransform = part.FindModelTransform(ThicknessTransformName); + origBreakingForce = part.breakingForce; + origBreakingTorque = part.breakingTorque; + 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 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, scaleneTri ? clamped ? maxScale / 2 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y / 2 : clamped ? maxScale : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y); + 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 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, clamped ? maxScale / 2 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y / 2); + 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 : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, clamped ? maxScale : BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y); + 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 * origBreakingForce; + stackNode.Current.breakingTorque = Width * origBreakingTorque; + 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 * origBreakingForce; + stackNode.Current.breakingTorque = scaleneTri ? scaleneWidth : Width * origBreakingTorque; + 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 * origBreakingForce; + stackNode.Current.breakingTorque = Length * origBreakingTorque; + 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..56cc0d0c7 --- /dev/null +++ b/BDArmory/Armor/HullInfo.cs @@ -0,0 +1,161 @@ +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 float radarMod { get; private set; } //radar reflectivity modifier, if no armor/radar-transparent armor + + 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, float radarMod) + { + this.name = name; + this.localizedName = localizedName; + this.massMod = massMod; + this.costMod = costMod; + this.healthMod = healthMod; + this.ignitionTemp = ignitionTemp; + this.maxTemp = maxTemp; + this.ImpactMod = ImpactMod; + this.radarMod = radarMod; + this.radarMod = radarMod; + } + + 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, + 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)), + (float)ParseField(node, "radarMod", typeof(float)) + ) + ); + materialNames.Add(name_); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.MaterialInfo]: Error Loading Material Config '{name_}' from '{nodes[i].parent.name}' | {e}"); + } + } + //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 (field == "name") throw; // Sanity check for field "name" to avoid potential stack overflow. + if (defaultMaterial != null) + { + // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. + string name = "unknown"; + try { name = (string)ParseField(node, "name", typeof(string)); } catch { } + var defaultValue = typeof(HullInfo).GetProperty(field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultMaterial); + Debug.LogError($"[BDArmory.MaterialInfo]: Using default value of {defaultValue} for {field} of {name} | {e}"); + 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/Armor/ModuleReactiveArmor.cs b/BDArmory/Armor/ModuleReactiveArmor.cs new file mode 100644 index 000000000..f61daf5e7 --- /dev/null +++ b/BDArmory/Armor/ModuleReactiveArmor.cs @@ -0,0 +1,204 @@ +using UnityEngine; + +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Utils; +using System; + +namespace BDArmory.Armor +{ + public class ModuleReactiveArmor : PartModule + { + [KSPField] + public string sectionTransformName = "sections"; + + [KSPField] + public string armorName = "Reactive Armor"; + + Transform[] sections; + int[] sectionIndexes; + + [KSPField] + public bool NXRA = false; //non-explosive reactive armor? + + [KSPField] + public float SectionHP = 300; //non-explosive reactive armor? + + [KSPField] + public float sensitivity = 30; //minimum caliber to trigger RA + + [KSPField] + public float armorModifier = 1.25f; //armor thickness modifier + + [KSPField] + public float ERAflyerPlateHalfDimension = 0.25f; //half of the average length of the flyer plate + + [KSPField] + public float ERAgurneyConstant = 2700f; //gurney specific energy of the ERA, equal to sqrt(2E) (in m/s) + + [KSPField] + public float ERArelativeEffectiveness = 1.72f; //tnt RE of the ERA explosive + + [KSPField] + public float ERAexplosiveMass = 5f; //ERA explosive mass (in kg) + + [KSPField] + public float ERAexplosiveDensity = 1650f; //ERA explosive density (in kg/m^3) + + [KSPField] + public bool ERAbackingPlate = true; //backing plate ? + + [KSPField] + public float ERAspacing = 0.1f; //spacing between back plate and armor + + [KSPField] + public float ERAdetonationDelay = 50f; //detonation delay (in microseconds) + + [KSPField] + public float ERAplateThickness = 16f; //plate thickness (in mm) + + [KSPField] + public string ERAplateMaterial = "Mild Steel"; //plate material + + public int sectionsRemaining = 1; + private int sectionsCount = 1; + public float ERAexplosiveThickness { get; private set; } = -1f; + + Vector3 direction = default(Vector3); + + private string ExploModelPath = "BDArmory/Models/explosion/CASEexplosion"; + private string explSoundPath = "BDArmory/Sounds/explode1"; + public string SourceVessel = ""; + + public void Start() + { + if (!NXRA) MakeArmorSectionArray(); //non-reactive armor doesn't need to compartmentalize HP into sections + //UpdateSectionScales(); + if (HighLogic.LoadedSceneIsFlight) + { + SourceVessel = part.vessel.GetName(); + } + } + + void OnGUI() + { + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_ARMOR) + { + try + { + for (int i = 0; i < sectionsCount; ++i) + { + if (sectionIndexes[i] >= 0) + { + GUIUtils.DrawLineBetweenWorldPositions(sections[sectionIndexes[i]].position, + sections[sectionIndexes[i]].position + sections[sectionIndexes[i]].forward, 1, Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(sections[sectionIndexes[i]].position, + sections[sectionIndexes[i]].position + sections[sectionIndexes[i]].up, 1, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(sections[sectionIndexes[i]].position, + sections[sectionIndexes[i]].position + sections[sectionIndexes[i]].right, 1, Color.green); + } + } + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.MissileLauncher]: Exception thrown in OnGUI: " + e.Message + "\n" + e.StackTrace); + } + } + } + void MakeArmorSectionArray() + { + Transform segmentsTransform = part.FindModelTransform(sectionTransformName); + sectionsCount = segmentsTransform.childCount; + sections = new Transform[sectionsCount]; + sectionIndexes = new int[sectionsCount]; + for (int i = 0; i < sectionsCount; i++) + { + string sectionName = segmentsTransform.GetChild(i).name; + int sectionIndex = int.Parse(sectionName.Substring(8)) - 1; + sections[sectionIndex] = segmentsTransform.GetChild(i); + sectionIndexes[sectionIndex] = i; + } + sectionIndexes.Shuffle(); + //sections.Shuffle(); //randomize order sections get removed + sectionsRemaining = sectionsCount; + var HP = part.FindModuleImplementing(); + if (HP != null) + { + HP.maxHitPoints = (sectionsCount * SectionHP); //set HP based on number of sections + HP.Hitpoints = (sectionsCount * SectionHP); + HP.SetupPrefab(); //and update hitpoint slider + + HP.Armor = ERAplateThickness; + HP.ArmorThickness = ERAplateThickness; + HP.ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == ERAplateMaterial) + 1; + if (HP.ArmorTypeNum == 0) + { + HP.ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == "None"); + Debug.LogWarning($"[BDArmory.ReactiveArmor] WARNING: Part {part.name} has invalid armor type: {ERAplateMaterial}. Defaulted to Aluminum. Please fix ASAP!"); + } + HP.ArmorSetup(null, null); + } + + if (ERAbackingPlate) + { + ERAexplosiveThickness = 1000f * (ERAexplosiveMass / (ERAflyerPlateHalfDimension * ERAflyerPlateHalfDimension * ERAexplosiveDensity)); + } + } + + public void UpdateSectionScales(int sectionDestroyed = -1, bool directionInput = false, Vector3 directionIn = default) + { + int destroyedIndex = -1; + if (sectionDestroyed < 0) + for (int i = 0; i < sectionsCount; ++i) + { + sectionDestroyed = sectionIndexes[i]; + if (sectionDestroyed >= 0) + { + destroyedIndex = i; + break; + } + } + else + for (int i = 0; i < sectionsCount; ++i) + { + if (sectionDestroyed == sectionIndexes[i]) + { + destroyedIndex = i; + break; + } + } + + if (directionInput) + direction = -directionIn; + else + direction = -sections[sectionDestroyed].forward; + + ExplosionFx.CreateExplosion(sections[sectionDestroyed].transform.position, ERAexplosiveMass * ERArelativeEffectiveness * (ERAbackingPlate ? 1.5f: 1f), ExploModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 30, part, SourceVessel, null, armorName, direction, 30, true); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ReactiveArmor]: Removing section: {sectionDestroyed}, " + sectionsRemaining + " sections left"); + sectionsRemaining--; + if (sectionsRemaining < 1 || destroyedIndex < 0) + { + part.Destroy(); + } + else + { + var HP = part.FindModuleImplementing(); + if (HP != null) + { + HP.Hitpoints = Mathf.Clamp(HP.Hitpoints, 0, sectionsRemaining * SectionHP); + } + if (HP.Hitpoints < 0) part.Destroy(); + } + + sections[sectionDestroyed].localScale = Vector3.zero; + sectionIndexes[destroyedIndex] = -1; + /*for (int i = 0; i < sectionsCount; i++) + { + if (i < sectionsRemaining) sections[i].localScale = Vector3.one; + else sections[i].localScale = Vector3.zero; + }*/ + } + } +} diff --git a/BDArmory/BDArmory.csproj b/BDArmory/BDArmory.csproj index 51d03eaf1..ed6634334 100644 --- a/BDArmory/BDArmory.csproj +++ b/BDArmory/BDArmory.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -7,12 +7,12 @@ Library BDArmory BDArmory - v4.7.2 + v4.8 - + true bin\Debug\ @@ -40,17 +40,18 @@ portable AnyCPU prompt - MinimumRecommendedRules.ruleset - default + + preview false bin\Release\ AnyCPU prompt - BasicCorrectnessRules.ruleset + true false + preview false @@ -126,124 +127,242 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + - - - - - {A6F1753E-9570-4C40-AF72-A179890582E5} - BDArmory.Core - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -259,7 +378,10 @@ + + + @@ -270,7 +392,7 @@ - + @@ -291,11 +413,13 @@ - + + + @@ -331,11 +455,13 @@ - - + + + + @@ -343,27 +469,63 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + @@ -374,6 +536,8 @@ + + @@ -392,23 +556,27 @@ + + + + - - - - - + + + + + @@ -420,8 +588,8 @@ - - + + @@ -438,12 +606,16 @@ + + + + @@ -481,7 +653,7 @@ - + @@ -555,10 +727,11 @@ - - + + + @@ -566,7 +739,11 @@ + + + + @@ -575,7 +752,17 @@ + + + + + + + + + + @@ -589,6 +776,10 @@ + + + + @@ -602,17 +793,19 @@ + + + - - + @@ -628,12 +821,17 @@ + + + + + @@ -698,11 +896,12 @@ @echo set lpath vars from "%25GIT_PATH%25_LocalDev\LocalDev" storage... set /p KSP_DIR=<"%25GIT_PATH%25_LocalDev\ksp_dir.txt" set /p PDB2MDB_EXE=<"%25GIT_PATH%25_LocalDev\pdb2mdb_exe.txt" - set /p ZA_DIR=<"%25GIT_PATH%25_LocalDev\7za_dir.txt" + set /p ZA_EXE=<"%25GIT_PATH%25_LocalDev\7za_exe.txt" set /p DIST_DIR=<"%25GIT_PATH%25_LocalDev\dist_dir.txt" @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/%25ModName%25/Plugins/"BDArmory* xcopy /E /Y "$(TargetDir)"BDArmory*.dll "$(ProjectDir)Distribution\GameData\%25ModName%25\Plugins\" if $(ConfigurationName) == Debug ( @@ -713,11 +912,11 @@ @echo deleting previous build ... if exist "%25DIST_DIR%25\%25ModName%25.*.zip" del "%25DIST_DIR%25\%25ModName%25.*.zip" @echo packaging new build... - call "%25ZA_DIR%25\7za.exe" a -tzip -r "%25DIST_DIR%25\%25ModName%25.@(VersionNumber)_%25DATE:~4,2%25%25DATE:~7,2%25%25DATE:~10,4%25%25time:~0,2%25%25time:~3,2%25.zip" "$(ProjectDir)Distribution\*.*" + call "%25ZA_EXE%25" a -tzip -r "%25DIST_DIR%25\%25ModName%25.@(VersionNumber)_%25DATE:~4,2%25%25DATE:~7,2%25%25DATE:~10,4%25%25time:~0,2%25%25time:~3,2%25.zip" "$(ProjectDir)Distribution\*.*" @echo Deploy $(ProjectDir) Distribution files to test env: %25KSP_DIR%25\GameData... @echo copying:"$(ProjectDir)Distribution\GameData" to "%25KSP_DIR%25\GameData" - xcopy /E /Y "$(ProjectDir)Distribution\GameData" "%25KSP_DIR%25\GameData" + xcopy /E /Y "$(ProjectDir)Distribution\GameData" "%25KSP_DIR%25\GameData\" @echo Build/deploy complete! @@ -727,6 +926,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 @@ -742,10 +942,14 @@ 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 + if [ ! -d "${KSP_DIR}" ]; 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! 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/Bullets/BulletInfo.cs b/BDArmory/Bullets/BulletInfo.cs deleted file mode 100644 index 0c7e6e002..000000000 --- a/BDArmory/Bullets/BulletInfo.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using UnityEngine; - -namespace BDArmory.Bullets -{ - public class BulletInfo - { - public string name { get; private set; } - public float caliber { get; private set; } - public float bulletMass { get; private set; } - public float bulletVelocity { get; private set; } - public bool explosive { get; private set; } - public float tntMass { get; private set; } - public string fuzeType { get; private set; } - public int subProjectileCount { get; private set; } - public float apBulletMod { get; private set; } - public string bulletDragTypeName { get; private set; } - public string projectileColor { get; private set; } - public string startColor { get; private set; } - public bool fadeColor { get; private set; } - - public static BulletInfos bullets; - public static HashSet bulletNames; - public static BulletInfo defaultBullet; - - public BulletInfo(string name, float caliber, float bulletVelocity, float bulletMass, - bool explosive, float tntMass, string fuzeType, float apBulletDmg, - int subProjectileCount, string bulletDragTypeName, string projectileColor, string startColor, bool fadeColor) - { - this.name = name; - this.caliber = caliber; - this.bulletVelocity = bulletVelocity; - this.bulletMass = bulletMass; - this.explosive = explosive; - this.tntMass = tntMass; - this.fuzeType = fuzeType; - this.apBulletMod = apBulletDmg; - this.subProjectileCount = subProjectileCount; - this.bulletDragTypeName = bulletDragTypeName; - this.projectileColor = projectileColor; - this.startColor = startColor; - this.fadeColor = fadeColor; - } - - public static void Load() - { - if (bullets != null) return; // Only load the bullet defs once on startup. - bullets = new BulletInfos(); - if (bulletNames == null) bulletNames = new HashSet(); - UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("BULLET"); - ConfigNode node; - - // First locate BDA's default bullet definition so we can fill in missing fields. - if (defaultBullet == null) - for (int i = 0; i < nodes.Length; ++i) - { - if (nodes[i].parent.name != "BD_Bullets") 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]: Parsing default bullet definition from " + nodes[i].parent.name); - defaultBullet = new BulletInfo( - "def", - (float)ParseField(node, "caliber", typeof(float)), - (float)ParseField(node, "bulletVelocity", typeof(float)), - (float)ParseField(node, "bulletMass", typeof(float)), - (bool)ParseField(node, "explosive", typeof(bool)), - (float)ParseField(node, "tntMass", typeof(float)), - (string)ParseField(node, "fuzeType", typeof(string)), - (float)ParseField(node, "apBulletMod", typeof(float)), - (int)ParseField(node, "subProjectileCount", typeof(int)), - (string)ParseField(node, "bulletDragTypeName", typeof(string)), - (string)ParseField(node, "projectileColor", typeof(string)), - (string)ParseField(node, "startColor", typeof(string)), - (bool)ParseField(node, "fadeColor", typeof(bool)) - ); - bullets.Add(defaultBullet); - bulletNames.Add("def"); - break; - } - if (defaultBullet == null) throw new ArgumentException("Failed to find BDArmory's default bullet definition.", "defaultBullet"); - - // Now add in the rest of the bullets. - for (int i = 0; i < nodes.Length; i++) - { - string name_ = ""; - try - { - node = nodes[i].config; - name_ = (string)ParseField(node, "name", typeof(string)); - if (bulletNames.Contains(name_)) // Avoid duplicates. - { - if (nodes[i].parent.name != "BD_Bullets" || name_ != "def") // Don't report the default bullet definition as a duplicate. - Debug.LogError("[BDArmory]: Bullet definition " + name_ + " from " + nodes[i].parent.name + " already exists, skipping."); - continue; - } - Debug.Log("[BDArmory]: Parsing definition of bullet " + name_ + " from " + nodes[i].parent.name); - bullets.Add( - new BulletInfo( - name_, - (float)ParseField(node, "caliber", typeof(float)), - (float)ParseField(node, "bulletVelocity", typeof(float)), - (float)ParseField(node, "bulletMass", typeof(float)), - (bool)ParseField(node, "explosive", typeof(bool)), - (float)ParseField(node, "tntMass", typeof(float)), - (string)ParseField(node, "fuzeType", typeof(string)), - (float)ParseField(node, "apBulletMod", typeof(float)), - (int)ParseField(node, "subProjectileCount", typeof(int)), - (string)ParseField(node, "bulletDragTypeName", typeof(string)), - (string)ParseField(node, "projectileColor", typeof(string)), - (string)ParseField(node, "startColor", typeof(string)), - (bool)ParseField(node, "fadeColor", typeof(bool)) - ) - ); - bulletNames.Add(name_); - } - catch (Exception e) - { - Debug.LogError("[BDArmory]: Error Loading Bullet Config '" + name_ + "' | " + e.ToString()); - } - } - } - - 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 (defaultBullet != null) - { - // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. - var defaultValue = typeof(BulletInfo).GetProperty(field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultBullet); - Debug.LogError("[BDArmory]: Using default value of " + defaultValue.ToString() + " for " + field + " | " + e.ToString()); - return defaultValue; - } - else - throw; - } - } - } - - public class BulletInfos : List - { - public BulletInfo this[string name] - { - get { return Find((value) => { return value.name == name; }); } - } - } -} diff --git a/BDArmory/Bullets/PooledBullet.cs b/BDArmory/Bullets/PooledBullet.cs deleted file mode 100644 index b23b848ea..000000000 --- a/BDArmory/Bullets/PooledBullet.cs +++ /dev/null @@ -1,960 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.FX; -using BDArmory.Parts; -using BDArmory.Shaders; -using BDArmory.UI; -using BDArmory.Control; -using BDArmory.Competition; -using UnityEngine; - -namespace BDArmory.Bullets -{ - public class PooledBullet : MonoBehaviour - { - #region Declarations - - public BulletInfo bullet; - public float leftPenetration; - - public enum PooledBulletTypes - { - Standard, - Explosive - } - - public enum BulletDragTypes - { - None, - AnalyticEstimate, - NumericalIntegration - } - - public PooledBulletTypes bulletType; - public BulletDragTypes dragType; - - public Vessel sourceVessel; - public string sourceVesselName; - public Color lightColor = Misc.Misc.ParseColor255("255, 235, 145, 255"); - public Color projectileColor; - public string bulletTexturePath; - public bool fadeColor; - public Color startColor; - Color currentColor; - public bool bulletDrop = true; - public float tracerStartWidth = 1; - public float tracerEndWidth = 1; - public float tracerLength = 0; - public float tracerDeltaFactor = 1.35f; - public float tracerLuminance = 1; - public float initialSpeed; - - public Vector3 currPosition; - - //explosive parameters - public float radius = 30; - public float tntMass = 0; - public float blastPower = 8; - public float blastHeat = -1; - public float bulletDmgMult = 1; - public string explModelPath; - public string explSoundPath; - - Vector3 startPosition; - public bool airDetonation = false; - public bool proximityDetonation = false; - public float detonationRange = 5f; - public float defaultDetonationRange = 3500f; - public float maxAirDetonationRange = 3500f; - float randomWidthScale = 1; - LineRenderer bulletTrail; - public float timeToLiveUntil; - Light lightFlash; - bool wasInitiated; - public Vector3 currentVelocity; - public float bulletMass; - public float caliber = 1; - public float bulletVelocity; //muzzle velocity - public bool explosive = false; - public float apBulletMod = 0; - public float ballisticCoefficient; - public float flightTimeElapsed; - public static Shader bulletShader; - public static bool shaderInitialized; - private float impactVelocity; - private float dragVelocityFactor; - - public bool hasPenetrated = false; - public bool hasDetonated = false; - public bool hasRichocheted = false; - - public int penTicker = 0; - - public Rigidbody rb; - - Ray bulletRay; - - #endregion Declarations - - private Vector3[] linePositions = new Vector3[2]; - - private double distanceTraveled = 0; - - void OnEnable() - { - startPosition = transform.position; - initialSpeed = currentVelocity.magnitude; // this is the velocity used for drag estimations (only), use total velocity, not muzzle velocity - distanceTraveled = 0; // Reset the distance travelled for the bullet (since it comes from a pool). - - if (!wasInitiated) - { - //projectileColor.a = projectileColor.a/2; - //startColor.a = startColor.a/2; - } - - projectileColor.a = Mathf.Clamp(projectileColor.a, 0.25f, 1f); - startColor.a = Mathf.Clamp(startColor.a, 0.25f, 1f); - currentColor = projectileColor; - if (fadeColor) - { - currentColor = startColor; - } - - if (lightFlash == null || !gameObject.GetComponent()) - { - lightFlash = gameObject.AddOrGetComponent(); - lightFlash.type = LightType.Point; - lightFlash.range = 8; - lightFlash.intensity = 1; - lightFlash.color = lightColor; - lightFlash.enabled = true; - } - - //tracer setup - if (bulletTrail == null || !gameObject.GetComponent()) - { - bulletTrail = gameObject.AddOrGetComponent(); - } - - if (!wasInitiated) - { - bulletTrail.positionCount = linePositions.Length; - } - linePositions[0] = transform.position; - linePositions[1] = transform.position; - bulletTrail.SetPositions(linePositions); - - if (!shaderInitialized) - { - shaderInitialized = true; - bulletShader = BDAShaderLoader.BulletShader; - } - - if (!wasInitiated) - { - bulletTrail.material = new Material(bulletShader); - randomWidthScale = UnityEngine.Random.Range(0.5f, 1f); - gameObject.layer = 15; - } - - bulletTrail.material.mainTexture = GameDatabase.Instance.GetTexture(bulletTexturePath, false); - bulletTrail.material.SetColor("_TintColor", currentColor); - bulletTrail.material.SetFloat("_Lum", tracerLuminance); - - tracerStartWidth *= 2f; - tracerEndWidth *= 2f; - - leftPenetration = 1; - wasInitiated = true; - StartCoroutine(FrameDelayedRoutine()); - - // Log shots fired. - if (this.sourceVessel) - { - var aName = this.sourceVessel.GetName(); - if (BDACompetitionMode.Instance && BDACompetitionMode.Instance.Scores.ContainsKey(aName)) - ++BDACompetitionMode.Instance.Scores[aName].shotsFired; - sourceVesselName = sourceVessel.GetName(); // Set the source vessel name as the vessel might have changed its name or died by the time the bullet hits. - } - else - { - sourceVesselName = null; - } - } - - void OnDisable() - { - sourceVessel = null; - } - - void OnDestroy() - { - StopCoroutine(FrameDelayedRoutine()); - } - - IEnumerator FrameDelayedRoutine() - { - yield return new WaitForFixedUpdate(); - lightFlash.enabled = false; - } - - void OnWillRenderObject() - { - if (!gameObject.activeInHierarchy) - { - return; - } - Camera currentCam = Camera.current; - if (TargetingCamera.IsTGPCamera(currentCam)) - { - UpdateWidth(currentCam, 4); - } - else - { - UpdateWidth(currentCam, 1); - } - } - - void FixedUpdate() - { - if (!gameObject.activeInHierarchy) - { - return; - } - - //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) - { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; - startPosition -= FloatingOrigin.OffsetNonKrakensbane; - } - - float distanceFromStart = Vector3.Distance(transform.position, startPosition); - - //calculate flight time for drag purposes - flightTimeElapsed += Time.fixedDeltaTime; - - // calculate flight distance for achievement purposes - distanceTraveled += currentVelocity.magnitude * Time.fixedDeltaTime; - - //Drag types currently only affect Impactvelocity - //Numerical Integration is currently Broken - switch (dragType) - { - case BulletDragTypes.None: - break; - - case BulletDragTypes.AnalyticEstimate: - CalculateDragAnalyticEstimate(); - break; - - case BulletDragTypes.NumericalIntegration: - CalculateDragNumericalIntegration(); - break; - } - - if (tracerLength == 0) - { - // visual tracer velocity is relative to the observer - linePositions[0] = transform.position + - ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()) * tracerDeltaFactor * 0.45f * Time.fixedDeltaTime); - } - else - { - linePositions[0] = transform.position + ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()).normalized * tracerLength); - } - - if (fadeColor) - { - FadeColor(); - bulletTrail.material.SetColor("_TintColor", currentColor * tracerLuminance); - } - linePositions[1] = transform.position; - - bulletTrail.SetPositions(linePositions); - currPosition = transform.position; - - if (Time.time > timeToLiveUntil) //kill bullet when TTL ends - { - KillBullet(); - return; - } - - // bullet collision block - { - //reset our hit variables to default state - hasPenetrated = true; - hasDetonated = false; - hasRichocheted = false; - penTicker = 0; - - float dist = currentVelocity.magnitude * Time.fixedDeltaTime; - bulletRay = new Ray(currPosition, currentVelocity); - var hits = Physics.RaycastAll(bulletRay, dist, 9076737); - if (hits.Length > 0) - { - var orderedHits = hits.OrderBy(x => x.distance); - - using (var hitsEnu = orderedHits.GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - if (!hasPenetrated || hasRichocheted || hasDetonated) break; - - RaycastHit hit = hitsEnu.Current; - Part hitPart = null; - KerbalEVA hitEVA = null; - - try - { - hitPart = hit.collider.gameObject.GetComponentInParent(); - hitEVA = hit.collider.gameObject.GetComponentUpwards(); - } - catch (NullReferenceException) - { - Debug.Log("[BDArmory]:NullReferenceException for Ballistic Hit"); - return; - } - - 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 = (currentVelocity * dragVelocityFactor - - (hitPart.rb.velocity + Krakensbane.GetFrameVelocityV3f())).magnitude; - else - impactVelocity = currentVelocity.magnitude * dragVelocityFactor; - ApplyDamage(hitPart, hit, 1, 1); - break; - } - - if (hitPart?.vessel == sourceVessel) continue; //avoid autohit; - - Vector3 impactVector = currentVelocity; - if (hitPart?.rb != null) - // using relative velocity vector instead of just bullet velocity - // since KSP vessels might move faster than bullets - impactVector = currentVelocity * dragVelocityFactor - (hitPart.rb.velocity + Krakensbane.GetFrameVelocityV3f()); - - float hitAngle = Vector3.Angle(impactVector, -hit.normal); - - if (CheckGroundHit(hitPart, hit)) - { - CheckBuildingHit(hit); - if (!RicochetScenery(hitAngle)) - { - ExplosiveDetonation(hitPart, hit, bulletRay); - KillBullet(); - } - else - { - DoRicochet(hitPart, hit, hitAngle); - } - return; - } - - //Standard Pipeline Hitpoints, Armor and Explosives - - impactVelocity = impactVector.magnitude; - float anglemultiplier = (float)Math.Cos(Math.PI * hitAngle / 180.0); - - float penetrationFactor = CalculateArmorPenetration(hitPart, anglemultiplier, hit); - - if (penetrationFactor >= 2) - { - //its not going to bounce if it goes right through - hasRichocheted = false; - } - else - { - if (RicochetOnPart(hitPart, hit, hitAngle, impactVelocity)) - hasRichocheted = true; - } - - if (penetrationFactor > 1 && !hasRichocheted) //fully penetrated continue ballistic damage - { - hasPenetrated = true; - ApplyDamage(hitPart, hit, 1, penetrationFactor); - penTicker += 1; - CheckPartForExplosion(hitPart); - - //Explosive bullets that penetrate should explode shortly after - //if penetration is very great, they will have moved on - //checking velocity as they would not be able to come out the other side - //if (explosive && penetrationFactor < 3 || currentVelocity.magnitude <= 800f) - if (explosive) - { - //move bullet - transform.position += (currentVelocity * Time.fixedDeltaTime) / 3; - - ExplosiveDetonation(hitPart, hit, bulletRay); - hasDetonated = true; - KillBullet(); - } - } - else if (!hasRichocheted) // explosive bullets that get stopped by armor will explode - { - //New method - - if (hitPart.rb != null) - { - float forceAverageMagnitude = impactVelocity * impactVelocity * - (1f / hit.distance) * (bulletMass - tntMass); - - float accelerationMagnitude = - forceAverageMagnitude / (hitPart.vessel.GetTotalMass() * 1000); - - hitPart?.rb.AddForceAtPosition(impactVector.normalized * accelerationMagnitude, hit.point, ForceMode.Acceleration); - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Force Applied " + Math.Round(accelerationMagnitude, 2) + "| Vessel mass in kgs=" + hitPart.vessel.GetTotalMass() * 1000 + "| bullet effective mass =" + (bulletMass - tntMass)); - } - - hasPenetrated = false; - ApplyDamage(hitPart, hit, 1, penetrationFactor); - ExplosiveDetonation(hitPart, hit, bulletRay); - hasDetonated = true; - KillBullet(); - } - - ///////////////////////////////////////////////////////////////////////////////// - // penetrated after a few ticks - ///////////////////////////////////////////////////////////////////////////////// - - //penetrating explosive - //richochets - - if ((penTicker >= 2 && explosive) || (hasRichocheted && explosive)) - { - //detonate - ExplosiveDetonation(hitPart, hit, bulletRay, airDetonation); - return; - } - - //bullet should not go any further if moving too slowly after hit - //smaller caliber rounds would be too deformed to do any further damage - if (currentVelocity.magnitude <= 100 && hasPenetrated) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Bullet Velocity too low, stopping"); - } - KillBullet(); - return; - } - //we need to stop the loop if the bullet has stopped,richochet or detonated - if (!hasPenetrated || hasRichocheted || hasDetonated) break; - }//end While - }//end enumerator - }//end if hits - }// end if collision - - ////////////////////////////////////////////////// - //Flak Explosion (air detonation/proximity fuse) - ////////////////////////////////////////////////// - - if (ProximityAirDetonation(distanceFromStart)) - { - //detonate - ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, currentVelocity); - KillBullet(); - - return; - } - - if (bulletDrop) - { - // Gravity??? - var gravity_ = FlightGlobals.getGeeForceAtPosition(transform.position); - //var gravity_ = Physics.gravity; - currentVelocity += gravity_ * TimeWarp.deltaTime; - } - - //move bullet - transform.position += currentVelocity * Time.fixedDeltaTime; - } - - private bool ProximityAirDetonation(float distanceFromStart) - { - bool detonate = false; - - if (distanceFromStart <= 500f) return false; - - if (explosive && airDetonation) - { - if (distanceFromStart > maxAirDetonationRange || distanceFromStart > defaultDetonationRange) - { - return detonate = true; - } - - if (proximityDetonation) - { - using (var hitsEnu = Physics.OverlapSphere(transform.position, detonationRange, 557057).AsEnumerable().GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - if (hitsEnu.Current == null) continue; - - try - { - Part partHit = hitsEnu.Current.GetComponentInParent(); - if (partHit?.vessel == sourceVessel) continue; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Bullet proximity sphere hit | Distance overlap = " + detonationRange + "| Part name = " + partHit.name); - - return detonate = true; - } - catch - { - // ignored - } - } - } - } - } - return detonate; - } - - private void ApplyDamage(Part hitPart, RaycastHit hit, float multiplier, float penetrationfactor) - { - //hitting a vessel Part - //No struts, they cause weird bugs :) -BahamutoD - if (hitPart == null) return; - if (hitPart.partInfo.name.Contains("Strut")) return; - - // Add decals - if (BDArmorySettings.BULLET_HITS) - { - BulletHitFX.CreateBulletHit(hitPart, hit.point, hit, hit.normal, hasRichocheted, caliber, penetrationfactor); - } - - // Apply damage - float damage; - if (explosive) - { - damage = hitPart.AddBallisticDamage(bulletMass - tntMass, caliber, multiplier, penetrationfactor, bulletDmgMult, impactVelocity); - } - else - { - damage = hitPart.AddBallisticDamage(bulletMass, caliber, multiplier, penetrationfactor, bulletDmgMult, impactVelocity); - } - // Debug.Log("DEBUG Ballistic damage to " + hitPart + ": " + damage + ", calibre: " + caliber + ", multiplier: " + multiplier + ", pen: " + penetrationfactor); - - // Update scoring structures - var aName = this.sourceVessel.GetName(); - var tName = hitPart.vessel.GetName(); - - if (aName != null && tName != null && aName != tName && BDACompetitionMode.Instance.Scores.ContainsKey(aName) && BDACompetitionMode.Instance.Scores.ContainsKey(tName)) - { - //Debug.Log("[BDArmory]: Weapon from " + aName + " damaged " + tName); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { - BDAScoreService.Instance.TrackHit(aName, tName, bullet.name, distanceTraveled); - BDAScoreService.Instance.TrackDamage(aName, tName, damage); - } - - // update scoring structure on attacker - { - var aData = BDACompetitionMode.Instance.Scores[aName]; - aData.Score += 1; - // keep track of who shot who for point keeping - - // competition logic for 'Pinata' mode - this means a pilot can't be named 'Pinata' - if (hitPart.vessel.GetName() == "Pinata") - { - aData.PinataHits++; - } - - } - - // update scoring structure on the defender. - { - var tData = BDACompetitionMode.Instance.Scores[tName]; - tData.lastPersonWhoHitMe = aName; - tData.lastHitTime = Planetarium.GetUniversalTime(); - tData.everyoneWhoHitMe.Add(aName); - // Track hits - if (tData.hitCounts.ContainsKey(aName)) - ++tData.hitCounts[aName]; - else - tData.hitCounts.Add(aName, 1); - // Track damage - if (tData.damageFromBullets.ContainsKey(aName)) - tData.damageFromBullets[aName] += damage; - else - tData.damageFromBullets.Add(aName, damage); - } - } - - } - - private void CalculateDragNumericalIntegration() - { - Vector3 dragAcc = currentVelocity * currentVelocity.magnitude * - (float) - FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(transform.position), - FlightGlobals.getExternalTemperature(transform.position)); - dragAcc *= 0.5f; - dragAcc /= ballisticCoefficient; - - currentVelocity -= dragAcc * TimeWarp.deltaTime; - //numerical integration; using Euler is silly, but let's go with it anyway - } - - private void CalculateDragAnalyticEstimate() - { - float analyticDragVelAdjustment = (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currPosition), FlightGlobals.getExternalTemperature(currPosition)); - analyticDragVelAdjustment *= flightTimeElapsed * initialSpeed; - analyticDragVelAdjustment += 2 * ballisticCoefficient; - - analyticDragVelAdjustment = 2 * ballisticCoefficient * initialSpeed / analyticDragVelAdjustment; - //velocity as a function of time under the assumption of a projectile only acted upon by drag with a constant drag area - - dragVelocityFactor = analyticDragVelAdjustment / initialSpeed; - } - - private float CalculateArmorPenetration(Part hitPart, float anglemultiplier, RaycastHit hit) - { - /////////////////////////////////////////////////////////////////////// - // Armor Penetration - /////////////////////////////////////////////////////////////////////// - - float penetration = CalculatePenetration(); - - //TODO: Extract bdarmory settings from this values - float thickness = CalculateThickness(hitPart, anglemultiplier); - if (thickness < 1) thickness = 1; //prevent divide by zero or other odd behavior - - var penetrationFactor = penetration / thickness; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Armor penetration = " + penetration + " | Thickness = " + thickness); - } - - bool fullyPenetrated = penetration > thickness; //check whether bullet penetrates the plate - - double massToReduce = Math.PI * Math.Pow((caliber * 0.001) / 2, 2) * (penetration); - - if (fullyPenetrated) - { - //lower velocity on penetrating armor plate - //does not affect low impact parts so that rounds can go through entire tank easily - //If round penetrates easily it should not loose much velocity - - //if (penetrationFactor < 2) - currentVelocity = currentVelocity * (float)Math.Sqrt(thickness / penetration); - //signifincanly reduce velocity on subsequent penetrations - if (penTicker > 0) currentVelocity *= 0.55f; - - //updating impact velocity - //impactVelocity = currentVelocity.magnitude; - - flightTimeElapsed -= Time.fixedDeltaTime; - } - else - { - massToReduce *= 0.125f; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Bullet Stopped by Armor"); - } - } - hitPart.ReduceArmor(massToReduce); - return penetrationFactor; - } - - private float CalculatePenetration() - { - float penetration = 0; - if (apBulletMod <= 0) // sanity check/legacy compatibility - { - apBulletMod = 1; - } - - if (caliber > 5) //use the "krupp" penetration formula for anything larger than HMGs - { - penetration = (float)(16f * impactVelocity * Math.Sqrt(bulletMass / 1000) / Math.Sqrt(caliber) * apBulletMod); //APBulletMod now actually implemented, serves as penetration multiplier, 1 being neutral, <1 for soft rounds, >1 for AP penetrators - } - - return penetration; - } - - private static float CalculateThickness(Part hitPart, float anglemultiplier) - { - float thickness = (float)hitPart.GetArmorThickness(); - return Mathf.Max(thickness / anglemultiplier, 1); - } - - private bool ExplosiveDetonation(Part hitPart, RaycastHit hit, Ray ray, bool airDetonation = false) - { - /////////////////////////////////////////////////////////////////////// - // High Explosive Detonation - /////////////////////////////////////////////////////////////////////// - - if (hitPart == null || hitPart.vessel != sourceVessel) - { - //if bullet hits and is HE, detonate and kill bullet - if (explosive) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Detonation Triggered | penetration: " + hasPenetrated + " penTick: " + penTicker + " airDet: " + airDetonation); - } - - if (airDetonation) - { - ExplosionFx.CreateExplosion(hit.point, GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName); - } - else - { - ExplosionFx.CreateExplosion(hit.point - (ray.direction * 0.1f), - GetExplosivePower(), - explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, direction: currentVelocity); - } - - KillBullet(); - hasDetonated = true; - return true; - } - } - return false; - } - - private bool CheckGroundHit(Part hitPart, RaycastHit hit) - { - if (hitPart == null) - { - if (BDArmorySettings.BULLET_HITS) - { - BulletHitFX.CreateBulletHit(hitPart, hit.point, hit, hit.normal, true, caliber, 0); - } - - return true; - } - return false; - } - - private bool CheckBuildingHit(RaycastHit hit) - { - DestructibleBuilding building = null; - try - { - building = hit.collider.gameObject.GetComponentUpwards(); - building.damageDecay = 600f; - } - catch (Exception) { } - - if (building != null && building.IsIntact) - { - float damageToBuilding = ((0.5f * (bulletMass * Mathf.Pow(currentVelocity.magnitude, 2))) - * (BDArmorySettings.DMG_MULTIPLIER / 100) * bulletDmgMult - * 1e-4f); - damageToBuilding /= 8f; - building.AddDamage(damageToBuilding); - if (building.Damage > building.impactMomentumThreshold * 150) - { - building.Demolish(); - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Ballistic hit destructible building! Hitpoints Applied: " + Mathf.Round(damageToBuilding) + - ", Building Damage : " + Mathf.Round(building.Damage) + - " Building Threshold : " + building.impactMomentumThreshold); - - return true; - } - return false; - } - - public void UpdateWidth(Camera c, float resizeFactor) - { - if (c == null) - { - return; - } - if (bulletTrail == null) - { - return; - } - if (!gameObject.activeInHierarchy) - { - return; - } - - float fov = c.fieldOfView; - float factor = (fov / 60) * resizeFactor * - Mathf.Clamp(Vector3.Distance(transform.position, c.transform.position), 0, 3000) / 50; - bulletTrail.startWidth = tracerStartWidth * factor * randomWidthScale; - bulletTrail.endWidth = tracerEndWidth * factor * randomWidthScale; - } - - void KillBullet() - { - gameObject.SetActive(false); - } - - void FadeColor() - { - Vector4 endColorV = new Vector4(projectileColor.r, projectileColor.g, projectileColor.b, projectileColor.a); - float delta = TimeWarp.deltaTime; - Vector4 finalColorV = Vector4.MoveTowards(currentColor, endColorV, delta); - currentColor = new Color(finalColorV.x, finalColorV.y, finalColorV.z, Mathf.Clamp(finalColorV.w, 0.25f, 1f)); - } - - bool RicochetOnPart(Part p, RaycastHit hit, float angleFromNormal, float impactVel) - { - float hitTolerance = p.crashTolerance; - //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]: Ricochet chance: " + chance); - if (random < chance) - { - DoRicochet(p, hit, angleFromNormal); - return true; - } - else - { - return false; - } - } - - bool RicochetScenery(float hitAngle) - { - float reflectRandom = UnityEngine.Random.Range(-75f, 90f); - if (reflectRandom > 90 - hitAngle && caliber <= 30f) - { - return true; - } - - return false; - } - - public void DoRicochet(Part p, RaycastHit hit, float hitAngle) - { - //ricochet - if (BDArmorySettings.BULLET_HITS) - { - BulletHitFX.CreateBulletHit(p, hit.point, hit, hit.normal, true, caliber, 0); - } - - tracerStartWidth /= 2; - tracerEndWidth /= 2; - - transform.position = hit.point; - currentVelocity = Vector3.Reflect(currentVelocity, hit.normal); - currentVelocity = (hitAngle / 150) * currentVelocity * 0.65f; - - Vector3 randomDirection = UnityEngine.Random.rotation * Vector3.one; - - currentVelocity = Vector3.RotateTowards(currentVelocity, randomDirection, - UnityEngine.Random.Range(0f, 5f) * Mathf.Deg2Rad, 0); - } - - public void CheckPartForExplosion(Part hitPart) - { - if (!hitPart.FindModuleImplementing()) return; - - switch (hitPart.GetExplodeMode()) - { - case "Always": - CreateExplosion(hitPart); - break; - - case "Dynamic": - float probability = CalculateExplosionProbability(hitPart); - if (probability >= 3) - CreateExplosion(hitPart); - break; - - case "Never": - break; - } - } - - private float CalculateExplosionProbability(Part part) - { - /////////////////////////////////////////////////////////////// - float probability = 0; - float fuelPct = 0; - for (int i = 0; i < part.Resources.Count; i++) - { - PartResource current = part.Resources[i]; - switch (current.resourceName) - { - case "LiquidFuel": - fuelPct = (float)(current.amount / current.maxAmount); - break; - //case "Oxidizer": - // probability += (float) (current.amount/current.maxAmount); - // break; - } - } - - if (fuelPct > 0 && fuelPct <= 0.60f) - { - probability = Core.Utils.BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 5f }); - } - else - { - probability = Core.Utils.BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 2f }); - } - - if (fuelPct == 1f || fuelPct == 0f) - probability = 0f; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Explosive Probablitliy " + probability); - } - - return probability; - } - - public void CreateExplosion(Part part) - { - float explodeScale = 0; - IEnumerator resources = part.Resources.GetEnumerator(); - while (resources.MoveNext()) - { - if (resources.Current == null) continue; - switch (resources.Current.resourceName) - { - case "LiquidFuel": - explodeScale += (float)resources.Current.amount; - break; - - case "Oxidizer": - explodeScale += (float)resources.Current.amount; - break; - } - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Penetration of bullet detonated fuel!"); - } - - resources.Dispose(); - - explodeScale /= 100; - part.explosionPotential = explodeScale; - - PartExploderSystem.AddPartToExplode(part); - } - - private float GetExplosivePower() - { - return tntMass > 0 ? tntMass : blastPower; - } - } -} diff --git a/BDArmory/Bullets/PooledRocket.cs b/BDArmory/Bullets/PooledRocket.cs deleted file mode 100644 index 6dfeb5404..000000000 --- a/BDArmory/Bullets/PooledRocket.cs +++ /dev/null @@ -1,409 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using BDArmory.Bullets; -using BDArmory.Competition; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.FX; -using BDArmory.Misc; -using BDArmory.UI; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Bullets -{ - public class PooledRocket : MonoBehaviour - { - public RocketInfo rocket; //get tracers, expFX urls moved to BulletInfo - //Seeker/homing rocket code (Image-Recognition tracking?) - - public Transform spawnTransform; - public Vessel sourceVessel; - public string sourceVesselName; - - public string rocketName; - public float rocketMass; - public float caliber; - public float thrust; - private Vector3 thrustVector; - public float thrustTime; - public bool shaped; - public float maxAirDetonationRange; - public bool flak; - public float detonationRange; - public float tntMass; - public float bulletDmgMult = 1; - public float blastRadius = 0; - public float randomThrustDeviation = 0.05f; - public float massScalar = 0.012f; - public string explModelPath; - public string explSoundPath; - - float startTime; - float stayTime = 0.04f; - float lifeTime = 10; - - Vector3 prevPosition; - Vector3 currPosition; - Vector3 startPosition; - public Vector3 currentVelocity; - - private float distanceFromStart = 0; - - //bool isThrusting = true; - - Rigidbody rb; - public Rigidbody parentRB; - - KSPParticleEmitter[] pEmitters; - - float randThrustSeed; - - public AudioSource audioSource; - - void OnEnable() - { - BDArmorySetup.numberOfParticleEmitters++; - - rb = gameObject.AddOrGetComponent(); - - pEmitters = gameObject.GetComponentsInChildren(); - - using (var pe = pEmitters.AsEnumerable().GetEnumerator()) - while (pe.MoveNext()) - { - if (pe.Current == null) continue; - if (FlightGlobals.getStaticPressure(transform.position) == 0 && pe.Current.useWorldSpace) - { - pe.Current.emit = false; - } - else if (pe.Current.useWorldSpace) - { - BDAGaplessParticleEmitter gpe = pe.Current.gameObject.AddComponent(); - gpe.rb = rb; - gpe.emit = true; - } - else - { - pe.Current.emit = true; - EffectBehaviour.AddParticleEmitter(pe.Current); - } - } - - prevPosition = transform.position; - currPosition = transform.position; - startPosition = transform.position; - startTime = Time.time; - - massScalar = 0.012f / rocketMass; - - rb.mass = rocketMass; - rb.isKinematic = true; - rb.velocity = Vector3.zero; - if (!FlightGlobals.RefFrameIsRotating) rb.useGravity = false; - - rb.useGravity = false; - - randThrustSeed = UnityEngine.Random.Range(0f, 100f); - thrustVector = new Vector3(0, 0, thrust); - - SetupAudio(); - - if (this.sourceVessel) - { - var aName = this.sourceVessel.GetName(); - if (BDACompetitionMode.Instance && BDACompetitionMode.Instance.Scores.ContainsKey(aName)) - ++BDACompetitionMode.Instance.Scores[aName].shotsFired; - sourceVesselName = sourceVessel.GetName(); // Set the source vessel name as the vessel might have changed its name or died by the time the bullet hits. - } - else - { - sourceVesselName = null; - } - } - - void onDisable() - { - BDArmorySetup.OnVolumeChange -= UpdateVolume; - BDArmorySetup.numberOfParticleEmitters--; - foreach (var pe in pEmitters) - if (pe != null) - { - pe.emit = false; - EffectBehaviour.RemoveParticleEmitter(pe); - } - sourceVesselName = null; - } - - void FixedUpdate() - { - if (!gameObject.activeInHierarchy) - { - return; - } - //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) - { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; - prevPosition -= FloatingOrigin.OffsetNonKrakensbane; - startPosition -= FloatingOrigin.OffsetNonKrakensbane; - } - distanceFromStart = Vector3.Distance(transform.position, startPosition); - - if (Time.time - startTime < stayTime && transform.parent != null) - { - transform.rotation = transform.parent.rotation; - transform.position = spawnTransform.position; - //+(transform.parent.rigidbody.velocity*Time.fixedDeltaTime); - } - else - { - if (transform.parent != null && parentRB) - { - transform.parent = null; - rb.isKinematic = false; - rb.velocity = parentRB.velocity + Krakensbane.GetFrameVelocityV3f(); - } - } - - if (rb && !rb.isKinematic) - { - //physics - if (FlightGlobals.RefFrameIsRotating) - { - rb.velocity += FlightGlobals.getGeeForceAtPosition(transform.position) * Time.fixedDeltaTime; - } - - //guidance and attitude stabilisation scales to atmospheric density. - float atmosMultiplier = - Mathf.Clamp01(2.5f * - (float) - FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(transform.position), - FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody)); - - //model transform. always points prograde - transform.rotation = Quaternion.RotateTowards(transform.rotation, - Quaternion.LookRotation(rb.velocity + Krakensbane.GetFrameVelocity(), transform.up), - atmosMultiplier * (0.5f * (Time.time - startTime)) * 50 * Time.fixedDeltaTime); - - - if (Time.time - startTime < thrustTime && Time.time - startTime > stayTime) - { - thrustVector.x = randomThrustDeviation * (1 - (Mathf.PerlinNoise(4 * Time.time, randThrustSeed) * 2)) / massScalar;//this needs to scale w/ rocket mass, or light projectiles will be - thrustVector.y = randomThrustDeviation * (1 - (Mathf.PerlinNoise(randThrustSeed, 4 * Time.time) * 2)) / massScalar;//far more affected than heavier ones - rb.AddRelativeForce(thrustVector); - }//0.012/rocketmass - use .012 as baseline, it's the mass of hte hydra, which the randomTurstdeviation was originally calibrated for - } - - if (Time.time - startTime > thrustTime) - { - foreach (var pe in pEmitters) - if (pe != null) - pe.emit = false; - } - - if (Time.time - startTime > 0.1f + stayTime) - { - currPosition = transform.position; - float dist = (currPosition - prevPosition).magnitude; - Ray ray = new Ray(prevPosition, currPosition - prevPosition); - RaycastHit hit; - KerbalEVA hitEVA = null; - //if (Physics.Raycast(ray, out hit, dist, 2228224)) - //{ - // try - // { - // hitEVA = hit.collider.gameObject.GetComponentUpwards(); - // if (hitEVA != null) - // Debug.Log("[BDArmory]:Hit on kerbal confirmed!"); - // } - // catch (NullReferenceException) - // { - // Debug.Log("[BDArmory]:Whoops ran amok of the exception handler"); - // } - - // if (hitEVA && hitEVA.part.vessel != sourceVessel) - // { - // Detonate(hit.point); - // } - //} - - if (!hitEVA) //TODO: port pooledbullet's kinetic damage code, let rockets that score direct hits do ballistic damage/penetrate/report hits to score - { - if (Physics.Raycast(ray, out hit, dist, 9076737)) - { - Part hitPart = null; - try - { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - hitPart = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - } - catch (NullReferenceException) - { - } - - if (hitPart == null)//TODO - expand collision/damage code; add ability for rockets to do kinetic(bullet) damage - useful for gyrojet rounds/ fast kinetic impactor rockets, or rockets against thin-armored stuff in general - { - Detonate(hit.point, false); - } - if (hitPart != null && hitPart.vessel != sourceVessel) - { - Detonate(hit.point, false); - var aName = sourceVesselName; - var tName = hitPart.vessel.GetName(); - - if (aName != tName && BDACompetitionMode.Instance.Scores.ContainsKey(aName) && BDACompetitionMode.Instance.Scores.ContainsKey(tName)) - { - //Debug.Log("[BDArmory]: Weapon from " + aName + " damaged " + tName); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { - BDAScoreService.Instance.TrackHit(aName, tName, rocketName, distanceFromStart); - } - - // update scoring structure on attacker - { - var aData = BDACompetitionMode.Instance.Scores[aName]; - aData.Score += 1; - // keep track of who shot who for point keeping - - // competition logic for 'Pinata' mode - this means a pilot can't be named 'Pinata' - if (hitPart.vessel.GetName() == "Pinata") - { - aData.PinataHits++; - } - - } - } - } - } - else if (FlightGlobals.getAltitudeAtPos(transform.position) < 0) - { - Detonate(transform.position, false); - } - } - } - else if (FlightGlobals.getAltitudeAtPos(currPosition) <= 0) - { - Detonate(currPosition, false); - } - prevPosition = currPosition; - - if (Time.time - startTime > lifeTime) // life's 10s, quite a long time for faster rockets - { - Detonate(transform.position, true); - } - if (distanceFromStart >= maxAirDetonationRange)//rockets are performance intensive, lets cull those that have flown too far away - { - Detonate(transform.position, false); - } - if (ProximityAirDetonation(distanceFromStart)) - { - Detonate(transform.position, false); - } - } - - private bool ProximityAirDetonation(float distanceFromStart) - { - bool detonate = false; - - if (distanceFromStart <= blastRadius) return false; - - if (flak) - { - using (var hitsEnu = Physics.OverlapSphere(transform.position, detonationRange, 557057).AsEnumerable().GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - if (hitsEnu.Current == null) continue; - try - { - Part partHit = hitsEnu.Current.GetComponentInParent(); - if (partHit?.vessel != sourceVessel) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Bullet proximity sphere hit | Distance overlap = " + detonationRange + "| Part name = " + partHit.name); - return detonate = true; - } - } - catch - { - } - } - } - } - return detonate; - } - - void Update() - { - if (!gameObject.activeInHierarchy) - { - return; - } - if (HighLogic.LoadedSceneIsFlight) - { - if (BDArmorySetup.GameIsPaused) - { - if (audioSource.isPlaying) - { - audioSource.Stop(); - } - } - else - { - if (!audioSource.isPlaying) - { - audioSource.Play(); - } - } - } - } - - void Detonate(Vector3 pos, bool missed) - { - if (!missed) - { - if (tntMass > 0) - { - Vector3 direction = default(Vector3); - if (shaped) - { - direction = (pos + rb.velocity * Time.deltaTime).normalized; - } - ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, direction); - } - } // needs to be Explosiontype Bullet since missile only returns Module MissileLauncher - gameObject.SetActive(false); - } - - void SetupAudio() - { - audioSource = gameObject.AddComponent(); - audioSource.loop = true; - audioSource.minDistance = 1; - audioSource.maxDistance = 2000; - audioSource.dopplerLevel = 0.5f; - audioSource.volume = 0.9f * BDArmorySettings.BDARMORY_WEAPONS_VOLUME; - audioSource.pitch = 1f; - audioSource.priority = 255; - audioSource.spatialBlend = 1; - audioSource.clip = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/rocketLoop"); - - UpdateVolume(); - BDArmorySetup.OnVolumeChange += UpdateVolume; - } - - void UpdateVolume() - { - if (audioSource) - { - audioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; - } - } - } -} diff --git a/BDArmory/Bullets/RocketInfo.cs b/BDArmory/Bullets/RocketInfo.cs deleted file mode 100644 index fd6b75ccf..000000000 --- a/BDArmory/Bullets/RocketInfo.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace BDArmory.Bullets -{ - public class RocketInfo - { - public string name { get; private set; } - public float rocketMass { get; private set; } - public float caliber { get; private set; } - public float thrust { get; private set; } - public float thrustTime { get; private set; } - public bool shaped { get; private set; } - public bool flak { get; private set; } - public bool explosive { get; private set; } - public float tntMass { get; private set; } - public int subProjectileCount { get; private set; } - public float thrustDeviation { get; private set; } - public string rocketModelPath { get; private set; } - - public static RocketInfos rockets; - - public RocketInfo(string name, float rocketMass, float caliber, float thrust, float thrustTime, - bool shaped, bool flak, bool explosive, float tntMass, int subProjectileCount, float thrustDeviation, string rocketModelPath) - - { - this.name = name; - this.rocketMass = rocketMass; - this.caliber = caliber; - this.thrust = thrust; - this.thrustTime = thrustTime; - this.shaped = shaped; - this.flak = flak; - this.explosive = explosive; - this.tntMass = tntMass; - this.subProjectileCount = subProjectileCount; - this.thrustDeviation = thrustDeviation; - this.rocketModelPath = rocketModelPath; - } - - public static void Load() - { - if (rockets != null) return; // Only load them once on startup. - rockets = new RocketInfos(); - UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("ROCKET"); - for (int i = 0; i < nodes.Length; i++) - { - string name_ = ""; - try - { - ConfigNode node = nodes[i].config; - name_ = (string)ParseField(node, "name", typeof(string)); - rockets.Add( - new RocketInfo( - name_, - (float)ParseField(node, "rocketMass", typeof(float)), - (float)ParseField(node, "caliber", typeof(float)), - (float)ParseField(node, "thrust", typeof(float)), - (float)ParseField(node, "thrustTime", typeof(float)), - (bool)ParseField(node, "shaped", typeof(bool)), - (bool)ParseField(node, "flak", typeof(bool)), - (bool)ParseField(node, "explosive", typeof(bool)), - (float)ParseField(node, "tntMass", typeof(float)), - (int)ParseField(node, "subProjectileCount", typeof(int)), - (float)ParseField(node, "thrustDeviation", typeof(float)), - (string)ParseField(node, "rocketModelPath", typeof(string)) - ) - ); - } - catch (Exception e) - { - Debug.LogError("[BDArmory]: Error Loading Rocket Config '" + name_ + "' | " + e.ToString()); - } - } - } - - private static object ParseField(ConfigNode node, string field, Type type) - { - 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); } - } - } - - public class RocketInfos : List - { - public RocketInfo this[string name] - { - get { return Find((value) => { return value.name == name; }); } - } - } -} diff --git a/BDArmory/Competition/BDACompetitionMode.cs b/BDArmory/Competition/BDACompetitionMode.cs new file mode 100644 index 000000000..9ebaa5f99 --- /dev/null +++ b/BDArmory/Competition/BDACompetitionMode.cs @@ -0,0 +1,4428 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.GameModes; +using BDArmory.Modules; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Competition +{ + public enum CompetitionStartFailureReason { None, OnlyOneTeam, TeamsChanged, TeamLeaderDisappeared, PilotDisappeared, Other }; + public enum CompetitionType { FFA, SEQUENCED, WAYPOINTS }; + + + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class BDACompetitionMode : MonoBehaviour + { + public static BDACompetitionMode Instance; + + #region Flags and variables + // Score tracking flags and variables. + 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. + + // Action groups + public static Dictionary KM_dictAG = new Dictionary { + { 0, KSPActionGroup.None }, + { 1, KSPActionGroup.Custom01 }, + { 2, KSPActionGroup.Custom02 }, + { 3, KSPActionGroup.Custom03 }, + { 4, KSPActionGroup.Custom04 }, + { 5, KSPActionGroup.Custom05 }, + { 6, KSPActionGroup.Custom06 }, + { 7, KSPActionGroup.Custom07 }, + { 8, KSPActionGroup.Custom08 }, + { 9, KSPActionGroup.Custom09 }, + { 10, KSPActionGroup.Custom10 }, + { 11, KSPActionGroup.Light }, + { 12, KSPActionGroup.RCS }, + { 13, KSPActionGroup.SAS }, + { 14, KSPActionGroup.Brakes }, + { 15, KSPActionGroup.Abort }, + { 16, KSPActionGroup.Gear } + }; + + // Tag mode flags and variables. + public bool startTag = false; // For tag mode + public int previousNumberCompetitive = 2; // Also for tag mode + + // KILLER GM - how we look for slowest planes + public Dictionary KillTimer = new Dictionary(); // Note that this is only used as an indicator, not a controller, now. + //public Dictionary AverageSpeed = new Dictionary(); + //public Dictionary AverageAltitude = new Dictionary(); + //public Dictionary FireCount = new Dictionary(); + //public Dictionary FireCount2 = new Dictionary(); + + // pilot actions + private Dictionary pilotActions = new Dictionary(); + + #endregion + /* + #region Competition Announcer //Competition on-kill soundclips, searchtag Announcer + AudioClip headshotClip; + AudioClip 2KillClip + AudioClip 3KillClip; + AudioClip 4KillClip; + AudioClip 5KillClip; + AudioClip 6KillClip; + AudioClip 7KillClip; + AudioClip 8KillClip; + + AudioSource audioSource; + List announcerBarks; + #endregion + */ + + #region GUI elements + GUIStyle statusStyle; + GUIStyle statusStyleShadow; + Rect statusRect; + Rect statusRectShadow; + Rect clockRect; + Rect clockRectShadow; + GUIStyle dateStyle; + GUIStyle dateStyleShadow; + Rect dateRect; + Rect dateRectShadow; + Rect versionRect; + Rect versionRectShadow; + string guiStatusString; + #endregion + + void Awake() + { + if (Instance) + { + Destroy(Instance); + } + + Instance = this; + } + + void Start() + { + UpdateGUIElements(); + /* + //Announcer + headshotClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/Headshot", true); + 2KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/2Kills", true); + 3KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/3Kills", true); + 4KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/4Kills", true); + 5KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/5Kills", true); + 6KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/6Kills", true); + 7KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/7Kills", true); + 8KillClip = SoundUtils.GetAudioClip("BDArmory/Sounds/Announcer/8Kills", true); + audioSource = gameObject.AddComponent(); + announcerBarks = [2KillClip, 3KillClip, 4KillClip, 5KillClip, 6KillClip, 7KillClip, 8KillClip]; + */ + } + + void OnGUI() + { + if (BDArmorySettings.DISPLAY_COMPETITION_STATUS) + { + // Clock + if (competitionIsActive || competitionStarting) // Show a competition clock (for post-processing synchronisation). + { + var gTime = (float)(Planetarium.GetUniversalTime() - (competitionIsActive ? competitionStartTime : competitionPreStartTime)); + var minutes = Mathf.FloorToInt(gTime / 60); + var seconds = gTime % 60; + // string pTime = minutes.ToString("0") + ":" + seconds.ToString("00.00"); + string pTime = $"{minutes:0}:{seconds:00.00}"; + GUI.Label(clockRectShadow, pTime, statusStyleShadow); + GUI.Label(clockRect, pTime, statusStyle); + 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 + guiStatusString = competitionStatus.ToString(); + if (BDArmorySetup.GAME_UI_ENABLED || BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI) // Append current pilot action to guiStatusString. + { + if (competitionStarting || competitionStartTime > 0) + { + string currentVesselStatus = ""; + if (FlightGlobals.ActiveVessel != null) + { + var vesselName = FlightGlobals.ActiveVessel.GetName(); + string postFix = ""; + if (pilotActions.ContainsKey(vesselName)) + { + postFix = pilotActions[vesselName]; + } + if (Scores.Players.Contains(vesselName)) + { + ScoringData vData = Scores.ScoreData[vesselName]; + if (Planetarium.GetUniversalTime() - vData.lastDamageTime < 2) + { + postFix = " is taking damage from " + vData.lastPersonWhoDamagedMe; + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vData.lastPersonWhoDamagedMe)) + { + if (!string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) + { + postFix += " (" + BDArmorySettings.HOS_BADGE + ")"; + } + } + } + } + if (postFix != "" || vesselName != competitionStatus.lastActiveVessel) + currentVesselStatus = vesselName + postFix; + competitionStatus.lastActiveVessel = vesselName; + } + guiStatusString += (string.IsNullOrEmpty(guiStatusString) ? "" : "\n") + currentVesselStatus; + if (BDArmorySettings.RUNWAY_PROJECT) + { + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + guiStatusString += $"\nCurrent Firing Rate: {BDArmorySettings.FIRE_RATE_OVERRIDE} shots/min."; + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 67 && pinataAlive && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled) + { + double hpPercent = 1; + float DmgTaken = Scores.ScoreData[BDArmorySettings.PINATA_NAME].damageFromGuns.Values.Sum() + Scores.ScoreData[BDArmorySettings.PINATA_NAME].damageFromRockets.Values.Sum() + Scores.ScoreData[BDArmorySettings.PINATA_NAME].damageFromMissiles.Values.Sum(); + hpPercent = Mathf.Clamp((BDArmorySettings.MAX_ACTIVE_RADAR_RANGE - DmgTaken) / BDArmorySettings.MAX_ACTIVE_RADAR_RANGE, 0, 1); + if (hpPercent > 0) + { + Rect barRect = new Rect((Screen.width / 2) - (Screen.width / 6) - 5, Screen.height / 6 + 10, (Screen.width / 3) + 10, 60); + Rect healthRect = new Rect((Screen.width / 2) - ((Screen.width / 6)), (Screen.height / 6) + 5, ((Screen.width / 3) * (float)hpPercent), 50); + Color temp = XKCDColors.Grey; + GUIUtils.DrawRectangle(barRect, temp); + temp = Color.HSVToRGB((85f * (float)hpPercent) / 255, 1f, 1f); + GUIUtils.DrawRectangle(healthRect, temp); + + } + Rect labelrect = new Rect((Screen.width / 2) - 75, (Screen.height / 6) + 70, Screen.width / 3, 60); + Rect shadowRect = new Rect((labelrect.x + 1), (labelrect.y + 1), Screen.width / 3, 60); + GUI.Label(shadowRect, "Asteroid HP:" + (BDArmorySettings.MAX_ACTIVE_RADAR_RANGE - DmgTaken).ToString("0"), statusStyleShadow); + GUI.Label(labelrect, "Asteroid HP:" + (BDArmorySettings.MAX_ACTIVE_RADAR_RANGE - DmgTaken).ToString("0"), statusStyle); + } + } + } + } + if (!BDArmorySetup.GAME_UI_ENABLED) + { + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) // Don't do the ALIVE / DEAD string in continuous spawn. + { if (!BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI) guiStatusString = ""; } + else + { + if (BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI) { guiStatusString = deadOrAlive + "\n" + guiStatusString; } + else { guiStatusString = deadOrAlive; } + } + } + GUI.Label(statusRectShadow, guiStatusString, statusStyleShadow); + GUI.Label(statusRect, guiStatusString, statusStyle); + } + if (KSP.UI.Dialogs.FlightResultsDialog.isDisplaying && KSP.UI.Dialogs.FlightResultsDialog.showExitControls) // Prevent the Flight Results window from interrupting things when a certain vessel dies. + { + KSP.UI.Dialogs.FlightResultsDialog.Close(); + } + } + + public void UpdateGUIElements() + { + statusStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); + statusStyle.fontStyle = FontStyle.Bold; + statusStyle.alignment = TextAnchor.UpperLeft; + dateStyle = new GUIStyle(statusStyle); + int shadowOffset = 2; + if (BDArmorySetup.GAME_UI_ENABLED) + { + float rectOffset = Mathf.Max(100, Mathf.CeilToInt(100 * BDArmorySettings.UI_SCALE_ACTUAL)); + clockRect = new Rect(10, Mathf.CeilToInt(42 * GameSettings.UI_SCALE), rectOffset, 30); + dateRect = new Rect(rectOffset, Mathf.CeilToInt(38 * GameSettings.UI_SCALE), rectOffset, 20); + versionRect = new Rect(rectOffset * 2, Mathf.CeilToInt(46 * GameSettings.UI_SCALE), rectOffset, 20); + statusRect = new Rect(30, Mathf.CeilToInt(60 * GameSettings.UI_SCALE) + rectOffset / 5, Screen.width - 130, Mathf.FloorToInt(Screen.height / 2)); + statusStyle.fontSize = Mathf.Max(22, Mathf.CeilToInt(22 * BDArmorySettings.UI_SCALE_ACTUAL)); + dateStyle.fontSize = Mathf.Max(14, Mathf.CeilToInt(14 * BDArmorySettings.UI_SCALE_ACTUAL)); + } + else + { + float RectLength = Mathf.Max(100, Mathf.CeilToInt(100 * BDArmorySettings.UI_SCALE_ACTUAL)); + float RectHeight = Mathf.Max(20, Mathf.CeilToInt(20 * BDArmorySettings.UI_SCALE_ACTUAL)); + clockRect = new Rect(10, 6, RectLength, RectHeight); + dateRect = new Rect(10, RectHeight + 6, RectLength, RectHeight); + versionRect = new Rect(10, (RectHeight * 2) + 8, RectLength, RectHeight); + statusRect = new Rect(RectLength, 6, Screen.width - 80, Mathf.FloorToInt(Screen.height / 2)); + shadowOffset = 1; + statusStyle.fontSize = Mathf.Max(14, Mathf.CeilToInt(14 * BDArmorySettings.UI_SCALE_ACTUAL)); + dateStyle.fontSize = Mathf.Max(10, Mathf.CeilToInt(10 * BDArmorySettings.UI_SCALE_ACTUAL)); + } + clockRectShadow = new Rect(clockRect); + clockRectShadow.x += shadowOffset; + clockRectShadow.y += shadowOffset; + dateRectShadow = new Rect(dateRect); + dateRectShadow.x += shadowOffset; + dateRectShadow.y += shadowOffset; + versionRectShadow = new Rect(versionRect); + versionRectShadow.x += shadowOffset; + versionRectShadow.y += shadowOffset; + statusRectShadow = new Rect(statusRect); + statusRectShadow.x += shadowOffset; + statusRectShadow.y += shadowOffset; + statusStyleShadow = new GUIStyle(statusStyle); + statusStyleShadow.normal.textColor = new Color(0, 0, 0, 0.75f); + dateStyleShadow = new GUIStyle(dateStyle); + dateStyleShadow.normal.textColor = new Color(0, 0, 0, 0.75f); + } + + void OnDestroy() + { + StopCompetition(); + StopAllCoroutines(); + } + + #region Competition start/stop routines + //Competition mode + public bool competitionStarting = false; + public bool sequencedCompetitionStarting = false; + public bool competitionIsActive = false; + Coroutine competitionRoutine; + public CompetitionStartFailureReason competitionStartFailureReason; + + public class CompetitionStatus + { + private List> status = new List>(); + public void Add(string message) { if (BDArmorySettings.DISPLAY_COMPETITION_STATUS) { status.Add(new Tuple(Planetarium.GetUniversalTime(), message)); } } + public void Set(string message) { if (BDArmorySettings.DISPLAY_COMPETITION_STATUS) { status.Clear(); Add(message); } } + public override string ToString() + { + var now = Planetarium.GetUniversalTime(); + status = status.Where(s => now - s.Item1 < 5).ToList(); // Update the list of status messages. Only show messages for 5s. + return string.Join("\n", status.Select(s => s.Item2)); // Join them together to display them. + } + public int Count { get { return status.Count; } } + public string lastActiveVessel = ""; + } + + public CompetitionStatus competitionStatus = new CompetitionStatus(); + + bool startCompetitionNow = false; + Coroutine startCompetitionNowCoroutine; + public void StartCompetitionNow(float delay = 0) + { + startCompetitionNowCoroutine = StartCoroutine(StartCompetitionNowCoroutine(delay)); + } + IEnumerator StartCompetitionNowCoroutine(float delay = 0) // Skip the "Competition: Waiting for teams to get in position." + { + yield return new WaitForSeconds(delay); + if (competitionStarting) + { + competitionStatus.Add("No longer waiting for teams to get in position."); + startCompetitionNow = true; + } + } + + public void StartCompetitionMode(float distance, bool startDespiteFailures = false, string tag = "", CompetitionType compType = CompetitionType.FFA) + { + if (competitionStarting) return; + ResetCompetitionStuff(tag); + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Starting Competition"); + startCompetitionNow = false; + if (BDArmorySettings.GRAVITY_HACKS) + { + lastGravityMultiplier = 1f; + gravityMultiplier = 1f; + PhysicsGlobals.GraviticForceMultiplier = (double)gravityMultiplier; + 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(!hasPinata || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67)); + competitionStartFailureReason = CompetitionStartFailureReason.None; + competitionRoutine = StartCoroutine(DogfightCompetitionModeRoutine(distance, startDespiteFailures, compType)); + if (BDArmorySettings.COMPETITION_START_NOW_AFTER < 11) + { + if (BDArmorySettings.COMPETITION_START_NOW_AFTER > 5) + StartCompetitionNow((BDArmorySettings.COMPETITION_START_NOW_AFTER - 5) * 60); + else + StartCompetitionNow(BDArmorySettings.COMPETITION_START_NOW_AFTER * 10); + } + if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off) + KerbalSafetyManager.Instance.CheckAllVesselsForKerbals(); + if (BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS) + LoadedVesselSwitcher.Instance.StartVesselTracing(); + if (BDArmorySettings.AUTO_LOG_TIME_SYNC) + BDArmorySetup.Instance.SetTimeSyncLogging(true); + 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() + { + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) Scores.deathOrder.Clear(); // Clear the death order in cts spawning mode as we don't want to show it in the LVS. + 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 (BDArmorySettings.AUTO_LOG_TIME_SYNC) + BDArmorySetup.Instance.SetTimeSyncLogging(false, !string.IsNullOrEmpty(competitionTag) ? competitionTag : CompetitionID.ToString()); + if (competitionIsActive && ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + SpawnUtils.CancelSpawning(); + } + if (competitionRoutine != null) + { + StopCoroutine(competitionRoutine); + } + if (startCompetitionNowCoroutine != null) + { + StopCoroutine(startCompetitionNowCoroutine); + } + + competitionStarting = false; + competitionIsActive = false; + sequencedCompetitionStarting = false; + competitionStartTime = -1; + competitionType = CompetitionType.FFA; + competitionTag = ""; + if (PhysicsGlobals.GraviticForceMultiplier != 1) + { + lastGravityMultiplier = 1f; + gravityMultiplier = 1f; + PhysicsGlobals.GraviticForceMultiplier = (double)gravityMultiplier; + VehiclePhysics.Gravity.Refresh(); + } + GameEvents.onCollision.Remove(AnalyseCollision); + GameEvents.onVesselPartCountChanged.Remove(OnVesselModified); + GameEvents.onVesselCreate.Remove(OnVesselModified); + GameEvents.onCrewOnEva.Remove(OnCrewOnEVA); + GameEvents.onVesselCreate.Remove(DebrisDelayedCleanUp); + CometCleanup(); + rammingInformation = null; // Reset the ramming information. + deadOrAlive = ""; + if (BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS) + LoadedVesselSwitcher.Instance.StopVesselTracing(); + if (BDArmorySettings.TIME_OVERRIDE) + { Time.timeScale = 1f; } + } + + void CompetitionStarted() + { + competitionIsActive = true; //start logging ramming now that the competition has officially started + competitionStarting = false; + sequencedCompetitionStarting = false; + GameEvents.onCollision.Add(AnalyseCollision); // Start collision detection + GameEvents.onVesselCreate.Add(DebrisDelayedCleanUp); + 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 + finalGracePeriodStart = -1; + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Competition Started"); + } + + public void ResetCompetitionStuff(string tag = "", bool preSpawn = false) + { + // 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(); + nonCompetitorsToRemove.Clear(); + explodingWM.Clear(); + pilotActions.Clear(); // Clear the pilotActions, so we don't get " is Dead" on the next round of the competition. + rammingInformation = null; // Reset the ramming information. + if (BDArmorySettings.ASTEROID_FIELD) { AsteroidField.Instance.Reset(); RemoveDebrisNow(); } + 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.WAYPOINT_GUARD_INDEX < 0) ? 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; + BulletHitFX.CleanPartsOnFireInfo(); + dragLimiting.Clear(); + if (preSpawn) + { + Scores.ConfigurePlayers([]); // Clear the scores. + } + else + { + // Get a list of pilot vessels with unique names for the scoring. + var pilotVessels = GetAllPilots().Select(p => p.vessel).ToList(); + foreach (var vessel in pilotVessels) SpawnUtils.DeconflictVesselName(vessel); // Make sure the names are unique. + Scores.ConfigurePlayers(pilotVessels); // Get the competitors. + if (!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(); + GC.Collect(); // Clear out garbage at a convenient time. + } + + IEnumerator DogfightCompetitionModeRoutine(float distance, bool startDespiteFailures = false, CompetitionType compMode = CompetitionType.FFA) + { + competitionStarting = true; + competitionType = compMode; + startTag = true; // Tag entry condition, should be true even if tag is not currently enabled, so if tag is enabled later in the competition it will function + competitionStatus.Add("Competition: Pilots are taking off."); + var pilots = new Dictionary>(); + HashSet readyToLaunch = new HashSet(); + using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedVessels.MoveNext()) + { + if (loadedVessels.Current == null || !loadedVessels.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(loadedVessels.Current.vesselType)) + continue; + IBDAIControl pilot = loadedVessels.Current.ActiveController().AI; + if (pilot == null || pilot.WeaponManager == null || 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 (!pilots.TryGetValue(pilot.WeaponManager.Team, out List teamPilots)) + { + teamPilots = new List(); + pilots.Add(pilot.WeaponManager.Team, teamPilots); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adding Team " + pilot.WeaponManager.Team.Name); + } + teamPilots.Add(pilot); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adding Pilot " + pilot.vessel.GetName()); + readyToLaunch.Add(pilot); + } + + if (BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_LIST.Count > 0) + { + ConfigureMutator(); + } + foreach (var pilot in readyToLaunch) + { + pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[10]); // Modular Missiles use lower AGs (1-3) for staging, use a high AG number to not affect them + pilot.ActivatePilot(); + pilot.CommandTakeOff(); + if (pilot.WeaponManager.guardMode) + { + pilot.WeaponManager.ToggleGuardMode(); + pilot.WeaponManager.SetTarget(null); + } + 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.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 + { + SpawnUtils.ActivateAllEngines(pilot.vessel, false); + } + if (BDArmorySettings.HACK_INTAKES) SpawnUtils.HackIntakes(pilot.vessel, true); + if (BDArmorySettings.MUTATOR_MODE) SpawnUtils.ApplyMutators(pilot.vessel, true); + if (BDArmorySettings.ENABLE_HOS) SpawnUtils.ApplyHOS(pilot.vessel); + if (BDArmorySettings.RUNWAY_PROJECT) SpawnUtils.ApplyRWP(pilot.vessel); + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS) SpawnUtils.ApplyCompSettingsChecks(pilot.vessel); + /* + if (BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_LIST.Count > 0) + { + var MM = pilot.vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)pilot.vessel.rootPart.AddModule("BDAMutator"); + } + if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) //selected mutator applied globally + { + MM.EnableMutator(currentMutator); + } + if (BDArmorySettings.MUTATOR_APPLY_TIMER && !BDArmorySettings.MUTATOR_APPLY_GLOBAL) //mutator applied on a per-craft basis + { + MM.EnableMutator(); //random mutator + } + } + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Count > 0) + { + if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(pilot.vessel.GetName())) + { + using (List.Enumerator part = pilot.vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (BDArmorySettings.HOS_FIRE > 0.1f) + { + BulletHitFX.AttachFire(part.Current.transform.position, part.Current, BDArmorySettings.HOS_FIRE * 50, "GM", BDArmorySettings.COMPETITION_DURATION * 60, 1, true); + } + if (BDArmorySettings.HOS_MASS != 0) + { + var MM = part.Current.FindModuleImplementing(); + if (MM == null) + { + MM = (ModuleMassAdjust)part.Current.AddModule("ModuleMassAdjust"); + } + MM.duration = BDArmorySettings.COMPETITION_DURATION * 60; + MM.massMod += (float)(BDArmorySettings.HOS_MASS / pilot.vessel.Parts.Count); //evenly distribute mass change across entire vessel + } + if (BDArmorySettings.HOS_DMG != 1) + { + 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()) + while (engine.MoveNext()) + { + engine.Current.thrustPercentage = BDArmorySettings.HOS_THRUST; + } + } + if (!string.IsNullOrEmpty(BDArmorySettings.HOS_MUTATOR)) + { + var MM = pilot.vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)pilot.vessel.rootPart.AddModule("BDAMutator"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: adding Mutator module {pilot.vessel.vesselName}"); + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: Applying ({BDArmorySettings.HOS_MUTATOR})"); + MM.EnableMutator(BDArmorySettings.HOS_MUTATOR, true); + } + } + } + } + if (BDArmorySettings.HACK_INTAKES) + { + 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 && !SpawnUtils.IsModularMissilePart(part.Current)) //Non-MMG 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 = pilot.vessel.ActiveController().PilotAI; + 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 + BDATargetManager.ClearDatabase(); + CleanUpKSPsDeadReferences(); + RunDebugChecks(); + + if (pilots.Count < (competitionType != CompetitionType.WAYPOINTS ? 2 : 1)) + { + 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(); + yield break; + } + + var leaders = new List(); + 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(); + if (leaders.Any(leader => leader == null || leader.WeaponManager == null)) + { + 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, " + (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) + leader.WeaponManager.wingCommander.CommandAllFollow(); + + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67) + { // For S6R7 switch to piñata teams and enable guard mode prior to take-off to avoid orbiting issues. + if (!string.IsNullOrEmpty(BDArmorySettings.PINATA_NAME) && hasPinata) + { + SpawnUtils.SaveTeams(); + foreach (var pilot in GetAllPilots()) + { + if (!pilot.vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) + { + pilot.WeaponManager.SetTeam(BDTeam.Get("PinataPoppers")); + pilot.WeaponManager.guardMode = true; // Enable guard mode prior to take-off to avoid orbiting issues. + } + else + { + pilot.WeaponManager.SetTeam(BDTeam.Get("Pinata")); + } + Scores.ScoreData[pilot.vessel.vesselName].team = pilot.WeaponManager.Team.Name; + } + leaderNames = RefreshPilots(out pilots, out leaders, true); + } + } + + //wait till the leaders are ready to engage (airborne for PilotAI) + while (true) + { + if (leaders.Any(leader => leader == null || leader.WeaponManager == null)) + { + 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, " + (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); + } + + if (!(BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67)) + { // Switch to piñata teams after everyone is ready. + if (!string.IsNullOrEmpty(BDArmorySettings.PINATA_NAME) && hasPinata) + { + SpawnUtils.SaveTeams(); + foreach (var pilot in GetAllPilots()) + { + 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); + } + } + Scores.ScoreData[pilot.vessel.vesselName].team = pilot.WeaponManager.Team.Name; + } + leaderNames = RefreshPilots(out pilots, out leaders, true); + } + } + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67) startCompetitionNow = true; + + if (BDArmorySettings.ASTEROID_FIELD) { AsteroidField.Instance.SpawnField(BDArmorySettings.ASTEROID_FIELD_NUMBER, BDArmorySettings.ASTEROID_FIELD_ALTITUDE, BDArmorySettings.ASTEROID_FIELD_RADIUS, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS); } + if (BDArmorySettings.ASTEROID_RAIN) { AsteroidRain.Instance.SpawnRain(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS); } + + competitionStatus.Add("Competition: Sending pilots to start position."); + Vector3 center = Vector3.zero; + using (var leader = leaders.GetEnumerator()) + while (leader.MoveNext()) + center += leader.Current.vessel.CoM; + center /= leaders.Count; + 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 = leaders[i].vessel.ActiveController().PilotAI; // Adjust initial fly-to point for terrain and default altitudes. + 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; + } + + Vector3 centerGPS = VectorUtils.WorldPositionToGeoCoords(center, FlightGlobals.currentMainBody); + + //wait till everyone is in position + competitionStatus.Add("Competition: Waiting for teams to get in position."); + bool waiting = true; + var sqrDistance = distance * distance; + while (waiting && !startCompetitionNow) + { + waiting = false; + + if (leaders.Any(leader => leader == null || leader.WeaponManager == null)) + { + 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, " + (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; + } + } + + 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. + { + 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) + { + foreach (var otherLeader in leaders) + { + if (leader == otherLeader) + continue; + if ((leader.transform.position - otherLeader.transform.position).sqrMagnitude < sqrDistance) + waiting = true; + } + + // Increase the distance for large teams + if (!pilots.ContainsKey(leader.WeaponManager.Team)) + { + 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.FurtherFromThan(pilot.commandLeader.vessel.CoM, teamDistance)) + waiting = true; + + 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 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; + } + } + + yield return null; + } + 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"; + competitionStatus.Set("Competition: " + message); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + competitionStartFailureReason = CompetitionStartFailureReason.TeamsChanged; + StopCompetition(); + yield break; + } + foreach (var pilot in teamPilots) + if (pilot == null) + { + var message = "A pilot has disappeared from team during competition start-up, aborting"; + competitionStatus.Set("Competition: " + 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; + } + } + // Refresh teams (after the above checks) in case fighters have split off from their motherships and we now have more pilots. + leaderNames = RefreshPilots(out pilots, out leaders, true); + if (BDATargetManager.LoadedVessels.Where(v => !VesselModuleRegistry.IgnoredVesselTypes.Contains(v.vesselType)).Any(v => VesselModuleRegistry.GetModuleCount(v) > 0)) // Update RCS if any vessels have radars. + { + try + { + RadarUtils.ForceUpdateRadarCrossSections(); + } + catch (Exception e) + { + 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; + } + } + } + // Update attack point (necessary for orbit) + var allPilots = pilots.Values.SelectMany(p => p).Where(pilot => pilot != null && pilot.vessel != null && gameObject != null).ToList(); + foreach (var pilot in allPilots) center += pilot.vessel.CoM; + center /= allPilots.Count; + centerGPS = VectorUtils.WorldPositionToGeoCoords(center, FlightGlobals.currentMainBody); + + // Command attack + if (competitionType != CompetitionType.WAYPOINTS) + { + foreach (var teamPilots in pilots) + foreach (var pilot in teamPilots.Value) + { + if (pilot == null) continue; + + if (!pilot.WeaponManager.guardMode) + pilot.WeaponManager.ToggleGuardMode(); + + // foreach (var leader in leaders) + // BDATargetManager.ReportVessel(pilot.vessel, leader.WeaponManager); + + pilot.ReleaseCommand(); + pilot.CommandAttack(centerGPS); + pilot.vessel.altimeterDisplayState = AltimeterDisplayState.AGL; + } + } + competitionStatus.Add("Competition starting! Good luck!"); + CompetitionStarted(); + } + #endregion + + public List GetAllPilots() + { + var pilots = new List(); + foreach (var vessel in BDATargetManager.LoadedVessels) + { + if (vessel == null || !vessel.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType)) continue; + var pilot = vessel.ActiveController().AI; + if (pilot == null || pilot.WeaponManager == null) + { + VesselModuleRegistry.OnVesselModified(vessel, true); + pilot = vessel.ActiveController().AI; + if (pilot == null || pilot.WeaponManager == null) continue; // Unfixable, ignore the vessel. + } + if (IsValidVessel(vessel) != InvalidVesselReason.None) continue; + if (pilot.WeaponManager.Team.Neutral) continue; // Ignore the neutrals. + pilots.Add(pilot); + } + 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.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)); //no check if mutator_list contains a mutator not defined in the loaded mutatordefs + 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(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); + } + } + /* + //Announcer function for playing sequential soundclips on kill + public void PlayAnnouncer(int killcount, bool headshot, string killerVessel) + { + if (FlightGlobals.ActiveVessel.vesselName != killerVessel) return; + if (!BDArmorySettings.GG_ANNOUNCER) return; + killcount -= 1; //first bark is doublekill, adjust to account for that + if (headshot) audioSource.PlayOneShot(headshotClip); + else + { + if (killcount > announcerBarks.Count - 1) killcount = announcerBarks.Count - 1; + if (killcount >= 0) + { + if (announcerBarks[killcount] != null) audioSource.PlayOneShot(announcerBarks[killcount]); //only play barks if killsThisLife > 1 + } + } + } + */ + #region Vessel validity + public enum InvalidVesselReason { None, NullVessel, NoAI, NoWeaponManager, NoCommand }; + /// + /// Check that a vessel is valid for a competition. + /// + /// + /// + /// + public InvalidVesselReason IsValidVessel(Vessel vessel, bool attemptFix = true) + { + if (vessel == null) + return InvalidVesselReason.NullVessel; + var ac = vessel.ActiveController(); + if (ac == null || ac.WM == null) // Check for a weapon manager. + return InvalidVesselReason.NoWeaponManager; + if (ac.AI == null) // Check for an AI. + return InvalidVesselReason.NoAI; + if (attemptFix && VesselModuleRegistry.GetModuleCount(vessel) == 0 && VesselModuleRegistry.GetModuleCount(vessel) == 0) // Check for a cockpit or command seat. + CheckVesselType(vessel); // Attempt to fix it. + if (VesselModuleRegistry.GetModuleCount(vessel) == 0 && VesselModuleRegistry.GetModuleCount(vessel) == 0) // Check for a cockpit or command seat again. + return InvalidVesselReason.NoCommand; + return InvalidVesselReason.None; + } + + void OnCrewOnEVA(GameEvents.FromToAction fromToAction) + { + 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); + } + } + + public void OnVesselModified(Vessel vessel) + { + if (vessel == null) return; + VesselModuleRegistry.OnVesselModified(vessel); + CheckVesselType(vessel); + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType)) return; + if (!BDArmorySettings.AUTONOMOUS_COMBAT_SEATS) CheckForAutonomousCombatSeat(vessel); + if (BDArmorySettings.DESTROY_UNCONTROLLED_WMS) CheckForUncontrolledVessel(vessel); + if (BDArmorySettings.COMPETITION_GM_KILL_TIME > -1 && (BDArmorySettings.COMPETITION_GM_KILL_WEAPON || BDArmorySettings.COMPETITION_GM_KILL_ENGINE || BDArmorySettings.COMPETITION_GM_KILL_DISABLED || (BDArmorySettings.COMPETITION_GM_KILL_HP > 0))) CheckForGMCulling(vessel); + } + + public void CheckVesselType(Vessel vessel) + { + if (!BDArmorySettings.RUNWAY_PROJECT) return; + if (vessel != null && vessel.vesselName != null) + { + var vesselTypeIsValid = VesselModuleRegistry.ValidVesselTypes.Contains(vessel.vesselType); + if (!vesselTypeIsValid && vessel.ActiveController().WM != null) // Found an invalid vessel type with a weapon manager. + { + var message = $"Found weapon manager on {vessel.vesselName} of type {vessel.vesselType}"; + if (vessel.vesselName.EndsWith($" {vessel.vesselType}")) + vessel.vesselName = vessel.vesselName.Remove(vessel.vesselName.Length - vessel.vesselType.ToString().Length - 1); + vessel.vesselType = VesselType.Plane; + message += $", changing vessel name and type to {vessel.vesselName}, {vessel.vesselType}"; + Debug.Log("[BDArmory.BDACompetitionMode]: " + message); + return; + } + if (vesselTypeIsValid) + { + if (vessel.vesselName.EndsWith($" {vessel.vesselType}") && !Scores.Players.Contains(vessel.vesselName) && Scores.Players.Contains(vessel.vesselName.Remove(vessel.vesselName.Length - $" {vessel.vesselType}".Length)) && IsValidVessel(vessel, false) == InvalidVesselReason.None) + { + var message = $"Found a valid vessel ({vessel.vesselName}) tagged with '{vessel.vesselType}' when it shouldn't be, renaming."; + Debug.Log("[BDArmory.BDACompetitionMode]: " + message); + vessel.vesselName = vessel.vesselName.Remove(vessel.vesselName.Length - $" {vessel.vesselType}".Length); + return; + } + } + } + } + + public void CheckForAutonomousCombatSeat(Vessel vessel) + { + if (vessel == null) return; + if (VesselModuleRegistry.GetModuleCount(vessel) > 0) + { + if (vessel.parts.Count == 1) // Check for a falling combat seat. + { + Debug.Log($"[BDArmory.BDACompetitionMode]: Found a lone combat seat ({vessel.vesselName}), killing it."); + PartExploderSystem.AddPartToExplode(vessel.parts[0]); + return; + } + // Check for a lack of control. + var AI = vessel.ActiveController().AI; + if (VesselModuleRegistry.GetModuleCount(vessel) == 0 && AI != null && AI.pilotEnabled) // If not controlled by a kerbalEVA in a KerbalSeat, check the regular ModuleCommand parts. + { + 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."); + AI.DeactivatePilot(); + } + }//no srfAI/VTOLAI/OAI crew check? FIXME later + } + } + + void CheckForUncontrolledVessel(Vessel vessel) + { + if (vessel == null || vessel.vesselName == null) return; + if (vessel.ActiveController().WM == null) return; // The weapon managers are already dead. + if (vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) return; //don't delete uncontrolled pinata + // Check for partial or full control state. + foreach (var moduleCommand in VesselModuleRegistry.GetModuleCommands(vessel)) { moduleCommand.UpdateNetwork(); } + foreach (var kerbalSeat in VesselModuleRegistry.GetKerbalSeats(vessel)) { kerbalSeat.UpdateNetwork(); } + // Check for any command modules with partial or full control state + if (!VesselModuleRegistry.GetModuleCommands(vessel).Any(c => (c.UpdateControlSourceState() & (CommNet.VesselControlState.Partial | CommNet.VesselControlState.Full)) > CommNet.VesselControlState.None) + && !VesselModuleRegistry.GetKerbalSeats(vessel).Any(c => (c.GetControlSourceState() & (CommNet.VesselControlState.Partial | CommNet.VesselControlState.Full)) > CommNet.VesselControlState.None)) + { + StartCoroutine(DelayedExplodeWMs(vessel, 5f, UncontrolledReason.Uncontrolled)); // Uncontrolled vessel, destroy its weapon manager in 5s. + } + var craftbricked = VesselModuleRegistry.GetModule(vessel); + if (craftbricked != null && craftbricked.bricked) + { + StartCoroutine(DelayedExplodeWMs(vessel, 2f, UncontrolledReason.Bricked)); // Vessel fried by EMP, destroy its weapon manager in 2s. + } + } + void CheckForGMCulling(Vessel vessel) + { + if (vessel == null || vessel.vesselName == null) return; + if (BDArmorySettings.COMPETITION_GM_KILL_ENGINE) + { + if (SpawnUtils.CountActiveEngines(vessel, true) == 0) + StartCoroutine(DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " lost all engines. Terminated by GM.")); + } + if (BDArmorySettings.COMPETITION_GM_KILL_WEAPON) + { + var mf = vessel.ActiveController().WM; + if (mf != null) + { + if (!vessel.IsControllable || !mf.HasWeaponsAndAmmo()) // Check first for not controllable or no weapons or ammo + StartCoroutine(DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " lost all weapons. Terminated by GM.")); + } + } + if (BDArmorySettings.COMPETITION_GM_KILL_DISABLED) + { + var mf = vessel.ActiveController().WM; + if (mf != null) + { + if (!vessel.IsControllable || !mf.HasWeaponsAndAmmo()) // Check first for not controllable or no weapons or ammo + StartCoroutine(DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " lost all weapons or ammo. Terminated by GM.")); + else // Check for engines first, then wheels for tanks/amphibious if needed + { + if (SpawnUtils.CountActiveEngines(vessel, true) == 0) + { + var surfaceAI = vessel.ActiveController().SurfaceAI; // Get the surface AI if the vessel has one. + if (surfaceAI == null || !surfaceAI.pilotEnabled) // No engines on an AI that needs them, craft is disabled + StartCoroutine(DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " lost all engines. Terminated by GM.")); + else if ((surfaceAI.SurfaceType & AIUtils.VehicleMovementType.Land) != 0) // Check for wheels on craft capable of moving on land + { + if ((VesselModuleRegistry.GetModuleCount(vessel) + + VesselModuleRegistry.GetModuleCount(vessel, "KSPWheelBase") + + VesselModuleRegistry.GetModuleCount(vessel, "FSwheel")) == 0) + StartCoroutine(DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " lost wheels or tracks. Terminated by GM.")); + } + } + } + } + } + if (BDArmorySettings.COMPETITION_GM_KILL_HP > 0) + { + var mf = vessel.ActiveController().WM; + if (mf != null) + if (mf.currentHP / mf.totalHP * 100 < BDArmorySettings.COMPETITION_GM_KILL_HP) + StartCoroutine(DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " crippled. Terminated by GM.")); + } + } + + enum UncontrolledReason { Uncontrolled, Bricked }; + HashSet explodingWM = []; + IEnumerator DelayedExplodeWMs(Vessel vessel, float delay = 1f, UncontrolledReason reason = UncontrolledReason.Uncontrolled) + { + if (explodingWM.Contains(vessel)) yield break; // Already scheduled for exploding. + explodingWM.Add(vessel); + yield return new WaitForSecondsFixed(delay); + if (vessel == null) // It's already dead. + { + explodingWM = explodingWM.Where(v => v != null).ToHashSet(); // Clean the hashset. + yield break; + } + bool stillValid = true; + switch (reason) // Check that the reason for killing the WMs is still valid. + { + case UncontrolledReason.Uncontrolled: + if (VesselModuleRegistry.GetModuleCommands(vessel).Any(c => (c.UpdateControlSourceState() & (CommNet.VesselControlState.Partial | CommNet.VesselControlState.Full)) > CommNet.VesselControlState.None) + || VesselModuleRegistry.GetKerbalSeats(vessel).Any(c => (c.GetControlSourceState() & (CommNet.VesselControlState.Partial | CommNet.VesselControlState.Full)) > CommNet.VesselControlState.None)) // No longer uncontrolled. + { + stillValid = false; + } + break; + case UncontrolledReason.Bricked: // A craft can't recover from being bricked. + break; + } + if (stillValid) + { + // Kill off all the weapon managers. + Debug.Log("[BDArmory.BDACompetitionMode]: " + vessel.vesselName + " has no form of control, killing the weapon managers."); + foreach (var weaponManager in VesselModuleRegistry.GetMissileFires(vessel)) + { PartExploderSystem.AddPartToExplode(weaponManager.part); } + } + explodingWM.Remove(vessel); + } + + public IEnumerator DelayedGMKill(Vessel vessel, float delay, string killReason) + { + if (explodingWM.Contains(vessel)) yield break; // Already scheduled for exploding. + explodingWM.Add(vessel); + yield return new WaitForSecondsFixed(delay); + if (vessel == null) // It's already dead. + { + explodingWM = [.. explodingWM.Where(v => v != null)]; // Clean the hashset. + yield break; + } + if (killReason.Contains("engines") && SpawnUtils.CountActiveEngines(vessel, true) != 0) + { + explodingWM.Remove(vessel); //reset this so future DelayedGMKill calls don't immediately abort + yield break; //engine(s) (re)activated since delayedGMKill triggered, abort + } + + var vesselName = vessel.GetName(); + var killerName = ""; + if (Scores.Players.Contains(vesselName)) + { + killerName = Scores.ScoreData[vesselName].lastPersonWhoDamagedMe; + if (killerName == "") + { + Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = "Killed by GM"; // only do this if it's not already damaged + killerName = "Killed By GM"; + } + Scores.RegisterDeath(vesselName, GMKillReason.GM); + competitionStatus.Add(vesselName + killReason); + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + vesselName + ":REMOVED:" + killerName); + VesselUtils.ForceDeadVessel(vessel); + explodingWM.Remove(vessel); + } + + void CheckForBadlyNamedVessels() + { + foreach (var wm in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).ToList()) + if (wm != null && wm.vessel != null && wm.vessel.vesselName != null && VesselModuleRegistry.ValidVesselTypes.Contains(wm.vessel.vesselType)) + { + if (wm.vessel.vesselName.EndsWith($" {wm.vessel.vesselType}") && !Scores.Players.Contains(wm.vessel.vesselName) && Scores.Players.Contains(wm.vessel.vesselName.Remove(wm.vessel.vesselName.Length - $" {wm.vessel.vesselType}".Length)) && IsValidVessel(wm.vessel) == InvalidVesselReason.None) + { + var message = "Found a valid vessel (" + wm.vessel.vesselName + ") tagged with 'Plane' when it shouldn't be, renaming."; + Debug.Log("[BDArmory.BDACompetitionMode]: " + message); + wm.vessel.vesselName = wm.vessel.vesselName.Remove(wm.vessel.vesselName.Length - $" {wm.vessel.vesselType}".Length); + } + } + } + #endregion + + #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, string tag = "") + { + if (!BDArmorySettings.RUNWAY_PROJECT) return; + if (sequencedCompetitionStarting) return; + ResetCompetitionStuff(tag); + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Starting Rapid Deployment "); + RemoveDebrisNow(); + GameEvents.onVesselPartCountChanged.Add(OnVesselModified); + GameEvents.onVesselCreate.Add(OnVesselModified); + if (BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING) + LoadedVesselSwitcher.Instance.EnableAutoVesselSwitching(true); + if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off) + KerbalSafetyManager.Instance.CheckAllVesselsForKerbals(); + List commandSequence; + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + 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 + "0:ActionGroup:4", // t=0, AG4 - Launch: Activate base craft engine, retract airbrakes + "0:ActionGroup:13:1", // t=0, AG4 - Enable SAS + "0:SetThrottle:100", // t=0, Full throttle + "35:ActionGroup:1", // t=35, AG1 - Engine shutdown, extend airbrakes + "10:ActionGroup:2", // t=45, AG2 - Deploy fairing + "3:RemoveFairings", // t=48, Remove fairings from the game + "0:ActionGroup:3", // t=48, AG3 - Decouple base craft (-> add your custom engine activations and timers here <-) + "0:ActionGroup:12:1", // t=48, Enable RCS + "0:ActivateEngines", // t=48, Activate engines (if they're not activated by AG3) + "1:TogglePilot:1", // t=49, Activate pilots + "0:ActionGroup:16:0", // t=55, Retract gear (if it's not retracted) + "6:ToggleGuard:1", // t=55, Activate guard mode (attack) + "5:RemoveDebris", // t=60, Remove any other debris and spectators + // "0:EnableGM", // t=60, Activate the killer GM + }; + break; + case 44: //S4R4 Eve Seaplane spawn + commandSequence = new List{ + "0:ActionGroup:13:1", // t=0, AG4 - Enable SAS + "0:ActionGroup:10:1", // t=0, AG10 + "0:TogglePilot:1", // t=0, Activate pilots + "0:ActivateEngines", // t=0, Activate engines + "0:ActionGroup:16:0", // t=0, Retract gear (if it's not retracted) + "0:ToggleGuard:0", // t=0, Disable guard mode (for those who triggered it early) + "24:HackGravity:0.9", // t=24, Lower gravity to 0.9x + "2:HackGravity:0.8", // t=26, Lower gravity to 0.8x + "2:HackGravity:0.7", // t=28, Lower gravity to 0.7x + "2:HackGravity:0.6", // t=30, Lower gravity to 0.6x + "2:HackGravity:0.5", // t=32, Lower gravity to 0.5x + "2:HackGravity:0.4", // t=34, Lower gravity to 0.4x + "2:HackGravity:0.3", // t=36, Lower gravity to 0.3x + "2:HackGravity:0.2", // t=38, Lower gravity to 0.2x + "2:HackGravity:0.1", // t=40, Lower gravity to 0.1x + "5:HackGravity:0.25", //t=45, Raise gravity to 0.25x + "5:HackGravity:0.5", //t=50, Raise gravity to 0.5x + "5:HackGravity:0.75", //t=55, Raise gravity to 0.75x + "5:HackGravity:1", //t=60, Reset gravity + "0:RemoveDebris", // t=60, Remove any other debris and spectators + "5:ToggleGuard:1", // t=65, Enable guard mode + }; + break; + 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: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 67: //Asteroid Interception + 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=0, Activate pilots + "0:SetTeam:1", //t=0, Set everyone to same team + "0:ToggleGuard:1", // t=0, Activate guard mode (attack) + "5:RemoveDebris", // t=5, Remove any other debris and spectators + // "0:EnableGM", // t=60, Activate the killer GM + }; + break; + case 77: //Shuttle launch + 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:14:0", // t=0, Disable brakes + "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:AttackCenter", // t=30, "Attack" center point + "0:ToggleGuard:0", // t=0, Disable guard mode (for those who triggered it early) + "0:ToggleGuard:77", // t=30, Activate guard mode (attack) + "5:RemoveDebris", // t=35, Remove any other debris and spectators + }; + break; + default: // Same as S3R3 for now, until we do something different. + commandSequence = new List{ + "0:MassTrim", // t=0, mass trim + "0:ActionGroup:14:0", // t=0, Disable brakes + "0:ActionGroup:4", // t=0, AG4 - Launch: Activate base craft engine, retract airbrakes + "0:ActionGroup:13:1", // t=0, AG4 - Enable SAS + "0:SetThrottle:100", // t=0, Full throttle + "35:ActionGroup:1", // t=35, AG1 - Engine shutdown, extend airbrakes + "10:ActionGroup:2", // t=45, AG2 - Deploy fairing + "3:RemoveFairings", // t=48, Remove fairings from the game + "0:ActionGroup:3", // t=48, AG3 - Decouple base craft (-> add your custom engine activations and timers here <-) + "0:ActionGroup:12:1", // t=48, Enable RCS + "0:ActivateEngines", // t=48, Activate engines (if they're not activated by AG3) + "1:TogglePilot:1", // t=49, Activate pilots + "0:ActionGroup:16:0", // t=55, Retract gear (if it's not retracted) + "6:ToggleGuard:1", // t=55, Activate guard mode (attack) + "5:RemoveDebris", // t=60, Remove any other debris and spectators + // "0:EnableGM", // t=60, Activate the killer GM + }; + break; + } + competitionRoutine = StartCoroutine(SequencedCompetition(commandSequence)); + } + + private void DoPreflightChecks() + { + if (BDArmorySettings.RUNWAY_PROJECT) + { + var pilots = GetAllPilots(); + foreach (var pilot in pilots) + { + if (pilot.vessel == null) continue; + + enforcePartCount(pilot.vessel); + } + } + } + // "JetEngine", "miniJetEngine", "turboFanEngine", "turboJet", "turboFanSize2", "RAPIER" + static string[] allowedEngineList = { "JetEngine", "miniJetEngine", "turboFanEngine", "turboJet", "turboFanSize2", "RAPIER" }; + static HashSet allowedEngines = new HashSet(allowedEngineList); + + // allow duplicate landing gear + static string[] allowedDuplicateList = { "GearLarge", "GearFixed", "GearFree", "GearMedium", "GearSmall", "SmallGearBay", "fuelLine", "strutConnector" }; + static HashSet allowedLandingGear = new HashSet(allowedDuplicateList); + + // don't allow "SaturnAL31" + static string[] bannedPartList = { "SaturnAL31" }; + static HashSet bannedParts = new HashSet(bannedPartList); + + // ammo boxes + static string[] ammoPartList = { "baha20mmAmmo", "baha30mmAmmo", "baha50CalAmmo", "BDAcUniversalAmmoBox", "UniversalAmmoBoxBDA" }; + static HashSet ammoParts = new HashSet(ammoPartList); + + public void enforcePartCount(Vessel vessel) + { + if (!BDArmorySettings.RUNWAY_PROJECT) return; + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + case 18: + break; + default: + return; + } + using (List.Enumerator parts = vessel.parts.GetEnumerator()) + { + Dictionary partCounts = new Dictionary(); + List partsToKill = new List(); + List ammoBoxes = new List(); + int engineCount = 0; + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + var partName = parts.Current.name; + if (partCounts.ContainsKey(partName)) + { + partCounts[partName]++; + } + else + { + partCounts[partName] = 1; + } + if (allowedEngines.Contains(partName)) + { + engineCount++; + } + if (bannedParts.Contains(partName)) + { + partsToKill.Add(parts.Current); + } + if (allowedLandingGear.Contains(partName)) + { + // duplicates allowed + continue; + } + if (ammoParts.Contains(partName)) + { + // can only figure out limits after counting engines. + ammoBoxes.Add(parts.Current); + continue; + } + if (partCounts[partName] > 1) + { + partsToKill.Add(parts.Current); + } + } + if (engineCount == 0) + { + engineCount = 1; + } + + while (ammoBoxes.Count > engineCount * 3) + { + partsToKill.Add(ammoBoxes[ammoBoxes.Count - 1]); + ammoBoxes.RemoveAt(ammoBoxes.Count - 1); + } + if (partsToKill.Count > 0) + { + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] Vessel Breaking Part Count Rules " + vessel.GetName()); + foreach (var part in partsToKill) + { + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] KILLPART:" + part.name + ":" + vessel.GetName()); + PartExploderSystem.AddPartToExplode(part); + } + } + } + } + + private void DoRapidDeploymentMassTrim(float targetMass = 65f) + { + // in rapid deployment this verified masses etc. + var oreID = PartResourceLibrary.Instance.GetDefinition("Ore").id; + var pilots = GetAllPilots(); + var lowestMass = float.MaxValue; + var highestMass = targetMass; // Trim to highest mass or target mass, whichever is higher. + foreach (var pilot in pilots) + { + + if (pilot.vessel == null) continue; + + var notShieldedCount = 0; + using (List.Enumerator parts = pilot.vessel.parts.GetEnumerator()) + { + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + // count the unshielded parts + if (!parts.Current.ShieldedFromAirstream) + { + notShieldedCount++; + } + // Empty the ore tank and set the fuel tanks to the correct amount. + using (IEnumerator resources = parts.Current.Resources.GetEnumerator()) + while (resources.MoveNext()) + { + if (resources.Current == null) continue; + + if (resources.Current.resourceName == "Ore") + { + if (resources.Current.maxAmount == 1500) + { + resources.Current.amount = 0; + } + } + else if (resources.Current.resourceName == "LiquidFuel") + { + if (resources.Current.maxAmount == 3240) + { + resources.Current.amount = 2160; + } + } + else if (resources.Current.resourceName == "Oxidizer") + { + if (resources.Current.maxAmount == 3960) + { + resources.Current.amount = 2640; + } + } + } + } + } + var mass = pilot.vessel.GetTotalMass(); + + 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; + } + if (mass > highestMass) + { + highestMass = mass; + } + } + + foreach (var pilot in pilots) + { + if (pilot.vessel == null) continue; + var mass = pilot.vessel.GetTotalMass(); + var extraMass = highestMass - mass; + using (List.Enumerator parts = pilot.vessel.parts.GetEnumerator()) + while (parts.MoveNext()) + { + bool massAdded = false; + if (parts.Current == null) continue; + using (IEnumerator resources = parts.Current.Resources.GetEnumerator()) + while (resources.MoveNext()) + { + if (resources.Current == null) continue; + if (resources.Current.resourceName == "Ore") + { + // oreMass = 10; + // ore to add = difference / 10; + if (resources.Current.maxAmount == 1500) + { + var oreAmount = extraMass / 0.01; // 10kg per unit of ore + if (oreAmount > 1500) oreAmount = 1500; + resources.Current.amount = oreAmount; + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESOURCEUPDATE:" + pilot.vessel.GetName() + ":" + resources.Current.amount); + massAdded = true; + } + } + if (massAdded) break; + } + } + } + + IEnumerator SequencedCompetition(List commandSequence) + { + 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(); + double nextStep = startTime; + + if (BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_LIST.Count > 0) + { + ConfigureMutator(); + } + foreach (var cmdEvent in commandSequence) + { + // parse the event + competitionStatus.Add(cmdEvent); + var parts = cmdEvent.Split(':'); + if (parts.Count() == 1) + { + Debug.LogWarning("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Competition Command not parsed correctly " + cmdEvent); + StopCompetition(); + yield break; + } + var timeStep = int.Parse(parts[0]); + nextStep = Planetarium.GetUniversalTime() + timeStep; + 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]; + + switch (command) + { + case "Stage": + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Staging."); + // activate stage + foreach (var pilot in pilots) + { + VesselUtils.fireNextNonEmptyStage(pilot.vessel); + } + break; + } + case "ActionGroup": + { + if (parts.Count() < 3 || parts.Count() > 4) + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Competition Command not parsed correctly " + cmdEvent); + StopCompetition(); + yield break; + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Jiggling action group " + parts[2] + "."); + foreach (var pilot in pilots) + { + if (parts.Count() == 3) + { + pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[int.Parse(parts[2])]); + } + else if (parts.Count() == 4) + { + bool state = false; + if (parts[3] != "0") + { + state = true; + } + pilot.vessel.ActionGroups.SetGroup(KM_dictAG[int.Parse(parts[2])], state); + } + } + break; + } + case "TogglePilot": + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Toggling autopilot."); + if (parts.Count() == 3) + { + var newState = true; + if (parts[2] == "0") + { + newState = false; + } + foreach (var pilot in pilots) + { + if (newState != pilot.pilotEnabled) + pilot.TogglePilot(); + } + if (newState) + competitionStarting = true; + } + else + { + foreach (var pilot in pilots) + { + pilot.TogglePilot(); + } + } + break; + } + case "ToggleGuard": + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Toggling guard mode."); + if (parts.Count() == 3) + { + switch (parts[2]) + { + 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; + case "77": // Shuttle Launch + foreach (var pilot in pilots) + StartCoroutine(EnableGuardModeWhen(pilot, () => (pilot == null || pilot.vessel == null || pilot.vessel.radarAltitude > BDArmorySettings.GUARD_MODE_TRIGGER_ALT))); + break; + } + } + else // FIXME This branch isn't taken as all the ToggleGuard commands have 3 parts. + { + foreach (var pilot in pilots) + { + if (pilot.WeaponManager != null) + { + pilot.WeaponManager.ToggleGuardMode(); + if (!pilot.WeaponManager.guardMode) pilot.WeaponManager.SetTarget(null); + } + if (BDArmorySettings.HACK_INTAKES) SpawnUtils.HackIntakes(pilot.vessel, true); + if (BDArmorySettings.MUTATOR_MODE) SpawnUtils.ApplyMutators(pilot.vessel, true); + if (BDArmorySettings.ENABLE_HOS) SpawnUtils.ApplyHOS(pilot.vessel); + if (BDArmorySettings.RUNWAY_PROJECT) SpawnUtils.ApplyRWP(pilot.vessel); + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS) SpawnUtils.ApplyCompSettingsChecks(pilot.vessel); + /* + if (BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_LIST.Count > 0) + { + var MM = pilot.vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)pilot.vessel.rootPart.AddModule("BDAMutator"); + } + if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) //selected mutator applied globally + { + MM.EnableMutator(currentMutator); + } + if (BDArmorySettings.MUTATOR_APPLY_TIMER && !BDArmorySettings.MUTATOR_APPLY_GLOBAL) //mutator applied on a per-craft basis + { + 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())) + { + using (List.Enumerator part = pilot.vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (BDArmorySettings.HOS_FIRE > 0.1f) + { + BulletHitFX.AttachFire(part.Current.transform.position, part.Current, BDArmorySettings.HOS_FIRE * 50, "GM", BDArmorySettings.COMPETITION_DURATION * 60, 1, true); + } + if (BDArmorySettings.HOS_MASS != 0) + { + var MM = part.Current.FindModuleImplementing(); + if (MM == null) + { + MM = (ModuleMassAdjust)part.Current.AddModule("ModuleMassAdjust"); + } + MM.duration = BDArmorySettings.COMPETITION_DURATION * 60; + MM.massMod += (BDArmorySettings.HOS_MASS / pilot.vessel.Parts.Count); //evenly distribute mass change across entire vessel + } + if (BDArmorySettings.HOS_DMG != 1) + { + 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); + } + } + } + if (!string.IsNullOrEmpty(BDArmorySettings.HOS_MUTATOR)) + { + var MM = pilot.vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)pilot.vessel.rootPart.AddModule("BDAMutator"); + } + MM.EnableMutator(BDArmorySettings.HOS_MUTATOR, true); + } + } + }*/ + } + } + 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); + Vector3 attackGPS; + foreach (var pilot in pilots) + { + attackGPS = centerGPS; + var pAI = pilot.vessel.ActiveController().PilotAI; + if (pAI != null) + { + attackGPS.z = (float)BodyUtils.GetTerrainAltitudeAtPos(center) + 1000; // Target 1km above the terrain at the center. + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 77) pAI.minAltitude = 5; //set minAlt to 5 so AI doesn't go into Gaining Alt routine while below MinAlt and will maintain a stright-up course + } + pilot.ReleaseCommand(); + pilot.CommandAttack(attackGPS); + } + break; + } + case "SetTeam": + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: setting team."); + SpawnUtils.SaveTeams(); + 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")); + } + Scores.ScoreData[pilot.vessel.vesselName].team = pilot.WeaponManager.Team.Name; + } + break; + } + case "SetThrottle": + { + if (parts.Count() == 3 && pilots.Count > 1) + { + 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) + { + bool currentVesselIsActive = pilot.vessel == FlightGlobals.ActiveVessel; + if (currentVesselIsActive) LoadedVesselSwitcher.Instance.ForceSwitchVessel(someOtherVessel); // Temporarily switch away so that the throttle change works. + var throttle = int.Parse(parts[2]) * 0.01f; + pilot.vessel.ctrlState.killRot = true; + pilot.vessel.ctrlState.mainThrottle = throttle; + if (currentVesselIsActive) LoadedVesselSwitcher.Instance.ForceSwitchVessel(pilot.vessel); // Switch back again. + } + } + break; + } + case "RemoveDebris": + { + 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(); + break; + } + case "RemoveFairings": + { + 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) + { + if (phyObj.name == "FairingPanel") rmObj.Add(phyObj); + } + foreach (var phyObj in rmObj) + { + FlightGlobals.removePhysicalObject(phyObj); + } + break; + } + case "EnableGM": + { + 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(); + break; + } + case "ActivateEngines": + { + 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.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 + { + SpawnUtils.ActivateAllEngines(pilot.vessel, false); + } + if (BDArmorySettings.HACK_INTAKES) + { + SpawnUtils.HackIntakes(pilot.vessel, true); + } + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Count > 0) + { + if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(pilot.vessel.GetName())) + { + if (BDArmorySettings.HOS_THRUST != 100) + { + using (var engine = VesselModuleRegistry.GetModuleEngines(pilot.vessel).GetEnumerator()) + while (engine.MoveNext()) + { + engine.Current.thrustPercentage = BDArmorySettings.HOS_THRUST; + } + } + } + } + } + break; + } + case "MassTrim": + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Performing mass trim."); + DoRapidDeploymentMassTrim(); + break; + } + case "HackGravity": + { + if (parts.Count() == 3) + { + 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!"); + } + 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.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Unknown sequenced command: " + command + "."); + StopCompetition(); + yield break; + } + } + } + // will need a terminator routine + 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 + // obviosly this is evil. + // it's enabled by right clicking the M button. + // I also had it hooked up to the death of the Pinata but that's disconnected right now + private void FindVictim() + { + if (!BDArmorySettings.RUNWAY_PROJECT) return; + if (decisionTick < 0) return; + if (Planetarium.GetUniversalTime() < decisionTick) return; + decisionTick = BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? -1 : Planetarium.GetUniversalTime() + BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY; + if (!killerGMenabled) return; + if (Planetarium.GetUniversalTime() - competitionStartTime < BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD) return; + // arbitrary and capbricious decisions of life and death + + bool hasFired = true; + Vessel worstVessel = null; + double slowestSpeed = 100000; + int vesselCount = 0; + using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedVessels.MoveNext()) + { + if (loadedVessels.Current == null || !loadedVessels.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(loadedVessels.Current.vesselType)) + continue; + IBDAIControl pilot = loadedVessels.Current.ActiveController().AI; + if (pilot == null || !pilot.WeaponManager || pilot.WeaponManager.Team.Neutral) + continue; + + var vesselName = loadedVessels.Current.GetName(); + if (!Scores.Players.Contains(vesselName)) + continue; + + vesselCount++; + ScoringData vData = Scores.ScoreData[vesselName]; + + var averageSpeed = vData.AverageSpeed / vData.averageCount; + var averageAltitude = vData.AverageAltitude / vData.averageCount; + averageSpeed = averageAltitude + (averageSpeed * averageSpeed / 200); // kinetic & potential energy + if (pilot.WeaponManager != null) + { + if (!pilot.WeaponManager.guardMode) averageSpeed *= 0.5; + } + + bool vesselNotFired = (Planetarium.GetUniversalTime() - vData.lastFiredTime) > 120; // if you can't shoot in 2 minutes you're at the front of line + + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Victim Check " + vesselName + " " + averageSpeed.ToString() + " " + vesselNotFired.ToString()); + if (hasFired) + { + if (vesselNotFired) + { + // we found a vessel which hasn't fired + worstVessel = loadedVessels.Current; + slowestSpeed = averageSpeed; + hasFired = false; + } + else if (averageSpeed < slowestSpeed) + { + // this vessel fired but is slow + worstVessel = loadedVessels.Current; + slowestSpeed = averageSpeed; + } + } + else + { + if (vesselNotFired) + { + // this vessel was slow and hasn't fired + worstVessel = loadedVessels.Current; + slowestSpeed = averageSpeed; + } + } + } + // if we have 3 or more vessels kill the slowest + if (vesselCount > 2 && worstVessel != null) + { + var vesselName = worstVessel.GetName(); + if (Scores.Players.Contains(vesselName)) + { + Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = "GM"; + } + Scores.RegisterDeath(vesselName, GMKillReason.GM); + competitionStatus.Add(vesselName + " was killed by the GM for being too slow."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: GM killing " + vesselName + " for being too slow."); + VesselUtils.ForceDeadVessel(worstVessel); + } + ResetSpeeds(); + } + + 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 (!Scores.ScoreData.ContainsKey(weaponManager.vessel.vesselName)) continue; + 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 == "") + { + killerName = "Flew too high!"; + Scores.ScoreData[weaponManager.vessel.vesselName].lastPersonWhoDamagedMe = killerName; + } + Scores.RegisterDeath(weaponManager.vessel.vesselName, GMKillReason.GM); + competitionStatus.Add(weaponManager.vessel.vesselName + " flew too high!"); + 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); + 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 || BDArmorySettings.ALTITUDE_HACKS) // Kill off those flying too low. + { + float limit; + 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 (!Scores.ScoreData.ContainsKey(weaponManager.vessel.vesselName)) continue; + 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 == "") + { + killerName = "Flew too low!"; + Scores.ScoreData[weaponManager.vessel.vesselName].lastPersonWhoDamagedMe = killerName; + } + Scores.RegisterDeath(weaponManager.vessel.vesselName, GMKillReason.GM); + competitionStatus.Add(weaponManager.vessel.vesselName + " flew too low!"); + 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); + 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; + } + } + } + } + } + } + // 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.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] resetting kill clock"); + foreach (var player in Scores.Players) + { + if (Scores.ScoreData[player].averageCount == 0) + { + Scores.ScoreData[player].AverageAltitude = 0; + Scores.ScoreData[player].AverageSpeed = 0; + } + else + { + // ensures we always have a sensible value in here + Scores.ScoreData[player].AverageAltitude /= Scores.ScoreData[player].averageCount; + Scores.ScoreData[player].AverageSpeed /= Scores.ScoreData[player].averageCount; + Scores.ScoreData[player].averageCount = 1; + } + } + } + + List craftToCull = []; + void CullSlowWaypointRunners(double threshold) + { + //if (BDArmorySettings.WAYPOINT_GUARD_INDEX >= 0) return; + 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; + var ai = weaponManager.AI as BDGenericAIBase; + if (ai == null || !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.WAYPOINTS_MODE) message = $"{vesselName} failed to reach a waypoint within {threshold:0}s and was killed by {(BDArmorySettings.RUNWAY_PROJECT_ROUND == 55 ? "a Tusken Raider" : "the GM")}."; + 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) + { + float originalMinAlt = 200; + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 77) + { + var pAI = VesselModuleRegistry.GetBDModulePilotAI(pilot.vessel); + if (pAI != null) + originalMinAlt = pAI.minAltitude; //store the original minAlt for later + } + 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(); + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 77) + { + var pAI = VesselModuleRegistry.GetBDModulePilotAI(pilot.vessel); + if (pAI != null) + pAI.minAltitude = originalMinAlt; //combat's started, reset minAlt so craft won't crash later + } + } + } + + 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 = vessel.ActiveController().AI; + 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 + private HashSet nonCompetitorsToRemove = new HashSet(); + public void RemoveNonCompetitors(bool now = false) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null) continue; + 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; + if (vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) + { + activePilot = true; + } + else + { + int foundActiveParts = 0; // Note: this checks for exactly one of each part. + if (vessel.ActiveController().WM != null) // Has a weapon manager + { ++foundActiveParts; } + + + if (vessel.ActiveController().AI != null) // Has an AI + { ++foundActiveParts; } + + if (VesselModuleRegistry.GetModule(vessel) != null || VesselModuleRegistry.GetModule(vessel) != null) // Has a command module or command seat. + { ++foundActiveParts; } + activePilot = foundActiveParts == 3; + + if (VesselModuleRegistry.GetModule(vessel) != null) // Allow missiles. + { activePilot = true; } + } + if (!activePilot) + { + nonCompetitorsToRemove.Add(vessel); + if (vessel.vesselType == VesselType.SpaceObject) // Deal with any new comets or asteroids that have appeared immediately. + { + RemoveSpaceObject(vessel); + } + else + StartCoroutine(DelayedVesselRemovalCoroutine(vessel, now ? 0f : BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY)); + } + } + } + + public void RemoveDebrisNow() + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null) continue; + if (vessel.vesselType == VesselType.Debris) // Clean up any old debris. + StartCoroutine(DelayedVesselRemovalCoroutine(vessel, 0)); + else if (vessel.vesselType == VesselType.SpaceObject) // Remove comets and asteroids to try to avoid null refs. (Still get null refs from comets, but it seems better with this than without it.) + RemoveSpaceObject(vessel); + } + } + + public void RemoveSpaceObject(Vessel vessel) + { + StartCoroutine(DelayedVesselRemovalCoroutine(vessel, 0.1f)); // We need a small delay to make sure the new asteroids get registered if they're being used for Asteroid Rain or Asteroid Field. + } + + HashSet debrisTypes = new HashSet { VesselType.Debris, VesselType.SpaceObject }; // Consider space objects as debris. + void DebrisDelayedCleanUp(Vessel debris) + { + try + { + if (debris != null && debrisTypes.Contains(debris.vesselType)) + { + if (debris.vesselType == VesselType.SpaceObject) + RemoveSpaceObject(debris); + else + StartCoroutine(DelayedVesselRemovalCoroutine(debris, BDArmorySettings.DEBRIS_CLEANUP_DELAY)); + } + } + catch (Exception e) + { + Debug.LogError("[BDArmory.BDACompetitionMode]: Exception in DebrisDelayedCleanup: debris " + debris + " is a component? " + (debris is Component) + ", is a monobehaviour? " + (debris is MonoBehaviour) + ". Exception: " + e.GetType() + ": " + e.Message + "\n" + e.StackTrace); + } + } + + 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) + { + Debug.Log("[BDArmory.BDACompetitionMode]: Found a newly spawned " + (vessel.FindVesselModuleImplementing() != null ? "comet" : "asteroid") + " vessel! Removing it."); + RemoveSpaceObject(vessel); + } + } + + private IEnumerator DelayedVesselRemovalCoroutine(Vessel vessel, float delay) + { + var vesselType = vessel.vesselType; + yield return new WaitForSeconds(delay); + if (vessel != null && debrisTypes.Contains(vesselType) && !debrisTypes.Contains(vessel.vesselType)) + { + 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 (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); + yield break; + // var kerbalEVA = VesselModuleRegistry.GetKerbalEVA(vessel); + // if (kerbalEVA != null) + // StartCoroutine(KerbalSafetyManager.Instance.RecoverWhenPossible(kerbalEVA)); + } + else + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Removing " + vessel.vesselName); + yield return SpawnUtilsInstance.Instance.RemoveVesselCoroutine(vessel); + } + } + if (nonCompetitorsToRemove.Contains(vessel)) + { + if (BDArmorySettings.DEBUG_COMPETITION && vessel.vesselName != null) { Debug.Log($"[BDArmory.BDACompetitionMode]: {vessel.vesselName} removed."); } + nonCompetitorsToRemove.Remove(vessel); + } + } + + void DisableCometSpawning() + { + var cometManager = CometManager.Instance; + if (!cometManager.isActiveAndEnabled) return; + cometManager.spawnChance = new FloatCurve(new Keyframe[] { new Keyframe(0f, 0f), new Keyframe(1f, 0f) }); // Set the spawn chance to 0. + foreach (var comet in cometManager.DiscoveredComets) // Clear known comets. + RemoveCometVessel(comet); + foreach (var comet in cometManager.Comets) // Clear all comets. + RemoveCometVessel(comet); + } + #endregion + + void FixedUpdate() + { + s4r1FiringRateUpdatedFromShotThisFrame = false; + s4r1FiringRateUpdatedFromHitThisFrame = false; + if (competitionIsActive) + { + //Do the per-frame stuff. + LogRamming(); + // Do the lower frequency stuff. + DoUpdate(); + } + } + + // the competition update system + // cleans up dead vessels, tries to track kills (badly) + // all of these are based on the vessel name which is probably sub optimal + // This is triggered every Time.deltaTime. + HashSet vesselsToKill = new HashSet(); + HashSet alive = new HashSet(); + public void DoUpdate() + { + if (competitionStartTime < 0) return; // Note: this is the same condition as competitionIsActive and could probably be dropped. + if (competitionType == CompetitionType.WAYPOINTS && (BDArmorySettings.RUNWAY_PROJECT_ROUND != 55 && BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD <= 0 && BDArmorySettings.WAYPOINT_GUARD_INDEX < 0)) return; // Don't do anything below when running waypoints unless guardmode is set to activate at somepoint or if set to podracers (for tuskenRaider GM culling of slow pods) + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55 && 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("[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; + CheckForBadlyNamedVessels(); + double updateTickLength = BDArmorySettings.TAG_MODE ? 0.1 : BDArmorySettings.GRAVITY_HACKS ? 0.5 : 1; + vesselsToKill.Clear(); + nextUpdateTick = nextUpdateTick + updateTickLength; + int numberOfCompetitiveVessels = 0; + alive.Clear(); + string deadOrAliveString = "ALIVE: "; + // check all the planes + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType)) // || vessel.packed) // Allow packed craft to avoid the packed craft being considered dead (e.g., when command seats spawn). + continue; + + var mf = vessel.ActiveController().WM; + if (mf != null) + { + // things to check + // does it have fuel? + string vesselName = vessel.GetName(); + ScoringData vData = null; + if (Scores.Players.Contains(vesselName)) + { + vData = Scores.ScoreData[vesselName]; + } + + // this vessel really is alive + if ((vessel.vesselType != VesselType.Debris) && !vesselName.EndsWith("Debris")) // && !vesselName.EndsWith("Plane") && !vesselName.EndsWith("Probe")) + { + // vessel is still alive + alive.Add(vesselName); + deadOrAliveString += " *" + vesselName + "* "; + numberOfCompetitiveVessels++; + } + pilotActions[vesselName] = ""; + + // try to create meaningful activity strings + var ai = mf.AI; + if (ai != null && ai.currentStatus != null && BDArmorySettings.DISPLAY_COMPETITION_STATUS) + { + pilotActions[vesselName] = ""; + if (mf.vessel.LandedOrSplashed) + { + if (mf.vessel.Landed) + pilotActions[vesselName] = " is landed"; + else + pilotActions[vesselName] = " is splashed"; + } + var activity = ai.currentStatus; + if (activity == "Taking off") + pilotActions[vesselName] = " is taking off"; + else if (activity == "Follow") + { + if (ai.commandLeader != null && ai.commandLeader.vessel != null) + pilotActions[vesselName] = " is following " + ai.commandLeader.vessel.GetName(); + } + else if (activity.StartsWith("Gain Alt")) + pilotActions[vesselName] = " is gaining altitude"; + else if (activity.StartsWith("Terrain")) + pilotActions[vesselName] = " is avoiding terrain"; + else if (activity == "Orbiting") + pilotActions[vesselName] = " is orbiting"; + else if (activity == "Extending") + pilotActions[vesselName] = " is extending "; + else if (activity == "AvoidCollision") + pilotActions[vesselName] = " is avoiding collision"; + else if (activity == "Evading") + { + if (mf.incomingThreatVessel != null) + pilotActions[vesselName] = " is evading " + mf.incomingThreatVessel.GetName(); + else + pilotActions[vesselName] = " is taking evasive action"; + } + else if (activity == "Attack") + { + if (mf.currentTarget != null && mf.currentTarget.name != null) + pilotActions[vesselName] = " is attacking " + mf.currentTarget.Vessel.GetName(); + else + pilotActions[vesselName] = " is attacking"; + } + else if (activity == "Ramming Speed!") + { + if (mf.currentTarget != null && mf.currentTarget.name != null) + pilotActions[vesselName] = " is trying to ram " + mf.currentTarget.Vessel.GetName(); + else + pilotActions[vesselName] = " is in ramming speed"; + } + } + + // update the vessel scoring structure + if (vData != null) + { + var partCount = vessel.parts.Count(); + if (BDArmorySettings.RUNWAY_PROJECT) + { + if (partCount != vData.previousPartCount) + { + // part count has changed, check for broken stuff + enforcePartCount(vessel); + } + } + if (vData.previousPartCount < vessel.parts.Count) + vData.lastLostPartTime = now; + vData.previousPartCount = vessel.parts.Count; + + if (vessel.LandedOrSplashed) + { + var surfaceAI = VesselModuleRegistry.GetModule(vessel); + if (surfaceAI != null) + { + if (surfaceAI.currentStatusMode == BDModuleSurfaceAI.StatusMode.Panic) + { + if (!vData.landedState) + { + vData.lastLandedTime = now; + vData.landedState = true; + if (vData.landedKillTimer == 0) + { + vData.landedKillTimer = now; + } + } + } + else + { + if (vData.landedState) + { + vData.lastLandedTime = now; + vData.landedState = false; + } + if (vData.landedKillTimer != 0) + { + // safely mobile for 15 seconds + if (now - vData.landedKillTimer > 15) + { + vData.landedKillTimer = 0; + } + } + } + } + else + { + if (!vData.landedState) + { + // was flying, is now landed + vData.lastLandedTime = now; + vData.landedState = true; + if (vData.landedKillTimer == 0) + { + vData.landedKillTimer = now; + } + } + } + } + else + { + if (vData.landedState) + { + vData.lastLandedTime = now; + vData.landedState = false; + } + if (vData.landedKillTimer != 0) + { + // safely airborne for 15 seconds + if (now - vData.landedKillTimer > 15) + { + vData.landedKillTimer = 0; + } + } + } + } + + // after this point we're checking things that might result in kills. + if (now - competitionStartTime < BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD) continue; + + // keep track if they're shooting for the GM + if (mf.currentGun != null) + { + if (mf.currentGun.recentlyFiring) + { + // keep track that this aircraft was shooting things + if (vData != null) + { + vData.lastFiredTime = now; + } + if (mf.currentTarget != null && mf.currentTarget.Vessel != null) + { + pilotActions[vesselName] = " is shooting at " + mf.currentTarget.Vessel.GetName(); + } + } + } + // does it have ammunition: no ammo => Disable guard mode + if (!BDArmorySettings.INFINITE_AMMO) + { + if (mf.outOfAmmo && !outOfAmmo.Contains(vesselName)) // Report being out of weapons/ammo once. + { + outOfAmmo.Add(vesselName); + if (vData != null && (now - vData.lastDamageTime < 2)) + { + competitionStatus.Add(vesselName + " damaged by " + vData.lastPersonWhoDamagedMe + " and lost weapons"); + } + else + { + competitionStatus.Add(vesselName + " is out of Ammunition"); + } + } + if (mf.guardMode) // If we're in guard mode, check to see if we should disable it. + { + if (ai == null || mf.outOfAmmo && (BDArmorySettings.DISABLE_RAMMING || ai.aiType switch + { + AIType.PilotAI => !(ai as BDModulePilotAI).allowRamming, + AIType.OrbitalAI => !(ai as BDModuleOrbitalAI).allowRamming, + _ => true + })) // if we've lost the AI or the vessel is out of weapons/ammo and ramming is not allowed. + mf.guardMode = false; + } + } + + // update the vessel scoring structure + if (vData != null) + { + vData.AverageSpeed += vessel.srfSpeed; + vData.AverageAltitude += vessel.altitude; + vData.averageCount++; + if (vData.landedState && BDArmorySettings.COMPETITION_KILL_TIMER > 0) + { + 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 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)) + KillTimer.Remove(vesselName); + } + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67 && vesselName.Contains(BDArmorySettings.PINATA_NAME)) + { + if (vessel.radarAltitude <= 1000) + { + competitionStatus.Add("Failed to stop the Asteroid in time!"); + PartExploderSystem.AddPartToExplode(vessel.rootPart); + NukeFX.CreateExplosion(vessel.CoM, ExplosionSourceType.BattleDamage, "Asteroid", "Impact", 0, 5000, 20, 0, true, "BDArmory/Models/explosion/nuke/nukeBoom", "", "BDArmory/Models/explosion/nuke/nukeShock", "BDArmory/Models/explosion/nuke/nukeBlast", "", "", "", "", nukePart: vessel.rootPart); + pinataAlive = false; + if (alive.Contains(BDArmorySettings.PINATA_NAME)) alive.Remove(BDArmorySettings.PINATA_NAME); + // Don't immediately stop the competition, so that craft caught in the explosion get a chance to die. + } + } + } + } + string aliveString = string.Join(",", alive.ToArray()); + previousNumberCompetitive = numberOfCompetitiveVessels; + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] STILLALIVE: " + aliveString); // This just fills the logs needlessly. + if (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(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(BDArmorySettings.PINATA_NAME)) + { + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67) + { + competitionStatus.Add("Asteroid destroyed by " + Scores.ScoreData[BDArmorySettings.PINATA_NAME].lastPersonWhoDamagedMe + "!"); + 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 + Scores.RegisterDeath(BDArmorySettings.PINATA_NAME, GMKillReason.None, now); + foreach (string key in alive) + { + competitionStatus.Add(key + " wins the round!"); + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]:Asteroid Intercept complete, Automatically dumping scores"); + StopCompetition(); + return; + } + // switch everyone onto separate teams when the Pinata Dies + LoadedVesselSwitcher.Instance.MassTeamSwitch(originalTeams: true); + pinataAlive = false; + 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) + { + // disabled for now, should be in a competition settings UI + //killerGMenabled = true; + } + + } + + } + deadOrAliveString += " DEAD: "; + foreach (string player in Scores.Players) + { + // check everyone who's no longer alive + if (!alive.Contains(player)) + { + if (player == BDArmorySettings.PINATA_NAME) continue; + if (Scores.ScoreData[player].aliveState == AliveState.Alive) + { + var timeOfDeath = now; + // If player was involved in a collision, we need to wait until the collision is resolved before registering the death. + 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.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: + timeOfDeath = rammingInformation[player].timeOfDeath; + break; + } + Scores.RegisterDeath(player, GMKillReason.None, timeOfDeath); + pilotActions[player] = " is Dead"; + var statusMessage = player; + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(player) && !string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) + { + statusMessage += $" ({BDArmorySettings.HOS_BADGE})"; + } + switch (Scores.ScoreData[player].lastDamageWasFrom) + { + case DamageFrom.Guns: + statusMessage += " was killed by "; + break; + case DamageFrom.Rockets: + statusMessage += " was fragged by "; + break; + case DamageFrom.Missiles: + statusMessage += " was exploded by "; + break; + 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; + case DamageFrom.None: + statusMessage += $" {Scores.ScoreData[player].gmKillReason}"; + break; + } + bool canAssignMutator = true; + switch (Scores.ScoreData[player].aliveState) + { + case AliveState.CleanKill: // Damaged recently and only ever took damage from the killer. + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(Scores.ScoreData[player].lastPersonWhoDamagedMe) && !string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) + { + statusMessage += Scores.ScoreData[player].lastPersonWhoDamagedMe + " (" + BDArmorySettings.HOS_BADGE + ")" + " (NAILED 'EM! CLEAN KILL!)"; + } + else + { + statusMessage += Scores.ScoreData[player].lastPersonWhoDamagedMe + " (NAILED 'EM! CLEAN KILL!)"; + } + //canAssignMutator = true; + break; + case AliveState.HeadShot: // Damaged recently, but took damage a while ago from someone else. + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(Scores.ScoreData[player].lastPersonWhoDamagedMe) && !string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) + { + statusMessage += Scores.ScoreData[player].lastPersonWhoDamagedMe + " (" + BDArmorySettings.HOS_BADGE + ")" + " (BOOM! HEAD SHOT!)"; + } + else + { + statusMessage += Scores.ScoreData[player].lastPersonWhoDamagedMe + " (BOOM! HEAD SHOT!)"; + } + //canAssignMutator = true; + break; + case AliveState.KillSteal: // Damaged recently, but took damage from someone else recently too. + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(Scores.ScoreData[player].lastPersonWhoDamagedMe) && !string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) + { + statusMessage += Scores.ScoreData[player].lastPersonWhoDamagedMe + " (" + BDArmorySettings.HOS_BADGE + ")" + " (KILL STEAL!)"; + } + else + { + statusMessage += Scores.ScoreData[player].lastPersonWhoDamagedMe + " (KILL STEAL!)"; + } + //canAssignMutator = true; + break; + case AliveState.AssistedKill: // Assist (not damaged recently or GM kill). + if (Scores.ScoreData[player].gmKillReason != GMKillReason.None) Scores.ScoreData[player].everyoneWhoDamagedMe.Add(Scores.ScoreData[player].gmKillReason.ToString()); // Log the GM kill reason. + //canAssignMutator = false; //comment out if wanting last person to deal damage to be awarded a On Kill mutator + if (Scores.ScoreData[player].gmKillReason != GMKillReason.None) // Note: LandedTooLong is handled separately. + canAssignMutator = false; //GM kill, no mutator, else award last player to deal damage + statusMessage += string.Join(", ", Scores.ScoreData[player].everyoneWhoDamagedMe) + " (" + string.Join(", ", Scores.ScoreData[player].damageTypesTaken) + ")"; + break; + case AliveState.Dead: // Suicide/Incompetance (never took damage from others). + canAssignMutator = false; + break; + } + competitionStatus.Add(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) ApplyOnKillMutator(player); + else Debug.Log($"[BDArmory.BDACompetitionMode]: Mutator mode, but no assigned mutators! Can't apply mutator on Kill!"); + } + } + deadOrAliveString += " :" + player + ": "; + } + } + deadOrAlive = deadOrAliveString; + + var numberOfCompetitiveTeams = LoadedVesselSwitcher.Instance.WeaponManagers.Count; + if (now - competitionStartTime > BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD && (numberOfCompetitiveVessels < competitiveTeamsAliveLimit || (!BDArmorySettings.TAG_MODE && numberOfCompetitiveTeams < competitiveTeamsAliveLimit)) && !ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + if (finalGracePeriodStart < 0) + finalGracePeriodStart = now; + bool runningWPs = false; + if (BDArmorySettings.WAYPOINTS_MODE) + { + using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedVessels.MoveNext()) + { + if (loadedVessels.Current == null || !loadedVessels.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(loadedVessels.Current.vesselType)) // || vessel.packed) // Allow packed craft to avoid the packed craft being considered dead (e.g., when command seats spawn). + continue; + IBDAIControl pilot = loadedVessels.Current.ActiveController().AI; + if (pilot == null || !pilot.WeaponManager || pilot.WeaponManager.Team.Neutral) continue; + if (((BDGenericAIBase)pilot).IsRunningWaypoints) + { + runningWPs = true; + break;//if only one craft left, but WP mode and craft still running WPs, don't prematurely exit + } + } + } + if (!runningWPs && BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD <= 60 && now - finalGracePeriodStart > BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD) + { + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67) + { + foreach (var vessel in FlightGlobals.Vessels) + { + Scores.RegisterDeath(vessel.vesselName, GMKillReason.GM, now); + VesselUtils.ForceDeadVessel(vessel); + } + alive.Clear(); + } + competitionStatus.Add("All Pilots are Dead"); + foreach (string key in alive) + { + competitionStatus.Add(key + " wins the round!"); + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]:No viable competitors, Automatically dumping scores"); + StopCompetition(); + return; + } + } + + //Reset gravity + if (BDArmorySettings.GRAVITY_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; + gravityMultiplier = 1f + 7f * (float)(Scores.deathCount % maxVesselsActive) / (float)(maxVesselsActive - 1); // From 1G to 8G. + 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. + { + lastGravityMultiplier = gravityMultiplier; + competitionStatus.Add("Competition: Adjusting gravity to " + gravityMultiplier.ToString("0.0") + "G!"); + } + } + + //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 = pilots.Current.vessel.ActiveController().PilotAI; // Get the pilot AI if the vessel has one. + if (pilotAI != null) 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) + { + var vesselName = vessel.GetName(); + var killerName = ""; + if (Scores.Players.Contains(vesselName)) + { + killerName = Scores.ScoreData[vesselName].lastPersonWhoDamagedMe; + if (killerName == "") + { + Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = "Landed Too Long"; // only do this if it's not already damaged + killerName = "Landed Too Long"; + } + Scores.RegisterDeath(vesselName, GMKillReason.LandedTooLong); + competitionStatus.Add(vesselName + " was landed too long."); + if (BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_APPLY_KILL && BDArmorySettings.MUTATOR_LIST.Count > 0) + ApplyOnKillMutator(vesselName); // Apply mutators for LandedTooLong kills, which count as assists. + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + vesselName + ":REMOVED:" + killerName); + if (KillTimer.ContainsKey(vesselName)) KillTimer.Remove(vesselName); + VesselUtils.ForceDeadVessel(vessel); + } + + if (!(BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY > 60)) + RemoveNonCompetitors(); + + 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(); + } + // Debug.Log("[BDArmory.BDACompetitionMode" + CompetitionID.ToString() + "]: Done With Update"); + + if (BDArmorySettings.COMPETITION_DURATION > 0 && now - competitionStartTime >= BDArmorySettings.COMPETITION_DURATION * 60d) + { + 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(StringUtils.Localize("#LOC_BDArmory_UI_MutatorShuffle"), 5, ScreenMessageStyle.UPPER_CENTER); + ConfigureMutator(); + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType)) // || vessel.packed) // Allow packed craft to avoid the packed craft being considered dead (e.g., when command seats spawn). + continue; + + var mf = vessel.ActiveController().WM; + if (mf != null) + { + var MM = vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)vessel.rootPart.AddModule("BDAMutator"); + } + if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) + { + MM.EnableMutator(currentMutator); + } + else + { + MM.EnableMutator(); + } + } + } + } + } + + void ApplyOnKillMutator(string player) + { + using var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator(); + while (loadedVessels.MoveNext()) + { + if (loadedVessels.Current == null || !loadedVessels.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(loadedVessels.Current.vesselType)) + continue; + var craftName = loadedVessels.Current.GetName(); + if (!Scores.Players.Contains(craftName)) continue; + if (BDArmorySettings.MUTATOR_APPLY_GUNGAME && Scores.ScoreData[player].aliveState == AliveState.AssistedKill && Scores.ScoreData[player].everyoneWhoDamagedMe.Contains(craftName)) + SpawnUtils.ApplyMutators(loadedVessels.Current, true); // Reward everyone involved on assists. + else if (Scores.ScoreData[player].lastPersonWhoDamagedMe == craftName || (BDArmorySettings.MUTATOR_APPLY_GUNGAME && Scores.ScoreData[player].aliveState == AliveState.KillSteal && Scores.ScoreData[player].previousPersonWhoDamagedMe == craftName)) + SpawnUtils.ApplyMutators(loadedVessels.Current, true); // Reward clean kills and those whom have had their kills stolen. + else continue; + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: Assigning On Kill mutator for {player} to {craftName}"); + } + } + + // This now also writes the competition logs to GameData/BDArmory/Logs/[-tag].log + public void LogResults(string message = "", string tag = "") + { + if (competitionStartTime < 0) + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: No active competition, not dumping results."); + return; + } + // RunDebugChecks(); + // CheckMemoryUsage(); + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) // Dump continuous spawning scores instead. + { + ContinuousSpawning.Instance.DumpContinuousSpawningScores(tag); + return; + } + + if (BDArmorySettings.DEBUG_COMPETITION) competitionStatus.Add("Dumping scores for competition " + CompetitionID.ToString() + (tag != "" ? " " + tag : "")); + Scores.LogResults(CompetitionID.ToString(), message, tag); + } + + #region Ramming + // Ramming Logging + public class RammingTargetInformation + { + public Vessel vessel; // The other vessel involved in a collision. + public double lastUpdateTime = 0; // Last time the timeToCPA was updated. + public float timeToCPA = 0f; // Time to closest point of approach. + public bool potentialCollision = false; // Whether a collision might happen shortly. + public double potentialCollisionDetectionTime = 0; // The latest time the potential collision was detected. + public int partCountJustPriorToCollision; // The part count of the colliding vessel just prior to the collision. + public float sqrDistance; // Distance^2 at the time of collision. + public float angleToCoM = 0f; // The angle from a vessel's velocity direction to the center of mass of the target. + public bool collisionDetected = false; // Whether a collision has actually been detected. + public double collisionDetectedTime; // The time that the collision actually occurs. + public bool ramming = false; // True if a ram was attempted between the detection of a potential ram and the actual collision. + }; + public class RammingInformation + { + public Vessel vessel; // This vessel. + public string vesselName; // The GetName() name of the vessel (in case vessel gets destroyed and we can't get it from there). + public int partCount; // The part count of a vessel. + public float radius; // The vessels "radius" at the time the potential collision was detected. + public double timeOfDeath = -1; // The time of death of a vessel, for keeping track of when it died. + public Dictionary targetInformation; // Information about the ramming target. + }; + public Dictionary rammingInformation; + + // Initialise the rammingInformation dictionary with the required vessels. + public void InitialiseRammingInformation() + { + rammingInformation = []; + var pilots = GetAllPilots(); + foreach (var pilot in pilots) + { + //if (pilot as BDModulePilotAI == null && pilot as BDModuleOrbitalAI == null) continue; // Ignore those without valid AIs. + var targetRammingInformation = new Dictionary(); + foreach (var otherPilot in pilots) + { + if (otherPilot == pilot) continue; // Don't include same-vessel information. + //if (otherPilot as BDModulePilotAI == null && otherPilot as BDModuleOrbitalAI == null) continue; // Ignore those without valid AIs. + targetRammingInformation.Add(otherPilot.vessel.vesselName, new RammingTargetInformation { vessel = otherPilot.vessel }); + } + rammingInformation.Add(pilot.vessel.vesselName, new RammingInformation + { + vessel = pilot.vessel, + vesselName = pilot.vessel.GetName(), + partCount = pilot.vessel.parts.Count, + radius = pilot.vessel.GetRadius(), + targetInformation = targetRammingInformation, + }); + } + } + + bool GetAIRammingState(IBDAIControl AI) + { + var pilotAI = AI as BDModulePilotAI; + if (pilotAI != null) return pilotAI.ramming; + var orbitalAI = AI as BDModuleOrbitalAI; + if (orbitalAI != null) return orbitalAI.currentStatusMode == BDModuleOrbitalAI.StatusMode.Ramming; + return false; // The other AIs don't have ramming modes currently. + } + + /// + /// Add a vessel to the rammingInformation datastructure after a competition has started. + /// + /// + public void AddPlayerToRammingInformation(Vessel vessel) + { + if (rammingInformation == null) return; // Not set up yet. + if (!rammingInformation.ContainsKey(vessel.vesselName)) // Vessel information hasn't been added to rammingInformation datastructure yet. + { + rammingInformation.Add(vessel.vesselName, new RammingInformation { vesselName = vessel.vesselName, targetInformation = new Dictionary() }); + foreach (var otherVesselName in rammingInformation.Keys) + { + if (otherVesselName == vessel.vesselName) continue; + rammingInformation[vessel.vesselName].targetInformation.Add(otherVesselName, new RammingTargetInformation { vessel = rammingInformation[otherVesselName].vessel }); + } + } + // Create or update ramming information for the vesselName. + rammingInformation[vessel.vesselName].vessel = vessel; + rammingInformation[vessel.vesselName].partCount = vessel.parts.Count; + rammingInformation[vessel.vesselName].radius = vessel.GetRadius(); + // Update each of the other vesselNames in the rammingInformation. + foreach (var otherVesselName in rammingInformation.Keys) + { + if (otherVesselName == vessel.vesselName) continue; + rammingInformation[otherVesselName].targetInformation[vessel.vesselName] = new RammingTargetInformation { vessel = vessel }; + } + } + + /// + /// Remove a vessel from the rammingInformation datastructure after a competition has started. + /// + /// + public void RemovePlayerFromRammingInformation(string player) + { + if (rammingInformation == null) return; // Not set up yet. + if (!rammingInformation.ContainsKey(player)) return; // Player isn't in the ramming information + rammingInformation.Remove(player); // Remove the player. + foreach (var otherVesselName in rammingInformation.Keys) // Remove the player's target information from the other players. + { + if (rammingInformation[otherVesselName].targetInformation.ContainsKey(player)) // It should unless something has gone wrong. + { rammingInformation[otherVesselName].targetInformation.Remove(player); } + } + } + + // Update the ramming information dictionary with expected times to closest point of approach. + private float maxTimeToCPA = 5f; // Don't look more than 5s ahead. + public void UpdateTimesToCPAs() + { + double currentTime = Planetarium.GetUniversalTime(); + foreach (var vesselName in rammingInformation.Keys) + { + var vessel = rammingInformation[vesselName].vessel; + var AI = vessel == null ? null : vessel.ActiveController().AI; // Get the pilot AI if the vessel has one. + + foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) + { + var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; + var otherAI = otherVessel == null ? null : otherVessel.ActiveController().AI; // Get the pilot AI if the vessel has one. + if (AI == null || otherAI == null) // One of the vessels or pilot AIs has been destroyed. + { + rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA = maxTimeToCPA; // Set the timeToCPA to maxTimeToCPA, so that it's not considered for new potential collisions. + rammingInformation[otherVesselName].targetInformation[vesselName].timeToCPA = maxTimeToCPA; // Set the timeToCPA to maxTimeToCPA, so that it's not considered for new potential collisions. + } + else + { + if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].lastUpdateTime > rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA / 2f) // When half the time is gone, update it. + { + 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. + rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA = maxTimeToCPA; + // This is symmetric, so update the symmetric value and set the lastUpdateTime for both so that we don't bother calculating the same thing twice. + rammingInformation[otherVesselName].targetInformation[vesselName].timeToCPA = rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA; + rammingInformation[vesselName].targetInformation[otherVesselName].lastUpdateTime = currentTime; + rammingInformation[otherVesselName].targetInformation[vesselName].lastUpdateTime = currentTime; + } + } + } + } + } + + // Get the upcoming collisions ordered by predicted separation^2 (for Scott to adjust camera views). + public IOrderedEnumerable, Tuple>> UpcomingCollisions(float distanceThreshold, bool sortByDistance = true) + { + var upcomingCollisions = new Dictionary, Tuple>(); + if (rammingInformation != null) + foreach (var vesselName in rammingInformation.Keys) + foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) + if (rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision && rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA < maxTimeToCPA && string.Compare(vesselName, otherVesselName) < 0) + if (rammingInformation[vesselName].vessel != null && rammingInformation[otherVesselName].vessel != null) + { + var predictedSqrSeparation = Vector3.SqrMagnitude(rammingInformation[vesselName].vessel.CoM - rammingInformation[otherVesselName].vessel.CoM); + if (predictedSqrSeparation < distanceThreshold * distanceThreshold) + upcomingCollisions.Add( + new Tuple(vesselName, otherVesselName), + new Tuple(predictedSqrSeparation, rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA) + ); + } + return upcomingCollisions.OrderBy(d => sortByDistance ? d.Value.Item1 : d.Value.Item2); + } + + // Check for potential collisions in the near future and update data structures as necessary. + private float potentialCollisionDetectionTime = 1f; // 1s ought to be plenty. + private void CheckForPotentialCollisions() + { + float collisionMargin = 4f; // Sum of radii is less than this factor times the separation. + double currentTime = Planetarium.GetUniversalTime(); + foreach (var vesselName in rammingInformation.Keys) + { + var vessel = rammingInformation[vesselName].vessel; + foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) + { + if (!rammingInformation.ContainsKey(otherVesselName)) + { + Debug.Log("[BDArmory.BDACompetitionMode]: other vessel (" + otherVesselName + ") is missing from rammingInformation!"); + return; + } + if (!rammingInformation[vesselName].targetInformation.ContainsKey(otherVesselName)) + { + Debug.Log("[BDArmory.BDACompetitionMode]: other vessel (" + otherVesselName + ") is missing from rammingInformation[vessel].targetInformation!"); + return; + } + if (!rammingInformation[otherVesselName].targetInformation.ContainsKey(vesselName)) + { + Debug.Log("[BDArmory.BDACompetitionMode]: vessel (" + vesselName + ") is missing from rammingInformation[otherVessel].targetInformation!"); + return; + } + var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; + if (rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA < potentialCollisionDetectionTime) // Closest point of approach is within the detection time. + { + if (vessel != null && otherVessel != null) // If one of the vessels has been destroyed, don't calculate new potential collisions, but allow the timer on existing potential collisions to run out so that collision analysis can still use it. + { + var separation = Vector3.Magnitude(vessel.transform.position - otherVessel.transform.position); + if (separation < collisionMargin * (vessel.GetRadius() + otherVessel.GetRadius())) // Potential collision detected. + { + if (!rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision) // Register the part counts and angles when the potential collision is first detected. + { // Note: part counts and vessel radii get updated whenever a new potential collision is detected, but not angleToCoM (which is specific to a colliding pair). + rammingInformation[vesselName].partCount = vessel.parts.Count; + rammingInformation[otherVesselName].partCount = otherVessel.parts.Count; + rammingInformation[vesselName].radius = vessel.GetRadius(); + rammingInformation[otherVesselName].radius = otherVessel.GetRadius(); + rammingInformation[vesselName].targetInformation[otherVesselName].angleToCoM = VectorUtils.Angle(vessel.srf_vel_direction, otherVessel.CoM - vessel.CoM); + rammingInformation[otherVesselName].targetInformation[vesselName].angleToCoM = VectorUtils.Angle(otherVessel.srf_vel_direction, vessel.CoM - otherVessel.CoM); + } + + // Update part counts if vessels get shot and potentially lose parts before the collision happens. + if (!Scores.Players.Contains(rammingInformation[vesselName].vesselName)) CheckVesselType(rammingInformation[vesselName].vessel); // It may have become a "vesselName Plane" if the WM is badly placed. + try + { + if (Scores.ScoreData[rammingInformation[vesselName].vesselName].lastDamageWasFrom != DamageFrom.Ramming && Scores.ScoreData[rammingInformation[vesselName].vesselName].lastDamageTime > rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollisionDetectionTime) + { + if (rammingInformation[vesselName].partCount != vessel.parts.Count) + { + 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; + } + } + if (!Scores.Players.Contains(rammingInformation[otherVesselName].vesselName)) CheckVesselType(rammingInformation[otherVesselName].vessel); // It may have become a "vesselName Plane" if the WM is badly placed. + if (Scores.ScoreData[rammingInformation[otherVesselName].vesselName].lastDamageWasFrom != DamageFrom.Ramming && Scores.ScoreData[rammingInformation[otherVesselName].vesselName].lastDamageTime > rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime) + { + if (rammingInformation[otherVesselName].partCount != otherVessel.parts.Count) + { + 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; + } + } + } + catch (KeyNotFoundException e) + { + List badVesselNames = new List(); + if (!Scores.Players.Contains(rammingInformation[vesselName].vesselName)) badVesselNames.Add(rammingInformation[vesselName].vesselName); + if (!Scores.Players.Contains(rammingInformation[otherVesselName].vesselName)) badVesselNames.Add(rammingInformation[otherVesselName].vesselName); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: A badly named vessel is messing up the collision detection: " + string.Join(", ", badVesselNames) + " | " + e.Message); + } + + // Set the potentialCollision flag to true and update the latest potential collision detection time. + rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision = true; + rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime = currentTime; + rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollision = true; + rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollisionDetectionTime = currentTime; + + // Register intent to ram. + var AI = vessel.ActiveController().AI; + rammingInformation[vesselName].targetInformation[otherVesselName].ramming |= GetAIRammingState(AI); // The AI is alive and trying to ram. + var otherAI = otherVessel.ActiveController().AI; + rammingInformation[otherVesselName].targetInformation[vesselName].ramming |= GetAIRammingState(otherAI); // The other AI is alive and trying to ram. + } + } + } + else if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime > 2f * potentialCollisionDetectionTime) // Potential collision is no longer relevant. + { + rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision = false; + rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollision = false; + } + } + } + } + + // Analyse a collision to figure out if someone rammed someone else and who should get awarded for it. + private void AnalyseCollision(EventReport data) + { + if (rammingInformation == null) return; // Ramming information hasn't been set up yet (e.g., between competitions). + if (data.origin == null) return; // The part is gone. Nothing much we can do about it. + double currentTime = Planetarium.GetUniversalTime(); + float collisionMargin = 2f; // Compare the separation to this factor times the sum of radii to account for inaccuracies in the vessels size and position. Hopefully, this won't include other nearby vessels. + var vessel = data.origin.vessel; + if (vessel == null) // Can vessel be null here? It doesn't appear so. + { + 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. + 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, + { + 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 (!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; + } + } + 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. + { + 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.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. + rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollision = false; // Set potential collisions to false. + } + } + } + } + catch (Exception e) + { + Debug.LogError("[BDArmory.BDACompetitionMode]: Exception in AnalyseCollision: " + e.Message + "\n" + e.StackTrace); + try { Debug.Log("[BDArmory.DEBUG] rammingInformation is null? " + (rammingInformation == null)); } catch (Exception e2) { Debug.Log("[BDArmory.DEBUG]: rammingInformation: " + e2.Message); } + try { Debug.Log("[BDArmory.DEBUG] rammingInformation[vesselName] is null? " + (rammingInformation[vessel.vesselName] == null)); } catch (Exception e2) { Debug.Log("[BDArmory.DEBUG]: rammingInformation[vesselName]: " + e2.Message); } + try { Debug.Log("[BDArmory.DEBUG] rammingInformation[vesselName].targetInformation is null? " + (rammingInformation[vessel.vesselName].targetInformation == null)); } catch (Exception e2) { Debug.Log("[BDArmory.DEBUG]: rammingInformation[vesselName].targetInformation: " + e2.Message); } + try + { + foreach (var otherVesselName in rammingInformation[vessel.vesselName].targetInformation.Keys) + { + try { Debug.Log("[BDArmory.DEBUG] rammingInformation[" + vessel.vesselName + "].targetInformation[" + otherVesselName + "] is null? " + (rammingInformation[vessel.vesselName].targetInformation[otherVesselName] == null)); } catch (Exception e2) { Debug.Log("[BDArmory.DEBUG]: rammingInformation[" + vessel.vesselName + "].targetInformation[" + otherVesselName + "]: " + e2.Message); } + try { Debug.Log("[BDArmory.DEBUG] rammingInformation[" + otherVesselName + "].targetInformation[" + vessel.vesselName + "] is null? " + (rammingInformation[otherVesselName].targetInformation[vessel.vesselName] == null)); } catch (Exception e2) { Debug.Log("[BDArmory.DEBUG]: rammingInformation[" + otherVesselName + "].targetInformation[" + vessel.vesselName + "]: " + e2.Message); } + } + } + catch (Exception e3) + { + Debug.Log("[BDArmory.DEBUG]: " + e3.Message); + } + } + } + + 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 (rammingInformation == null) // The competition is finished / KSP is changing scenes or exiting. + { + asteroidCollisions.Remove(vesselName); + yield break; + } + if (vessel == null || vessel.ActiveController().WM == 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() + { + double currentTime = Planetarium.GetUniversalTime(); + float headOnLimit = 20f; + var collidingVesselsBySeparation = new Dictionary>>>(); + + // First, we're going to filter the potentially colliding vessels and sort them by separation. + foreach (var vesselName in rammingInformation.Keys) + { + var vessel = rammingInformation[vesselName].vessel; + var collidingVesselDistances = new Dictionary(); + + // For each potential collision that we've waited long enough for, refine the potential colliding vessels. + foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) + { + if (!rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. + if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime > potentialCollisionDetectionTime) // We've waited long enough for the parts that are going to explode to explode. + { + // First, check the vessels marked as colliding with this vessel for lost parts. If at least one other lost parts or was destroyed, exclude any that didn't lose parts (set collisionDetected to false). + bool someOneElseLostParts = false; + foreach (var tmpVesselName in rammingInformation[vesselName].targetInformation.Keys) + { + if (!rammingInformation[vesselName].targetInformation[tmpVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. + var tmpVessel = rammingInformation[vesselName].targetInformation[tmpVesselName].vessel; + if (tmpVessel == null || rammingInformation[vesselName].targetInformation[tmpVesselName].partCountJustPriorToCollision - tmpVessel.parts.Count > 0) + { + someOneElseLostParts = true; + break; + } + } + if (someOneElseLostParts) // At least one other vessel lost parts or was destroyed. + { + foreach (var tmpVesselName in rammingInformation[vesselName].targetInformation.Keys) + { + if (!rammingInformation[vesselName].targetInformation[tmpVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. + var tmpVessel = rammingInformation[vesselName].targetInformation[tmpVesselName].vessel; + if (tmpVessel != null && rammingInformation[vesselName].targetInformation[tmpVesselName].partCountJustPriorToCollision == tmpVessel.parts.Count) // Other vessel didn't lose parts, mark it as not involved in this collision. + { + rammingInformation[vesselName].targetInformation[tmpVesselName].collisionDetected = false; + rammingInformation[tmpVesselName].targetInformation[vesselName].collisionDetected = false; + } + } + } // Else, the collided with vessels didn't lose any parts, so we don't know who this vessel really collided with. + + // If the other vessel is still a potential collider, add it to the colliding vessels dictionary with its distance to this vessel. + if (rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) + collidingVesselDistances.Add(otherVesselName, rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance); + } + } + + // If multiple vessels are involved in a collision with this vessel, the lost parts counts are going to be skewed towards the first vessel processed. To counteract this, we'll sort the colliding vessels by their distance from this vessel. + var collidingVessels = collidingVesselDistances.OrderBy(d => d.Value); + if (collidingVesselDistances.Count > 0) + collidingVesselsBySeparation.Add(vesselName, new KeyValuePair>>(collidingVessels.First().Value, collidingVessels)); + + 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); + } + } + var sortedCollidingVesselsBySeparation = collidingVesselsBySeparation.OrderBy(d => d.Value.Key); // Sort the outer dictionary by minimum separation from the nearest colliding vessel. + + // Then we're going to try to figure out who should be awarded the ram. + foreach (var vesselNameKVP in sortedCollidingVesselsBySeparation) + { + var vesselName = vesselNameKVP.Key; + var vessel = rammingInformation[vesselName].vessel; + foreach (var otherVesselNameKVP in vesselNameKVP.Value.Value) + { + var otherVesselName = otherVesselNameKVP.Key; + if (!rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. + if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime > potentialCollisionDetectionTime) // We've waited long enough for the parts that are going to explode to explode. + { + var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; + var AI = vessel == null ? null : vessel.ActiveController().AI; + var otherAI = otherVessel == null ? null : otherVessel.ActiveController().AI; + + // Count the number of parts lost. + var rammedPartsLost = (otherAI == null) ? rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision : rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision - otherVessel.parts.Count; + var rammingPartsLost = (AI == 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: {(otherAI == 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: {(AI == 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. + foreach (var tmpVesselName in rammingInformation[vesselName].targetInformation.Keys) + if (rammingInformation[tmpVesselName].targetInformation[vesselName].collisionDetected) + rammingInformation[tmpVesselName].targetInformation[vesselName].partCountJustPriorToCollision = rammingInformation[vesselName].partCount; + foreach (var tmpVesselName in rammingInformation[otherVesselName].targetInformation.Keys) + if (rammingInformation[tmpVesselName].targetInformation[otherVesselName].collisionDetected) + rammingInformation[tmpVesselName].targetInformation[otherVesselName].partCountJustPriorToCollision = rammingInformation[otherVesselName].partCount; + + // Figure out who should be awarded the ram. + var rammingVessel = rammingInformation[vesselName].vesselName; + var rammedVessel = rammingInformation[otherVesselName].vesselName; + var headOn = false; + var accidental = false; + if (rammingInformation[vesselName].targetInformation[otherVesselName].ramming ^ rammingInformation[otherVesselName].targetInformation[vesselName].ramming) // Only one of the vessels was ramming. + { + if (!rammingInformation[vesselName].targetInformation[otherVesselName].ramming) // Switch who rammed who if the default is backwards. + { + rammingVessel = rammingInformation[otherVesselName].vesselName; + rammedVessel = rammingInformation[vesselName].vesselName; + var tmp = rammingPartsLost; + rammingPartsLost = rammedPartsLost; + rammedPartsLost = tmp; + } + } + else // Both or neither of the vessels were ramming. + { + if (rammingInformation[vesselName].targetInformation[otherVesselName].angleToCoM < headOnLimit && rammingInformation[otherVesselName].targetInformation[vesselName].angleToCoM < headOnLimit) // Head-on collision detected, both get awarded with ramming the other. + { + headOn = true; + } + else + { + if (rammingInformation[vesselName].targetInformation[otherVesselName].angleToCoM > rammingInformation[otherVesselName].targetInformation[vesselName].angleToCoM) // Other vessel had a better angleToCoM, so switch who rammed who. + { + rammingVessel = rammingInformation[otherVesselName].vesselName; + rammedVessel = rammingInformation[vesselName].vesselName; + var tmp = rammingPartsLost; + rammingPartsLost = rammedPartsLost; + rammedPartsLost = tmp; + } + if (!rammingInformation[rammingVessel].targetInformation[rammedVessel].ramming && rammingInformation[rammingVessel].targetInformation[rammedVessel].angleToCoM > headOnLimit) accidental = true; + } + } + + LogRammingVesselScore(rammingVessel, rammedVessel, rammedPartsLost, rammingPartsLost, headOn, accidental, true, false, rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetectedTime); // Log the ram. + + // Set the collisionDetected flag to false, since we've now logged this collision. We set both so that the collision only gets logged once. + rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = false; + rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetected = false; + } + } + } + } + + // Actually log the ram to various places. Note: vesselName and targetVesselName need to be those returned by the GetName() function to match the keys in Scores. + public void LogRammingVesselScore(string rammingVesselName, string rammedVesselName, int rammedPartsLost, int rammingPartsLost, bool headOn, bool accidental, bool logToCompetitionStatus, bool logToDebug, double timeOfCollision) + { + if (logToCompetitionStatus) + { + if (!headOn) + competitionStatus.Add(rammedVesselName + " got " + (accidental ? "ACCIDENTALLY " : "") + "RAMMED by " + rammingVesselName + " and lost " + rammedPartsLost + " parts (" + rammingVesselName + " lost " + rammingPartsLost + " parts)."); + else + competitionStatus.Add(rammedVesselName + " and " + rammingVesselName + " RAMMED each other and lost " + rammedPartsLost + " and " + rammingPartsLost + " parts, respectively."); + } + if (logToDebug) + { + if (!headOn) + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + rammedVesselName + " got " + (accidental ? "ACCIDENTALLY " : "") + "RAMMED by " + rammingVesselName + " and lost " + rammedPartsLost + " parts (" + rammingVesselName + " lost " + rammingPartsLost + " parts)."); + else + Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + rammedVesselName + " and " + rammingVesselName + " RAMMED each other and lost " + rammedPartsLost + " and " + rammingPartsLost + " parts, respectively."); + } + if (accidental) return; // Don't score from accidental rams. + + // Log score information for the ramming vessel. + Scores.RegisterRam(rammingVesselName, rammedVesselName, timeOfCollision, rammedPartsLost); + // If it was a head-on, log scores for the rammed vessel too. + if (headOn) + { + Scores.RegisterRam(rammedVesselName, rammingVesselName, timeOfCollision, rammingPartsLost); + } + } + + Dictionary partsCheck; + void CheckForMissingParts() + { + if (partsCheck == null) + { + partsCheck = new Dictionary(); + foreach (var vesselName in rammingInformation.Keys) + { + if (rammingInformation[vesselName].vessel == null) continue; + partsCheck.Add(vesselName, rammingInformation[vesselName].vessel.parts.Count); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + vesselName + " started with " + partsCheck[vesselName] + " parts."); + } + } + foreach (var vesselName in rammingInformation.Keys) + { + if (!partsCheck.ContainsKey(vesselName)) continue; + var vessel = rammingInformation[vesselName].vessel; + if (vessel != null) + { + if (partsCheck[vesselName] != vessel.parts.Count) + { + 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.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Parts Check: " + vesselName + " has been destroyed, losing " + partsCheck[vesselName] + " parts."); + partsCheck[vesselName] = 0; + } + } + } + + // Main calling function to control ramming logging. + private void LogRamming() + { + if (!competitionIsActive) return; + if (rammingInformation == null) InitialiseRammingInformation(); + UpdateTimesToCPAs(); + CheckForPotentialCollisions(); + CheckForDamagedParts(); + if (BDArmorySettings.DEBUG_COMPETITION) CheckForMissingParts(); // DEBUG + } + #endregion + + #region Tag + // Note: most of tag is now handled directly in the scoring datastructure. + public void TagResetTeams() + { + char T = 'A'; + var pilots = GetAllPilots(); + foreach (var pilot in pilots) + { + 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" + T++; + } + foreach (var pilot in pilots) + pilot.WeaponManager.ForceScan(); // Update targets. + startTag = true; + } + #endregion + + #region Post-start helper functions + /// + /// Add the vessel the WM is on to an active (or starting) competition when it's loaded and ready. + /// Note: this is only called for "fighters" that are detached from other vessels and times out after 10s. + /// + /// + public void AddToCompetitionWhenReady(MissileFire weaponManager, bool weaponsFree) => StartCoroutine(AddToCompetitionWhenReadyCoroutine(weaponManager, weaponsFree)); + IEnumerator AddToCompetitionWhenReadyCoroutine(MissileFire weaponManager, bool weaponsFree) + { + Vessel vessel = weaponManager.vessel; + var start = Time.time; + bool notValid() => IsValidVessel(vessel) != InvalidVesselReason.None || !vessel.loaded || string.IsNullOrEmpty(vessel.vesselName); + yield return new WaitWhileFixed(() => Time.time - start < 10 && notValid()); + if (notValid()) + { + if (!SpawnUtils.IsModularMissilePart(vessel.rootPart)) yield break; // Silently abort for MMG missiles containing a WM. + Debug.Log($"[BDArmory.BDACompetitionMode]: Vessel '{(vessel != null ? vessel.vesselName : "null")}' failed to become valid within 10s (loaded: {vessel.loaded}, invalid: {IsValidVessel(vessel)}), unable to add to competition."); + yield break; + } + + // Fix any naming issues. + SpawnUtils.DeconflictVesselName(vessel, reuse: ContinuousSpawning.Instance.vesselsSpawningContinuously); + + AddToActiveCompetition(vessel, false, weaponsFree); // Don't assign a new team to detached fighters. + } + + /// + /// Add a vessel to an active competition. + /// + /// Note: this can be called before a competition actually starts, e.g., during the initial spawn of continuous spawn. + /// + /// The vessel to add. + /// Assign a new team to the vessel. + /// Enable guard mode and free the AI to attack. + public void AddToActiveCompetition(Vessel vessel, bool assignTeam = true, bool weaponsFree = true) + { + var weaponManager = vessel.ActiveController().WM; + if (weaponManager == null) return; // Not a valid competition craft. + + var vesselName = vessel.vesselName; + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: Adding {vessel.vesselName} ({vessel.persistentId}) to the competition."); + + // If a competition is active, update the scoring structure. + bool competitionStartingOrStarted = competitionStarting || competitionIsActive; + if (competitionStartingOrStarted && !Scores.Players.Contains(vesselName)) + 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; + } + if (BDATournament.Instance.tournamentStatus == TournamentStatus.Running) + BDATournament.Instance.AddPlayer(vessel); + + // Set the vessel on the appropriate team. + if (BDArmorySettings.TAG_MODE && !string.IsNullOrEmpty(Scores.currentlyIT)) + { weaponManager.SetTeam(BDTeam.Get("NO")); } + else if (assignTeam) + { + // 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 (weaponsFree) + { + // Update the WM's internal lists (weapons, radars, etc.). + weaponManager.UpdateList(); + + // Enable guard mode if a competition is active, otherwise deactivate it. + if (weaponManager.guardMode) weaponManager.ToggleGuardMode(); // First, disable guard mode to reset weapon stuff. + if (competitionIsActive) weaponManager.ToggleGuardMode(); // Then, if the competition has actually started, enable guard mode. + var ai = weaponManager.AI; + if (ai != null) + { + ai.ActivatePilot(); // Make sure the AI is active. + ai.ReleaseCommand(); // Make sure it's free to attack. + } + weaponManager.ForceScan(); + } + + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + // Adjust BDACompetitionMode's scoring structures. + ContinuousSpawning.Instance.UpdateCompetitionScores(vessel, true); + ++ContinuousSpawning.Instance.continuousSpawningScores[vesselName].spawnCount; + } + if (competitionIsActive) // For competitions that are starting these should already be applied. + { + if (BDArmorySettings.HACK_INTAKES) SpawnUtils.HackIntakes(vessel, true); + if (BDArmorySettings.MUTATOR_MODE) SpawnUtils.ApplyMutators(vessel, true); + if (BDArmorySettings.ENABLE_HOS) SpawnUtils.ApplyHOS(vessel); + if (BDArmorySettings.RUNWAY_PROJECT) SpawnUtils.ApplyRWP(vessel); + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS) SpawnUtils.ApplyCompSettingsChecks(vessel); + } + } + #endregion + + public void CheckMemoryUsage() // DEBUG + { + List strings = + [ + "System memory: " + SystemInfo.systemMemorySize + "MB", + "Reserved: " + UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong() / 1024 / 1024 + "MB", + "Allocated: " + UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024 / 1024 + "MB", + "Mono heap: " + UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong() / 1024 / 1024 + "MB", + "Mono used: " + UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024 / 1024 + "MB", + "GfxDriver: " + UnityEngine.Profiling.Profiler.GetAllocatedMemoryForGraphicsDriver() / 1024 / 1024 + "MB", + "plus unspecified runtime (native) memory.", + ]; + Debug.Log("[BDArmory.BDACompetitionMode]: Memory Usage: " + string.Join(", ", strings)); + } + + public void CheckNumbersOfThings() // DEBUG + { + List strings = + [ + "FlightGlobals.Vessels: " + FlightGlobals.Vessels.Count, + "Non-competitors to remove: " + nonCompetitorsToRemove.Count, + "EffectBehaviour: " + EffectBehaviour.FindObjectsOfType().Length, + "EffectBehaviour: " + EffectBehaviour.FindObjectsOfType().Length, + "KSPParticleEmitters: " + FindObjectsOfType().Length, + "KSPParticleEmitters including inactive: " + Resources.FindObjectsOfTypeAll(typeof(KSPParticleEmitter)).Length, + ]; + Debug.Log("DEBUG " + string.Join(", ", strings)); + Dictionary emitterNames = new Dictionary(); + foreach (var pe in Resources.FindObjectsOfTypeAll(typeof(KSPParticleEmitter)).Cast()) + { + if (!pe.isActiveAndEnabled) + { + if (emitterNames.ContainsKey(pe.gameObject.name)) + ++emitterNames[pe.gameObject.name]; + else + emitterNames.Add(pe.gameObject.name, 1); + } + } + Debug.Log("DEBUG inactive/disabled emitter names: " + string.Join(", ", emitterNames.OrderByDescending(kvp => kvp.Value).Select(pe => pe.Key + ":" + pe.Value))); + + strings.Clear(); + strings.Add("Parts: " + FindObjectsOfType().Length + " active of " + Resources.FindObjectsOfTypeAll(typeof(Part)).Length); + strings.Add("Vessels: " + FindObjectsOfType().Length + " active of " + Resources.FindObjectsOfTypeAll(typeof(Vessel)).Length); + strings.Add("GameObjects: " + FindObjectsOfType().Length + " active of " + Resources.FindObjectsOfTypeAll(typeof(GameObject)).Length); + strings.Add($"FlightState ProtoVessels: {HighLogic.CurrentGame.flightState.protoVessels.Where(pv => pv.vesselRef != null).Count()} active of {HighLogic.CurrentGame.flightState.protoVessels.Count}"); + Debug.Log("DEBUG " + string.Join(", ", strings)); + + strings.Clear(); + foreach (var pool in FindObjectsOfType()) + strings.Add($"{pool.poolObjectName}:{pool.size}"); + Debug.Log("DEBUG Object Pools: " + string.Join(", ", strings)); + } + + public void RunDebugChecks() + { + CheckMemoryUsage(); +#if DEBUG + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) CheckNumbersOfThings(); +#endif + } + + public IEnumerator CheckGCPerformance() + { + var wait = new WaitForFixedUpdate(); + var wait2 = new WaitForSeconds(0.5f); + var startRealTime = Time.realtimeSinceStartup; + var startTime = Time.time; + int countFrames = 0; + int countVessels = 0; + int countParts = 0; + while (Time.time - startTime < 1d) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var moduleList = vessel.FindPartModulesImplementing(); + foreach (var module in moduleList) + if (module != null) ++countParts; + ++countVessels; + } + ++countFrames; + yield return wait; + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {countFrames} frames: {countParts} on {countVessels} vessels have HP (using FindPartModulesImplementing)"); + + startRealTime = Time.realtimeSinceStartup; + startTime = Time.time; + countFrames = 0; + countVessels = 0; + countParts = 0; + while (Time.time - startTime < 1d) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + foreach (var module in VesselModuleRegistry.GetModules(vessel)) if (module != null) ++countParts; + ++countVessels; + } + ++countFrames; + yield return wait; + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {countFrames} frames: {countParts} on {countVessels} vessels have HP (using VesselModuleRegistry)"); + + startRealTime = Time.realtimeSinceStartup; + startTime = Time.time; + countFrames = 0; + countVessels = 0; + countParts = 0; + while (Time.time - startTime < 1d) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var moduleList = vessel.FindPartModulesImplementing(); + foreach (var module in moduleList) + if (module != null) ++countParts; + ++countVessels; + } + ++countFrames; + yield return wait; + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {countFrames} frames: {countParts} engines on {countVessels} vessels (using FindPartModulesImplementing)"); + + startRealTime = Time.realtimeSinceStartup; + startTime = Time.time; + countFrames = 0; + countVessels = 0; + countParts = 0; + while (Time.time - startTime < 1d) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + foreach (var module in VesselModuleRegistry.GetModuleEngines(vessel)) if (module != null) ++countParts; + ++countVessels; + } + ++countFrames; + yield return wait; + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {countFrames} frames: {countParts} engines on {countVessels} vessels (using VesselModuleRegistry)"); + + startRealTime = Time.realtimeSinceStartup; + startTime = Time.time; + countFrames = 0; + countVessels = 0; + countParts = 0; + while (Time.time - startTime < 1d) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var module = vessel.FindPartModuleImplementing(); + if (module != null) ++countParts; + ++countVessels; + } + ++countFrames; + yield return wait; + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {countFrames} frames: {countParts} of {countVessels} vessels have MF (using FindPartModuleImplementing)"); + + startRealTime = Time.realtimeSinceStartup; + startTime = Time.time; + countFrames = 0; + countVessels = 0; + countParts = 0; + while (Time.time - startTime < 1d) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var module = VesselModuleRegistry.GetModule(vessel); + if (module != null) ++countParts; + ++countVessels; + } + ++countFrames; + yield return wait; + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {countFrames} frames: {countParts} of {countVessels} vessels have MF (using VesselModuleRegistry)"); + + + // Single frame performance + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var moduleList = vessel.FindPartModulesImplementing(); + foreach (var module in moduleList) + if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} on {countVessels} vessels have HP (using FindPartModulesImplementing)"); + + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + foreach (var module in VesselModuleRegistry.GetModules(vessel)) if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} on {countVessels} vessels have HP (using VesselModueeRegistry)"); + + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var moduleList = vessel.FindPartModulesImplementing(); + foreach (var module in moduleList) + if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} engines on {countVessels} vessels (using FindPartModulesImplementing)"); + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + foreach (var module in VesselModuleRegistry.GetModuleEngines(vessel)) if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} engines on {countVessels} vessels (using VesselModuleRegistry)"); + + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var moduleList = vessel.FindPartModulesImplementing(); + foreach (var module in moduleList) + if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} WMs on {countVessels} vessels (using FindPartModulesImplementing)"); + + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + foreach (var module in VesselModuleRegistry.GetModules(vessel)) if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} WMs on {countVessels} vessels (using VesselModuleRegistry)"); + + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var module = vessel.FindPartModuleImplementing(); + if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} WMs on {countVessels} vessels (using Find single)"); + + yield return wait2; + + startRealTime = Time.realtimeSinceStartup; + countVessels = 0; + countParts = 0; + for (int i = 0; i < 500; ++i) + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || vessel.packed || !vessel.loaded) continue; + var module = VesselModuleRegistry.GetModule(vessel); + if (module != null) ++countParts; + ++countVessels; + } + } + Debug.Log($"DEBUG {Time.realtimeSinceStartup - startRealTime}s {500} iters: {countParts} WMs on {countVessels} vessels (using VesselModuleRegistry)"); + + competitionStatus.Add("Done."); + } + + public void CleanUpKSPsDeadReferences() + { + var toRemove = new List(); + foreach (var key in FlightGlobals.PersistentVesselIds.Keys) + if (FlightGlobals.PersistentVesselIds[key] == null) toRemove.Add(key); + 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); + 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); + 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 (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; + 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) + { + foreach (var protoPart in protoVessel.protoPartSnapshots) + { + protoPart.modules.Clear(); + protoPart.pVesselRef = null; + protoPart.partRef = null; + } + protoVessel.protoPartSnapshots.Clear(); + } + } + } + } +} diff --git a/BDArmory/Competition/BDAScoreClient.cs b/BDArmory/Competition/BDAScoreClient.cs deleted file mode 100644 index 42002cac4..000000000 --- a/BDArmory/Competition/BDAScoreClient.cs +++ /dev/null @@ -1,400 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using UnityEngine; -using UnityEngine.Networking; -using BDArmory.Core; - -namespace BDArmory.Competition -{ - - public class BDAScoreClient - { - private BDAScoreService service; - - private string baseUrl = "https://bdascores.herokuapp.com"; - - public string vesselPath = ""; - - private string competitionHash = ""; - - public bool pendingRequest = false; - - public CompetitionModel competition = null; - - public HeatModel activeHeat = null; - - public Dictionary heats = new Dictionary(); - - public Dictionary vessels = new Dictionary(); - - public Dictionary players = new Dictionary(); - - - public BDAScoreClient(BDAScoreService service, string vesselPath, string hash) - { - this.service = service; - this.vesselPath = vesselPath + "/" + hash; - this.competitionHash = hash; - } - - public IEnumerator GetCompetition(string hash) - { - if (pendingRequest) - { - Debug.Log("[BDAScoreClient] Request already pending"); - yield break; - } - pendingRequest = true; - - string uri = string.Format("{0}/competitions/{1}.json", baseUrl, hash); - Debug.Log(string.Format("[BDAScoreClient] GET {0}", uri)); - using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - ReceiveCompetition(webRequest.downloadHandler.text); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to get competition {0}: {1}", hash, webRequest.error)); - } - } - - pendingRequest = false; - } - - private void ReceiveCompetition(string response) - { - if (response == null || "".Equals(response)) - { - Debug.Log(string.Format("[BDAScoreClient] Received empty competition response")); - return; - } - CompetitionModel competition = JsonUtility.FromJson(response); - if (competition == null) - { - Debug.Log(string.Format("[BDAScoreClient] Failed to parse competition: {0}", response)); - } - else - { - this.competition = competition; - Debug.Log(string.Format("[BDAScoreClient] Competition: {0}", competition.ToString())); - } - } - - public IEnumerator GetHeats(string hash) - { - if (pendingRequest) - { - Debug.Log("[BDAScoreClient] Request already pending"); - yield break; - } - pendingRequest = true; - - string uri = string.Format("{0}/competitions/{1}/heats.csv", baseUrl, hash); - Debug.Log(string.Format("[BDAScoreClient] GET {0}", uri)); - using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - ReceiveHeats(webRequest.downloadHandler.text); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to get heats for {0}: {1}", hash, webRequest.error)); - } - } - - pendingRequest = false; - } - - private void ReceiveHeats(string response) - { - if (response == null || "".Equals(response)) - { - Debug.Log(string.Format("[BDAScoreClient] Received empty heat collection response")); - return; - } - List collection = HeatModel.FromCsv(response); - heats.Clear(); - if (collection == null) - { - Debug.Log(string.Format("[BDAScoreClient] Failed to parse heat collection: {0}", response)); - return; - } - foreach (HeatModel heatModel in collection) - { - Debug.Log(string.Format("[BDAScoreClient] Heat: {0}", heatModel.ToString())); - heats.Add(heatModel.id, heatModel); - } - Debug.Log(string.Format("[BDAScoreClient] Heats: {0}", heats.Count)); - } - - public IEnumerator GetPlayers(string hash) - { - if (pendingRequest) - { - Debug.Log("[BDAScoreClient] Request already pending"); - yield break; - } - pendingRequest = true; - - string uri = string.Format("{0}/competitions/{1}/players.csv", baseUrl, hash); - Debug.Log(string.Format("[BDAScoreClient] GET {0}", uri)); - using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - ReceivePlayers(webRequest.downloadHandler.text); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to get players for {0}: {1}", hash, webRequest.error)); - } - } - - pendingRequest = false; - } - - private void ReceivePlayers(string response) - { - if (response == null || "".Equals(response)) - { - Debug.Log(string.Format("[BDAScoreClient] Received empty player collection response")); - return; - } - List collection = PlayerModel.FromCsv(response); - players.Clear(); - if (collection == null) - { - Debug.Log(string.Format("[BDAScoreClient] Failed to parse player collection: {0}", response)); - return; - } - foreach (PlayerModel playerModel in collection) - { - Debug.Log(string.Format("[BDAScoreClient] Player {0}", playerModel.ToString())); - if (!players.ContainsKey(playerModel.id)) - players.Add(playerModel.id, playerModel); - else - Debug.Log("[BDAScoreClient] Player " + playerModel.id + " already exists in the competition."); - } - Debug.Log(string.Format("[BDAScoreClient] Players: {0}", players.Count)); - } - - public IEnumerator GetVessels(string hash, HeatModel heat) - { - if (pendingRequest) - { - Debug.Log("[BDAScoreClient] Request already pending"); - yield break; - } - pendingRequest = true; - - string uri = string.Format("{0}/competitions/{1}/heats/{2}/vessels.csv", baseUrl, hash, heat.id); - Debug.Log(string.Format("[BDAScoreClient] GET {0}", uri)); - using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - ReceiveVessels(webRequest.downloadHandler.text); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to get vessels {0}/{1}: {2}", hash, heat, webRequest.error)); - } - } - - pendingRequest = false; - } - - private void ReceiveVessels(string response) - { - if (response == null || "".Equals(response)) - { - Debug.Log(string.Format("[BDAScoreClient] Received empty vessel collection response")); - return; - } - List collection = VesselModel.FromCsv(response); - vessels.Clear(); - if (collection == null) - { - Debug.Log(string.Format("[BDAScoreClient] Failed to parse vessel collection: {0}", response)); - return; - } - foreach (VesselModel vesselModel in collection) - { - if (!vessels.ContainsKey(vesselModel.id)) // Skip duplicates. - { - Debug.Log(string.Format("[BDAScoreClient] Vessel {0}", vesselModel.ToString())); - vessels.Add(vesselModel.id, vesselModel); - } - else - { - Debug.Log("[BDAScoreClient]: Vessel " + vesselModel.ToString() + " is already in the vessel list, skipping."); - } - } - Debug.Log(string.Format("[BDAScoreClient] Vessels: {0}", vessels.Count)); - } - - public IEnumerator PostRecords(string hash, int heat, List records) - { - List recordsJson = records.Select(e => e.ToJSON()).ToList(); - Debug.Log(string.Format("[BDAScoreClient] Prepare records for {0} players", records.Count())); - string recordsJsonStr = string.Join(",", recordsJson); - string requestBody = string.Format("{{\"records\":[{0}]}}", recordsJsonStr); - - byte[] rawBody = Encoding.UTF8.GetBytes(requestBody); - string uri = string.Format("{0}/competitions/{1}/heats/{2}/records/batch.json?client_secret={3}", baseUrl, hash, heat, BDArmorySettings.REMOTE_CLIENT_SECRET); - string uriWithoutSecret = string.Format("{0}/competitions/{1}/heats/{2}/records/batch.json?client_secret=****", baseUrl, hash, heat); - Debug.Log(string.Format("[BDAScoreClient] POST {0}:\n{1}", uriWithoutSecret, requestBody)); - using (UnityWebRequest webRequest = new UnityWebRequest(uri)) - { - webRequest.SetRequestHeader("Content-Type", "application/json"); - webRequest.uploadHandler = new UploadHandlerRaw(rawBody); - webRequest.downloadHandler = new DownloadHandlerBuffer(); - webRequest.method = UnityWebRequest.kHttpVerbPOST; - - yield return webRequest.SendWebRequest(); - - Debug.Log(string.Format("[BDAScoreClient] score reporting status: {0}", webRequest.downloadHandler.text)); - if (webRequest.isHttpError) - { - Debug.Log(string.Format("[BDAScoreClient] Failed to post records: {0}", webRequest.error)); - } - } - } - - public IEnumerator GetCraftFiles(string hash, HeatModel model) - { - pendingRequest = true; - // DO NOT DELETE THE DIRECTORY. Delete the craft files inside it. - // This is much safer. - if (Directory.Exists(vesselPath)) - { - Debug.Log("[BDAScoreClient] Deleting existing craft in spawn directory " + vesselPath); - DirectoryInfo info = new DirectoryInfo(vesselPath); - FileInfo[] craftFiles = info.GetFiles("*.craft") - .Where(e => e.Extension == ".craft") - .ToArray(); - foreach (FileInfo file in craftFiles) - { - File.Delete(file.FullName); - } - } - else - { - Directory.CreateDirectory(vesselPath); - } - - // already have the vessels in memory; just need to fetch the files - foreach (VesselModel v in vessels.Values) - { - Debug.Log(string.Format("[BDAScoreClient] GET {0}", v.craft_url)); - using (UnityWebRequest webRequest = UnityWebRequest.Get(v.craft_url)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - byte[] rawBytes = webRequest.downloadHandler.data; - SaveCraftFile(v, rawBytes); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to get craft for {0}: {1}", v.id, webRequest.error)); - } - } - } - pendingRequest = false; - } - - private void SaveCraftFile(VesselModel vessel, byte[] bytes) - { - PlayerModel p = players[vessel.player_id]; - if (p == null) - { - Debug.Log(string.Format("[BDAScoreClient] Failed to save craft for vessel {0}, player {1}", vessel.id, vessel.player_id)); - return; - } - - string vesselName = string.Format("{0}_{1}", p.name, vessel.name); - string filename = string.Format("{0}/{1}.craft", vesselPath, vesselName); - System.IO.File.WriteAllBytes(filename, bytes); - - // load the file and modify its vessel name to match the player - string[] lines = File.ReadAllLines(filename); - string pattern = ".*ship = (.+)"; - string[] modifiedLines = lines - .Select(e => Regex.Replace(e, pattern, "ship = " + vesselName)) - .Where(e => !e.Contains("VESSELNAMING")) - .ToArray(); - File.WriteAllLines(filename, modifiedLines); - Debug.Log(string.Format("[BDAScoreClient] Saved craft for player {0}", vesselName)); - } - - public IEnumerator StartHeat(string hash, HeatModel heat) - { - if (pendingRequest) - { - Debug.Log("[BDAScoreClient] Request already pending"); - yield break; - } - pendingRequest = true; - - this.activeHeat = heat; - UI.RemoteOrchestrationWindow.Instance.UpdateClientStatus(); - - string uri = string.Format("{0}/competitions/{1}/heats/{2}/start", baseUrl, hash, heat.id); - using (UnityWebRequest webRequest = new UnityWebRequest(uri)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - Debug.Log(string.Format("[BDAScoreClient] Started heat {1} in {0}", hash, heat.order)); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to start heat {1} in {0}: {2}", hash, heat.order, webRequest.error)); - } - } - - pendingRequest = false; - } - - public IEnumerator StopHeat(string hash, HeatModel heat) - { - if (pendingRequest) - { - Debug.Log("[BDAScoreClient] Request already pending"); - yield break; - } - pendingRequest = true; - - this.activeHeat = null; - - string uri = string.Format("{0}/competitions/{1}/heats/{2}/stop", baseUrl, hash, heat.id); - using (UnityWebRequest webRequest = new UnityWebRequest(uri)) - { - yield return webRequest.SendWebRequest(); - if (!webRequest.isHttpError) - { - Debug.Log(string.Format("[BDAScoreClient] Stopped heat {1} in {0}", hash, heat.order)); - } - else - { - Debug.Log(string.Format("[BDAScoreClient] Failed to stop heat {1} in {0}: {2}", hash, heat.order, webRequest.error)); - } - } - - pendingRequest = false; - } - - } -} diff --git a/BDArmory/Competition/BDATournament.cs b/BDArmory/Competition/BDATournament.cs new file mode 100644 index 000000000..f27b34a95 --- /dev/null +++ b/BDArmory/Competition/BDATournament.cs @@ -0,0 +1,2674 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using UnityEngine; + +using BDArmory.Competition.OrchestrationStrategies; +using BDArmory.Evolution; +using BDArmory.Extensions; +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 : CircularSpawnConfig + { + 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; + [SerializeField] List _teams; + public void SerializeTeams() + { + if (teamsSpecific == null) + { + _teams = null; + return; + } + _teams = teamsSpecific.Select(team => JsonUtility.ToJson(new RoundConfigTeam { team = team })).ToList(); + craftFiles = null; // Avoid including the file list twice in the tournament.state file. + } + public void DeserializeTeams() + { + if (teamsSpecific == null) teamsSpecific = new List>(); + else teamsSpecific.Clear(); + if (_teams != null) + { + try { teamsSpecific = _teams.Select(team => JsonUtility.FromJson(team).team).ToList(); } + catch (Exception e) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize teams: {e.Message}"); } + } + if (teamsSpecific.Count == 0) teamsSpecific = null; + } + + [Serializable] + class RoundConfigTeam // Serialisation helper for List> + { + public List team; + } + } + + // A serializable configuration for loading and saving the tournament state for custom template spawning. + // Note: there's a fair bit of duplication here as C# doesn't allow seem to allow the following: + // public class RoundConfig where T : SpawnConfig { public RoundConfig(int round, int heat, bool completed, T config) : base(config) { ... } } + [Serializable] + public class TemplateRoundConfig : CustomSpawnConfig + { + public TemplateRoundConfig(int round, int heat, bool completed, CustomSpawnConfig config) : base(config) { this.round = round; this.heat = heat; this.completed = completed; SerializeTeams(); } + public int round; + public int heat; + public bool completed; + [SerializeField] List _teams; + public void SerializeTeams() + { + if (teamsSpecific == null) + { + _teams = null; + return; + } + _teams = teamsSpecific.Select(team => JsonUtility.ToJson(new RoundConfigTeam { team = team })).ToList(); + craftFiles = null; // Avoid including the file list twice in the tournament.state file. + } + public void DeserializeTeams() + { + if (teamsSpecific == null) teamsSpecific = new List>(); + else teamsSpecific.Clear(); + if (_teams != null) + { + try { teamsSpecific = _teams.Select(team => JsonUtility.FromJson(team).team).ToList(); } + catch (Exception e) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize teams: {e.Message}"); } + } + if (teamsSpecific.Count == 0) teamsSpecific = null; + } + + [Serializable] + class RoundConfigTeam // Serialisation helper for List> + { + public List team; + } + } + + [Serializable] + public class TournamentScores + { + public Dictionary playersToFileNames = []; // Match players with craft filenames for extending ranks rounds. + public Dictionary playersToTeamNames = []; // Match the players with team names (for teams competitions). + public Dictionary playerIsFighter = []; // Track which players are fighters (spawned from other craft). + public Dictionary scores = []; // The current scores for the tournament. + public float lastUpdated = 0; + HashSet npcs = []; + Dictionary> scoreDetails = []; // Scores per player per round. Rounds players weren't involved in contain default ScoringData entries. + List competitionOutcomes = []; + public static readonly Dictionary defaultWeights = new() + { + {"Wins", 1f}, + {"Survived", 0f}, + {"MIA", 0f}, + {"Deaths", -1f}, + {"Death Order", 1f}, + {"Death Time", 0.002f}, + {"Clean Kills", 3f}, + {"Assists", 1.5f}, + {"Hits", 0.004f}, + {"Hits Taken", 0f}, + {"Bullet Damage", 0.0001f}, + {"Bullet Damage Taken", 4e-05f}, + {"Rocket Hits", 0.01f}, + {"Rocket Hits Taken", 0f}, + {"Rocket Parts Hit", 0.0005f}, + {"Rocket Parts Hit Taken", 0f}, + {"Rocket Damage", 0.0001f}, + {"Rocket Damage Taken", 4e-05f}, + {"Missile Hits", 0.15f}, + {"Missile Hits Taken", 0f}, + {"Missile Parts Hit", 0.002f}, + {"Missile Parts Hit Taken", 0f}, + {"Missile Damage", 3e-05f}, + {"Missile Damage Taken", 1.5e-05f}, + {"RamScore", 0.075f}, + {"RamScore Taken", 0f}, + {"Battle Damage", 0f}, + {"Parts Lost To Asteroids", 0f}, + {"HP Remaining", 0f}, + {"Accuracy", 0f}, + {"Rocket Accuracy", 0f}, + {"Waypoint Count", 1f}, // Waypoint weighting logic: 1 for passing a gate, 1s = 5 deviation, break-even at 60s + 200 deviation per gate. + {"Waypoint Time", -0.01f}, + {"Waypoint Deviation", -0.002f} + }; + public static Dictionary weights = new(defaultWeights); + + /// + /// Reset scores for a new tournament. + /// + public void Reset() + { + playersToFileNames.Clear(); + playersToTeamNames.Clear(); + scoreDetails.Clear(); + scores.Clear(); + competitionOutcomes.Clear(); + npcs.Clear(); + lastUpdated = Time.time; + } + + /// + /// Add a player to the tournament scoring data. + /// + /// The player (vesselName). + /// The craft file belonging to the player (required for generating ranked rounds). + /// The current round (fills previous rounds with empty score data). + /// Whether the player is an NPC or not. + /// Whether the player is a fighter (detached from parent vessel) or not. + /// true if successfully added, false otherwise. + public bool AddPlayer(string player, string fileName, int currentRound = -1, bool npc = false, bool fighter = false) + { + if (playersToFileNames.ContainsKey(player)) return false; // They're already there. + if (string.IsNullOrEmpty(fileName) || !File.Exists(fileName)) { Debug.LogWarning($"[BDArmory.BDATournament]: {fileName} does not exist for {player}."); return false; } + if (currentRound < 0) currentRound = BDATournament.Instance.currentRound; + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDATournament]: Adding {player} with file {fileName} in round {currentRound}"); + playersToFileNames.Add(player, fileName); + playerIsFighter.Add(player, fighter); + scoreDetails.Add(player, []); + if (npc) npcs.Add(player); + return true; + } + + public bool IsNPC(string player) => npcs.Contains(player); + /// + /// Update score weights. + /// Only valid weights in the newWeights dictionary are updated. + /// + /// A dictionary of weights to update. + public static void ConfigureScoreWeights(Dictionary newWeights) + { + if (newWeights == null) return; + foreach (var key in newWeights.Keys) + { + if (!weights.ContainsKey(key)) + { + Debug.LogWarning($"[BDArmory.BDATournament]: Invalid score key {key}"); + continue; + } + weights[key] = newWeights[key]; + } + SaveWeights(); + } + + /// + /// Add the scores for a heat to the current tournament scores. + /// Note: this doesn't update the scores data, only the scoreDetails data. Call ComputeScores() between rounds to update the scores data. + /// + /// + public void AddHeatScores(CompetitionScores competitionScores) + { + competitionOutcomes.Add(new CompetitionOutcome + { + competitionResult = competitionScores.competitionResult, + survivingTeams = competitionScores.survivingTeams.Select(t => t.ToList()).ToList(), // Perform deep copies of the lists. + deadTeams = competitionScores.deadTeams.Select(t => t.ToList()).ToList() + }); + foreach (var player in competitionScores.Players) + { + if (!scoreDetails.ContainsKey(player)) continue; // Ignore players that weren't registered with the tournament. + scoreDetails[player].Add(competitionScores.ScoreData[player].Clone()); // Add the player's score to the tournament scores. + } + } + + /// + /// For each player in the competition, compute a new score based on their scoreDetails. + /// + public void ComputeScores() + { + scores.Clear(); + foreach (var player in scoreDetails.Keys) + { + if (IsNPC(player)) continue; // Ignore NPCs for overall score totals. + scores[player] = ComputeScore(player); + } + lastUpdated = Time.time; + if (BDArmorySettings.DEBUG_COMPETITION) + { + Debug.Log($"[BDArmory.BDATournament]: Tournament scores: {string.Join(", ", scores.Select(s => $"{s.Key}: {s.Value}"))}"); + Debug.Log($"[BDArmory.BDATournament]: NPC scores: {string.Join(", ", npcs.Select(npc => $"{npc}: {ComputeScore(npc)}"))}"); + } + } + + HashSet cleanKills = [AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal]; + /// + /// Compute the score for a player. + /// + /// The player. + /// The player's score. + public float ComputeScore(string player) + { + if (!scoreDetails.ContainsKey(player)) return 0; + var scoreData = scoreDetails[player]; + var shotsFired = scoreData.Sum(sd => sd.shotsFired); + var rocketsFired = scoreData.Sum(sd => sd.rocketsFired); + Dictionary playerScore = new() + { + {"Wins", competitionOutcomes.Count(comp => comp.competitionResult == CompetitionResult.Win && comp.survivingTeams.Any(team => team.Contains(player)))}, + {"Survived", scoreData.Count(sd => sd.survivalState == SurvivalState.Alive)}, + {"MIA", scoreData.Count(sd => sd.survivalState == SurvivalState.MIA)}, + {"Deaths", scoreData.Count(sd => sd.survivalState == SurvivalState.Dead)}, + {"Death Order", scoreData.Sum(sd => sd.deathOrder > -1 ? sd.deathOrder / (float)sd.numberOfCompetitors : 1f)}, + {"Death Time", (float)scoreData.Sum(sd => sd.deathTime > -1 ? sd.deathTime : sd.compDuration)}, + {"Clean Kills", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Count(sd => cleanKills.Contains(sd.aliveState) && sd.gmKillReason == GMKillReason.None && sd.lastPersonWhoDamagedMe == player))}, + {"Assists", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Count(sd => sd.aliveState == AliveState.AssistedKill && ((sd.hitCounts.ContainsKey(player) && sd.hitCounts[player] > 0) || (sd.rocketStrikeCounts.ContainsKey(player) && sd.rocketStrikeCounts[player] > 0) || (sd.missileHitCounts.ContainsKey(player) && sd.missileHitCounts[player] > 0) || (sd.rammingPartLossCounts.ContainsKey(player) && sd.rammingPartLossCounts[player] > 0))))}, + {"Hits", scoreData.Sum(sd => sd.hits)}, + {"Hits Taken", scoreData.Sum(sd => sd.hitCounts.Values.Sum())}, + {"Bullet Damage", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Sum(sd => sd.damageFromGuns.ContainsKey(player) ? sd.damageFromGuns[player] : 0f))}, + {"Bullet Damage Taken", scoreData.Sum(sd => sd.damageFromGuns.Values.Sum())}, + {"Rocket Hits", scoreData.Sum(sd => sd.rocketStrikes)}, + {"Rocket Hits Taken", scoreData.Sum(sd => sd.rocketStrikeCounts.Values.Sum())}, + {"Rocket Parts Hit", scoreData.Sum(sd => sd.totalDamagedPartsDueToRockets)}, + {"Rocket Parts Hit Taken", scoreData.Sum(sd => sd.rocketPartDamageCounts.Values.Sum())}, + {"Rocket Damage", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Sum(sd => sd.damageFromRockets.ContainsKey(player) ? sd.damageFromRockets[player] : 0f))}, + {"Rocket Damage Taken", scoreData.Sum(sd => sd.damageFromRockets.Values.Sum())}, + {"Missile Hits", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Sum(sd => sd.missileHitCounts.ContainsKey(player) ? sd.missileHitCounts[player] : 0))}, + {"Missile Hits Taken", scoreData.Sum(sd => sd.missileHitCounts.Values.Sum())}, + {"Missile Parts Hit", scoreData.Sum(sd => sd.totalDamagedPartsDueToMissiles)}, + {"Missile Parts Hit Taken", scoreData.Sum(sd => sd.missilePartDamageCounts.Values.Sum())}, + {"Missile Damage", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Sum(sd => sd.damageFromMissiles.ContainsKey(player) ? sd.damageFromMissiles[player] : 0f))}, + {"Missile Damage Taken", scoreData.Sum(sd => sd.damageFromMissiles.Values.Sum())}, + {"RamScore", scoreData.Sum(sd => sd.totalDamagedPartsDueToRamming)}, + {"RamScore Taken", scoreData.Sum(sd => sd.rammingPartLossCounts.Values.Sum())}, + {"Battle Damage", scoreDetails.Where(details => details.Key != player).Sum(details => details.Value.Sum(sd => sd.battleDamageFrom.ContainsKey(player) ? sd.battleDamageFrom[player] : 0f))}, + {"Parts Lost To Asteroids", scoreData.Sum(sd => sd.partsLostToAsteroids)}, + {"HP Remaining", (float)scoreData.Sum(sd => sd.remainingHP)}, + {"Accuracy", shotsFired > 0 ? scoreData.Sum(sd => sd.hits) / (float)shotsFired : 0f}, + {"Rocket Accuracy", rocketsFired > 0 ? scoreData.Sum(sd => sd.rocketStrikes) / (float)rocketsFired : 0f} + }; + // Waypoints are scored per round and clamped non-negative to avoid excessive negative scores for bad runs. + var wpScore = scoreData.Sum(sd => Mathf.Max(0, weights["Waypoint Count"] * sd.totalWPReached + weights["Waypoint Time"] * sd.totalWPTime + weights["Waypoint Deviation"] * sd.totalWPDeviation)); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDATournament]: Score components for {player}: {string.Join(", ", playerScore.Select(kvp => $"{kvp.Key}: {kvp.Value}"))}"); + if (scoreData.Count > 0) + { + var teamName = scoreData.First().team; + if (scoreData.All(sd => sd.team == teamName)) // If the team is consistent, populate the team names dictionary. + playersToTeamNames[player] = teamName; + } + else playersToTeamNames[player] = ""; + return playerScore.Sum(kvp => kvp.Value * weights[kvp.Key]) + wpScore; + } + + /// + /// Get the craft files in ascending order of the currently computed scores (summing over those deriving from the same craft file, i.e., fighters). + /// Notes: + /// - This ignores NPCs since they're not included in the overall scoring. + /// - Having multiple craft from the same file (other than fighters) in the ranked tournament will mess with this. + /// + public List GetRankedCraftFiles() + { + List nonFighters = [.. playerIsFighter.Where(kvp => !kvp.Value).Select(kvp => kvp.Key)]; + var combinedScores = nonFighters.ToDictionary(player => player, player => playersToFileNames.Where(kvp => !string.IsNullOrEmpty(kvp.Value) && kvp.Value == playersToFileNames[player]).Sum(kvp => scores[kvp.Key])); + List ranking = [.. combinedScores.OrderBy(kvp => kvp.Value).Select(kvp => playersToFileNames[kvp.Key])]; + return ranking; + } + public List GetRankedTeams(List> teamFiles) + { + // While the reverse of playersToFileNames is not necessarily 1-to-1 (due to the full teams option), duplicates of the same craft file should be on the same team. + // The following accumulates the scores for all players with those vessels in each team, which is then used to rank the teams. + List teamScores = [.. teamFiles.Select(tm => scores.Where(kvp => tm.Contains(playersToFileNames[kvp.Key])).Sum(kvp => kvp.Value))]; + return [.. Enumerable.Range(0, teamFiles.Count).OrderBy(i => teamScores[i])]; + } + + #region Serialization + [SerializeField] List _weightKeys; + [SerializeField] List _weightValues; + [SerializeField] List _players; + [SerializeField] List _npcs; + [SerializeField] List _fighters; + [SerializeField] List _scores; + [SerializeField] List _files; + [SerializeField] List _results; + public TournamentScores PrepareSerialization() + { + _weightKeys = [.. weights.Keys]; + _weightValues = [.. _weightKeys.Select(k => weights[k])]; + _players = [.. scoreDetails.Keys]; + _npcs = [.. npcs]; + _fighters = [.. playerIsFighter.Where(p => p.Value).Select(p => p.Key)]; + _scores = [.. _players.Where(scoreDetails.ContainsKey).Select(p => JsonUtility.ToJson(new SerializedScoreDataList().Serialize(p, scoreDetails[p], _players)))]; + _files = [.. _players.Where(playersToFileNames.ContainsKey).Select(p => playersToFileNames[p])]; // If the craft file has been removed, try to cope without it. + _results = [.. competitionOutcomes.Select(r => JsonUtility.ToJson(r.PreSerialize()))]; + return this; + } + public void PostDeserialization() + { + Reset(); + ConfigureScoreWeights(Enumerable.Range(0, _weightKeys.Count).ToDictionary(i => _weightKeys[i], i => _weightValues[i])); + for (int i = 0; i < _players.Count; ++i) + { + var player = _players[i]; + AddPlayer(player: player, fileName: _files[i], 0, npc: _npcs.Contains(player), fighter: _fighters.Contains(player)); + } + try + { + scoreDetails = Enumerable.Range(0, _players.Count).ToDictionary(i => _players[i], i => JsonUtility.FromJson(_scores[i])).ToDictionary(kvp => kvp.Key, kvp => + { + if (kvp.Value == null) + { + Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize List."); + return []; + } + return kvp.Value.Deserialize(_players); + }); + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize tournament scores: {e.Message}\n{e.StackTrace}"); } + try + { + competitionOutcomes = [.. _results.Select(r => JsonUtility.FromJson(r).PostDeserialize())]; + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize competition outcomes: {e.Message}\n{e.StackTrace}"); } + } + + [Serializable] + class SerializedScoreDataList + { + [SerializeField] List serializedScoreData; + + public SerializedScoreDataList Serialize(string player, List scoreDetails, List players) + { + serializedScoreData = [.. scoreDetails.Select(sd => JsonUtility.ToJson(new SerializedScoreData().Serialize(sd, players)))]; + return this; + } + public List Deserialize(List players) + { + List ssdl = [.. serializedScoreData.Select(JsonUtility.FromJson)]; + List sdl = []; + foreach (var ssd in ssdl) + { + if (ssd == null) + { + Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize ScoreData."); + sdl.Add(new ScoringData()); + } + else + { + sdl.Add(ssd.Deserialize(players)); + } + } + return sdl; + } + } + + /// + /// A class for serializing the ScoreData. + /// All non-basic types and non-Lists need to be converted to strings or basic Lists. + /// + [Serializable] + class SerializedScoreData + { + public string scoreData; // Easily serialisable fields. + public List hitCounts; + public List damageFromGuns; + public List damageFromRockets; + public List rocketPartDamageCounts; + public List rocketStrikeCounts; + public List rammingPartLossCounts; + public List damageFromMissiles; + public List missilePartDamageCounts; + public List missileHitCounts; + public List battleDamageFrom; + public List damageTypesTaken; + public List everyoneWhoDamagedMe; + + /// + /// Serialize ScoringData to SerializedScoreData prior to saving to JSON. + /// + /// The scoring data. + /// The list of players in the tournament. + public SerializedScoreData Serialize(ScoringData scores, List players) + { + scoreData = JsonUtility.ToJson(scores); + hitCounts = []; + damageFromGuns = []; + damageFromRockets = []; + rocketPartDamageCounts = []; + rocketStrikeCounts = []; + rammingPartLossCounts = []; + damageFromMissiles = []; + missilePartDamageCounts = []; + missileHitCounts = []; + battleDamageFrom = []; + foreach (var player in players) + { + hitCounts.Add(scores.hitCounts.ContainsKey(player) ? scores.hitCounts[player] : 0); + damageFromGuns.Add(scores.damageFromGuns.ContainsKey(player) ? scores.damageFromGuns[player] : 0); + damageFromRockets.Add(scores.damageFromRockets.ContainsKey(player) ? scores.damageFromRockets[player] : 0); + rocketPartDamageCounts.Add(scores.rocketPartDamageCounts.ContainsKey(player) ? scores.rocketPartDamageCounts[player] : 0); + rocketStrikeCounts.Add(scores.rocketStrikeCounts.ContainsKey(player) ? scores.rocketStrikeCounts[player] : 0); + rammingPartLossCounts.Add(scores.rammingPartLossCounts.ContainsKey(player) ? scores.rammingPartLossCounts[player] : 0); + damageFromMissiles.Add(scores.damageFromMissiles.ContainsKey(player) ? scores.damageFromMissiles[player] : 0); + missilePartDamageCounts.Add(scores.missilePartDamageCounts.ContainsKey(player) ? scores.missilePartDamageCounts[player] : 0); + missileHitCounts.Add(scores.missileHitCounts.ContainsKey(player) ? scores.missileHitCounts[player] : 0); + battleDamageFrom.Add(scores.battleDamageFrom.ContainsKey(player) ? scores.battleDamageFrom[player] : 0); + } + damageTypesTaken = [.. scores.damageTypesTaken]; + everyoneWhoDamagedMe = [.. scores.everyoneWhoDamagedMe]; + return this; + } + + /// + /// Deserialize SerializedScoreData after loading from JSON. + /// + /// The players that were originally used to serialize the score data. + /// The ScoringData for a player. + public ScoringData Deserialize(List players) + { + var scores = JsonUtility.FromJson(scoreData); + scores.hitCounts = []; + scores.damageFromGuns = []; + scores.damageFromRockets = []; + scores.rocketPartDamageCounts = []; + scores.rocketStrikeCounts = []; + scores.rammingPartLossCounts = []; + scores.damageFromMissiles = []; + scores.missilePartDamageCounts = []; + scores.missileHitCounts = []; + scores.battleDamageFrom = []; + scores.damageTypesTaken = []; + scores.everyoneWhoDamagedMe = []; + try + { + foreach (var i in Enumerable.Range(0, players.Count)) + { + var player = players[i]; + scores.hitCounts[player] = hitCounts[i]; + scores.damageFromGuns[player] = damageFromGuns[i]; + scores.damageFromRockets[player] = damageFromRockets[i]; + scores.rocketPartDamageCounts[player] = rocketPartDamageCounts[i]; + scores.rocketStrikeCounts[player] = rocketStrikeCounts[i]; + scores.rammingPartLossCounts[player] = rammingPartLossCounts[i]; + scores.damageFromMissiles[player] = damageFromMissiles[i]; + scores.missilePartDamageCounts[player] = missilePartDamageCounts[i]; + scores.missileHitCounts[player] = missileHitCounts[i]; + scores.battleDamageFrom[player] = battleDamageFrom[i]; + } + scores.damageTypesTaken = [.. damageTypesTaken]; + scores.everyoneWhoDamagedMe = [.. everyoneWhoDamagedMe]; + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize tournament score data: {e.Message}\n{e.StackTrace}"); } + return scores; + } + } + + [Serializable] + class CompetitionOutcome + { + public CompetitionResult competitionResult; + public List> survivingTeams; + public List> deadTeams; + [SerializeField] List _survivingTeams; + [SerializeField] List _deadTeams; + public CompetitionOutcome PreSerialize() + { + _survivingTeams = survivingTeams.Select(t => JsonUtility.ToJson(new StringList { ls = t })).ToList(); + _deadTeams = deadTeams.Select(t => JsonUtility.ToJson(new StringList { ls = t })).ToList(); + return this; + } + public CompetitionOutcome PostDeserialize() + { + survivingTeams = _survivingTeams.Select(t => JsonUtility.FromJson(t).ls).ToList(); + deadTeams = _deadTeams.Select(t => JsonUtility.FromJson(t).ls).ToList(); + return this; + } + } + + public static void SaveWeights() + { + ConfigNode fileNode = ConfigNode.Load(ScoreWindow.scoreWeightsURL) ?? new ConfigNode(); + + if (!fileNode.HasNode("ScoreWeights")) + { + fileNode.AddNode("ScoreWeights"); + } + + ConfigNode settings = fileNode.GetNode("ScoreWeights"); + + foreach (var kvp in weights) + { + settings.SetValue(kvp.Key, kvp.Value.ToString(), true); + } + fileNode.Save(ScoreWindow.scoreWeightsURL); + } + + public static void LoadWeights() + { + ConfigNode fileNode = ConfigNode.Load(ScoreWindow.scoreWeightsURL); + if (fileNode == null || !fileNode.HasNode("ScoreWeights")) return; + + ConfigNode settings = fileNode.GetNode("ScoreWeights"); + + foreach (var key in weights.Keys.ToList()) + { + if (!settings.HasValue(key)) continue; + + object parsedValue = BDAPersistentSettingsField.ParseValue(typeof(float), settings.GetValue(key), key); + if (parsedValue != null) + { + weights[key] = (float)parsedValue; + } + } + } + #endregion + } + + public enum TournamentType { FFA, Teams }; + public enum TournamentRoundType { Shuffled, Ranked }; + public enum TournamentStyle { RNG, nCk, Gauntlet, TemplateRNG }; + + [Serializable] + public class TournamentState + { + public static string defaultStateFile = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "PluginData", "tournament.state")); + public uint tournamentID; + 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 int vesselsPerHeat; + public int numberOfRounds; + public int npcsPerHeat; + public List npcFiles = []; + public TournamentType tournamentType = TournamentType.FFA; + public TournamentStyle tournamentStyle = TournamentStyle.RNG; + public TournamentRoundType tournamentRoundType = TournamentRoundType.Shuffled; + [NonSerialized] public Dictionary> rounds; // > + [NonSerialized] public Dictionary> completed = []; + [NonSerialized] private List> teamSpawnQueues = []; + [NonSerialized] private List> opponentTeamSpawnQueues = []; + private string message; + public TournamentScores scores = new(); + [SerializeField] string _scores; + [SerializeField] List _heats; + [SerializeField] List _teamFiles; + [SerializeField] List _deconflictionURLs; + [SerializeField] List _deconflictionSuffixes; + + /// + /// Generate a tournament.state file for FFA tournaments. + /// Any deficit from splitting the vessels into heats is distributed amongst the heats such that there is always N or N-1 vessels per heat. + /// + /// + /// + /// + /// + /// + /// + /// + public bool GenerateFFATournament(string folder, int numberOfRounds, int vesselsPerHeat, int npcsPerHeat, TournamentStyle tournamentStyle, TournamentRoundType tournamentRoundType) + { + folder ??= ""; // Sanitise null strings. + tournamentID = (uint)DateTime.UtcNow.Subtract(new DateTime(2020, 1, 1)).TotalSeconds; + savegame = HighLogic.SaveFolder; + tournamentType = TournamentType.FFA; + this.tournamentStyle = tournamentStyle; + if (tournamentStyle != TournamentStyle.RNG && tournamentRoundType == TournamentRoundType.Ranked) + { + message = "Ranked tournament mode is invalid for non-RNG style tournaments."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDATournament]: " + message); + return false; + } + this.tournamentRoundType = tournamentRoundType; + numberOfRounds = tournamentRoundType == TournamentRoundType.Ranked ? 1 : numberOfRounds; // Ranked tournaments generate a single Shuffled round, then just go until the current number of rounds slider +1 is satisfied. + this.numberOfRounds = numberOfRounds; + this.vesselsPerHeat = vesselsPerHeat; + var abs_folder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", folder)); + if (!Directory.Exists(abs_folder)) + { + message = "Tournament folder (" + folder + ") containing craft files does not exist."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + return false; + } + craftFiles = Directory.GetFiles(abs_folder, "*.craft").ToList(); + vesselCount = craftFiles.Count; + var npc_folder = Path.Combine(abs_folder, "NPCs"); + npcFiles = Directory.Exists(npc_folder) ? Directory.GetFiles(npc_folder, "*.craft").ToList() : new List(); + if (npcsPerHeat > 0 && npcFiles.Count == 0) + { + message = $"{npcsPerHeat} NPCs requested, but none exist in {Path.Combine("AutoSpawn", folder, "NPCs")}"; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDATournament]: {message}"); + return false; + } + this.npcsPerHeat = npcsPerHeat; + int fullHeatCount; + switch (vesselsPerHeat) + { + case -1: // Auto + var autoVesselsPerHeat = OptimiseVesselsPerHeat(craftFiles.Count, npcsPerHeat); + vesselsPerHeat = autoVesselsPerHeat.Item1; + fullHeatCount = Mathf.CeilToInt(craftFiles.Count / (float)vesselsPerHeat) - autoVesselsPerHeat.Item2; + break; + case 0: // Unlimited (all vessels in one heat). + vesselsPerHeat = craftFiles.Count; + fullHeatCount = 1; + break; + default: + vesselsPerHeat = Mathf.Clamp(Mathf.Max(1, vesselsPerHeat - npcsPerHeat), 1, craftFiles.Count); + fullHeatCount = Mathf.CeilToInt(craftFiles.Count / (float)vesselsPerHeat) - (Mathf.CeilToInt(craftFiles.Count / (float)vesselsPerHeat) * vesselsPerHeat - craftFiles.Count); // Spread the deficit amongst the other heats if possible. + if (fullHeatCount <= 0) fullHeatCount = craftFiles.Count / vesselsPerHeat; + break; + } + rounds = []; + switch (tournamentStyle) + { + case TournamentStyle.RNG: // RNG + { + message = $"Generating {numberOfRounds} randomised rounds for {(tournamentRoundType == TournamentRoundType.Ranked ? "ranked " : "")}tournament {tournamentID} for {vesselCount} vessels in AutoSpawn{(folder == "" ? "" : "/" + folder)}, each with up to {vesselsPerHeat} vessels per heat{(npcsPerHeat > 0 ? $" and {npcsPerHeat} NPCs per heat" : "")}."; + Debug.Log("[BDArmory.BDATournament]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) + { + craftFiles.Shuffle(); + int vesselsThisHeat = vesselsPerHeat; + int count = 0; + List selectedFiles = craftFiles.Take(vesselsThisHeat).ToList(); + rounds.Add(rounds.Count, []); + int heatIndex = 0; + while (selectedFiles.Count > 0) + { + if (npcsPerHeat > 0) // Add in some NPCs. + { + npcFiles.Shuffle(); + selectedFiles.AddRange(Enumerable.Repeat(npcFiles, Mathf.CeilToInt((float)npcsPerHeat / (float)npcFiles.Count)).SelectMany(x => x).Take(npcsPerHeat)); // Repeat the shuffled npcFiles list enough times, then take the required number. + } + 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_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING, + true, // Kill everything first. + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. + 0, // Number of teams. + null, // List of team numbers. + null, // List of List of teams' vessels. + null, // No folder, we're going to specify the craft files. + selectedFiles.ToList() // Add a copy of the craft files list. + )); + count += vesselsThisHeat; + vesselsThisHeat = ++heatIndex < fullHeatCount ? vesselsPerHeat : vesselsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of craft files. + selectedFiles = craftFiles.Skip(count).Take(vesselsThisHeat).ToList(); + } + } + break; + } + case TournamentStyle.nCk: // N-choose-K + { + var nCr = N_Choose_K(vesselCount, vesselsPerHeat); + message = $"Generating a round-robin style tournament for {vesselCount} vessels in AutoSpawn{(folder == "" ? "" : "/" + folder)} with up to {vesselsPerHeat} vessels per heat and {numberOfRounds} rounds. This requires {numberOfRounds * nCr} heats."; + Debug.Log($"[BDArmory.BDATournament]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + // Generate all combinations of vessels for a round. + var heatList = new List(); + foreach (var combination in Combinations(vesselCount, vesselsPerHeat)) + { + 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, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING, + true, // Kill everything first. + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. + 0, // Number of teams. + null, // List of team numbers. + null, // List of List of teams' vessels. + null, // No folder, we're going to specify the craft files. + combination.Select(i => craftFiles[i]).ToList() // Add a copy of the craft files list. + )); + } + // Populate the rounds. + 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)); + } + break; + } + default: + { + BDACompetitionMode.Instance.competitionStatus.Add($"Tournament style {tournamentStyle} not implemented yet for FFA."); + throw new ArgumentOutOfRangeException("tournamentStyle", $"Invalid tournament style {tournamentStyle} - not implemented."); + } + } + teamFiles = null; // Clear the teams lists. + return true; + } + + /// + /// Generate a tournament.state file for teams tournaments. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public bool GenerateTeamsTournament(string folder, int numberOfRounds, int teamsPerHeat, int vesselsPerTeam, int numberOfTeams, TournamentStyle tournamentStyle, TournamentRoundType tournamentRoundType) + { + folder ??= ""; // Sanitise null strings. + tournamentID = (uint)DateTime.UtcNow.Subtract(new DateTime(2020, 1, 1)).TotalSeconds; + savegame = HighLogic.SaveFolder; + tournamentType = TournamentType.Teams; + this.tournamentStyle = tournamentStyle; + if (!(tournamentStyle == TournamentStyle.RNG || tournamentStyle == TournamentStyle.TemplateRNG) && tournamentRoundType == TournamentRoundType.Ranked) + { + message = "Ranked tournament mode is invalid for non-RNG style tournaments."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDATournament]: " + message); + return false; + } + this.tournamentRoundType = tournamentRoundType; + numberOfRounds = tournamentRoundType == TournamentRoundType.Ranked ? 1 : numberOfRounds; // Ranked tournaments generate a single Shuffled round, then just go until the current number of rounds slider +1 is satisfied. + var absFolder = Path.GetFullPath(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); + Debug.Log("[BDArmory.BDATournament]: " + message); + return false; + } + if (numberOfTeams > 1) // Make teams from the files in the spawn folder. + { + craftFiles = Directory.GetFiles(absFolder, "*.craft").ToList(); + if (craftFiles.Count < numberOfTeams) + { + message = "Insufficient vessels in AutoSpawn" + (!string.IsNullOrEmpty(folder) ? "/" + folder : "") + " to make " + numberOfTeams + " teams."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + return false; + } + craftFiles.Shuffle(); + + int numberPerTeam = craftFiles.Count / numberOfTeams; + int residue = craftFiles.Count - numberPerTeam * numberOfTeams; + teamFiles = []; + for (int teamCount = 0, count = 0; teamCount < numberOfTeams; ++teamCount) + { + var toTake = numberPerTeam + (teamCount < residue ? 1 : 0); + teamFiles.Add(craftFiles.Skip(count).Take(toTake).ToList()); + count += toTake; + } + } + else // Make teams from the folders under the spawn folder. + { + var teamDirs = Directory.GetDirectories(absFolder); + if (tournamentStyle == TournamentStyle.Gauntlet) // 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 != TournamentStyle.Gauntlet ? 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(absFolder, "*.craft").ToList(); + teamFiles = craftFiles.Select(f => new List { f }).ToList(); + } + else + { + teamFiles = []; + foreach (var teamDir in teamDirs) + { + var currentTeamFiles = Directory.GetFiles(teamDir, "*.craft").ToList(); + if (currentTeamFiles.Count > 0) + teamFiles.Add(currentTeamFiles); + } + foreach (var team in teamFiles) + team.Shuffle(); + craftFiles = teamFiles.SelectMany(v => v).ToList(); + } + } + vesselCount = craftFiles.Count; + npcFiles.Clear(); // NPCs aren't supported in teams tournaments yet. + if (teamFiles.Count < (tournamentStyle != TournamentStyle.Gauntlet ? 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); + Debug.Log("[BDArmory.BDATournament]: " + message); + return false; + } + teamCount = teamFiles.Count; + teamsPerHeat = Mathf.Clamp(teamsPerHeat, tournamentStyle != TournamentStyle.Gauntlet ? 2 : 1, teamFiles.Count); + this.teamsPerHeat = teamsPerHeat; + this.vesselsPerTeam = vesselsPerTeam; + fullTeams = BDArmorySettings.TOURNAMENT_FULL_TEAMS; + var teamsIndex = Enumerable.Range(0, teamFiles.Count).ToList(); + teamSpawnQueues.Clear(); + + rounds = []; + int fullHeatCount = teamFiles.Count / teamsPerHeat; + switch (tournamentStyle) + { + case TournamentStyle.RNG: // RNG + { + message = $"Generating {numberOfRounds} randomised rounds for tournament {tournamentID} for {teamCount} teams in AutoSpawn{(folder == "" ? "" : "/" + folder)}, each with {teamsPerHeat} teams per heat."; + Debug.Log("[BDArmory.BDATournament]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) + { + teamsIndex.Shuffle(); + int teamsThisHeat = teamsPerHeat; + int count = 0; + var selectedTeams = teamsIndex.Take(teamsThisHeat).ToList(); + var selectedCraft = SelectTeamCraft(selectedTeams, vesselsPerTeam, fullTeams); + rounds.Add(rounds.Count, []); + int heatIndex = 0; + while (selectedTeams.Count > 0) + { + 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_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING, + true, // Kill everything first. + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. + numberOfTeams, // Number of teams indicator. + null, //selectedCraft.Select(c => c.Count).ToList(), // Not used here. + selectedCraft, // 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. + )); + count += teamsThisHeat; + teamsThisHeat = heatIndex++ < fullHeatCount ? teamsPerHeat : teamsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of teams. + selectedTeams = teamsIndex.Skip(count).Take(teamsThisHeat).ToList(); + selectedCraft = SelectTeamCraft(selectedTeams, vesselsPerTeam, fullTeams); + } + } + break; + } + case TournamentStyle.TemplateRNG: // RNG with spawn templates + { + var spawnTemplate = CustomTemplateSpawning.customSpawnConfig; + if (string.IsNullOrEmpty(spawnTemplate.name)) + { + message = $"No template selected. Unable to generated template RNG tournament."; + Debug.Log("[BDArmory.BDATournament]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + return false; + } + teamsPerHeat = Mathf.Min(teamsPerHeat, spawnTemplate.customVesselSpawnConfigs.Count); + this.teamsPerHeat = teamsPerHeat; + fullHeatCount = teamFiles.Count / teamsPerHeat; + vesselsPerTeam = Mathf.Min(vesselsPerTeam > 1 ? vesselsPerTeam : int.MaxValue, spawnTemplate.customVesselSpawnConfigs.Select(x => x.Count).Max()); + message = $"Generating {numberOfRounds} randomised templated rounds for tournament {tournamentID} for {teamCount} teams in AutoSpawn{(folder == "" ? "" : "/" + folder)}, each with up to {teamsPerHeat} teams per heat."; + Debug.Log("[BDArmory.BDATournament]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + List templateSpawnPointOrder = Enumerable.Range(0, spawnTemplate.customVesselSpawnConfigs.Count).ToList(); + for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) + { + teamsIndex.Shuffle(); + int teamsThisHeat = teamsPerHeat; + int count = 0; + var selectedTeams = teamsIndex.Take(teamsThisHeat).ToList(); + templateSpawnPointOrder.Shuffle(); + var selectedCraft = SelectTeamCraft( + selectedTeams, + templateSpawnPointOrder.Select(i => Mathf.Min(vesselsPerTeam, spawnTemplate.customVesselSpawnConfigs[i].Count)).ToList(), + fullTeams); + rounds.Add(rounds.Count, []); + int heatIndex = 0; + while (selectedTeams.Count > 0) + { + rounds[roundIndex].Add( + rounds[roundIndex].Count, + new CustomSpawnConfig(CustomTemplateSpawning.customSpawnConfig) + { + altitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, + killEverythingFirst = true, + assignTeams = BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + teamsSpecific = selectedCraft, + numberOfTeams = numberOfTeams == -1 ? -1 : 1 // Flag the number of teams as per file / per folder for sourcing craft files. + }); + count += teamsThisHeat; + teamsThisHeat = heatIndex++ < fullHeatCount ? teamsPerHeat : teamsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of teams. + selectedTeams = teamsIndex.Skip(count).Take(teamsThisHeat).ToList(); + templateSpawnPointOrder.Shuffle(); + selectedCraft = SelectTeamCraft( + selectedTeams, + templateSpawnPointOrder.Select(i => Mathf.Min(vesselsPerTeam, spawnTemplate.customVesselSpawnConfigs[i].Count)).ToList(), + fullTeams); + } + } + break; + } + case TournamentStyle.nCk: // N-choose-K + { + var nCr = N_Choose_K(teamCount, teamsPerHeat); + message = $"Generating a round-robin style tournament for {teamCount} teams in AutoSpawn{(folder == "" ? "" : "/" + folder)} with {teamsPerHeat} 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); + // 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, fullTeams); // 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, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING, + true, // Kill everything first. + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. + numberOfTeams, // Number of teams indicator. + null, //selectedCraft.Select(c => c.Count).ToList(), // Not used here. + selectedCraft, // 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)); + } + break; + } + case TournamentStyle.Gauntlet: // 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.GetFullPath(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."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDATournament]: " + 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, fullTeams); // 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, fullTeams, 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, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING, + 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: + { + BDACompetitionMode.Instance.competitionStatus.Add($"Tournament style {tournamentStyle} not implemented yet for Teams."); + throw new ArgumentOutOfRangeException("tournamentStyle", $"Invalid tournament style {tournamentStyle} - not implemented."); + } + } + return true; + } + + /// + /// Generate a new ranked round for the tournament based on the current scores. + /// Note: The scores should be computed before calling this. + /// + /// true on success, false otherwise + public bool GenerateRankedRound() + { + if (rounds == null || rounds.Values.First() == null || rounds.Values.First().Values.First() == null) + { + Debug.LogWarning($"[BDArmory.BDATournament]: The initial round hasn't been set up yet. Unable to extend ranked rounds."); + return false; + } + int roundIndex = rounds.Count; + message = $"Generating ranked round {roundIndex} for tournament {tournamentID}."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDATournament]: {message}"); + switch (tournamentType) + { + case TournamentType.FFA: + { + var craftFiles = scores.GetRankedCraftFiles(); // NPCs are already filtered out from the overall scoring. + int vesselsPerHeat = this.vesselsPerHeat; // Convert from the flag to the correct number per heat. + vesselCount = craftFiles.Count; + int fullHeatCount; + switch (vesselsPerHeat) + { + case -1: // Auto + var autoVesselsPerHeat = OptimiseVesselsPerHeat(craftFiles.Count, npcsPerHeat); + vesselsPerHeat = autoVesselsPerHeat.Item1; + fullHeatCount = Mathf.CeilToInt(craftFiles.Count / vesselsPerHeat) - autoVesselsPerHeat.Item2; + break; + case 0: // Unlimited (all vessels in one heat). + vesselsPerHeat = craftFiles.Count; + fullHeatCount = 1; + break; + default: + vesselsPerHeat = Mathf.Clamp(Mathf.Max(1, vesselsPerHeat - npcsPerHeat), 1, craftFiles.Count); + fullHeatCount = craftFiles.Count / vesselsPerHeat; + break; + } + int vesselsThisHeat = vesselsPerHeat; + int count = 0; + List selectedFiles = craftFiles.Take(vesselsThisHeat).ToList(); + var circularSpawnConfigTemplate = rounds.Values.First().Values.First() as CircularSpawnConfig; + rounds.Add(roundIndex, []); // Extend the rounds by 1. + int heatIndex = 0; + while (selectedFiles.Count > 0) + { + if (npcsPerHeat > 0) // Add in some NPCs. + { + npcFiles.Shuffle(); + selectedFiles.AddRange(Enumerable.Repeat(npcFiles, Mathf.CeilToInt((float)npcsPerHeat / (float)npcFiles.Count)).SelectMany(x => x).Take(npcsPerHeat)); + } + rounds[roundIndex].Add(rounds[roundIndex].Count, new CircularSpawnConfig(circularSpawnConfigTemplate) + { + craftFiles = selectedFiles // Set the craft file list to the currently selected ones. + }); // Add a copy of the template to the heats. + count += vesselsThisHeat; + vesselsThisHeat = heatIndex++ < fullHeatCount ? vesselsPerHeat : vesselsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of craft files. + selectedFiles = craftFiles.Skip(count).Take(vesselsThisHeat).ToList(); + } + break; + } + case TournamentType.Teams: + { + switch (tournamentStyle) + { + case TournamentStyle.RNG: + { + int fullHeatCount = teamFiles.Count / teamsPerHeat; + var teamsIndex = scores.GetRankedTeams(teamFiles); + int teamsThisHeat = teamsPerHeat; + int count = 0; + var selectedTeams = teamsIndex.Take(teamsThisHeat).ToList(); + var selectedCraft = SelectTeamCraft(selectedTeams, vesselsPerTeam, fullTeams); + var circularSpawnConfigTemplate = rounds.Values.First().Values.First() as CircularSpawnConfig; + rounds.Add(roundIndex, []); // Extend the rounds by 1. + int heatIndex = 0; + while (selectedTeams.Count > 0) + { + rounds[roundIndex].Add(rounds[roundIndex].Count, new CircularSpawnConfig(circularSpawnConfigTemplate) + { + teamsSpecific = selectedCraft + }); // Add a copy of the template to the heats, but use the new set of vessels. + count += teamsThisHeat; + teamsThisHeat = heatIndex++ < fullHeatCount ? teamsPerHeat : teamsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of teams. + selectedTeams = teamsIndex.Skip(count).Take(teamsThisHeat).ToList(); + selectedCraft = SelectTeamCraft(selectedTeams, vesselsPerTeam, fullTeams); + } + break; + } + case TournamentStyle.TemplateRNG: + { + var spawnConfig = CustomTemplateSpawning.customSpawnConfig; + int fullHeatCount = teamFiles.Count / teamsPerHeat; + var teamsIndex = scores.GetRankedTeams(teamFiles); + int teamsThisHeat = teamsPerHeat; + int count = 0; + List templateSpawnPointOrder = Enumerable.Range(0, spawnConfig.customVesselSpawnConfigs.Count).ToList(); + templateSpawnPointOrder.Shuffle(); + var selectedTeams = teamsIndex.Take(teamsThisHeat).ToList(); + var selectedCraft = SelectTeamCraft( + selectedTeams, + templateSpawnPointOrder.Select(i => Mathf.Min(vesselsPerTeam, spawnConfig.customVesselSpawnConfigs[i].Count)).ToList(), + fullTeams); + var customSpawnConfigTemplate = rounds.Values.First().Values.First() as CustomSpawnConfig; + rounds.Add(roundIndex, []); // Extend the rounds by 1. + int heatIndex = 0; + while (selectedTeams.Count > 0) + { + rounds[roundIndex].Add(rounds[roundIndex].Count, new CustomSpawnConfig(customSpawnConfigTemplate) + { + teamsSpecific = selectedCraft + }); // Add a copy of the template to the heats, but use the new set of vessels. + count += teamsThisHeat; + teamsThisHeat = heatIndex++ < fullHeatCount ? teamsPerHeat : teamsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of teams. + selectedTeams = teamsIndex.Skip(count).Take(teamsThisHeat).ToList(); + templateSpawnPointOrder.Shuffle(); + selectedCraft = SelectTeamCraft( + selectedTeams, + templateSpawnPointOrder.Select(i => Mathf.Min(vesselsPerTeam, spawnConfig.customVesselSpawnConfigs[i].Count)).ToList(), + fullTeams); + } + break; + } + default: + Debug.LogError($"[BDArmory.BDATournament]: Invalid tournament style for ranked tournaments."); + break; + } + break; + } + default: + Debug.LogError($"[BDArmory.BDATournament]: Invalid tournament type."); + return false; + } + return true; + } + + List> SelectTeamCraft(List selectedTeams, List vesselsPerTeam, bool fullTeams) + { + if (selectedTeams.Count != vesselsPerTeam.Count) return []; + + List> selectedCraft = []; + for (int i = 0; i < selectedTeams.Count; ++i) + selectedCraft.Add(SelectTeamCraft([selectedTeams[i]], vesselsPerTeam[i], fullTeams, false).First()); + return selectedCraft; + } + + List> SelectTeamCraft(List selectedTeams, int vesselsPerTeam, bool fullTeams, bool opponentQueue = false) + { + if (vesselsPerTeam == 0) // Each team consist of all the craft in the team. + { + return selectedTeams.Select(index => (opponentQueue ? opponentTeamFiles : teamFiles)[index].ToList()).ToList(); + } + + // 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 teams) + spawnQueues.Add(new Queue()); + } + + List> selectedCraft = []; + List currentTeam = []; + foreach (var index in selectedTeams) + { + if (spawnQueues[index].Count < vesselsPerTeam) + { + // First append craft files that aren't already in the queue. + var craftToAdd = teams[index].Where(c => !spawnQueues[index].Contains(c)).ToList(); + craftToAdd.Shuffle(); + foreach (var craft in craftToAdd) + { + spawnQueues[index].Enqueue(craft); + } + if (fullTeams) + { + // Then continue to fill the queue with craft files until we have enough. + while (spawnQueues[index].Count < vesselsPerTeam) + { + craftToAdd = teams[index].ToList(); + craftToAdd.Shuffle(); + foreach (var craft in craftToAdd) + { + spawnQueues[index].Enqueue(craft); + } + } + } + } + currentTeam.Clear(); + while (currentTeam.Count < vesselsPerTeam && spawnQueues[index].Count > 0) + { + currentTeam.Add(spawnQueues[index].Dequeue()); + } + selectedCraft.Add(currentTeam.ToList()); + } + return selectedCraft; + } + + Tuple OptimiseVesselsPerHeat(int count, int extras) + { + if (BDArmorySettings.TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE.y < BDArmorySettings.TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE.x) BDArmorySettings.TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE.y = BDArmorySettings.TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE.x; + var limits = new Vector2Int(Math.Max(1, BDArmorySettings.TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE.x - extras), Math.Max(1, BDArmorySettings.TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE.y - extras)); + var options = count > limits.y && count < 2 * limits.x - 1 ? + Enumerable.Range(limits.y / 2, limits.y - limits.y / 2 + 1).Reverse().ToList() // Tweak the range when just over the upper limit to give more balanced heats. + : Enumerable.Range(limits.x, limits.y - limits.x + 1).Reverse().ToList(); + foreach (var val in options) + { + if (count % val == 0) + return new Tuple(val, 0); + } + var result = OptimiseVesselsPerHeat(count + 1, extras); + return new Tuple(result.Item1, result.Item2 + 1); + } + + public bool SaveState(string stateFile) + { + if (rounds == null) return false; // Nothing to save. + try + { + // Encode the scores into the _scores field. + if (scores != null) _scores = JsonUtility.ToJson(scores.PrepareSerialization()); + else _scores = null; + + // Encode the rounds into the _rounds field. + if (rounds != null) + { + _heats = []; + foreach (var round in rounds.Keys) + foreach (var heat in rounds[round].Keys) + if (tournamentStyle == TournamentStyle.TemplateRNG) + { + _heats.Add(JsonUtility.ToJson(new TemplateRoundConfig(round, heat, completed.ContainsKey(round) && completed[round].Contains(heat), rounds[round][heat] as CustomSpawnConfig))); + } + else + { + _heats.Add(JsonUtility.ToJson(new RoundConfig(round, heat, completed.ContainsKey(round) && completed[round].Contains(heat), rounds[round][heat] as CircularSpawnConfig))); + } + } + else _heats = null; + + // Encode team files (for team competitions). + _teamFiles = teamFiles != null ? teamFiles.Select(t => JsonUtility.ToJson(new StringList { ls = t })).ToList() : null; + + if (!Directory.GetParent(stateFile).Exists) + { Directory.GetParent(stateFile).Create(); } + try // Write the state with gzip compression to reduce bloat. + { + using FileStream fileStream = File.Create(stateFile); + using GZipStream gzStream = new(fileStream, CompressionMode.Compress); + var stateBytes = Encoding.UTF8.GetBytes(JsonUtility.ToJson(this)); + gzStream.Write(stateBytes, 0, stateBytes.Length); + } + catch // Revert to plain UTF8. + { + File.WriteAllText(stateFile, JsonUtility.ToJson(this)); + } + Debug.Log($"[BDArmory.BDATournament]: Tournament state saved to {stateFile}"); + return true; + } + catch (Exception e) + { + Debug.LogError("[BDArmory.BDATournament]: Exception thrown in SaveState: " + e.Message + "\n" + e.StackTrace); + return false; + } + } + + public bool LoadState(string stateFile) + { + try + { + if (!(File.Exists(stateFile) || File.Exists(stateFile))) return false; + TournamentState data; + try // Try with gzip compression. + { + using FileStream fileStream = File.OpenRead(stateFile); + using GZipStream gZipStream = new(fileStream, CompressionMode.Decompress); + using StreamReader streamReader = new(gZipStream, Encoding.UTF8); + data = JsonUtility.FromJson(streamReader.ReadToEnd()); + } + catch // Revert to plain ASCII text. + { + data = JsonUtility.FromJson(File.ReadAllText(stateFile)); + } + tournamentID = data.tournamentID; + savegame = data.savegame; + vesselCount = data.vesselCount; + teamCount = data.teamCount; + teamsPerHeat = data.teamsPerHeat; + vesselsPerTeam = data.vesselsPerTeam; + fullTeams = data.fullTeams; + vesselsPerHeat = data.vesselsPerHeat; + numberOfRounds = data.numberOfRounds; + tournamentType = data.tournamentType; + tournamentStyle = data.tournamentStyle; + tournamentRoundType = data.tournamentRoundType; + npcsPerHeat = data.npcsPerHeat; + npcFiles = data.npcFiles.ToList(); + _heats = data._heats; + rounds = []; + completed = []; + try // Deserialize team files + { + _teamFiles = data._teamFiles; + teamFiles = _teamFiles != null ? _teamFiles.Select(t => JsonUtility.FromJson(t).ls).ToList() : null; + } + catch (Exception e_scores) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize the team files: {e_scores.Message}\n{e_scores.StackTrace}"); } + try // Deserialize tournament scores + { + _scores = data._scores; + scores = JsonUtility.FromJson(_scores); + if (scores != null) scores.PostDeserialization(); + else scores = new TournamentScores(); + scores.ComputeScores(); + } + catch (Exception e_scores) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize the tournament scores: {e_scores.Message}\n{e_scores.StackTrace}"); } + try // Deserialize rounds / heats + { + if (_heats != null) + { + if (tournamentStyle == TournamentStyle.TemplateRNG) + { + bool templateLoaded = false; + foreach (var serializedRound in _heats) + { + var roundConfig = JsonUtility.FromJson(serializedRound); + if (roundConfig == null) { Debug.LogWarning($"[BDArmory.BDATournament]: Failed to decode a valid round config."); continue; } + if (!templateLoaded) + { + CustomTemplateSpawning.LoadTemplate(roundConfig.name, fromDisk: true); + if (string.IsNullOrEmpty(CustomTemplateSpawning.customSpawnConfig.name)) + { + message = $"Unable to load template tournament as the template {roundConfig.name} does not exist."; + if (BDACompetitionMode.Instance) BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogWarning($"[BDArmory.BDATournament]: " + message); + return false; + } + } + if (!serializedRound.Contains("worldIndex")) roundConfig.worldIndex = 1; // Default old tournament states to be on Kerbin. + roundConfig.DeserializeTeams(); + if (!rounds.ContainsKey(roundConfig.round)) rounds.Add(roundConfig.round, []); + rounds[roundConfig.round].Add(roundConfig.heat, new CustomSpawnConfig(CustomTemplateSpawning.customSpawnConfig) + { + altitude = roundConfig.altitude, + killEverythingFirst = true, + assignTeams = roundConfig.assignTeams, + teamsSpecific = roundConfig.teamsSpecific, + numberOfTeams = roundConfig.numberOfTeams // Flag the number of teams as per file / per folder for sourcing craft files. + }); + if (roundConfig.completed) + { + if (!completed.ContainsKey(roundConfig.round)) completed.Add(roundConfig.round, new HashSet()); + completed[roundConfig.round].Add(roundConfig.heat); + } + } + } + else + { + foreach (var serializedRound in _heats) + { + var roundConfig = JsonUtility.FromJson(serializedRound); + if (roundConfig == null) { Debug.LogWarning($"[BDArmory.BDATournament]: Failed to decode a valid round config."); continue; } + if (!serializedRound.Contains("worldIndex")) roundConfig.worldIndex = 1; // Default old tournament states to be on Kerbin. + roundConfig.DeserializeTeams(); + if (!rounds.ContainsKey(roundConfig.round)) rounds.Add(roundConfig.round, []); + rounds[roundConfig.round].Add(roundConfig.heat, new CircularSpawnConfig( + roundConfig.worldIndex, + roundConfig.latitude, + roundConfig.longitude, + roundConfig.altitude, + roundConfig.distance, + roundConfig.absDistanceOrFactor, + roundConfig.refHeading, + roundConfig.killEverythingFirst, + roundConfig.assignTeams, + roundConfig.numberOfTeams, + roundConfig.teamCounts == null || roundConfig.teamCounts.Count == 0 ? null : roundConfig.teamCounts, + roundConfig.teamsSpecific == null || roundConfig.teamsSpecific.Count == 0 ? null : roundConfig.teamsSpecific, + roundConfig.folder, + roundConfig.craftFiles + )); + if (roundConfig.completed) + { + if (!completed.ContainsKey(roundConfig.round)) completed.Add(roundConfig.round, new HashSet()); + completed[roundConfig.round].Add(roundConfig.heat); + } + } + } + } + } + catch (Exception e_rounds) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize the tournament rounds: {e_rounds.Message}\n{e_rounds.StackTrace}"); } + try // Deserialize deconfliction dictionaries + { + _deconflictionURLs = data._deconflictionURLs; + _deconflictionSuffixes = data._deconflictionSuffixes; + } + catch (Exception e_deconfliction) { Debug.LogError($"[BDArmory.BDATournament]: Failed to deserialize the vessel naming deconfliction data: {e_deconfliction.Message}\n{e_deconfliction.StackTrace}"); } + return true; + } + catch (Exception e) + { + Debug.LogError("[BDArmory.BDATournament]: " + e.Message); + return false; + } + } + + public void StoreDeconflictionData() + { + _deconflictionURLs = [.. SpawnUtils.SpawnedVesselURLs.Select(kvp => JsonUtility.ToJson(new StringList { ls = [kvp.Key, kvp.Value] }))]; + _deconflictionSuffixes = [.. SpawnUtils.DeconflictionSuffixes.Select(kvp => JsonUtility.ToJson(new StringList { ls = [kvp.Key, kvp.Value] }))]; + } + + public void RestoreDeconflictionData() + { + SpawnUtils.SpawnedVesselURLs = _deconflictionURLs.Select(json => JsonUtility.FromJson(json).ls).ToDictionary(ls => ls[0], ls => ls[1]); + SpawnUtils.DeconflictionSuffixes = _deconflictionSuffixes.Select(json => JsonUtility.FromJson(json).ls).ToDictionary(ls => ls[0], ls => ls[1]); + } + + #region Helper functions + /// + /// Calculate N-choose-K. + /// + /// N + /// K + /// The number of ways of choosing K unique items of a collection of N. + public static int N_Choose_K(int n, int k) + { + k = Mathf.Clamp(k, 0, n); + k = Math.Min(n, k); + var numer = Enumerable.Range(n - k + 1, k).Aggregate(1, (acc, val) => acc * val); + var denom = Enumerable.Range(1, k).Aggregate(1, (acc, val) => acc * val); + return Mathf.RoundToInt(numer / denom); + } + /// + /// Generate all combinations of N-choose-K. + /// + /// N + /// K + /// List of list of unique combinations of K indices from 0 to N-1. + public static List> Combinations(int n, int k) + { + k = Mathf.Clamp(k, 0, n); + var combinations = new List>(); + var temp = new List(); + GenerateCombinations(ref combinations, temp, 0, n, k); + return combinations; + } + /// + /// Recursively generate all combinations of N-choose-K. + /// Helper function. + /// + /// The combinations are accumulated in this list of lists. + /// Temporary buffer containing current chosen values. + /// Current choice + /// N + /// K remaining to choose + static void GenerateCombinations(ref List> combinations, List temp, int i, int n, int k) + { + if (k == 0) + { + combinations.Add(temp.ToList()); // Take a copy otherwise C# disposes of it. + return; + } + for (int j = i; j < n; ++j) + { + temp.Add(j); + GenerateCombinations(ref combinations, temp, j + 1, n, k - 1); + temp.RemoveAt(temp.Count - 1); + } + } + #endregion + } + + public enum TournamentStatus { Stopped, Running, Waiting, Completed }; + + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class BDATournament : MonoBehaviour + { + public static BDATournament Instance; + + #region Flags and Variables + TournamentState tournamentState; + string stateFile; + string message; + private Coroutine runTournamentCoroutine; + public TournamentStatus tournamentStatus = TournamentStatus.Stopped; + public uint tournamentID = 0; + public TournamentType tournamentType = TournamentType.FFA; + public int numberOfRounds = 0; + public int currentRound = 0; + public int numberOfHeats = 0; + public int currentHeat = 0; + public int heatsRemaining = 0; + public int vesselCount = 0; + public int teamCount = 0; + public int teamsPerHeat = 0; + public int vesselsPerTeam = 0; + public bool fullTeams = false; + bool competitionStarted = false; + public bool warpingInProgress = false; + VesselSpawnerBase spawnerBase; + #endregion + + void Awake() + { + if (Instance) + Destroy(Instance); + Instance = this; + stateFile = TournamentState.defaultStateFile; + } + + void Start() + { + BDArmorySettings.LAST_USED_SAVEGAME = HighLogic.SaveFolder; + StartCoroutine(LoadStateWhenReady()); + } + + IEnumerator LoadStateWhenReady() + { + while (BDACompetitionMode.Instance == null) + yield return null; + LoadTournamentState(); // Load the last state. + } + + void OnDestroy() + { + StopTournament(); // Stop any running tournament. + SaveTournamentState(); // Save the last state. + } + + // Load tournament state from disk + bool LoadTournamentState(string stateFile = "") + { + if (stateFile != "") this.stateFile = stateFile; + tournamentState = new TournamentState(); + if (tournamentState.LoadState(this.stateFile)) + { + message = "Tournament state loaded from " + this.stateFile; + tournamentID = tournamentState.tournamentID; + tournamentType = tournamentState.tournamentType; + vesselCount = tournamentState.vesselCount; + teamCount = tournamentState.teamCount; + teamsPerHeat = tournamentState.teamsPerHeat; + vesselsPerTeam = tournamentState.vesselsPerTeam; + fullTeams = tournamentState.fullTeams; + numberOfRounds = tournamentState.tournamentRoundType == TournamentRoundType.Ranked ? BDArmorySettings.TOURNAMENT_ROUNDS + 1 : tournamentState.rounds.Count; + numberOfHeats = numberOfRounds > 0 ? tournamentState.rounds[0].Count : 0; + heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum() + (tournamentState.tournamentRoundType == TournamentRoundType.Ranked ? (BDArmorySettings.TOURNAMENT_ROUNDS + 1 - tournamentState.rounds.Count) * tournamentState.rounds.First().Value.Count : 0); + } + else + message = "Failed to load tournament state."; + Debug.Log("[BDArmory.BDATournament]: " + message); + // if (BDACompetitionMode.Instance != null) + // BDACompetitionMode.Instance.competitionStatus.Add(message); + tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; + return true; + } + + // Save tournament state to disk + bool SaveTournamentState(bool backup = false) + { + var saveTo = stateFile; + if (backup) + { + var saveToDir = Path.GetDirectoryName(TournamentState.defaultStateFile); + saveToDir = Path.Combine(saveToDir, "Unfinished Tournaments"); + if (!Directory.Exists(saveToDir)) Directory.CreateDirectory(saveToDir); + saveTo = Path.ChangeExtension(Path.Combine(saveToDir, Path.GetFileName(stateFile)), $".state-{tournamentID}"); + } + return tournamentState.SaveState(saveTo); + } + + /// + /// Setup a tournament. + /// + /// The base folder where the craft files or teams are located + /// The number of rounds to in the tournament. + /// The number of vessels per heat for FFA tournaments. + /// The number of teams per heat for teams tournaments. + /// The number of vessels per team for teams tournaments. + /// The number of teams: 0 for FFA, 1 for auto based on files/folders, >1 for splitting evenly. + /// The tournament style: RNG, N-choose-K, etc. + /// The tournament type: FFA, Teams, RankedFFA, RankedTeams, etc. + /// The tournament statefile to use (if different from the usual one). + public void SetupTournament(string folder, int rounds, int vesselsPerHeat = 0, int npcsPerHeat = 0, int teamsPerHeat = 0, int vesselsPerTeam = 0, int numberOfTeams = 0, TournamentStyle tournamentStyle = TournamentStyle.RNG, TournamentRoundType tournamentRoundType = TournamentRoundType.Shuffled, string stateFile = "") + { + if (tournamentState != null && tournamentState.rounds != null && tournamentState.rounds.Count > 0) + { + heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum() + (tournamentState.tournamentRoundType == TournamentRoundType.Ranked ? (BDArmorySettings.TOURNAMENT_ROUNDS + 1 - tournamentState.rounds.Count) * tournamentState.rounds.First().Value.Count : 0); + if (heatsRemaining > 0 && heatsRemaining < numberOfRounds * numberOfHeats) // Started, but incomplete tournament. + { + SaveTournamentState(BDArmorySettings.TOURNAMENT_BACKUPS); + } + } + if (stateFile != "") this.stateFile = stateFile; + if (BDArmorySettings.WAYPOINTS_MODE && BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME) vesselsPerHeat = 1; // Override vessels per heat. + tournamentState = new TournamentState(); + if (numberOfTeams == 0) // FFA + { + if (!tournamentState.GenerateFFATournament(folder, rounds, vesselsPerHeat, npcsPerHeat, tournamentStyle, tournamentRoundType)) return; + } + else // Folders or random teams + { + if (!tournamentState.GenerateTeamsTournament(folder, rounds, teamsPerHeat, vesselsPerTeam, numberOfTeams, tournamentStyle, tournamentRoundType)) return; + } + tournamentID = tournamentState.tournamentID; + tournamentType = tournamentState.tournamentType; + vesselCount = tournamentState.vesselCount; + teamCount = tournamentState.teamCount; + this.teamsPerHeat = tournamentState.teamsPerHeat; + this.vesselsPerTeam = tournamentState.vesselsPerTeam; + fullTeams = tournamentState.fullTeams; + numberOfRounds = tournamentState.tournamentRoundType == TournamentRoundType.Ranked ? BDArmorySettings.TOURNAMENT_ROUNDS + 1 : tournamentState.rounds.Count; + numberOfHeats = numberOfRounds > 0 ? tournamentState.rounds[0].Count : 0; + heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum() + (tournamentState.tournamentRoundType == TournamentRoundType.Ranked ? (BDArmorySettings.TOURNAMENT_ROUNDS + 1 - tournamentState.rounds.Count) * tournamentState.rounds.First().Value.Count : 0); + tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; + tournamentState.scores.Reset(); + SaveTournamentState(); + } + + public void RunTournament() + { + tournamentState.savegame = HighLogic.SaveFolder; + BDACompetitionMode.Instance.StopCompetition(); + SpawnUtils.CancelSpawning(); + if (runTournamentCoroutine != null) + StopCoroutine(runTournamentCoroutine); + runTournamentCoroutine = StartCoroutine(RunTournamentCoroutine()); + if (BDArmorySettings.AUTO_DISABLE_UI) SetGameUI(false); + ScoreWindow.SetMode(ScoreWindow.Mode.Tournament, tournamentState.tournamentType == TournamentType.FFA ? Toggle.Off : Toggle.On); + } + + public void StopTournament() + { + if (runTournamentCoroutine != null) + { + StopCoroutine(runTournamentCoroutine); + runTournamentCoroutine = null; + } + tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; + if (BDArmorySettings.AUTO_DISABLE_UI) SetGameUI(true); + } + + IEnumerator RunTournamentCoroutine() + { + bool firstRun = true; // Whether a heat has been run yet (particularly for loading partway through a tournament). + yield return new WaitForFixedUpdate(); + int roundIndex = -1; + spawnerBase = tournamentState.tournamentStyle == TournamentStyle.TemplateRNG ? CustomTemplateSpawning.Instance : CircularSpawning.Instance; + while (++roundIndex < tournamentState.rounds.Count) // tournamentState.rounds can change during the loop, so we can't just use an iterator now. + { + if (BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS < 0) + { + var spawnConfig = tournamentState.rounds[roundIndex][0]; + var body = FlightGlobals.Bodies[spawnConfig.worldIndex]; + if (!EarlyBird.IsDayTime(spawnConfig.latitude, spawnConfig.longitude, body, 10)) + { + SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude); + BDACompetitionMode.Instance.competitionStatus.Add($"Warping ahead to morning, then running the next round."); + yield return WarpAhead(EarlyBird.TimeToDaylight( + spawnConfig.latitude, + spawnConfig.longitude, + body, + 10 + )); // Warp to morning +10mins. + } + } + + currentRound = roundIndex; + foreach (var heatIndex in tournamentState.rounds[roundIndex].Keys) + { + currentHeat = heatIndex; + if (tournamentState.completed.ContainsKey(roundIndex) && tournamentState.completed[roundIndex].Contains(heatIndex)) continue; // We've done that heat. + + message = $"Running heat {heatIndex} of round {roundIndex} of tournament {tournamentState.tournamentID} ({heatsRemaining} heats remaining in the tournament)."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + + if (firstRun) SpawnUtilsInstance.Instance.gunGameProgress.Clear(); // Clear gun-game progress. + int attempts = 0; + bool unrecoverable = false; + competitionStarted = false; + if (roundIndex == 0 && heatIndex == 0) + SpawnUtils.ResetVesselNamingDeconfliction(); // Start fresh with vessel naming deconfliction. + else + tournamentState.RestoreDeconflictionData(); // Restore the deconfliction data to the most recently used in the tournament (to avoid outside interference). + while (!competitionStarted && attempts++ < 3) // 3 attempts is plenty + { + SpawnUtils.ResetVesselNamingDeconfliction(fightersOnly: !fullTeams); + tournamentStatus = TournamentStatus.Running; + if (BDArmorySettings.WAYPOINTS_MODE) + yield return ExecuteWaypointHeat(roundIndex, heatIndex); + else + yield return ExecuteHeat(roundIndex, heatIndex, attempts == BDArmorySettings.TOURNAMENT_START_DESPITE_FAILURES_ON_ATTEMPT && BDArmorySettings.COMPETITION_START_DESPITE_FAILURES, firstRun); // On the third attempt, start despite failures if the option is set. + if (!competitionStarted) + { + switch (spawnerBase.spawnFailureReason) + { + case SpawnFailureReason.None: // Successful spawning, but competition failed to start for some reason. + BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + BDACompetitionMode.Instance.competitionStartFailureReason + ", trying again."); + break; + case SpawnFailureReason.VesselLostParts: // Recoverable spawning failure. + BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + spawnerBase.spawnFailureReason + ", trying again with increased altitude."); + if (tournamentState.rounds[roundIndex][heatIndex].altitude < 10) tournamentState.rounds[roundIndex][heatIndex].altitude = Math.Min(tournamentState.rounds[roundIndex][heatIndex].altitude + 3, 10); // Increase the spawning altitude for ground spawns and try again. + break; + case SpawnFailureReason.TimedOut: // Recoverable spawning failure. + BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + spawnerBase.spawnFailureReason + ", trying again."); + break; + case SpawnFailureReason.NoTerrain: // Failed to find the terrain when ground spawning. + BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + spawnerBase.spawnFailureReason + ", trying again."); + attempts = Math.Max(attempts, 2); // Try only once more. + break; + case SpawnFailureReason.DependencyIssues: + message = $"Failed to start heat due to {spawnerBase.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 " + spawnerBase.spawnFailureReason + ", aborting."); + attempts = 3; + unrecoverable = true; + break; + } + } + } + if (!competitionStarted) + { + message = $"Failed to run heat {(unrecoverable ? "due to unrecoverable error" : $"after 3 spawning attempts")}, failure reasons: " + spawnerBase.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; + yield break; + } + firstRun = false; + tournamentState.StoreDeconflictionData(); // Update the deconfliction data from this heat. + + // Register the heat as completed. + if (!tournamentState.completed.ContainsKey(roundIndex)) tournamentState.completed.Add(roundIndex, new HashSet()); + tournamentState.completed[roundIndex].Add(heatIndex); + tournamentState.scores.AddHeatScores(BDACompetitionMode.Instance.Scores); // Note: this is done after LogResults is called. + SaveTournamentState(); + heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum() + (tournamentState.tournamentRoundType == TournamentRoundType.Ranked ? (BDArmorySettings.TOURNAMENT_ROUNDS - roundIndex) * tournamentState.rounds.First().Value.Count : 0); + + if (TournamentAutoResume.Instance != null && TournamentAutoResume.Instance.CheckMemoryUsage()) yield break; + + if (tournamentState.completed[roundIndex].Count < tournamentState.rounds[roundIndex].Count) + { + // Wait a bit for any user action + tournamentStatus = TournamentStatus.Waiting; + double startTime = Planetarium.GetUniversalTime(); + 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 running the next heat."); + yield return new WaitForSeconds(1); + } + } + } + if (!firstRun) + { + tournamentState.scores.ComputeScores(); + message = "All heats in round " + roundIndex + " have been run."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + LogScores(tournamentState.tournamentType == TournamentType.Teams); + if (BDArmorySettings.WAYPOINTS_MODE) + { + /* commented out until this is made functional + foreach (var tracer in WaypointFollowingStrategy.Ghosts) //clear and reset vessel ghosts each new Round + { + tracer.gameObject.SetActive(false); + } + WaypointFollowingStrategy.Ghosts.Clear(); + */ + } + if (tournamentState.tournamentRoundType == TournamentRoundType.Ranked && roundIndex < BDArmorySettings.TOURNAMENT_ROUNDS) // Generate the next ranked round. + { + tournamentState.GenerateRankedRound(); + heatsRemaining = (BDArmorySettings.TOURNAMENT_ROUNDS - roundIndex) * tournamentState.rounds.First().Value.Count; + } + if (heatsRemaining > 0) + { + if (BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS > 0) + { + BDACompetitionMode.Instance.competitionStatus.Add($"Warping ahead {BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS} mins, then running the next round."); + yield return WarpAhead(BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS * 60); + } + else + { + // Wait a bit for any user action + tournamentStatus = TournamentStatus.Waiting; + double startTime = Planetarium.GetUniversalTime(); + 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 running the next round."); + yield return new WaitForSeconds(1); + } + } + } + } + } + message = "All rounds in tournament " + tournamentState.tournamentID + " have been run."; + 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_RESUME_CONTINUOUS_SPAWN) && BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT && TournamentAutoResume.Instance != null) + { + 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); + yield break; + } + } + + IEnumerator ExecuteWaypointHeat(int roundIndex, int heatIndex) + { + if (TournamentCoordinator.Instance.IsRunning) TournamentCoordinator.Instance.Stop(); + var spawnConfig = tournamentState.rounds[roundIndex][heatIndex] as CircularSpawnConfig; + 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(WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].waypoints), + spawnerBase + ); + + // Run the waypoint competition. + TournamentCoordinator.Instance.Run(); + // Wait until spawning is completed and the competition is actually about to start. + yield return new WaitWhile(() => TournamentCoordinator.Instance.IsRunning && !BDACompetitionMode.Instance.competitionIsActive); + competitionStarted = true; + // Register all the active vessels as part of the tournament. + foreach (var kvp in SpawnUtils.SpawnedVesselURLs) + tournamentState.scores.AddPlayer(kvp.Key, kvp.Value, roundIndex, tournamentState.npcFiles.Contains(kvp.Value)); + yield return new WaitWhile(() => TournamentCoordinator.Instance.IsRunning); + } + + IEnumerator ExecuteHeat(int roundIndex, int heatIndex, bool startDespiteFailures = false, bool firstRun = false) + { + if (tournamentState.tournamentStyle == TournamentStyle.TemplateRNG) + { + CustomSpawnConfig customSpawnConfig = tournamentState.rounds[roundIndex][heatIndex] as CustomSpawnConfig; + // Populate the customSpawnConfig.customVesselSpawnConfigs with the vessels + if (BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER) customSpawnConfig.customVesselSpawnConfigs.Shuffle(); // Randomise the team spawn points. + for (int teamIndex = 0; teamIndex < customSpawnConfig.customVesselSpawnConfigs.Count; ++teamIndex) + { + if (BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER) customSpawnConfig.customVesselSpawnConfigs[teamIndex].Shuffle(); // Randomise the positions within each team spawn point. + for (int craftIndex = 0; craftIndex < customSpawnConfig.customVesselSpawnConfigs[teamIndex].Count; ++craftIndex) + { + if (teamIndex < customSpawnConfig.teamsSpecific.Count && craftIndex < customSpawnConfig.teamsSpecific[teamIndex].Count) + { + customSpawnConfig.customVesselSpawnConfigs[teamIndex][craftIndex].craftURL = customSpawnConfig.teamsSpecific[teamIndex][craftIndex]; + customSpawnConfig.customVesselSpawnConfigs[teamIndex][craftIndex].teamIndex = teamIndex; + } + else // Clear the remaining entries. + customSpawnConfig.customVesselSpawnConfigs[teamIndex][craftIndex].craftURL = ""; + customSpawnConfig.customVesselSpawnConfigs[teamIndex][craftIndex].kerbalName = ""; // Use random crew. + } + } + CustomTemplateSpawning.Instance.SpawnCustomTemplate(customSpawnConfig); + while (CustomTemplateSpawning.Instance.vesselsSpawning) + yield return new WaitForFixedUpdate(); + if (!CustomTemplateSpawning.Instance.vesselSpawnSuccess) + { + tournamentStatus = TournamentStatus.Stopped; + yield break; + } + // Populate the VS window's UI entries with the spawned vessels. + CustomTemplateSpawning.Instance.PopulateEntriesFromConfig(customSpawnConfig); + } + else + { + CircularSpawning.Instance.SpawnAllVesselsOnce(tournamentState.rounds[roundIndex][heatIndex] as CircularSpawnConfig); + while (CircularSpawning.Instance.vesselsSpawning) + yield return new WaitForFixedUpdate(); + if (!CircularSpawning.Instance.vesselSpawnSuccess) + { + tournamentStatus = TournamentStatus.Stopped; + yield break; + } + } + yield return new WaitForFixedUpdate(); + if (firstRun) + { + if (!BDTISettings.STORE_TEAM_COLORS) BDTISetup.Instance.ResetColors(); // Get some good colours on the first run instead of random ones. + } + // NOTE: runs in separate coroutine + if (BDArmorySettings.RUNWAY_PROJECT) + { + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + case 33: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 44: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 53: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 67: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 77: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + default: + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, startDespiteFailures); + break; + } + } + else + 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 = 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); + while (BDACompetitionMode.Instance.competitionStarting || BDACompetitionMode.Instance.sequencedCompetitionStarting) + yield return new 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.BDATournament]: " + message); + tournamentStatus = TournamentStatus.Stopped; + yield break; + } + competitionStarted = true; + // Register all the active vessels as part of the tournament. + foreach (var kvp in SpawnUtils.SpawnedVesselURLs) + tournamentState.scores.AddPlayer(kvp.Key, kvp.Value, roundIndex, tournamentState.npcFiles.Contains(kvp.Value)); + // Wait for the competition to finish. + while (BDACompetitionMode.Instance.competitionIsActive) + yield return new WaitForSeconds(1); + } + + #region Warping + GameObject warpCamera; + IEnumerator WarpAhead(double warpTimeBetweenHeats) + { + if (!FlightGlobals.currentMainBody.hasSolidSurface) + { + message = "Sorry, unable to TimeWarp without a solid surface to place the spawn probe on."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + yield return new WaitForSeconds(5f); + yield break; + } + warpingInProgress = true; + Vessel spawnProbe; + var vesselsToKill = FlightGlobals.Vessels.ToList(); + int tries = 0; + do + { + spawnProbe = VesselSpawner.SpawnSpawnProbe(); + yield return new WaitWhile(() => spawnProbe != null && (!spawnProbe.loaded || spawnProbe.packed)); + while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe) + { + LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnProbe); + yield return null; + } + } while (++tries < 3 && spawnProbe == null); + if (spawnProbe == null) + { + message = "Failed to spawn spawnProbe, aborting warp."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogWarning("[BDArmory.BDATournament]: " + message); + yield break; + } + var up = spawnProbe.up; + 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 - 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. + // Kill all other vessels (including debris). + foreach (var vessel in vesselsToKill) + SpawnUtils.RemoveVessel(vessel); + while (SpawnUtils.removingVessels) yield return null; + + // Adjust the camera for a nice view. + if (warpCamera == null) warpCamera = new GameObject("WarpCamera"); + var cameraLocalPosition = 3f * Vector3.Cross(up, refDirection).normalized + up; + warpCamera.SetActive(true); + warpCamera.transform.position = spawnProbe.transform.position; + warpCamera.transform.rotation = Quaternion.LookRotation(-cameraLocalPosition, up); + var flightCamera = FlightCamera.fetch; + var originalCameraParentTransform = flightCamera.transform.parent; + var originalCameraNearClipPlane = flightCamera.mainCamera.nearClipPlane; + flightCamera.SetTargetNone(); + flightCamera.transform.parent = warpCamera.transform; + flightCamera.transform.localPosition = cameraLocalPosition; + flightCamera.transform.localRotation = Quaternion.identity; + flightCamera.SetDistance(3000f); + + var warpTo = Planetarium.GetUniversalTime() + warpTimeBetweenHeats; + var startTime = Time.time; + do + { + if (TimeWarp.WarpMode != TimeWarp.Modes.HIGH && TimeWarp.CurrentRate > 1) // Warping in low mode, abort. + { + 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 - 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. + } + startTime = Time.time; + while (TimeWarp.WarpMode != TimeWarp.Modes.HIGH && Time.time - startTime < 3) + { + spawnProbe.SetWorldVelocity(Vector3d.zero); // Set the velocity to zero so that warp goes in high mode. + yield return null; // Give it a second to switch to high warp mode. + } + TimeWarp.fetch.WarpTo(warpTo); + startTime = Time.time; + while (TimeWarp.CurrentRate < 2 && Time.time - startTime < 1) yield return null; // Give it a second to get going. + } while (TimeWarp.WarpMode != TimeWarp.Modes.HIGH && TimeWarp.CurrentRate > 1); // Warping, but not high warp, bugger. Try again. FIXME KSP isn't the focused app, it doesn't want to go into high warp! + while (TimeWarp.CurrentRate > 1) yield return null; // Wait for the warping to stop. + + // Put the camera parent back. + flightCamera.transform.parent = originalCameraParentTransform; + flightCamera.mainCamera.nearClipPlane = originalCameraNearClipPlane; + warpCamera.SetActive(false); + + warpingInProgress = false; + } + + private class EarlyBird // Based on, but not the same as the EarlyBird mod. + { + /// + /// The time until the next sunrise (plus offset in minutes). + /// + /// Latitude + /// Longitude + /// The celestial body. + /// An offset in minutes. + /// The time until the next sunrise. + public static double TimeToDaylight(double lat, double lon, CelestialBody body, double offset = 0) + { + if (body.isStar) return 0; + var sun = Planetarium.fetch.Sun; + var localTime = GetLocalTime(lon, body, sun); + offset *= 60 / body.solarDayLength; + double dayLength = GetDayLength(lat, body, sun); + double timeOfDawn = 0.5 - dayLength / 2 + offset; + return (timeOfDawn - localTime + 1.0) % 1.0 * body.solarDayLength; + } + /// + /// Check whether it's daytime (within the margin) at the give location. + /// + /// Latitude + /// Longitude + /// The celestial body. + /// Margin in minutes. + /// True if it's daytime, otherwise false. + public static bool IsDayTime(double lat, double lon, CelestialBody body, double margin = 0) + { + if (body.isStar) return true; + var sun = Planetarium.fetch.Sun; + var localTime = GetLocalTime(lon, body, sun); + margin *= 60 / body.solarDayLength; + double dayLength = GetDayLength(lat, body, sun); + double timeOfDawn = 0.5 - dayLength / 2 + margin; + double timeOfDusk = 0.5 + dayLength / 2 - margin; + return localTime > timeOfDawn && localTime < timeOfDusk; + } + /// + /// Gets the day length in the range 0—1. + /// + /// + /// + /// + /// + public static double GetDayLength(double lat, CelestialBody body, CelestialBody sun) + { + if (body.isStar) return 1; + // cos ω₀ = -tan φ * tan δ + // ω₀ = solar hour angle, φ = observer latitude, δ = solar declination + var solarDeclination = body.GetLatitude(sun.position - body.position, true); + var cosW0 = -Math.Tan(lat * Math.PI / 180) * Math.Tan(solarDeclination * Math.PI / 180); + if (cosW0 <= -1) return 1; + if (cosW0 >= 1) return 0; + var solarHourAngle = Math.Acos(cosW0); + return solarHourAngle / Math.PI; + } + /// + /// Gets the local time in the range 0—1 with 0.5 being noon. + /// + /// + /// + /// + /// + public static double GetLocalTime(double lon, CelestialBody body, CelestialBody sun) + { + if (body.isStar) return 0.5; + return ((lon - body.GetLongitude(sun.position - body.position, true)) % 360 / 360.0 + 1.5) % 1.0; + } + } + + public IEnumerator WarpIfNeeded(SpawnConfig spawnConfig) + { + if (BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS < 0) + { + var body = FlightGlobals.Bodies[spawnConfig.worldIndex]; + if (!EarlyBird.IsDayTime(spawnConfig.latitude, spawnConfig.longitude, body, 10)) + { + SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude); + BDACompetitionMode.Instance.competitionStatus.Add($"Warping ahead to morning, then running the next round."); + yield return WarpAhead(EarlyBird.TimeToDaylight( + spawnConfig.latitude, + spawnConfig.longitude, + body, + 10 + )); // Warp to morning +10mins. + } + } + } + #endregion + + 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(); } + } + + List> rankedScores = new List>(); + float lastUpdatedRankedScores = 0; + public List> GetRankedScores // Get a list of the scores in ranked order. + { + get + { + if (tournamentState.scores.lastUpdated > lastUpdatedRankedScores) + { + rankedScores = tournamentState.scores.scores.OrderByDescending(kvp => kvp.Value).Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)).ToList(); + lastUpdatedRankedScores = tournamentState.scores.lastUpdated; + if (ScoreWindow.Instance != null) ScoreWindow.Instance.ResetWindowSize(); + } + return rankedScores; + } + } + + List> rankedTeamScores = new List>(); + float lastUpdatedRankedTeamScores = 0; + public List> GetRankedTeamScores + { + get + { + if (tournamentState.scores.lastUpdated > lastUpdatedRankedTeamScores) + { + // Get the unique teams, then make a dictionary with team names as keys and the sum of scores as values and sort them by the scores. + var teamNames = tournamentState.scores.playersToTeamNames.Values.ToHashSet(); + var teamScores = teamNames.ToDictionary( + teamName => teamName, + teamName => tournamentState.scores.scores.Where(kvp => tournamentState.scores.playersToTeamNames.ContainsKey(kvp.Key) && tournamentState.scores.playersToTeamNames[kvp.Key] == teamName).Sum(kvp => kvp.Value)); + rankedTeamScores = teamScores.OrderByDescending(kvp => kvp.Value).ToList(); + lastUpdatedRankedTeamScores = tournamentState.scores.lastUpdated; + if (ScoreWindow.Instance != null) ScoreWindow.Instance.ResetWindowSize(); + } + return rankedTeamScores; + } + } + + void LogScores(bool teams) + { + var scores = teams ? GetRankedTeamScores : GetRankedScores; + if (scores.Count == 0) return; + var logsFolder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs")); + var fileName = Path.Combine(logsFolder, $"Tournament {tournamentID}", teams ? "team scores.log" : "ranked scores.log"); + var maxNameLength = scores.Max(kvp => kvp.Key.Length); + var lines = scores.Select((kvp, rank) => $"{rank + 1,3:D} - {kvp.Key} {new string(' ', maxNameLength - kvp.Key.Length)}{kvp.Value,8:F3}").ToList(); + if (tournamentState.tournamentRoundType == TournamentRoundType.Ranked) + lines.Insert(0, $"Tournament {tournamentID}, round {currentRound} / {BDArmorySettings.TOURNAMENT_ROUNDS}"); // Round 0 is the initial shuffled round. + else + lines.Insert(0, $"Tournament {tournamentID}, round {currentRound + 1} / {numberOfRounds}"); // For non-ranked rounds, start counting at 1. + File.WriteAllLines(fileName, lines); + } + + public Tuple GetTournamentProgress() + { + if (tournamentState.tournamentRoundType == TournamentRoundType.Ranked) + return new Tuple(currentRound, BDArmorySettings.TOURNAMENT_ROUNDS, currentHeat + 1, numberOfHeats); // Round 0 is the initial shuffled round. + else + return new Tuple(currentRound + 1, numberOfRounds, currentHeat + 1, numberOfHeats); // For non-ranked rounds, start counting at 1. + } + + public void RecomputeScores() => tournamentState.scores.ComputeScores(); + + /// + /// Add a player to the tournament scoring while a tournament is running. + /// + /// The craft's name. + /// The file the craft originated from. + /// Whether or not the vessel is a fighter. + public void AddPlayer(Vessel vessel) + { + if (vessel == null) return; + var ac = vessel.ActiveController(); + if (ac.WM == null) return; // Not a valid vessel for combat. + tournamentState.scores.AddPlayer(vessel.GetName(), ac.WM.SourceVesselURL, fighter: ac.IsFighter); + } + } + + /// + /// A class to automatically load and resume a tournament upon starting KSP. + /// Borrows heavily from the AutoLoadGame mod. + /// + [KSPAddon(KSPAddon.Startup.MainMenu, false)] + public class TournamentAutoResume : MonoBehaviour + { + public static TournamentAutoResume Instance; + public static bool firstRun = true; + string savesDir; + string savegame; + string save = "persistent"; + string game; + bool sceneLoaded = false; + public static float memoryUsage + { + get + { + if (_memoryUsage > 0) return _memoryUsage; + _memoryUsage = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong() + UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong(); + var gfxDriver = UnityEngine.Profiling.Profiler.GetAllocatedMemoryForGraphicsDriver(); + _memoryUsage += gfxDriver > 0 ? gfxDriver : 5f * (1 << 30); // Use the GfxDriver memory usage if available, otherwise estimate it at 5GB (which is a little more than what I get with no extra visual mods at ~4.5GB). + _memoryUsage /= (1 << 30); // In GB. + return _memoryUsage; + } + set { _memoryUsage = 0; } // Reset condition for calculating it again. + } + static float _memoryUsage; + + void Awake() + { + if (Instance != null || !firstRun) // Only the first loaded instance gets to run. + { + Destroy(this); + return; + } + Instance = this; + DontDestroyOnLoad(this); + GameEvents.onLevelWasLoadedGUIReady.Add(onLevelWasLoaded); + savesDir = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "saves")); + } + + void OnDestroy() + { + GameEvents.onLevelWasLoadedGUIReady.Remove(onLevelWasLoaded); + } + + void onLevelWasLoaded(GameScenes scene) + { + sceneLoaded = true; + if (scene != GameScenes.MAINMENU) return; + if (!firstRun) return; + firstRun = false; + StartCoroutine(WaitForSettings()); + } + + IEnumerator WaitForSettings() + { + yield return new WaitForSeconds(0.5f); + var tic = Time.realtimeSinceStartup; + 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 continuous spawn: {BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN}, auto-resume evolution: {BDArmorySettings.AUTO_RESUME_EVOLUTION}, generate clean save: {BDArmorySettings.GENERATE_CLEAN_SAVE}."); + if (BDArmorySettings.AUTO_RESUME_TOURNAMENT || BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN || BDArmorySettings.AUTO_RESUME_EVOLUTION || BDArmorySettings.AUTO_LOAD_TO_KSC) + { yield return StartCoroutine(AutoResumeTournament()); } + else if (BDArmorySettings.GENERATE_CLEAN_SAVE && TryLoadCleanSlate()) + { GenerateCleanGame(false); } + } + + IEnumerator AutoResumeTournament() + { + bool resumingEvolution = false; + bool resumingTournament = false; + bool resumingContinuousSpawn = false; + bool generateNewTournament = false; + EvolutionWorkingState evolutionState = null; + if (BDArmorySettings.AUTO_RESUME_EVOLUTION) // Auto-resume evolution overrides auto-resume tournament. + { + evolutionState = TryLoadEvolutionState(); + resumingEvolution = evolutionState != null; + } + if (!resumingEvolution && BDArmorySettings.AUTO_RESUME_TOURNAMENT) + { + resumingTournament = TryLoadTournamentState(out generateNewTournament); + } + if (!(resumingEvolution || resumingTournament) && BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN) + { + resumingContinuousSpawn = TryResumingContinuousSpawn(); + } + if (!(resumingEvolution || resumingTournament || resumingContinuousSpawn)) // Auto-Load To KSC + { + if (!TryLoadCleanSlate()) yield break; + } + // Load saved game. + var tic = Time.time; + sceneLoaded = false; + if (!(BDArmorySettings.GENERATE_CLEAN_SAVE ? GenerateCleanGame() : LoadGame())) yield break; + yield return new WaitUntil(() => sceneLoaded || Time.time - tic > 10); + if (!sceneLoaded) { Debug.Log("[BDArmory.BDATournament]: Failed to load scene."); yield break; } + if (!(resumingEvolution || resumingTournament || resumingContinuousSpawn)) yield break; // Just load to the KSC. + var lastUsedWorldIndex = BDArmorySettings.VESSEL_SPAWN_WORLDINDEX; // Store the last used world index as it gets reset when entering flight mode. + + // Switch to flight mode. + sceneLoaded = false; + 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; } + // Resume the tournament. + yield return new WaitForSeconds(1); + if (resumingEvolution) // Auto-resume evolution overrides auto-resume tournament. + { + tic = Time.time; + 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; + LoadedVesselSwitcher.Instance.SetVisible(true); + BDArmorySetup.Instance.showEvolutionGUI = true; + BDAModuleEvolution.Instance.ResumeEvolution(evolutionState); + } + else if (resumingTournament) + { + tic = Time.time; + if (generateNewTournament) + { + yield return new WaitWhile(() => (BDATournament.Instance == null && Time.time - tic < 10)); // Wait for the BDATournament instance to be started or time out. + if (BDATournament.Instance == null) yield break; + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX = lastUsedWorldIndex; + BDATournament.Instance.SetupTournament( + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION, + BDArmorySettings.TOURNAMENT_ROUNDS, + BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, + BDArmorySettings.TOURNAMENT_NPCS_PER_HEAT, + BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, + BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + (TournamentStyle)BDArmorySettings.TOURNAMENT_STYLE, + (TournamentRoundType)BDArmorySettings.TOURNAMENT_ROUND_TYPE + ); + } + 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; + LoadedVesselSwitcher.Instance.SetVisible(true); + VesselSpawnerWindow.Instance.SetVisible(true); + RWPSettings.SetRWP(BDArmorySettings.RUNWAY_PROJECT, BDArmorySettings.RUNWAY_PROJECT_ROUND); // Reapply the RWP settings if RWP is active as some may be overridden by the above. + BDATournament.Instance.RunTournament(); + } + else if (resumingContinuousSpawn) + { + tic = Time.time; + yield return new WaitWhile(() => ContinuousSpawning.Instance == null && Time.time - tic < 10); // Wait up to 10s for the continuous spawning instance to be valid. + if (ContinuousSpawning.Instance == null) yield break; + BDArmorySetup.windowBDAToolBarEnabled = true; + LoadedVesselSwitcher.Instance.SetVisible(true); + VesselSpawnerWindow.Instance.SetVisible(true); + RWPSettings.SetRWP(BDArmorySettings.RUNWAY_PROJECT, BDArmorySettings.RUNWAY_PROJECT_ROUND); // Reapply the RWP settings if RWP is active as some may be overridden by the above. + ContinuousSpawning.Instance.SpawnVesselsContinuously( + new CircularSpawnConfig( // Spawn config that would be used by clicking the continuous spawn button. + 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, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING + ) + ); + } + } + + EvolutionWorkingState TryLoadEvolutionState() + { + Debug.Log("[BDArmory.BDATournament]: Attempting to auto-resume evolution."); + EvolutionWorkingState evolutionState = null; + evolutionState = BDAModuleEvolution.LoadState(); + if (string.IsNullOrEmpty(evolutionState.savegame)) { Debug.Log($"[BDArmory.BDATournament]: No savegame found in evolution state."); return null; } + if (string.IsNullOrEmpty(evolutionState.evolutionId) || !File.Exists(Path.Combine(BDAModuleEvolution.configDirectory, evolutionState.evolutionId + ".cfg"))) { Debug.Log($"[BDArmory.BDATournament]: No saved evolution configured."); return null; } + savegame = Path.Combine(savesDir, evolutionState.savegame, save + ".sfs"); + game = evolutionState.savegame; + return evolutionState; + } + bool TryLoadTournamentState(out bool generateNewTournament) + { + generateNewTournament = false; + // Check that there is an incomplete tournament, otherwise abort. + bool incompleteTournament = false; + if (File.Exists(TournamentState.defaultStateFile)) // Tournament state file exists. + { + var tournamentState = new TournamentState(); + if (!tournamentState.LoadState(TournamentState.defaultStateFile)) return false; // Failed to load + savegame = Path.Combine(savesDir, tournamentState.savegame, save + ".sfs"); + if (File.Exists(savegame) && tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum() > 0) // Tournament state includes the savegame and has some rounds remaining —> Let's try resuming it! + { + incompleteTournament = true; + game = tournamentState.savegame; + } + } + if (!incompleteTournament && BDArmorySettings.AUTO_GENERATE_TOURNAMENT_ON_RESUME) // Generate a new tournament based on the current settings. + { + generateNewTournament = true; + game = BDArmorySettings.LAST_USED_SAVEGAME; + savegame = Path.Combine(savesDir, game, save + ".sfs"); + if (File.Exists(savegame)) // Found a usable savegame and we assume the generated tournament will be usable. (It should just show error messages in-game otherwise.) + incompleteTournament = true; + } + return incompleteTournament; + } + bool TryResumingContinuousSpawn() + { + game = BDArmorySettings.LAST_USED_SAVEGAME; + savegame = Path.Combine(savesDir, game, save + ".sfs"); + if (!File.Exists(savegame)) return false; // Unable to find a usable savegame. + // Check if the spawn config would be valid and return success if it is. + var AutoSpawnPath = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, VesselSpawnerBase.AutoSpawnFolder)); + var spawnPath = Path.Combine(AutoSpawnPath, BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION); + if (!Directory.Exists(spawnPath)) return false; + if (Directory.GetFiles(spawnPath, "*.craft").Length < 2) return false; + return true; + } + bool TryLoadCleanSlate() + { + game = BDArmorySettings.LAST_USED_SAVEGAME; + if (string.IsNullOrEmpty(game)) game = "sandbox"; // Set the game to the default "sandbox" name if no previous name has been used. + savegame = Path.Combine(savesDir, game, save + ".sfs"); + return File.Exists(savegame) || BDArmorySettings.GENERATE_CLEAN_SAVE; + } + + bool GenerateCleanGame(bool startGame = true) + { + // Grab the scenarios from the previous persistent game. + HighLogic.CurrentGame = GamePersistence.LoadGame("persistent", game, true, false); + var scenarios = HighLogic.CurrentGame?.scenarios; + + if (BDArmorySettings.GENERATE_CLEAN_SAVE) + { + // Generate a new clean game and add in the scenarios. + HighLogic.CurrentGame = new Game(); + HighLogic.CurrentGame.startScene = GameScenes.SPACECENTER; + HighLogic.CurrentGame.Mode = Game.Modes.SANDBOX; + HighLogic.SaveFolder = game; + if (scenarios != null) foreach (var scenario in scenarios) { CheckForScenario(scenario.moduleName, scenario.targetScenes); } + + // Generate the default roster and make them all badass pilots. + HighLogic.CurrentGame.CrewRoster = KerbalRoster.GenerateInitialCrewRoster(HighLogic.CurrentGame.Mode); + foreach (var kerbal in HighLogic.CurrentGame.CrewRoster.Kerbals(ProtoCrewMember.RosterStatus.Available)) + { + kerbal.isBadass = true; // Make them badass. + KerbalRoster.SetExperienceTrait(kerbal, KerbalRoster.pilotTrait); // Make the kerbal a pilot (so they can use SAS properly). + KerbalRoster.SetExperienceLevel(kerbal, KerbalRoster.GetExperienceMaxLevel()); // Make them experienced. + kerbal.courage = 0.5f; + } + } + else + { + GamePersistence.UpdateScenarioModules(HighLogic.CurrentGame); + } + // Update the game state and save it to the persistent save (since that's what eventually ends up getting loaded when we call Start()). + HighLogic.CurrentGame.Updated(); + GamePersistence.SaveGame("persistent", game, SaveMode.OVERWRITE); + if (startGame) HighLogic.CurrentGame.Start(); + return true; + } + + bool LoadGame() + { + var gameNode = GamePersistence.LoadSFSFile(save, game); + if (gameNode == null) + { + Debug.LogWarning($"[BDArmory.BDATournament]: Unable to load the save game: {savegame}"); + return false; + } + Debug.Log($"[BDArmory.BDATournament]: Loaded save game: {savegame}"); + KSPUpgradePipeline.Process(gameNode, game, SaveUpgradePipeline.LoadContext.SFS, OnLoadDialogPiplelineFinished, (opt, n) => Debug.LogWarning($"[BDArmory.BDATournament]: KSPUpgradePipeline finished with error: {savegame}")); + return true; + } + + void OnLoadDialogPiplelineFinished(ConfigNode node) + { + HighLogic.CurrentGame = GamePersistence.LoadGameCfg(node, game, true, false); + if (HighLogic.CurrentGame == null) return; + if (GamePersistence.UpdateScenarioModules(HighLogic.CurrentGame)) + { + if (node != null) + { GameEvents.onGameStatePostLoad.Fire(node); } + GamePersistence.SaveGame(HighLogic.CurrentGame, save, game, SaveMode.OVERWRITE); + } + HighLogic.CurrentGame.startScene = GameScenes.SPACECENTER; + HighLogic.SaveFolder = game; + HighLogic.CurrentGame.Start(); + } + + /// + /// Look for the scenario in the currently loaded assemblies and add the scenario to the requested scenes. + /// These come from a previous persistent.sfs save. + /// + /// Name of the scenario. + /// The scenes the scenario should be present in. + void CheckForScenario(string scenarioName, List targetScenes) + { + if (scenarioName == "ProgressTracking") return; // Skip "ProgressTracking", which can trigger tutorials again. + foreach (var assy in AssemblyLoader.loadedAssemblies) + { + foreach (var type in assy.assembly.GetTypes()) + { + if (type == null) continue; + if (type.Name == scenarioName) + { + HighLogic.CurrentGame.AddProtoScenarioModule(type, [.. targetScenes]); + return; + } + } + } + } + + /// + /// Check the non-native memory usage and automatically quit if it's above the configured threshold. + /// Note: only the managed (non-native) memory is checked, the amount of native memory may or may not be comparable to the amount of non-native memory. FIXME This needs checking in a long tournament. + /// + /// + public bool CheckMemoryUsage() + { + if (!(BDArmorySettings.AUTO_RESUME_TOURNAMENT || BDArmorySettings.AUTO_RESUME_EVOLUTION) || BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD > BDArmorySetup.SystemMaxMemory) return false; // Only trigger if Auto-Resume Tournaments is enabled and the Quit Memory Usage Threshold is set. + memoryUsage = 0; // Trigger recalculation of memory usage. + if (memoryUsage >= BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD) + { + if (BDACompetitionMode.Instance != null) BDACompetitionMode.Instance.competitionStatus.Add("Quitting in 3s due to memory usage threshold reached."); + Debug.LogWarning($"[BDArmory.BDATournament]: Quitting KSP due to reaching Auto-Quit Memory Threshold: {memoryUsage} / {BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD}GB"); + StartCoroutine(AutoQuitCoroutine(3)); // Trigger quit in 3s to give the tournament coroutine time to stop and the message to be shown. + return true; + } + return false; + } + + public static void AutoQuit(float delay = 1) => Instance.StartCoroutine(Instance.AutoQuitCoroutine(delay)); + + /// + /// Automatically quit KSP after a delay. + /// + /// + /// + IEnumerator AutoQuitCoroutine(float delay = 1) + { + yield return new WaitForSeconds(delay); + SpawnUtils.CancelSpawning(); // Make sure any current spawning is stopped. + 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(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Misc/BDTeam.cs b/BDArmory/Competition/BDTeam.cs similarity index 87% rename from BDArmory/Misc/BDTeam.cs rename to BDArmory/Competition/BDTeam.cs index 20d418f57..5014f9392 100644 --- a/BDArmory/Misc/BDTeam.cs +++ b/BDArmory/Competition/BDTeam.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using UnityEngine; + using BDArmory.UI; +using BDArmory.Utils; -namespace BDArmory.Misc +namespace BDArmory.Competition { [Serializable] public class BDTeam @@ -54,22 +57,23 @@ public static BDTeam Deserialize(string teamString) return BDTeam.Get("B"); try { - BDTeam team = UnityEngine.JsonUtility.FromJson(Misc.JsonDecompat(teamString)); + BDTeam team = UnityEngine.JsonUtility.FromJson(OtherUtils.JsonDecompat(teamString)); if (!BDArmorySetup.Instance.Teams.ContainsKey(team.Name)) { BDArmorySetup.Instance.Teams.Add(team.Name, team); } return BDArmorySetup.Instance.Teams[team.Name]; } - catch + catch (Exception e) { + Debug.LogWarning("[BDArmory.BDTeam]: Exception thrown in Deserialize: " + e.Message + "\n" + e.StackTrace); return BDTeam.Get("A"); } } public string Serialize() { - return Misc.JsonCompat(UnityEngine.JsonUtility.ToJson(this)); + return OtherUtils.JsonCompat(UnityEngine.JsonUtility.ToJson(this)); } public override int GetHashCode() => Name.GetHashCode(); diff --git a/BDArmory/Competition/OrchestrationStrategies/OrchestrationStrategy.cs b/BDArmory/Competition/OrchestrationStrategies/OrchestrationStrategy.cs new file mode 100644 index 000000000..adff6c0d4 --- /dev/null +++ b/BDArmory/Competition/OrchestrationStrategies/OrchestrationStrategy.cs @@ -0,0 +1,24 @@ +using System.Collections; + +using BDArmory.Competition.RemoteOrchestration; + +namespace BDArmory.Competition.OrchestrationStrategies +{ + public interface OrchestrationStrategy + { + /// + /// Part 2 of Remote Orchestration + /// + /// Receives a pre-configured environment with spawned craft ready to fly. + /// + /// + /// + /// + public IEnumerator Execute(BDAScoreClient client, BDAScoreService service); + + /// + /// Perform any necessary cleanup if the Execute coroutine is interrupted early. + /// + public void CleanUp(); + } +} diff --git a/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs b/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs new file mode 100644 index 000000000..46eac9967 --- /dev/null +++ b/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs @@ -0,0 +1,98 @@ +using System.Collections; +using UnityEngine; + +using BDArmory.Settings; +using BDArmory.Competition.RemoteOrchestration; +using static BDArmory.Competition.RemoteOrchestration.BDAScoreService; + +namespace BDArmory.Competition.OrchestrationStrategies +{ + public class RankedFreeForAllStrategy : OrchestrationStrategy + { + private BDAScoreService service; + private BDAScoreClient client; + + public RankedFreeForAllStrategy() + { + } + + public IEnumerator Execute(BDAScoreClient client, BDAScoreService service) + { + this.client = client; + this.service = service; + yield return new WaitForSeconds(1.0f); + + yield return FetchAndExecuteHeat(client.competitionHash, client.activeHeat); + } + + private IEnumerator FetchAndExecuteHeat(string hash, HeatModel model) + { + yield return ExecuteHeat(hash, model); + } + + private IEnumerator ExecuteHeat(string hash, HeatModel model) + { + Debug.Log(string.Format("[BDArmory.BDAScoreService] Running heat {0}/{1}", hash, model.order)); + + // orchestrate the match + service.ClearScores(); + + service.status = StatusType.RunningHeat; + 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; + case 67: + BDACompetitionMode.Instance.StartRapidDeployment(0, tag: $"{model.competition_id}-{model.stage}-{model.order}"); + break; + case 77: + 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 = 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); + + // Wait for the competition to actually start. + yield return new WaitWhile(() => BDACompetitionMode.Instance.competitionStarting || BDACompetitionMode.Instance.sequencedCompetitionStarting); + + if (!BDACompetitionMode.Instance.competitionIsActive) + { + message = "Competition failed to start for heat " + hash + "."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDAScoreService]: " + message); + yield break; + } + + // Wait for the competition to finish (limited duration and log dumping is handled directly by the competition now). + yield return new WaitWhile(() => BDACompetitionMode.Instance.competitionIsActive); + + CleanUp(); + } + + public void CleanUp() + { + if (BDACompetitionMode.Instance.competitionIsActive) BDACompetitionMode.Instance.StopCompetition(); // Competition is done, so stop it and do the rest of the book-keeping. + } + } +} diff --git a/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs b/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs new file mode 100644 index 000000000..55f98805f --- /dev/null +++ b/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs @@ -0,0 +1,471 @@ +using System; +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.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Extensions; + +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 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; + public static List Ghosts = new List(); + + public static string ModelPath = "BDArmory/Models/WayPoint/model"; + + public WaypointFollowingStrategy(List waypoints) + { + this.waypoints = waypoints; + } + + float liftMultiplier = 0; + + public IEnumerator Execute(BDAScoreClient client, BDAScoreService service) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.WaypointFollowingStrategy]: Started"); + pilots = BDACompetitionMode.Instance.GetAllPilots().Select(p => p.vessel.ActiveController().AI as BDGenericAIBase).ToList(); + if (BDACompetitionMode.Instance.competitionIsActive) BDACompetitionMode.Instance.StopCompetition(); // Stop any currently active competition. + BDACompetitionMode.Instance.ResetCompetitionStuff(preSpawn: true); // Reset a bunch of stuff related to competitions so they don't interfere. + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES, "", CompetitionType.WAYPOINTS); + if (BDArmorySettings.WAYPOINTS_INFINITE_FUEL_AT_START) + { + foreach (var pilot in pilots) pilot.MaintainFuelLevels(true); + } //waypoints is dependent on PilotCommands.Waypoint, which gets overridden to PilotCommands.FlyTo by the start of comp. + yield return new WaitWhile(() => BDACompetitionMode.Instance.competitionStarting); + yield return new WaitWhile(() => BDACompetitionMode.Instance.pinataAlive); + PrepareCompetition(); + + // Configure the pilots' waypoints. + 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.DEBUG_OTHER) Debug.Log(string.Format("[BDArmory.WaypointFollowingStrategy]: Setting {0} waypoints", mappedWaypoints.Count)); + + foreach (var pilot in pilots) + { + pilot.SetWaypoints(mappedWaypoints); + pilot.MaintainFuelLevelsUntilWaypoint(); + 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; + } + } + + // Wait for the pilots to complete the course. + var startedAt = Planetarium.GetUniversalTime(); + if (BDArmorySettings.WAYPOINT_GUARD_INDEX != -1) + { + yield return new WaitWhile(() => BDACompetitionMode.Instance.competitionIsActive); //DoUpdate handles the deathmatch half of the combat waypoint race and ends things when only 1 team left + } + else + { + yield return new WaitWhile(() => pilots.Any( + pilot => pilot != null && pilot.WeaponManager != null && pilot.IsRunningWaypoints && + (pilot.TakingOff || (pilot.aiType switch { AIType.SurfaceAI => false, _ => true } && !pilot.vessel.LandedOrSplashed)) + )); + } + var endedAt = Planetarium.GetUniversalTime(); + + BDACompetitionMode.Instance.competitionStatus.Add("Waypoints competition finished. Scores:"); + foreach (var player in BDACompetitionMode.Instance.Scores.Players) + { + var waypointScores = BDACompetitionMode.Instance.Scores.ScoreData[player].waypointsReached; + var waypointCount = waypointScores.Count(); + var deviation = waypointScores.Sum(w => w.deviation); + var elapsedTime = waypointCount == 0 ? 0 : waypointScores.Last().timestamp - waypointScores.First().timestamp; + if (service != null) service.TrackWaypoint(player, (float)elapsedTime, waypointCount, deviation); + + var displayName = player; + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(player) && !string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) + { + displayName += " (" + BDArmorySettings.HOS_BADGE + ")"; + } + BDACompetitionMode.Instance.competitionStatus.Add($" - {displayName}: Time: {elapsedTime:F2}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)); + } + + CleanUp(); + } + + void PrepareCompetition() + { + // Scores are already configure in ResetCompetitionStuff prior to this being called. + pilots = pilots.Where(p => p != null).ToList(); // Remove any already dead pilots. + if (pilots.Count > 1) //running multiple craft through the waypoints at the same time + LoadedVesselSwitcher.Instance.MassTeamSwitch(true); + else //increment team each heat + { + char T = (char)(Convert.ToUInt16('A') + BDATournament.Instance.currentHeat); + pilots[0].WeaponManager.SetTeam(BDTeam.Get(T.ToString())); + } + 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; + //FlightGlobals.currentMainBody.GetLatLonAlt(FlightGlobals.ActiveVessel.transform.position, out previousLocation.x, out previousLocation.y, out previousLocation.z); + //previousLocation.z = BDArmorySettings.WAYPOINTS_ALTITUDE; + for (int i = 0; i < waypoints.Count; i++) + { + if (!string.IsNullOrEmpty(waypoints[i].model)) ModelPath = ModelPath = "BDArmory/Models/WayPoint/" + waypoints[i].model; + if (!string.IsNullOrEmpty(VesselSpawnerWindow.Instance.SelectedModel) && VesselSpawnerWindow.Instance.SelectedGate >= 0) + ModelPath = "BDArmory/Models/WayPoint/" + VesselSpawnerWindow.Instance.SelectedModel; + 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 > 0 ? BDArmorySettings.WAYPOINTS_SCALE : waypoints[i].scale); + + previousLocation = WorldCoords; + 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) + " Model: " + ModelPath); + } + } + + if (BDArmorySettings.WAYPOINTS_MODE) + { + 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 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)) // S4R10 alt limiter + { + 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 + { + if (Ghosts.Count > 0) + { + foreach (var tracer in Ghosts) + { + if (tracer != null) + tracer.gameObject.SetActive(false); + } + } + Ghosts.Clear(); //need to have Ghosts also clear every round start + WayPointTracing.CreateTracer(WorldCoords, pilot); + } + else + { + if (Ghosts.Count > 0) + { + foreach (var tracer in Ghosts) + { + if (tracer != null) + tracer.resetRenderer(); + } + } + WayPointTracing.CreateTracer(WorldCoords, pilots[0]); + } + */ + } + } + } + + 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; + } + } + } + + public class WayPointMarker : MonoBehaviour + { + //public static ObjectPool WaypointPool; + public static Dictionary WaypointPools = new Dictionary(); + public Vector3 Position { get; set; } + public bool disabled = false; + static void CreateObjectPool(string ModelPath) + { + var key = ModelPath; + if (!WaypointPools.ContainsKey(key) || WaypointPools[key] == null) + { + var WPTemplate = GameDatabase.Instance.GetModel(ModelPath); + if (WPTemplate == null) + { + Debug.LogError("[BDArmory.WayPointMarker]: " + ModelPath + " was not found, using the default model instead. Please fix your model."); + WPTemplate = GameDatabase.Instance.GetModel("BDArmory/Models/WayPoint/Ring"); + } + WPTemplate.SetActive(false); + WPTemplate.AddComponent(); + WaypointPools[key] = ObjectPool.CreateObjectPool(WPTemplate, 10, true, true); + } + } + public static void CreateWaypoint(Vector3 position, Vector3 direction, string ModelPath, float scale) + { + CreateObjectPool(ModelPath); + + GameObject newWayPoint = WaypointPools[ModelPath].GetPooledObject(); + Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(position, FlightGlobals.currentMainBody); + Quaternion rotation = Quaternion.LookRotation(direction, VectorUtils.GetUpDirection(WorldCoords)); //this needed, so the model is aligned to the ground normal, not the body transform orientation + + newWayPoint.transform.SetPositionAndRotation(position, rotation); + + newWayPoint.transform.RotateAround(position, newWayPoint.transform.up, VectorUtils.Angle(newWayPoint.transform.forward, direction)); //rotate model on horizontal plane towards last gate + newWayPoint.transform.RotateAround(position, newWayPoint.transform.right, VectorUtils.Angle(newWayPoint.transform.forward, direction)); //and on vertical plane if elevation change between the two + + float WPScale = scale / 500; //default ring/torii models scaled for 500m + newWayPoint.transform.localScale = new Vector3(WPScale, WPScale, WPScale); + WayPointMarker NWP = newWayPoint.GetComponent(); + NWP.Position = position; + if (BDArmorySetup.Instance.hasWPCourseSpawner) CourseBuilderGUI.Instance.loadedGates.Add(NWP); + newWayPoint.SetActive(true); + } + + public void UpdateWaypoint(Waypoint waypoint, int wpIndex, List wpList) + { + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(waypoint.location.x, waypoint.location.y); + Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(waypoint.location.x, waypoint.location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE < 0 ? waypoint.location.z : BDArmorySettings.WAYPOINTS_ALTITUDE) + (float)terrainAltitude), FlightGlobals.currentMainBody); + Vector3d previousLocation = WorldCoords; + if (wpIndex > 0) + previousLocation = VectorUtils.GetWorldSurfacePostion(new Vector3(wpList[wpIndex - 1].location.x, wpList[wpIndex - 1].location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE < 0 ? wpList[wpIndex - 1].location.z : BDArmorySettings.WAYPOINTS_ALTITUDE) + (float)terrainAltitude), FlightGlobals.currentMainBody); + + var direction = (WorldCoords - previousLocation).normalized; + Quaternion rotation = Quaternion.LookRotation(direction, VectorUtils.GetUpDirection(WorldCoords)); //this needed, so the model is aligned to the ground normal, not the body transform orientation + + transform.SetPositionAndRotation(WorldCoords, rotation); + + transform.RotateAround(WorldCoords, transform.up, VectorUtils.Angle(transform.forward, direction)); //rotate model on horizontal plane towards last gate + transform.RotateAround(WorldCoords, transform.right, VectorUtils.Angle(transform.forward, direction)); //and on vertical plane if elevation change between the two + + float WPScale = waypoint.scale / 500; //default ring/torii models scaled for 500m + transform.localScale = new Vector3(WPScale, WPScale, WPScale); + Position = waypoint.location; + } + + void Awake() + { + transform.parent = FlightGlobals.ActiveVessel.mainBody.transform; + } + private void OnEnable() + { + disabled = false; + } + void Update() + { + if (!gameObject.activeInHierarchy) return; + if (disabled || (!BDACompetitionMode.Instance.competitionIsActive && !BDArmorySetup.showWPBuilderGUI) || !HighLogic.LoadedSceneIsFlight) + { + gameObject.SetActive(false); + return; + } + //this.transform.LookAt(FlightCamera.fetch.mainCamera.transform); //Always face the camera + //if adding Races! style waypoint customization for building custom tracks, have the camera follow be a toggle, to allow for different models? Aub's Torii, etc. + } + } + public class WayPointTracing : MonoBehaviour + { + public static ObjectPool TracePool; + public Vector3 Position { get; set; } + public BDGenericAIBase AI { get; set; } + + private List pathPoints = new List(); + + LineRenderer tracerRenderer; + + public bool disabled = false; + public bool replayGhost = false; + static void CreateObjectPool() + { + GameObject ghost = GameDatabase.Instance.GetModel("BDArmory/Models/shell/model"); //could have just done this as a gameObject instead of a tiny model. + ghost.SetActive(false); + ghost.AddComponent(); + TracePool = ObjectPool.CreateObjectPool(ghost, 120, true, true); + } + + public static void CreateTracer(Vector3 position, BDGenericAIBase AI) + { + if (TracePool == null) + { + CreateObjectPool(); + } + GameObject newTrace = TracePool.GetPooledObject(); + newTrace.transform.position = position; + + WayPointTracing NWP = newTrace.GetComponent(); + NWP.Position = position; + NWP.AI = AI; + NWP.setupRenderer(); + newTrace.SetActive(true); + WaypointFollowingStrategy.Ghosts.Add(NWP); + } + + void Awake() + { + transform.parent = FlightGlobals.ActiveVessel.mainBody.transform; //FIXME need to update this to grab worldindex for non-kerbin spawns for custom track building + } + void Start() + { + setupRenderer(); //one linerenderer per vessel + nodes = 0; + timer = 0; + } + private void OnEnable() + { + disabled = false; + setupRenderer(); + pathPoints.Clear(); + nodes = 0; + timer = 0; + } + void setupRenderer() + { + if (tracerRenderer == null) + { + tracerRenderer = new LineRenderer(); + } + Debug.Log("[WayPointTracer] setting up Renderer"); + Transform tf = this.transform; + tracerRenderer = tf.gameObject.AddOrGetComponent(); + 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); + tracerRenderer.material.SetTextureScale("_MainTex", new Vector2(0.01f, 1)); + tracerRenderer.textureMode = LineTextureMode.Tile; + tracerRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; //= false; + tracerRenderer.receiveShadows = false; + tracerRenderer.startWidth = 5; + tracerRenderer.endWidth = 5; + tracerRenderer.positionCount = 2; + tracerRenderer.SetPosition(0, Vector3.zero); + tracerRenderer.SetPosition(1, Vector3.zero); + tracerRenderer.useWorldSpace = false; + tracerRenderer.enabled = false; + } + public void resetRenderer() + {//reset things for ghost mode replay while the current vessel races + if (tracerRenderer == null) + { + return; + } + Transform tf = this.transform; + tracerRenderer = tf.gameObject.AddOrGetComponent(); + tracerRenderer.positionCount = 2; + tracerRenderer.SetPosition(0, Vector3.zero); + tracerRenderer.SetPosition(1, Vector3.zero); + tracerRenderer.useWorldSpace = false; + tracerRenderer.enabled = false; + timer = 0; + nodes = 0; + replayGhost = true; + } + private float timer = 0; + private int nodes = 0; + void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight) + { + if (BDArmorySetup.GameIsPaused) return; + if (AI.vessel == null) return; + if (AI.vessel.situation == Vessel.Situations.ORBITING || AI.vessel.situation == Vessel.Situations.ESCAPING) return; + + if ((!replayGhost && AI.CurrentWaypointIndex > 0) || (replayGhost && WaypointFollowingStrategy.activePilots[0].CurrentWaypointIndex > 0)) + { //don't record before first WP + timer += Time.fixedDeltaTime; + if (timer > 1) + { + timer = 0; + nodes++; + //Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(wm.Current.vessel.transform.position, FlightGlobals.currentMainBody); + if (!replayGhost) + { + pathPoints.Add(AI.vessel.transform.position); + } + + //if (BDArmorySettings.DRAW_VESSEL_TRAILS) + { + if (tracerRenderer == null) + { + setupRenderer(); + } + tracerRenderer.enabled = true; + tracerRenderer.positionCount = nodes; //clamp count to elapsed time, for replaying ghosts from prior heats + for (int i = 0; i < nodes - 1; i++) + { + tracerRenderer.SetPosition(i, pathPoints[i]); //add Linerender positions for all but last position + } //this is working, to a point, at which the render diverges from where the vessel has gone. Need a krakenbane offset? + //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, AI.vessel.CoM); //have last position update real-time with vessel position + } + else + { + if (tracerRenderer != null) + { + tracerRenderer.enabled = false; + tracerRenderer = null; + } + } + } + } + } +} + diff --git a/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs b/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs new file mode 100644 index 000000000..bc447c0b6 --- /dev/null +++ b/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs @@ -0,0 +1,619 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; +using UnityEngine.Networking; + +using BDArmory.Settings; + +namespace BDArmory.Competition.RemoteOrchestration +{ + + public class BDAScoreClient + { + private BDAScoreService service; + + private string baseUrl; + + private string basePath; + + public string vesselPath = ""; + + public string vesselStagingPath = ""; + + public string competitionHash = ""; + + public string NPCPath = ""; + + + public bool pendingRequest = false; + + public CompetitionModel competition = null; + + public HeatModel activeHeat = null; + + public HashSet activeVessels = new HashSet(); + + public Dictionary heats = new Dictionary(); + + public Dictionary vessels = new Dictionary(); + + public Dictionary players = new Dictionary(); + + public Dictionary> playerVessels = new Dictionary>(); // Registry of in-game vessel names with actual player and vessel names. + + + public BDAScoreClient(BDAScoreService service, string basePath, string hash) + { + Debug.Log("[BDArmory.BDAScoreService] Client started with working directory: " + basePath); + //this.baseUrl = "http://localhost:3000"; + this.baseUrl = "https://" + BDArmorySettings.REMOTE_ORCHESTRATION_BASE_URL; + this.basePath = Path.GetFullPath(basePath); // Removes the 'KSP_x64_Data/../' + this.service = service; + this.vesselPath = basePath + "/" + hash; + this.vesselStagingPath = basePath + "/" + hash + "/staging"; + this.NPCPath = Path.Combine(basePath, "NPC"); + this.competitionHash = hash; + } + + /// + /// Acts as a runtime interface for resolving vessel ids into vessel concerns. + /// + private class ScoreClientVesselSource : VesselSource + { + private Dictionary vessels; + private Dictionary players; + private string stagingPath; + public ScoreClientVesselSource(Dictionary vessels, Dictionary players, string stagingPath) + { + this.vessels = vessels; + this.players = players; + this.stagingPath = stagingPath; + } + + public string GetLocalPath(int id) + { + var vessel = vessels[id]; + var player = players[vessel.player_id]; + return string.Format("{0}/{1}_{2}.craft", stagingPath, player.name, vessel.name); + } + + public VesselModel GetVessel(int id) + { + return vessels[id]; + } + } + + public VesselSource AsVesselSource() + { + return new ScoreClientVesselSource(vessels, players, vesselStagingPath); + } + + /// + /// Fetch competition metadata + /// + /// competition id + public IEnumerator GetCompetition(string hash) + { + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + string uri = string.Format("{0}/competitions/{1}.json", baseUrl, hash); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] GET {0}", uri)); + using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + ReceiveCompetition(webRequest.downloadHandler.text); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to get competition {0}: {1}", hash, webRequest.error)); + } + } + + pendingRequest = false; + } + + private void ReceiveCompetition(string response) + { + if (response == null || "".Equals(response)) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Received empty competition response")); + return; + } + CompetitionModel competition = JsonUtility.FromJson(response); + if (competition == null) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to parse competition: {0}", response)); + } + else + { + this.competition = competition; + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Competition: {0}", competition.ToString())); + } + } + + /// + /// Fetch heat manifest, which describes the groupings of players in various stages. + /// + /// competition id + public IEnumerator GetHeats(string hash) + { + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + string uri = string.Format("{0}/competitions/{1}/heats.csv", baseUrl, hash); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] GET {0}", uri)); + using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + ReceiveHeats(webRequest.downloadHandler.text); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to get heats for {0}: {1}", hash, webRequest.error)); + } + } + + pendingRequest = false; + } + + private void ReceiveHeats(string response) + { + if (response == null || "".Equals(response)) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Received empty heat collection response")); + return; + } + List collection = HeatModel.FromCsv(response); + heats.Clear(); + if (collection == null) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to parse heat collection: {0}", response)); + return; + } + foreach (HeatModel heatModel in collection) + { + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Heat: {0}", heatModel.ToString())); + heats.Add(heatModel.id, heatModel); + } + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Heats: {0}", heats.Count)); + } + + /// + /// Fetch player metadata for all participants + /// + /// competition id + public IEnumerator GetPlayers(string hash) + { + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + string uri = string.Format("{0}/competitions/{1}/players.csv", baseUrl, hash); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] GET {0}", uri)); + using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + ReceivePlayers(webRequest.downloadHandler.text); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to get players for {0}: {1}", hash, webRequest.error)); + } + } + + pendingRequest = false; + } + + private void ReceivePlayers(string response) + { + if (response == null || "".Equals(response)) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Received empty player collection response")); + return; + } + List collection = PlayerModel.FromCsv(response); + players.Clear(); + if (collection == null) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to parse player collection: {0}", response)); + return; + } + foreach (PlayerModel playerModel in collection) + { + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Player {0}", playerModel.ToString())); + if (!players.ContainsKey(playerModel.id)) + players.Add(playerModel.id, playerModel); + else + Debug.LogWarning("[BDArmory.BDAScoreClient] Player " + playerModel.id + " already exists in the competition."); + } + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Players: {0}", players.Count)); + } + + /// + /// Fetch all vessel metadata. + /// + /// competition hash + public IEnumerator GetVessels(string hash) + { + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + string uri = string.Format("{0}/competitions/{1}/vessels/manifest.csv", baseUrl, hash); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] GET {0}", uri)); + using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + ReceiveVessels(webRequest.downloadHandler.text); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to get vessels {0}: {1}", hash, webRequest.error)); + } + } + + pendingRequest = false; + } + + private void ReceiveVessels(string response) + { + if (response == null || "".Equals(response)) + { + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Received empty vessel collection response")); + return; + } + List collection = VesselModel.FromCsv(response); + vessels.Clear(); + if (collection == null) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to parse vessel collection: {0}", response)); + return; + } + foreach (VesselModel vesselModel in collection) + { + if (!vessels.ContainsKey(vesselModel.id)) // Skip duplicates. + { + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Vessel {0}", vesselModel.ToString())); + vessels.Add(vesselModel.id, vesselModel); + } + else + { + Debug.LogWarning("[BDArmory.BDAScoreClient]: Vessel " + vesselModel.ToString() + " is already in the vessel list, skipping."); + } + } + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Vessels: {0}", vessels.Count)); + } + + /// + /// Fetch vessel manifest for the given heat. + /// + /// competition hash + /// heat model + public IEnumerator GetHeatVessels(string hash, HeatModel heatModel) + { + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + activeVessels.Clear(); + + string uri = string.Format("{0}/competitions/{1}/heats/{2}/vessels.csv", baseUrl, hash, heatModel.id); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] GET {0}", uri)); + using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + ReceiveHeatVessels(webRequest.downloadHandler.text); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to get vessel manifest for {0}, heat {1}: {2}", hash, heatModel.id, webRequest.error)); + } + } + + pendingRequest = false; + } + + private void ReceiveHeatVessels(string response) + { + if (response == null || "".Equals(response)) + { + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Received empty heat vessel collection response")); + return; + } + List collection = VesselModel.FromCsv(response); + if (collection == null) + { + 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); + } + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Active vessels: {0}", activeVessels.Count)); + } + + /// + /// Submit scores for a heat. + /// + /// competition id + /// heat id + /// records to send + public IEnumerator PostRecords(string hash, int heat, List records) + { + List recordsJson = records.Select(e => e.ToJSON()).ToList(); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Prepare records for {0} players", records.Count())); + string recordsJsonStr = string.Join(",", recordsJson); + string requestBody = string.Format("{{\"records\":[{0}]}}", recordsJsonStr); + + byte[] rawBody = Encoding.UTF8.GetBytes(requestBody); + string uri = string.Format("{0}/competitions/{1}/heats/{2}/records/batch.json?client_secret={3}", baseUrl, hash, heat, BDArmorySettings.REMOTE_CLIENT_SECRET); + string uriWithoutSecret = string.Format("{0}/competitions/{1}/heats/{2}/records/batch.json?client_secret=****", baseUrl, hash, heat); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] POST {0}:\n{1}", uriWithoutSecret, requestBody)); + using (UnityWebRequest webRequest = new UnityWebRequest(uri)) + { + webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.uploadHandler = new UploadHandlerRaw(rawBody); + webRequest.downloadHandler = new DownloadHandlerBuffer(); + webRequest.method = UnityWebRequest.kHttpVerbPOST; + + yield return webRequest.SendWebRequest(); + + Debug.Log(string.Format("[BDArmory.BDAScoreClient] score reporting status: {0}", webRequest.downloadHandler.text)); + if (webRequest.isHttpError) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to post records: {0}", webRequest.error)); + } + } + } + + /// + /// Fetch vessel craft files from remote and store them locally in autospawn/:hash/staging + /// + /// competition id + public IEnumerator GetCraftFiles(string hash) + { + pendingRequest = true; + // DO NOT DELETE THE DIRECTORY. Delete the craft files inside it. + // This is much safer. + if (Directory.Exists(vesselPath)) + { + Debug.Log("[BDArmory.BDAScoreClient] Deleting existing craft in staging directory " + vesselStagingPath); + DirectoryInfo info = new DirectoryInfo(vesselStagingPath); + FileInfo[] craftFiles = info.GetFiles("*.craft") + .Where(e => e.Extension == ".craft") + .ToArray(); + foreach (FileInfo file in craftFiles) + { + File.Delete(file.FullName); + } + } + else + { + Debug.Log("[BDArmory.BDAScoreClient] Creating staging directory " + vesselStagingPath); + Directory.CreateDirectory(vesselStagingPath); + } + + playerVessels.Clear(); + // already have the vessels in memory; just need to fetch the files + foreach (VesselModel v in vessels.Values) + { + Debug.Log(string.Format("[BDArmory.BDAScoreClient] GET {0}", v.craft_url)); + using (UnityWebRequest webRequest = UnityWebRequest.Get(v.craft_url)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + byte[] rawBytes = webRequest.downloadHandler.data; + SaveCraftFile(v, rawBytes); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to get craft for {0}: {1}", v.id, webRequest.error)); + } + } + } + pendingRequest = false; + } + + int count = 0; + private void SaveCraftFile(VesselModel vessel, byte[] bytes) + { + PlayerModel p = players[vessel.player_id]; + if (p == null) + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to save craft for vessel {0}, player {1}", vessel.id, vessel.player_id)); + return; + } + + string vesselName = string.Format("{0}_{1}", p.name, vessel.name); + playerVessels.Add(vesselName, new Tuple(p.name, vessel.name)); + string filename; + try + { + filename = string.Format("{0}/{1}.craft", vesselStagingPath, vesselName); + System.IO.File.WriteAllBytes(filename, bytes); + } + catch (Exception e) + { + Debug.LogWarning($"[BDArmory.BDAScoreClient]: Invalid filename: {e.Message}"); + filename = string.Format("{0}/Invalid filename {1}.craft", vesselStagingPath, ++count); + System.IO.File.WriteAllBytes(filename, bytes); + } + + // load the file and modify its vessel name to match the player + string[] lines = File.ReadAllLines(filename); + string pattern = ".*ship = (.+)"; + string[] modifiedLines = lines + .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 + //} + } + + public void SwapCraftFiles() + { + + 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"); + + 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)); + } + } + } + + /// + /// Attempt to start a heat. Failed attempts are not retried. + /// + /// competition id + /// heat model + public IEnumerator StartHeat(string hash, HeatModel heat) + { + if (this.activeHeat != null) + { + Debug.Log("[BDArmory.BDAScoreClient] Attempted to start a heat while already active"); + yield break; + } + + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + string uri = string.Format("{0}/competitions/{1}/heats/{2}/start", baseUrl, hash, heat.id); + using (UnityWebRequest webRequest = new UnityWebRequest(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + // only set active heat on success + this.activeHeat = heat; + UI.RemoteOrchestrationWindow.Instance.UpdateClientStatus(); + + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Started heat {1} in stage {2} of {0}", hash, heat.order, heat.stage)); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to start heat {1} in stage {2} of {0}: {3}", hash, heat.order, heat.stage, webRequest.error)); + } + } + + pendingRequest = false; + } + + /// + /// Attempt to stop the active heat. Failed attempts are not retried. + /// + /// competition id + /// heat model + public IEnumerator StopHeat(string hash, HeatModel heat) + { + if (this.activeHeat == null) + { + Debug.Log("[BDArmory.BDAScoreClient] Attempted to stop a heat when none is active"); + yield break; + } + + if (pendingRequest) + { + Debug.Log("[BDArmory.BDAScoreClient] Request already pending"); + yield break; + } + pendingRequest = true; + + string uri = string.Format("{0}/competitions/{1}/heats/{2}/stop", baseUrl, hash, heat.id); + using (UnityWebRequest webRequest = new UnityWebRequest(uri)) + { + yield return webRequest.SendWebRequest(); + if (!webRequest.isHttpError) + { + // only clear active heat on success + this.activeHeat = null; + + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Stopped heat {1} in stage {2} of {0}", hash, heat.order, heat.stage)); + } + else + { + Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to stop heat {2} in stage {1} of {0}: {3}", hash, heat.stage, heat.order, webRequest.error)); + } + } + + pendingRequest = false; + } + + } +} diff --git a/BDArmory/Competition/BDAScoreModels.cs b/BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs similarity index 76% rename from BDArmory/Competition/BDAScoreModels.cs rename to BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs index 4950150a5..0d950b89b 100644 --- a/BDArmory/Competition/BDAScoreModels.cs +++ b/BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs @@ -1,9 +1,8 @@ using System; -using System.Collections; using System.Collections.Generic; using UnityEngine; -namespace BDArmory.Competition +namespace BDArmory.Competition.RemoteOrchestration { [Serializable] @@ -18,8 +17,14 @@ public class CompetitionModel public string ended_at; public string created_at; public string updated_at; + public string mode; - public override string ToString() { return "{id: " + id + ", name: " + name + ", status: " + status + ", stage: " + stage + ", started_at: " + started_at + ", ended_at: " + ended_at + ", created_at: " + created_at + ", updated_at: " + updated_at + "}"; } + public override string ToString() { return "{id: " + id + ", name: " + name + ", status: " + status + ", stage: " + stage + ", mode: " + mode + ", started_at: " + started_at + ", ended_at: " + ended_at + ", created_at: " + created_at + ", updated_at: " + updated_at + "}"; } + + public bool IsActive() + { + return ended_at == null || ended_at == ""; + } } [Serializable] @@ -39,7 +44,8 @@ public class PlayerModel { public int id; public string name; - public override string ToString() { return "{id: " + id + ", name: " + name + "}"; } + public bool is_human; + public override string ToString() { return "{id: " + id + ", name: " + name + ", is_human: " + is_human + "}"; } public static List FromCsv(string csv) { List results = new List(); @@ -48,7 +54,7 @@ public static List FromCsv(string csv) { for (var k = 1; k < lines.Length; k++) { - // Debug.Log(string.Format("PlayerModel.FromCsv line {0}", lines[k])); + // Debug.Log(string.Format("[BDArmory.BDAScoreModels]: PlayerModel.FromCsv line {0}", lines[k])); if (!lines[k].Contains(",")) { continue; @@ -56,17 +62,20 @@ public static List FromCsv(string csv) try { string[] values = lines[k].Split(','); + // AUBRANIUM, please consider encoding the player and vessel names on the API in base64 in the CSV file. This would allow any UTF8 character to be used for the player and vessel names (they would still need to be stripped of leading and trailing whitespace). Similarly for VesselModel. Not required for HeatModel. This uses System.Linq and System.Text. + // string[] values = lines[k].Split(',').Select(v => Encoding.UTF8.GetString(Convert.FromBase64String(v))).ToArray(); if (values.Length > 0) { PlayerModel model = new PlayerModel(); model.id = int.Parse(values[0]); model.name = values[1]; + model.is_human = bool.Parse(values[2]); results.Add(model); } } catch (Exception e) { - Debug.Log("PlayerModel.FromCsv error: " + e); + Debug.Log("[BDArmory.BDAScoreModels]: PlayerModel.FromCsv error: " + e); } } } @@ -101,7 +110,7 @@ public static List FromCsv(string csv) { for (var k = 1; k < lines.Length; k++) { - // Debug.Log(string.Format("HeatModel.FromCsv line {0}", lines[k])); + // Debug.Log(string.Format("[BDArmory.BDAScoreModels]: HeatModel.FromCsv line {0}", lines[k])); if (!lines[k].Contains(",")) { continue; @@ -123,7 +132,7 @@ public static List FromCsv(string csv) } catch (Exception e) { - Debug.Log("HeatModel.FromCsv error: " + e); + Debug.Log("[BDArmory.BDAScoreModels]: HeatModel.FromCsv error: " + e); } } } @@ -153,7 +162,7 @@ public static List FromCsv(string csv) { for (var k = 1; k < lines.Length; k++) { - // Debug.Log(string.Format("VesselModel.FromCsv line {0}", lines[k])); + // Debug.Log(string.Format("[BDArmory.BDAScoreModels]: VesselModel.FromCsv line {0}", lines[k])); if (!lines[k].Contains(",")) { continue; @@ -173,7 +182,7 @@ public static List FromCsv(string csv) } catch (Exception e) { - Debug.Log("VesselModel.FromCsv error: " + e); + Debug.Log("[BDArmory.BDAScoreModels]: VesselModel.FromCsv error: " + e); } } } @@ -193,23 +202,36 @@ public class RecordModel public double dmg_in; public int ram_parts_out; public int ram_parts_in; + public int mis_strikes_out; + public int mis_strikes_in; public int mis_parts_out; public int mis_parts_in; public double mis_dmg_out; public double mis_dmg_in; + public int roc_strikes_out; + public int roc_strikes_in; + public int roc_parts_out; + 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; + public double HPremaining; public float distance; public string weapon; public float death_order; public float death_time; public int wins; + public int waypoints; + public float elapsed_time; + public float deviation; public string ToJSON() { string result = JsonUtility.ToJson(this); - Debug.Log(string.Format("[RecordModel] json: {0}", result)); + Debug.Log(string.Format("[BDArmory.BDAScoreModels]: [RecordModel] json: {0}", result)); return result; } } diff --git a/BDArmory/Competition/BDAScoreService.cs b/BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs similarity index 51% rename from BDArmory/Competition/BDAScoreService.cs rename to BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs index a81d3c6d2..fcda14f68 100644 --- a/BDArmory/Competition/BDAScoreService.cs +++ b/BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Text; using UnityEngine; -using UnityEngine.Networking; -using BDArmory.Control; -using BDArmory.Core; + +using BDArmory.Settings; using BDArmory.UI; -namespace BDArmory.Competition +namespace BDArmory.Competition.RemoteOrchestration { [KSPAddon(KSPAddon.Startup.Flight, false)] @@ -28,14 +26,27 @@ public class BDAScoreService : MonoBehaviour public Dictionary> killsOnTarget = new Dictionary>(); public Dictionary assists = new Dictionary(); public Dictionary deaths = new Dictionary(); + public Dictionary HPremaining = new Dictionary(); public Dictionary longestHitWeapon = new Dictionary(); public Dictionary longestHitDistance = new Dictionary(); public Dictionary rammedPartsOut = new Dictionary(); public Dictionary rammedPartsIn = new Dictionary(); + public Dictionary missileStrikesOut = new Dictionary(); + public Dictionary missileStrikesIn = new Dictionary(); public Dictionary missilePartsOut = new Dictionary(); public Dictionary missilePartsIn = new Dictionary(); public Dictionary missileDamageOut = new Dictionary(); public Dictionary missileDamageIn = new Dictionary(); + public Dictionary rocketStrikesOut = new Dictionary(); + public Dictionary rocketStrikesIn = new Dictionary(); + public Dictionary rocketPartsOut = new Dictionary(); + 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(); public enum StatusType { @@ -81,21 +92,17 @@ public enum StatusType Invalid } - private bool pendingSync = false; - private bool competitionStarted = false; + private bool syncActive = false; public StatusType status = StatusType.Offline; private Coroutine syncCoroutine; - // protected CompetitionModel competition = null; - - // protected HeatModel activeHeat = null; - - public BDAScoreClient client; public string vesselPath; + private double waitStartedAt = -1; + void Awake() { if (Instance) @@ -108,10 +115,10 @@ void Awake() void Update() { - if (pendingSync && !Core.BDArmorySettings.REMOTE_LOGGING_ENABLED) + if (syncActive && !BDArmorySettings.REMOTE_LOGGING_ENABLED) { - Debug.Log("[BDAScoreService] Cancel due to disable"); - pendingSync = false; + Debug.Log("[BDArmory.BDAScoreService] Cancel due to disable"); + syncActive = false; if (syncCoroutine != null) StopCoroutine(syncCoroutine); return; @@ -133,71 +140,109 @@ public void Cancel() if (syncCoroutine != null) StopCoroutine(syncCoroutine); BDACompetitionMode.Instance.StopCompetition(); - pendingSync = false; + syncActive = false; status = status == StatusType.Waiting ? StatusType.Stopped : StatusType.Cancelled; if (status == StatusType.Cancelled) { - Debug.Log("[BDAScoreService]: Cancelling the heat"); + Debug.Log("[BDArmory.BDAScoreService]: Cancelling the heat"); // FIXME What else needs to be done to cancel a heat? } else { - Debug.Log("[BDAScoreService]: Stopping score service."); + Debug.Log("[BDArmory.BDAScoreService]: Stopping score service."); } } public IEnumerator SynchronizeWithService(string hash) { - if (pendingSync) + if (syncActive) { - Debug.Log("[BDAScoreService] Sync in progress"); + Debug.Log("[BDArmory.BDAScoreService] Sync in progress"); yield break; } - pendingSync = true; + syncActive = true; - Debug.Log(string.Format("[BDAScoreService] Sync started {0}", hash)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Sync started {0}", hash)); status = StatusType.FetchingCompetition; // first, get competition metadata yield return client.GetCompetition(hash); + // abort if we didn't receive a valid competition + if (client.competition == null) + { + status = StatusType.Invalid; + syncActive = false; + yield break; + } + status = StatusType.FetchingPlayers; // next, get player metadata yield return client.GetPlayers(hash); - // abort if we didn't receive a valid competition + status = StatusType.FetchingVessels; + // next, get vessel metadata + yield return client.GetVessels(hash); + + status = StatusType.DownloadingCraftFiles; + // finally, fetch all relevant craft files + yield return client.GetCraftFiles(hash); + + // and start the coordination + yield return CoordinateTournament(hash); + } + + private IEnumerator CoordinateTournament(string hash) + { if (client.competition == null) { + Debug.Log("[BDArmory.BDAScoreService] Unexpected null competition"); status = StatusType.Invalid; - pendingSync = false; yield break; } - switch (client.competition.status) + // use competition metadata to decide how to run the tournament + while (client.competition != null && client.competition.IsActive()) { - case 0: - status = StatusType.PendingPlayers; - // waiting for players; nothing to do - Debug.Log(string.Format("[BDAScoreService] Waiting for players {0}", hash)); - break; - case 1: - status = StatusType.FindingNextHeat; - // heats generated; find next heat - yield return FindNextHeat(hash); - break; - case 2: - status = StatusType.StalledNoPendingHeats; - Debug.Log(string.Format("[BDAScoreService] Competition status 2 {0}", hash)); - break; + switch (client.competition.status) + { + case 0: + status = StatusType.PendingPlayers; + // waiting for players; nothing to do + Debug.Log(string.Format("[BDArmory.BDAScoreService] Waiting for players {0}", hash)); + break; + case 1: + status = StatusType.FindingNextHeat; + // competition configured; finding next heat + yield return FindNextHeat(hash); + break; + case 2: + status = StatusType.Completed; + Debug.Log(string.Format("[BDArmory.BDAScoreService] Competition completed {0}", hash)); + break; + } + // wait some delay before fetching competition status again + waitStartedAt = Planetarium.GetUniversalTime(); + yield return WaitBetweenHeats(hash); + yield return client.GetCompetition(hash); } - pendingSync = false; - Debug.Log(string.Format("[BDAScoreService] Sync completed {0}", hash)); + syncActive = false; + } + + public double TimeUntilNextHeat() + { + return BDArmorySettings.REMOTE_INTERHEAT_DELAY - (Planetarium.GetUniversalTime() - waitStartedAt); + } + + private IEnumerator WaitBetweenHeats(string hash) // AUBRANIUM, is hash used in some code yet to be committed? Otherwise, it's superfluous here. + { + yield return new WaitForSeconds(BDArmorySettings.REMOTE_INTERHEAT_DELAY); } private IEnumerator FindNextHeat(string hash) { - Debug.Log(string.Format("[BDAScoreService] Find next heat for {0}", hash)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Find next heat for {0}", hash)); status = StatusType.FetchingHeat; // fetch heat metadata @@ -208,134 +253,61 @@ private IEnumerator FindNextHeat(string hash) if (model == null) { status = StatusType.StalledNoPendingHeats; - Debug.Log(string.Format("[BDAScoreService] No inactive heat found {0}", hash)); - yield return RetryFind(hash); + Debug.Log(string.Format("[BDArmory.BDAScoreService] No inactive heat found {0}", hash)); } else { - Debug.Log(string.Format("[BDAScoreService] Found heat {1} in {0}", hash, model.order)); - yield return FetchAndExecuteHeat(hash, model); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Found heat {1} in {0}", hash, model.order)); + yield return RunHeatCycle(hash, model); } } - public double retryFindStartedAt = -1; - private IEnumerator RetryFind(string hash) + private IEnumerator RunHeatCycle(string hash, HeatModel heat) { - retryFindStartedAt = Planetarium.GetUniversalTime(); - yield return new WaitWhile(() => Planetarium.GetUniversalTime() - retryFindStartedAt < 30); - if (status != StatusType.Cancelled) + status = StatusType.FetchingVessels; + // fetching vessel manifest for this heat + yield return client.GetHeatVessels(hash, heat); + + // check for active vessels + if (client.activeVessels.Count == 0) { - status = StatusType.FindingNextHeat; - yield return FindNextHeat(hash); + Debug.Log("[BDArmory.BDAScoreService] Unexpected empty active vessel set"); + yield break; } - } - private IEnumerator FetchAndExecuteHeat(string hash, HeatModel model) - { - status = StatusType.FetchingVessels; - // fetch vessel metadata for heat - yield return client.GetVessels(hash, model); + status = StatusType.StartingHeat; + // notifying web service to start heat + yield return client.StartHeat(hash, heat); - status = StatusType.DownloadingCraftFiles; - // fetch craft files for vessels - yield return client.GetCraftFiles(hash, model); + // check active heat (null means start failed) + if (client.activeHeat == null) + { + Debug.Log("[BDArmory.BDAScoreService] Unable to start heat"); + yield break; + } - status = StatusType.StartingHeat; - // notify web service to start heat - yield return client.StartHeat(hash, model); + // clear scores + ClearScores(); - // execute heat - int attempts = 0; - competitionStarted = false; - while (!competitionStarted && attempts++ < 3) // 3 attempts is plenty + // run heat using tournament coordinator + var coordinator = RemoteTournamentCoordinator.BuildFromDescriptor(client.competition); + if (coordinator == null) { - yield return ExecuteHeat(hash, model); - if (!competitionStarted) - switch (Control.VesselSpawner.Instance.spawnFailureReason) - { - case Control.VesselSpawner.SpawnFailureReason.None: // Successful spawning, but competition failed to start for some reason. - BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + BDACompetitionMode.Instance.competitionStartFailureReason + ", trying again."); - break; - case Control.VesselSpawner.SpawnFailureReason.VesselLostParts: // Recoverable spawning failure. - case Control.VesselSpawner.SpawnFailureReason.TimedOut: // Recoverable spawning failure. - BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + Control.VesselSpawner.Instance.spawnFailureReason + ", trying again."); - break; - default: // Spawning is unrecoverable. - BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + Control.VesselSpawner.Instance.spawnFailureReason + ", aborting."); - attempts = 3; - break; - } + Debug.Log("[BDArmory.BDAScoreService] Failed to build tournament coordinator"); + yield break; } + status = StatusType.RunningHeat; + yield return coordinator.Execute(); status = StatusType.ReportingResults; // report scores - yield return SendScores(hash, model); + yield return SendScores(hash, heat); status = StatusType.StoppingHeat; // notify web service to stop heat - yield return client.StopHeat(hash, model); + yield return client.StopHeat(hash, heat); status = StatusType.Waiting; - yield return RetryFind(hash); - } - - private IEnumerator ExecuteHeat(string hash, HeatModel model) - { - Debug.Log(string.Format("[BDAScoreService] Running heat {0}/{1}", hash, model.order)); - Control.VesselSpawner spawner = Control.VesselSpawner.Instance; - - // orchestrate the match - activePlayers.Clear(); - assists.Clear(); - damageIn.Clear(); - damageOut.Clear(); - deaths.Clear(); - hitsIn.Clear(); - hitsOnTarget.Clear(); - hitsOut.Clear(); - killsOnTarget.Clear(); - longestHitDistance.Clear(); - longestHitWeapon.Clear(); - missileDamageIn.Clear(); - missileDamageOut.Clear(); - missilePartsIn.Clear(); - missilePartsOut.Clear(); - rammedPartsIn.Clear(); - rammedPartsOut.Clear(); - - status = StatusType.SpawningVessels; - spawner.SpawnAllVesselsOnce(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, true, hash); - while (spawner.vesselsSpawning) - yield return new WaitForFixedUpdate(); - if (!spawner.vesselSpawnSuccess) - { - Debug.Log("[BDAScoreService] Vessel spawning failed."); // FIXME Now what? - yield break; - } - yield return new WaitForFixedUpdate(); - - status = StatusType.RunningHeat; - // NOTE: runs in separate coroutine - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - 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 message = "Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."; - Debug.Log("[BDAScoreService]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - while (BDACompetitionMode.Instance.competitionStarting) - yield return new WaitForFixedUpdate(); // Wait for the competition to actually start. - if (!BDACompetitionMode.Instance.competitionIsActive) - { - message = "Competition failed to start for heat " + hash + "."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDAScoreService]: " + message); - yield break; - } - competitionStarted = true; - 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); } private IEnumerator SendScores(string hash, HeatModel heat) @@ -347,33 +319,29 @@ private IEnumerator SendScores(string hash, HeatModel heat) private List BuildRecords(string hash, HeatModel heat) { List results = new List(); - var playerNames = activePlayers; - Debug.Log(string.Format("[BDAScoreService] Building records for {0} players", playerNames.Count)); + var playerNames = PlayerNames(); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Building records for {0} players", playerNames.Count)); foreach (string playerName in playerNames) { - var separatorIndex = playerName.IndexOf('_'); - if (separatorIndex<0) + if (!client.playerVessels.ContainsKey(playerName)) { - Debug.Log(string.Format("[BDAScoreService] Unmatched player {0}", playerName)); - Debug.Log("DEBUG players were " + string.Join(", ", client.players.Values)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Unmatched player {0}", playerName)); continue; } - var playerNamePart = playerName.Substring(0, separatorIndex); + var playerNamePart = client.playerVessels[playerName].Item1; PlayerModel player = client.players.Values.FirstOrDefault(e => e.name == playerNamePart); if (player == null) { - Debug.Log(string.Format("[BDAScoreService] Unmatched player {0}", playerNamePart)); - Debug.Log("DEBUG players were " + string.Join(", ", client.players.Values)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Unmatched player {0}", playerNamePart)); continue; } - var vesselNamePart = playerName.Substring(separatorIndex + 1); + var vesselNamePart = client.playerVessels[playerName].Item2; VesselModel vessel = client.vessels.Values.FirstOrDefault(e => e.player_id == player.id && e.name == vesselNamePart); if (vessel == null) { - Debug.Log(string.Format("[BDAScoreService] Unmatched vessel for playerId {0}", player.id)); - Debug.Log("DEBUG vessels were " + string.Join(", ", client.vessels.Values.Select(p => p.id))); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Unmatched vessel for playerId {0}", player.id)); continue; } @@ -387,28 +355,84 @@ private List BuildRecords(string hash, HeatModel heat) record.dmg_in = ComputeTotalDamageIn(playerName); record.ram_parts_out = ComputeTotalRammedPartsOut(playerName); record.ram_parts_in = ComputeTotalRammedPartsIn(playerName); + record.mis_strikes_out = ComputeTotalMissileStrikesOut(playerName); + record.mis_strikes_in = ComputeTotalMissileStrikesIn(playerName); record.mis_parts_out = ComputeTotalMissilePartsOut(playerName); record.mis_parts_in = ComputeTotalMissilePartsIn(playerName); record.mis_dmg_out = ComputeTotalMissileDamageOut(playerName); record.mis_dmg_in = ComputeTotalMissileDamageIn(playerName); + record.roc_strikes_out = ComputeTotalRocketStrikesOut(playerName); + record.roc_strikes_in = ComputeTotalRocketStrikesIn(playerName); + record.roc_parts_out = ComputeTotalRocketPartsOut(playerName); + 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); + record.HPremaining = ComputeAverageHPremaining(playerName); record.assists = ComputeTotalAssists(playerName); record.death_order = ComputeDeathOrder(playerName); record.death_time = ComputeDeathTime(playerName); - if (longestHitDistance.ContainsKey(playerName) && longestHitWeapon.ContainsKey(playerName)) - { - record.distance = (float)longestHitDistance[playerName]; - record.weapon = longestHitWeapon[playerName]; - } + var longestHitData = LongestHitForPlayer(playerName); + record.weapon = longestHitData.Key; + record.distance = longestHitData.Value; + record.waypoints = ComputeWaypoints(playerName); + record.elapsed_time = ComputeElapsedTime(playerName); + record.deviation = ComputeDeviation(playerName); + results.Add(record); } - Debug.Log(string.Format("[BDAScoreService] Built records for {0} players", results.Count)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Built records for {0} players", results.Count)); return results; } - private int ComputeTotalHitsOut(string playerName) + public List PlayerNames() + { + return activePlayers.ToList(); + } + + public KeyValuePair LongestHitForPlayer(string playerName) + { + if (longestHitDistance.ContainsKey(playerName) && longestHitWeapon.ContainsKey(playerName)) + { + return new KeyValuePair(longestHitWeapon[playerName], (float)longestHitDistance[playerName]); + } + else + { + return new KeyValuePair("", 0); + } + } + + public void ClearScores() + { + activePlayers.Clear(); + assists.Clear(); + damageIn.Clear(); + damageOut.Clear(); + deaths.Clear(); + HPremaining.Clear(); + hitsIn.Clear(); + hitsOnTarget.Clear(); + hitsOut.Clear(); + killsOnTarget.Clear(); + longestHitDistance.Clear(); + longestHitWeapon.Clear(); + rocketDamageIn.Clear(); + rocketDamageOut.Clear(); + rocketPartsIn.Clear(); + rocketPartsOut.Clear(); + missileDamageIn.Clear(); + missileDamageOut.Clear(); + missilePartsIn.Clear(); + missilePartsOut.Clear(); + rammedPartsIn.Clear(); + rammedPartsOut.Clear(); + asteroidPartsIn.Clear(); + } + + public int ComputeTotalHitsOut(string playerName) { int result = 0; if (hitsOut.ContainsKey(playerName)) @@ -418,7 +442,7 @@ private int ComputeTotalHitsOut(string playerName) return result; } - private int ComputeTotalHitsIn(string playerName) + public int ComputeTotalHitsIn(string playerName) { int result = 0; if (hitsIn.ContainsKey(playerName)) @@ -428,7 +452,7 @@ private int ComputeTotalHitsIn(string playerName) return result; } - private double ComputeTotalDamageOut(string playerName) + public double ComputeTotalDamageOut(string playerName) { double result = 0; if (damageOut.ContainsKey(playerName)) @@ -438,7 +462,7 @@ private double ComputeTotalDamageOut(string playerName) return result; } - private double ComputeTotalDamageIn(string playerName) + public double ComputeTotalDamageIn(string playerName) { double result = 0; if (damageIn.ContainsKey(playerName)) @@ -448,20 +472,34 @@ private double ComputeTotalDamageIn(string playerName) return result; } - private int ComputeTotalRammedPartsOut(string playerName) + public int ComputeTotalRammedPartsOut(string playerName) { if (rammedPartsOut.ContainsKey(playerName)) return rammedPartsOut[playerName]; return 0; } - private int ComputeTotalRammedPartsIn(string playerName) + public int ComputeTotalRammedPartsIn(string playerName) { if (rammedPartsIn.ContainsKey(playerName)) return rammedPartsIn[playerName]; return 0; } + private int ComputeTotalMissileStrikesOut(string playerName) + { + if (missileStrikesOut.ContainsKey(playerName)) + return missileStrikesOut[playerName]; + return 0; + } + + private int ComputeTotalMissileStrikesIn(string playerName) + { + if (missileStrikesIn.ContainsKey(playerName)) + return missileStrikesIn[playerName]; + return 0; + } + private int ComputeTotalMissilePartsOut(string playerName) { if (missilePartsOut.ContainsKey(playerName)) @@ -469,27 +507,76 @@ private int ComputeTotalMissilePartsOut(string playerName) return 0; } - private int ComputeTotalMissilePartsIn(string playerName) + public int ComputeTotalMissilePartsIn(string playerName) { if (missilePartsIn.ContainsKey(playerName)) return missilePartsIn[playerName]; return 0; } - private double ComputeTotalMissileDamageOut(string playerName) + public double ComputeTotalMissileDamageOut(string playerName) { if (missileDamageOut.ContainsKey(playerName)) return missileDamageOut[playerName]; return 0; } - private double ComputeTotalMissileDamageIn(string playerName) + public double ComputeTotalMissileDamageIn(string playerName) { if (missileDamageIn.ContainsKey(playerName)) return missileDamageIn[playerName]; return 0; } + private int ComputeTotalRocketStrikesOut(string playerName) + { + if (rocketStrikesOut.ContainsKey(playerName)) + return rocketStrikesOut[playerName]; + return 0; + } + + private int ComputeTotalRocketStrikesIn(string playerName) + { + if (rocketStrikesIn.ContainsKey(playerName)) + return rocketStrikesIn[playerName]; + return 0; + } + + private int ComputeTotalRocketPartsOut(string playerName) + { + if (rocketPartsOut.ContainsKey(playerName)) + return rocketPartsOut[playerName]; + return 0; + } + + private int ComputeTotalRocketPartsIn(string playerName) + { + if (rocketPartsIn.ContainsKey(playerName)) + return rocketPartsIn[playerName]; + return 0; + } + + private double ComputeTotalRocketDamageOut(string playerName) + { + if (rocketDamageOut.ContainsKey(playerName)) + return rocketDamageOut[playerName]; + return 0; + } + + private double ComputeTotalRocketDamageIn(string playerName) + { + if (rocketDamageIn.ContainsKey(playerName)) + return rocketDamageIn[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; @@ -500,7 +587,7 @@ private int ComputeTotalKills(string playerName) return result; } - private int ComputeTotalDeaths(string playerName) + public int ComputeTotalDeaths(string playerName) { int result = 0; if (deaths.ContainsKey(playerName)) @@ -510,7 +597,18 @@ private int ComputeTotalDeaths(string playerName) return result; } - private int ComputeTotalAssists(string playerName) + public double ComputeAverageHPremaining(string playerName) + { + double result = 0; + var HPLeft = BDACompetitionMode.Instance.Scores; + if (HPLeft.Players.Contains(playerName)) + { + result = HPLeft.ScoreData[playerName].remainingHP; + } + return result; + } + + public int ComputeTotalAssists(string playerName) { int result = 0; if (assists.ContainsKey(playerName)) @@ -520,20 +618,19 @@ private int ComputeTotalAssists(string playerName) return result; } - private int ComputeWins(string playerName) + public int ComputeWins(string playerName) { var stillAlive = !deaths.ContainsKey(playerName); var livingCount = activePlayers.Count - deaths.Count; return (stillAlive && livingCount == 1) ? 1 : 0; } - private float ComputeDeathOrder(string playerName) + public float ComputeDeathOrder(string playerName) { - var deathOrder = BDACompetitionMode.Instance.DeathOrder; - if (deathOrder.ContainsKey(playerName)) + var scoreData = BDACompetitionMode.Instance.Scores.ScoreData; + if (scoreData.ContainsKey(playerName) && scoreData[playerName].aliveState != AliveState.Alive) { - var orderData = deathOrder[playerName]; - return (float)orderData.Item1 / (float)activePlayers.Count; + return (float)scoreData[playerName].deathOrder / (float)activePlayers.Count; } else { @@ -541,25 +638,60 @@ private float ComputeDeathOrder(string playerName) } } - private float ComputeDeathTime(string playerName) + public float ComputeDeathTime(string playerName) + { + var scoreData = BDACompetitionMode.Instance.Scores.ScoreData; + if (scoreData.ContainsKey(playerName) && scoreData[playerName].aliveState != AliveState.Alive) + { + return (float)scoreData[playerName].deathTime; + } + else + { + return BDArmorySettings.COMPETITION_DURATION * 60.0f; + } + } + + public int ComputeWaypoints(string playerName) { - var deathOrder = BDACompetitionMode.Instance.DeathOrder; - if (deathOrder.ContainsKey(playerName)) + if (waypoints.ContainsKey(playerName)) { - var orderData = deathOrder[playerName]; - return (float)orderData.Item2; + return waypoints[playerName]; } else { - return BDArmorySettings.COMPETITION_DURATION*60.0f; + return 0; + } + } + + public float ComputeElapsedTime(string playerName) + { + if (elapsedTime.ContainsKey(playerName)) + { + return (float)elapsedTime[playerName]; + } + else + { + return 0; + } + } + + public float ComputeDeviation(string playerName) + { + if (deviation.ContainsKey(playerName)) + { + return (float)deviation[playerName]; + } + else + { + return 0; } } public void TrackDamage(string attacker, string target, double damage) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log(string.Format("[BDAScoreService] TrackDamage by {0} on {1} for {2}hp", target, attacker, damage)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackDamage by {0} on {1} for {2}hp", target, attacker, damage)); } activePlayers.Add(attacker); activePlayers.Add(target); @@ -581,11 +713,37 @@ public void TrackDamage(string attacker, string target, double damage) } } + public void TrackMissileStrike(string attacker, string target) + { + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackMissileStrike by {0} on {1}", target, attacker)); + } + activePlayers.Add(attacker); + activePlayers.Add(target); + if (missileStrikesOut.ContainsKey(attacker)) + { + missileStrikesOut[attacker]++; + } + else + { + missileStrikesOut.Add(attacker, 1); + } + if (missileStrikesIn.ContainsKey(target)) + { + missileStrikesIn[target]++; + } + else + { + missileStrikesIn.Add(target, 1); + } + } + public void TrackMissileDamage(string attacker, string target, double damage) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log(string.Format("[BDAScoreService] TrackMissileDamage by {0} on {1} for {2}hp", target, attacker, damage)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackMissileDamage by {0} on {1} for {2}hp", target, attacker, damage)); } activePlayers.Add(attacker); activePlayers.Add(target); @@ -609,8 +767,8 @@ public void TrackMissileDamage(string attacker, string target, double damage) public void TrackMissileParts(string attacker, string target, int count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log(string.Format("[BDAScoreService] TrackMissileParts by {0} on {1} for {2}parts", target, attacker, count)); + 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(); activePlayers.Add(attacker); @@ -637,10 +795,92 @@ public void TrackMissileParts(string attacker, string target, int count) } } + public void TrackRocketStrike(string attacker, string target) + { + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackRocketStrike by {0} on {1}", target, attacker)); + } + activePlayers.Add(attacker); + activePlayers.Add(target); + if (rocketStrikesOut.ContainsKey(attacker)) + { + rocketStrikesOut[attacker]++; + } + else + { + rocketStrikesOut.Add(attacker, 1); + } + if (rocketStrikesIn.ContainsKey(target)) + { + rocketStrikesIn[target]++; + } + else + { + rocketStrikesIn.Add(target, 1); + } + } + + public void TrackRocketDamage(string attacker, string target, double damage) + { + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackRocketDamage by {0} on {1} for {2}hp", target, attacker, damage)); + } + activePlayers.Add(attacker); + activePlayers.Add(target); + if (rocketDamageOut.ContainsKey(attacker)) + { + rocketDamageOut[attacker] += damage; + } + else + { + rocketDamageOut.Add(attacker, damage); + } + if (rocketDamageIn.ContainsKey(target)) + { + rocketDamageIn[target] += damage; + } + else + { + rocketDamageIn.Add(target, damage); + } + } + + public void TrackRocketParts(string attacker, string target, int count) + { + 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(); + activePlayers.Add(attacker); + activePlayers.Add(target); + if (rocketPartsOut.ContainsKey(attacker)) + rocketPartsOut[attacker] += count; + else + rocketPartsOut.Add(attacker, count); + if (rocketPartsIn.ContainsKey(target)) + rocketPartsIn[target] += count; + else + rocketPartsIn.Add(target, count); + + if (timeOfLastHitOnTarget.ContainsKey(attacker)) + { + if (timeOfLastHitOnTarget[attacker].ContainsKey(target)) + timeOfLastHitOnTarget[attacker][target] = now; + else + timeOfLastHitOnTarget[attacker].Add(target, now); + } + else + { + timeOfLastHitOnTarget.Add(attacker, new Dictionary { { target, now } }); + } + } + public void TrackRammedParts(string attacker, string target, int count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log(string.Format("[BDAScoreService] TrackRammedParts by {0} on {1} for {2}parts", target, attacker, count)); + 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(); activePlayers.Add(attacker); @@ -667,11 +907,20 @@ 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("[BDAScoreService] TrackHit by {0} on {1} with {2} at {3}m", target, attacker, weaponName, hitDistance)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackHit by {0} on {1} with {2} at {3}m", target, attacker, weaponName, hitDistance)); } double now = Planetarium.GetUniversalTime(); activePlayers.Add(attacker); @@ -711,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; @@ -769,9 +1018,9 @@ 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("[BDAScoreService] TrackDeath for {0}", target)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackDeath for {0}", target)); } activePlayers.Add(target); IncrementDeath(target); @@ -781,17 +1030,17 @@ private void IncrementDeath(string target) { if (deaths.ContainsKey(target)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log(string.Format("[BDAScoreService] IncrementDeaths for {0}", target)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] IncrementDeaths for {0}", target)); } ++deaths[target]; } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log(string.Format("[BDAScoreService] FirstDeath for {0}", target)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] FirstDeath for {0}", target)); } deaths.Add(target, 1); } @@ -802,15 +1051,15 @@ 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("[BDAScoreService] TrackKill {0} by {1}", target, attacker)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackKill {0} by {1}", target, attacker)); } activePlayers.Add(attacker); activePlayers.Add(target); IncrementKill(attacker, target); - ComputeAssists(target, attacker, 30); + // ComputeAssists(target, attacker, 30); } private void IncrementKill(string attacker, string target) @@ -820,26 +1069,26 @@ 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("[BDAScoreService] IncrementKills for {0} on {1}", attacker, target)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] IncrementKills for {0} on {1}", attacker, target)); } ++killsOnTarget[attacker][target]; } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log(string.Format("[BDAScoreService] Kill for {0} on {1}", attacker, target)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Kill for {0} on {1}", attacker, target)); } killsOnTarget[attacker].Add(target, 1); } } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log(string.Format("[BDAScoreService] FirstKill for {0} on {1}", attacker, target)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] FirstKill for {0} on {1}", attacker, target)); } var newKills = new Dictionary(); newKills.Add(target, 1); @@ -847,6 +1096,22 @@ private void IncrementKill(string attacker, string target) } } + // Register survivors in case they didn't really do anything and didn't get registered until now. + public void TrackSurvivors(List survivors) + { + foreach (var survivor in survivors) + activePlayers.Add(survivor); + } + + public void TrackWaypoint(string aPlayerName, float aElapsedTime, int aWaypointCount, float aDeviation) + { + // insert waypoint count, elapsed time, and deviation into the data store + activePlayers.Add(aPlayerName); + waypoints[aPlayerName] = aWaypointCount; + elapsedTime[aPlayerName] = aElapsedTime; + deviation[aPlayerName] = aDeviation; + } + public string Status() { return status.ToString(); @@ -870,7 +1135,7 @@ public List FromJSON(string json) wrapper.items = JsonUtility.FromJson(json); if (wrapper == null || wrapper.items == null) { - Debug.Log(string.Format("[BDAScoreService] Failed to decode {0}", json)); + Debug.Log(string.Format("[BDArmory.BDAScoreService] Failed to decode {0}", json)); return new List(); } else @@ -880,6 +1145,4 @@ public List FromJSON(string json) } } } - - } diff --git a/BDArmory/Competition/RemoteOrchestration/VesselSource.cs b/BDArmory/Competition/RemoteOrchestration/VesselSource.cs new file mode 100644 index 000000000..4d4fe8e74 --- /dev/null +++ b/BDArmory/Competition/RemoteOrchestration/VesselSource.cs @@ -0,0 +1,9 @@ +using System; +namespace BDArmory.Competition.RemoteOrchestration +{ + public interface VesselSource + { + VesselModel GetVessel(int id); + string GetLocalPath(int id); + } +} diff --git a/BDArmory/Competition/RemoteTournamentCoordinator.cs b/BDArmory/Competition/RemoteTournamentCoordinator.cs new file mode 100644 index 000000000..84213fc8a --- /dev/null +++ b/BDArmory/Competition/RemoteTournamentCoordinator.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition.OrchestrationStrategies; +using BDArmory.Competition.RemoteOrchestration; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; +using static BDArmory.Competition.OrchestrationStrategies.WaypointFollowingStrategy; + +namespace BDArmory.Competition +{ + public class RemoteTournamentCoordinator + { + private SpawnStrategy spawnStrategy; + private OrchestrationStrategy orchestrator; + private VesselSpawnerBase vesselSpawner; + + public RemoteTournamentCoordinator(SpawnStrategy spawner, OrchestrationStrategy orchestrator, VesselSpawnerBase vesselSpawner) + { + this.spawnStrategy = spawner; + this.orchestrator = orchestrator; + this.vesselSpawner = vesselSpawner; + } + + public IEnumerator Execute() + { + // clear all vessels + yield return SpawnUtils.RemoveAllVessels(); + + // first, spawn vessels + yield return spawnStrategy.Spawn(vesselSpawner); + + if (!spawnStrategy.DidComplete()) + { + Debug.Log("[BDArmory.BDAScoreService] TournamentCoordinator spawn failed"); + yield break; + } + + // now, hand off to orchestrator + yield return orchestrator.Execute(BDAScoreService.Instance.client, BDAScoreService.Instance); + } + + public static RemoteTournamentCoordinator BuildFromDescriptor(CompetitionModel competitionModel) + { + switch (competitionModel.mode) + { + case "ffa": + return BuildFFA(); + case "path": + return BuildWaypoint(); + case "chase": + return BuildChase(); + } + return null; + } + + private static RemoteTournamentCoordinator BuildFFA() + { + var scoreService = BDAScoreService.Instance; + var scoreClient = scoreService.client; + var vesselRegistry = scoreClient.vessels; + var activeVesselModels = scoreClient.activeVessels.ToList().Select(e => vesselRegistry[e]); + 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 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; + var spawnRadius = BDArmorySettings.VESSEL_SPAWN_DISTANCE; + var spawnStrategy = new CircularSpawnStrategy(scoreClient.AsVesselSource(), activeVesselIds, bodyIndex, latitude, longitude, altitude, spawnRadius); + var orchestrationStrategy = new RankedFreeForAllStrategy(); + var vesselSpawner = CircularSpawning.Instance; + return new RemoteTournamentCoordinator(spawnStrategy, orchestrationStrategy, vesselSpawner); + } + + private static RemoteTournamentCoordinator BuildWaypoint() + { + var scoreService = BDAScoreService.Instance; + var scoreClient = scoreService.client; + var vesselSource = scoreClient.AsVesselSource(); + var vesselRegistry = scoreClient.vessels; + var activeVesselModels = scoreClient.activeVessels.ToList().Select(e => vesselRegistry[e]); + var craftUrl = activeVesselModels.Select(e => vesselSource.GetLocalPath(e.id)).First(); + // 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 = 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 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_REF_HEADING + ) + ); + 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; + var scoreClient = scoreService.client; + var vesselSource = scoreClient.AsVesselSource(); + var vesselRegistry = scoreClient.vessels; + var activeVesselModels = scoreClient.activeVessels.ToList().Select(e => vesselRegistry[e]); + var craftUrl = activeVesselModels.Select(e => vesselSource.GetLocalPath(e.id)).First(); + // 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 latitude = 23.0f; + var longitude = -40.1f; + var altitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; + var spawnRadius = BDArmorySettings.VESSEL_SPAWN_DISTANCE; + var spawnStrategy = new PointSpawnStrategy(craftUrl, latitude, longitude, altitude, 315.0f); + // kerbin-canyon1 + // 23.3,-40.0 + // 24.47,-40.46 + // 24.95,-40.88 + // 25.91,-41.4 + // 26.23,-41.11 + // 26.8,-40.16 + // 27.05,-39.85 + // 27.15,-39.67 + // 27.58,-39.4 + // 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(23.2f, -40.0f, altitude), + new Waypoint(24.47f, -40.46f, altitude), + new Waypoint(24.95f, -40.88f, altitude), + new Waypoint(25.91f, -41.4f, altitude), + new Waypoint(26.23f, -41.11f, altitude), + new Waypoint(26.8f, -40.16f, altitude), + new Waypoint(27.05f, -39.85f, altitude), + new Waypoint(27.15f, -39.67f, altitude), + new Waypoint(27.58f, -39.4f, altitude), + 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 orchestrationStrategy = new WaypointFollowingStrategy(waypoints); + var vesselSpawner = SingleVesselSpawning.Instance; + return new RemoteTournamentCoordinator(spawnStrategy, orchestrationStrategy, vesselSpawner); + } + */ + private static RemoteTournamentCoordinator BuildGauntletCanyonWaypoint() + { + var scoreService = BDAScoreService.Instance; + var scoreClient = scoreService.client; + var vesselSource = scoreClient.AsVesselSource(); + + Func isHumanComparator = e => + { + var vessel = scoreClient.vessels[e]; + if (vessel == null) + { + return false; + } + var player = scoreClient.players[vessel.player_id]; + if (player == null) + { + return false; + } + return player.is_human; + }; + Func isNotHumanComparator = e => !isHumanComparator(e); + Func craftUrlMapper = e => vesselSource.GetLocalPath(e); + var activeVessels = scoreClient.activeVessels.ToList(); + var playerCraftUrl = activeVessels.Where(isHumanComparator).Select(craftUrlMapper).First(); + var npcCraftUrl = activeVessels.Where(isNotHumanComparator).Select(craftUrlMapper).First(); + + // 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 latitude = 28.3f; + var longitude = -39.2f; + var altitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; + var spawnRadius = BDArmorySettings.VESSEL_SPAWN_DISTANCE; + var playerStrategy = new PointSpawnStrategy(playerCraftUrl, latitude, longitude, 3 * altitude, 0.0f); + + // 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 + + List strategies = new List(); + + if (npcCraftUrl != null) + { + // turret locations (all spawned at 0m) + // 29.861150,-38.608205,0 + // 30.888611,-40.152778,90 + // 30.840590,-40.713150,90 + var turretLatitudes = new float[] + { + 29.858020f, + 30.888611f, + //30.840590f, + }; + var turretLongitudes = new float[] + { + -38.602660f, + -40.152778f, + //-40.713150f, + }; + var turretHeadings = new float[] + { + 0f, + 90f, + //90f, + }; + for (int k = 0; k < turretLatitudes.Count(); k++) + { + var turretStrategy = new PointSpawnStrategy(npcCraftUrl, turretLatitudes[k], turretLongitudes[k], 0.0f, turretHeadings[k], 0); + strategies.Add(turretStrategy); + } + } + + // add player after turrets, so the spawn doesn't leave the player orbiting during the turret spawning + strategies.Add(playerStrategy); + + + var waypoints = WaypointCourses.CourseLocations[0].waypoints; //Canyon Waypoint course + var orchestrationStrategy = new WaypointFollowingStrategy(waypoints); + var listStrategy = new ListSpawnStrategy(strategies); + var vesselSpawner = SingleVesselSpawning.Instance; + return new RemoteTournamentCoordinator(listStrategy, orchestrationStrategy, vesselSpawner); + } + + private static RemoteTournamentCoordinator BuildChase() + { + return null; + } + } + +} diff --git a/BDArmory/Competition/Scoring.cs b/BDArmory/Competition/Scoring.cs new file mode 100644 index 000000000..cb253f169 --- /dev/null +++ b/BDArmory/Competition/Scoring.cs @@ -0,0 +1,1126 @@ +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; +using BDArmory.Extensions; + +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, LandedTooLong, Asteroids, GM, OutOfAmmo, BigRedButton }; + public enum SurvivalState { Alive, MIA, Dead }; + public enum CompetitionResult { Win, Draw, MutualAnnihilation }; + + 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 = ""; + public CompetitionResult competitionResult = CompetitionResult.Draw; + public List> survivingTeams = new List>(); + public List> deadTeams = new List>(); + #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].competitionID = BDACompetitionMode.Instance.CompetitionID; + ScoreData[vessel.vesselName].team = vessel.ActiveController().WM.Team.Name; + } + deathCount = 0; + deathOrder.Clear(); + currentlyIT = ""; + competitionResult = CompetitionResult.Draw; + survivingTeams.Clear(); + deadTeams.Clear(); + } + /// + /// 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].competitionID = BDACompetitionMode.Instance.CompetitionID; + ScoreData[vessel.vesselName].team = vessel.ActiveController().WM.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. + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 74 && victim.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) ScoreData[attacker].hits += BDArmorySettings.VS_NPC_SCORE_MOD; //score double vs NPC + else + ++ScoreData[attacker].hits; + if (victim.Contains(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."); + + var now = Planetarium.GetUniversalTime(); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 74 && victim.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) damage *= BDArmorySettings.VS_NPC_SCORE_MOD; + if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) + { + ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; + ScoreData[victim].previousPersonWhoDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; + } + if (ScoreData[victim].damageFromGuns.ContainsKey(attacker)) { ScoreData[victim].damageFromGuns[attacker] += damage; } + else { ScoreData[victim].damageFromGuns[attacker] = damage; } + 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.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.Contains(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; } + // Last-damage tracking isn't needed here since RocketDamage and RocketHits are synchronous. + + 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 (victimVessel.ActiveController().WM == null) 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 (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 74 && victim.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) damage *= BDArmorySettings.VS_NPC_SCORE_MOD; + + 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. + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 74 && victim.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) partsLost *= BDArmorySettings.VS_NPC_SCORE_MOD; + 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; } + // Last-damage tracking isn't needed here since MissileDamage and MissileHits are synchronous. + + 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; } + + /* //Announcer + if (Players.Contains(ScoreData[vesselName].lastPersonWhoDamagedMe)) + { + ++ScoreData[ScoreData[vesselName].lastPersonWhoDamagedMe].killsThisLife; + BDACompetitionMode.Instance.PlayAnnouncer(ScoreData[ScoreData[vesselName].lastPersonWhoDamagedMe].killsThisLife, false, ScoreData[vesselName].lastPersonWhoDamagedMe); + } + */ + 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 (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 = vessels[vesselName].ActiveController().WM; + 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 = vessels[player].ActiveController().WM; + 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; + ScoreData[vesselName].totalWPReached++; + + 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 survivingTeamNames = new HashSet(); + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded || vessel.packed || VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType)) + continue; + var mf = vessel.ActiveController().WM; + var ai = vessel.ActiveController().AI; + 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; + survivingTeamNames.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.GetModuleEngines(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); + } + } + } + // Set survival state and various heat stats. + foreach (var player in Players) + { + ScoreData[player].survivalState = alive.Contains(player) ? SurvivalState.Alive : ScoreData[player].deathOrder > -1 ? SurvivalState.Dead : SurvivalState.MIA; + ScoreData[player].numberOfCompetitors = Players.Count; + ScoreData[player].compDuration = BDArmorySettings.COMPETITION_DURATION * 60d; + } + + // General result. (Note: uses hand-coded JSON to make parsing easier in python.) + if (survivingTeamNames.Count == 0) + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Mutual Annihilation"); + competitionResult = CompetitionResult.MutualAnnihilation; + } + else if (survivingTeamNames.Count == 1) + { // Win + var winningTeam = survivingTeamNames.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("\"", "\\\"")}\"")) + "]}"); + competitionResult = CompetitionResult.Win; + } + else + { // Draw + var drawTeamMembers = survivingTeamNames.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(", ", drawTeamMembers.Select(t => "{\"team\": " + $"\"{t.Key}\"" + ", \"members\": [" + string.Join(", ", t.Value.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}")) + "]"); + competitionResult = CompetitionResult.Draw; + } + survivingTeams = survivingTeamNames.Select(team => ScoreData.Where(kvp => kvp.Value.team == team).Select(kvp => kvp.Key).ToList()).ToList(); // Register the surviving teams for tournament scores. + { // Dead teams. + var deadTeamNames = ScoreData.Where(s => !survivingTeamNames.Contains(s.Value.team)).Select(s => s.Value.team).ToHashSet(); + var deadTeamMembers = 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(", ", deadTeamMembers.Select(t => "{\"team\": " + $"\"{t.Key}\"" + ", \"members\": [" + string.Join(", ", t.Value.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}")) + "]"); + deadTeams = deadTeamNames.Select(team => ScoreData.Where(kvp => kvp.Value.team == team).Select(kvp => kvp.Key).ToList()).ToList(); // Register the dead teams for tournament scores. + } + + // 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); + } + } + + [Serializable] + public class ScoringData + { + public int competitionID; + public AliveState aliveState = AliveState.Alive; // Current state of the vessel. + public SurvivalState survivalState = SurvivalState.Alive; // State of the vessel at the end of the tournament. + public string team; // The vessel's team. + public int numberOfCompetitors; + public double compDuration; + + #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. + // public int killsThisLife = 0; //number of kills tracking for Announcer barks + + #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 + [Serializable] + 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 = []; + public int totalWPReached = 0; // Convenience tracker for the Vessel Switcher and tournament (de-)serialisation. + 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 = []; + public HashSet everyoneWhoDamagedMe = []; // Every other vessel that damaged this vessel. + #endregion + + /// + /// Clone the current ScoringData. + /// + /// A new instance of ScoringData with the same information as the current version. + public ScoringData Clone() + { + return new ScoringData + { + // General + competitionID = competitionID, + aliveState = aliveState, + survivalState = survivalState, + team = team, + numberOfCompetitors = numberOfCompetitors, + compDuration = compDuration, + // Guns + hits = hits, + PinataHits = PinataHits, + shotsFired = shotsFired, + hitCounts = hitCounts.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + damageFromGuns = damageFromGuns.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + // Rockets + totalDamagedPartsDueToRockets = totalDamagedPartsDueToRockets, + rocketStrikes = rocketStrikes, + rocketsFired = rocketsFired, + damageFromRockets = damageFromRockets.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + rocketPartDamageCounts = rocketPartDamageCounts.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + rocketStrikeCounts = rocketStrikeCounts.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + // Ramming + totalDamagedPartsDueToRamming = totalDamagedPartsDueToRamming, + rammingPartLossCounts = rammingPartLossCounts.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + // Missiles + totalDamagedPartsDueToMissiles = totalDamagedPartsDueToMissiles, + damageFromMissiles = damageFromMissiles.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + missilePartDamageCounts = missilePartDamageCounts.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + missileHitCounts = missileHitCounts.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + // Special + partsLostToAsteroids = partsLostToAsteroids, + // Battle Damage + battleDamageFrom = battleDamageFrom.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + // GM + lastFiredTime = lastFiredTime, + landedState = landedState, + lastLandedTime = lastLandedTime, + landedKillTimer = landedKillTimer, + AltitudeKillTimer = AltitudeKillTimer, + AverageSpeed = AverageSpeed, + AverageAltitude = AverageAltitude, + averageCount = averageCount, + gmKillReason = gmKillReason, + // Tag + tagIsIt = tagIsIt, + tagKillsWhileIt = tagKillsWhileIt, + tagTimesIt = tagTimesIt, + tagTotalTime = tagTotalTime, + tagScore = tagScore, + tagLastUpdated = tagLastUpdated, + // Waypoints + waypointsReached = waypointsReached.ToList(), + totalWPReached = totalWPReached, + totalWPDeviation = totalWPDeviation, + totalWPTime = totalWPTime, + // Misc. + previousPartCount = previousPartCount, + lastLostPartTime = lastLostPartTime, + remainingHP = remainingHP, + lastDamageTime = lastDamageTime, + lastDamageWasFrom = lastDamageWasFrom, + lastPersonWhoDamagedMe = lastPersonWhoDamagedMe, + previousLastDamageTime = previousLastDamageTime, + previousPersonWhoDamagedMe = previousPersonWhoDamagedMe, + deathOrder = deathOrder, + deathTime = deathTime, + damageTypesTaken = damageTypesTaken.ToHashSet(), + everyoneWhoDamagedMe = everyoneWhoDamagedMe.ToHashSet() + }; + } + } +} diff --git a/BDArmory/Competition/TournamentCoordinator.cs b/BDArmory/Competition/TournamentCoordinator.cs new file mode 100644 index 000000000..3c41b1a8b --- /dev/null +++ b/BDArmory/Competition/TournamentCoordinator.cs @@ -0,0 +1,128 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition.OrchestrationStrategies; +using BDArmory.Settings; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; +using BDArmory.Utils; + +namespace BDArmory.Competition +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class TournamentCoordinator : MonoBehaviour + { + public static TournamentCoordinator Instance; + private SpawnStrategy spawnStrategy; + private OrchestrationStrategy orchestrator; + private VesselSpawnerBase vesselSpawner; + private Coroutine executing = null; + private Coroutine executingForEach = null; + public bool IsRunning { get; private set; } + + void Awake() + { + if (Instance != null) Destroy(Instance); + Instance = this; + } + + public void Configure(SpawnStrategy spawner, OrchestrationStrategy orchestrator, VesselSpawnerBase vesselSpawner) + { + this.spawnStrategy = spawner; + this.orchestrator = orchestrator; + this.vesselSpawner = vesselSpawner; + } + + public void Run() + { + Stop(); + executing = StartCoroutine(Execute()); + } + + public void Stop() + { + if (executing != null) + { + StopCoroutine(executing); + executing = null; + orchestrator.CleanUp(); + } + } + + public IEnumerator Execute() + { + yield return BDATournament.Instance.WarpIfNeeded((spawnStrategy as SpawnConfigStrategy).spawnConfig); + IsRunning = true; + + if (spawnStrategy != null && vesselSpawner != null) // Allow just running a course with the currently spawned vessels. + { + // clear all vessels + yield return SpawnUtils.RemoveAllVessels(); + + // first, spawn vessels + yield return spawnStrategy.Spawn(vesselSpawner); + + if (!spawnStrategy.DidComplete()) + { + Debug.Log($"[BDArmory.TournamentCoordinator]: TournamentCoordinator spawn failed: {vesselSpawner.spawnFailureReason}"); + IsRunning = false; + yield break; + } + } + else + { + var tic = Time.time; + yield return new WaitWhileFixed(() => Time.time - tic < 10 && FlightGlobals.ActiveVessel != null && (!FlightGlobals.ActiveVessel.loaded || FlightGlobals.ActiveVessel.packed)); + if (FlightGlobals.ActiveVessel == null || !FlightGlobals.ActiveVessel.loaded || FlightGlobals.ActiveVessel.packed) + { + Debug.Log($"[BDArmory.TournamentCoordinator]: Active vessel failed to be ready within 10s"); + IsRunning = false; + yield break; + } + } + + // now, hand off to orchestrator + yield return orchestrator.Execute(null, null); + + IsRunning = false; + } + + public void RunForEach(List strategies, OrchestrationStrategy orchestrator, VesselSpawnerBase spawner) where T : SpawnStrategy + { + StopForEach(); + executingForEach = StartCoroutine(ExecuteForEach(strategies, orchestrator, spawner)); + } + + public void StopForEach() + { + if (executingForEach != null) + { + StopCoroutine(executingForEach); + executingForEach = null; + orchestrator.CleanUp(); + } + } + + IEnumerator ExecuteForEach(List strategies, OrchestrationStrategy orchestrator, VesselSpawnerBase spawner) where T : SpawnStrategy + { + int i = 0; + foreach (var strategy in strategies) + { + Configure(strategy, orchestrator, spawner); + Run(); + yield return new WaitWhile(() => IsRunning); + if (++i < strategies.Count()) + { + double startTime = Planetarium.GetUniversalTime(); + 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 running the next round."); + yield return new WaitForSeconds(1); + } + } + } + } + } +} \ No newline at end of file diff --git a/BDArmory/Competition/_description b/BDArmory/Competition/_description new file mode 100644 index 000000000..fca836d82 --- /dev/null +++ b/BDArmory/Competition/_description @@ -0,0 +1,5 @@ +Competition related stuff: +- competition logic +- tournament logic +- spawning +- remote orchestration \ No newline at end of file diff --git a/BDArmory/Control/BDACompetitionMode.cs b/BDArmory/Control/BDACompetitionMode.cs deleted file mode 100644 index b602b3478..000000000 --- a/BDArmory/Control/BDACompetitionMode.cs +++ /dev/null @@ -1,2785 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.Competition; -using BDArmory.UI; -using UnityEngine; - -namespace BDArmory.Control -{ - // trivial score keeping structure - public class ScoringData - { - public Vessel vesselRef; // TODO Reuse these fields instead of looking for them each time. - public MissileFire weaponManagerRef; - public int Score; - public int PinataHits; - public int totalDamagedPartsDueToRamming = 0; - public int totalDamagedPartsDueToMissiles = 0; - public string lastPersonWhoHitMe = ""; - public string lastPersonWhoHitMeWithAMissile = ""; - public string lastPersonWhoRammedMe = ""; - public double lastHitTime; // Bullets - public bool tagIsIt = false; // For tag mode - public int tagKillsWhileIt = 0; // For tag mode - public int tagTimesIt = 0; // For tag mode - public double tagTotalTime = 0; // For tag mode - public double tagScore = 0; // For tag mode - public double lastMissileHitTime; // Missiles - public double lastFiredTime; - public double lastRammedTime; // Rams - public bool landedState; - public double lastLandedTime; - public double landedKillTimer; - public double AverageSpeed; - public double AverageAltitude; - public int averageCount; - public int previousPartCount; - public double lastLostPartTime = 0; // Time of losing last part (up to granularity of the updateTickLength). - public HashSet everyoneWhoHitMe = new HashSet(); - public HashSet everyoneWhoRammedMe = new HashSet(); - public HashSet everyoneWhoHitMeWithMissiles = new HashSet(); - public HashSet everyoneWhoDamagedMe = new HashSet(); - public Dictionary hitCounts = new Dictionary(); - public Dictionary damageFromBullets = new Dictionary(); - public Dictionary damageFromMissiles = new Dictionary(); - public int shotsFired = 0; - public Dictionary rammingPartLossCounts = new Dictionary(); - public Dictionary missilePartDamageCounts = new Dictionary(); - public GMKillReason gmKillReason = GMKillReason.None; - public bool cleanDeath = false; - - public double LastDamageTime() - { - var lastDamageWasFrom = LastDamageWasFrom(); - switch (lastDamageWasFrom) - { - case DamageFrom.Bullet: - return lastHitTime; - case DamageFrom.Missile: - return lastMissileHitTime; - case DamageFrom.Ram: - return lastRammedTime; - default: - return 0; - } - } - public DamageFrom LastDamageWasFrom() - { - double lastTime = 0; - var damageFrom = DamageFrom.None; - if (lastHitTime > lastTime) - { - lastTime = lastHitTime; - damageFrom = DamageFrom.Bullet; - } - if (lastMissileHitTime > lastTime) - { - lastTime = lastMissileHitTime; - damageFrom = DamageFrom.Missile; - } - if (lastRammedTime > lastTime) - { - lastTime = lastRammedTime; - damageFrom = DamageFrom.Ram; - } - return damageFrom; - } - public string LastPersonWhoDamagedMe() - { - var lastDamageWasFrom = LastDamageWasFrom(); - switch (lastDamageWasFrom) - { - case DamageFrom.Bullet: - return lastPersonWhoHitMe; - case DamageFrom.Missile: - return lastPersonWhoHitMeWithAMissile; - case DamageFrom.Ram: - return lastPersonWhoRammedMe; - default: - return ""; - } - } - - public HashSet EveryOneWhoDamagedMe() - { - foreach (var hit in everyoneWhoHitMe) - { - everyoneWhoDamagedMe.Add(hit); - } - - foreach (var ram in everyoneWhoRammedMe) - { - if (!everyoneWhoDamagedMe.Contains(ram)) - { - everyoneWhoDamagedMe.Add(ram); - } - } - - foreach (var hit in everyoneWhoHitMeWithMissiles) - { - if (!everyoneWhoDamagedMe.Contains(hit)) - { - everyoneWhoDamagedMe.Add(hit); - } - } - - return everyoneWhoDamagedMe; - } - } - public enum DamageFrom { None, Bullet, Missile, Ram }; - public enum GMKillReason { None, GM, OutOfAmmo, BigRedButton }; - public enum CompetitionStartFailureReason { None, OnlyOneTeam, TeamsChanged, TeamLeaderDisappeared, PilotDisappeared }; - - - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class BDACompetitionMode : MonoBehaviour - { - public static BDACompetitionMode Instance; - - #region Flags and variables - // Score tracking flags and variables. - public Dictionary Scores = new Dictionary(); - public Dictionary> DeathOrder = new Dictionary>(); - public Dictionary whoCleanShotWho = new Dictionary(); - public Dictionary whoCleanShotWhoWithMissiles = new Dictionary(); - public Dictionary whoCleanRammedWho = new Dictionary(); - - // Competition flags and variables - public int CompetitionID; // time competition was started - bool competitionShouldBeRunning = false; - public double competitionStartTime = -1; - public double nextUpdateTick = -1; - private double decisionTick = -1; - private double finalGracePeriodStart = -1; - public static int DeathCount = 0; - public static float gravityMultiplier = 1f; - float lastGravityMultiplier; - private string deadOrAlive = ""; - static HashSet outOfAmmo = new HashSet(); // outOfAmmo register for tracking which planes are out of ammo. - - // Action groups - public static Dictionary KM_dictAG = new Dictionary { - { 0, KSPActionGroup.None }, - { 1, KSPActionGroup.Custom01 }, - { 2, KSPActionGroup.Custom02 }, - { 3, KSPActionGroup.Custom03 }, - { 4, KSPActionGroup.Custom04 }, - { 5, KSPActionGroup.Custom05 }, - { 6, KSPActionGroup.Custom06 }, - { 7, KSPActionGroup.Custom07 }, - { 8, KSPActionGroup.Custom08 }, - { 9, KSPActionGroup.Custom09 }, - { 10, KSPActionGroup.Custom10 }, - { 11, KSPActionGroup.Light }, - { 12, KSPActionGroup.RCS }, - { 13, KSPActionGroup.SAS }, - { 14, KSPActionGroup.Brakes }, - { 15, KSPActionGroup.Abort }, - { 16, KSPActionGroup.Gear } - }; - - // Tag mode flags and variables. - public bool startTag = false; // For tag mode - public int previousNumberCompetitive = 2; // Also for tag mode - - // KILLER GM - how we look for slowest planes - public Dictionary KillTimer = new Dictionary(); // Note that this is only used as an indicator, not a controller, now. - //public Dictionary AverageSpeed = new Dictionary(); - //public Dictionary AverageAltitude = new Dictionary(); - //public Dictionary FireCount = new Dictionary(); - //public Dictionary FireCount2 = new Dictionary(); - - // pilot actions - private Dictionary pilotActions = new Dictionary(); - #endregion - - void Awake() - { - if (Instance) - { - Destroy(Instance); - } - - Instance = this; - } - - void OnGUI() - { - GUIStyle cStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); - cStyle.fontStyle = FontStyle.Bold; - cStyle.fontSize = 22; - cStyle.alignment = TextAnchor.UpperLeft; - - var displayRow = 100; - if (!BDArmorySetup.GAME_UI_ENABLED) - { - displayRow = 30; - } - - Rect cLabelRect = new Rect(30, displayRow, Screen.width, 100); - - GUIStyle cShadowStyle = new GUIStyle(cStyle); - Rect cShadowRect = new Rect(cLabelRect); - cShadowRect.x += 2; - cShadowRect.y += 2; - cShadowStyle.normal.textColor = new Color(0, 0, 0, 0.75f); - - string message = competitionStatus.ToString(); - if (competitionStarting || competitionStartTime > 0) - { - string currentVesselStatus = ""; - if (FlightGlobals.ActiveVessel != null) - { - var vesselName = FlightGlobals.ActiveVessel.GetName(); - string postFix = ""; - if (pilotActions.ContainsKey(vesselName)) - postFix = pilotActions[vesselName]; - if (Scores.ContainsKey(vesselName)) - { - ScoringData vData = Scores[vesselName]; - if (Planetarium.GetUniversalTime() - vData.lastHitTime < 2) - postFix = " is taking damage from " + vData.lastPersonWhoHitMe; - } - if (postFix != "" || vesselName != competitionStatus.lastActiveVessel) - currentVesselStatus = vesselName + postFix; - competitionStatus.lastActiveVessel = vesselName; - } - message += "\n" + currentVesselStatus; - } - - GUI.Label(cShadowRect, message, cShadowStyle); - GUI.Label(cLabelRect, message, cStyle); - - if (!BDArmorySetup.GAME_UI_ENABLED && competitionStartTime > 0) - { - Rect clockRect = new Rect(10, 6, Screen.width, 20); - GUIStyle clockStyle = new GUIStyle(cStyle); - clockStyle.fontSize = 14; - GUIStyle clockShadowStyle = new GUIStyle(clockStyle); - clockShadowStyle.normal.textColor = new Color(0, 0, 0, 0.75f); - Rect clockShadowRect = new Rect(clockRect); - clockShadowRect.x += 2; - clockShadowRect.y += 2; - var gTime = Planetarium.GetUniversalTime() - competitionStartTime; - var minutes = (int)(Math.Floor(gTime / 60)); - var seconds = (int)(gTime % 60); - string pTime = minutes.ToString("00") + ":" + seconds.ToString("00") + " " + deadOrAlive; - GUI.Label(clockShadowRect, pTime, clockShadowStyle); - GUI.Label(clockRect, pTime, clockStyle); - } - if (KSP.UI.Dialogs.FlightResultsDialog.isDisplaying && KSP.UI.Dialogs.FlightResultsDialog.showExitControls) // Prevent the Flight Results window from interrupting things when a certain vessel dies. - { - KSP.UI.Dialogs.FlightResultsDialog.Close(); - } - } - - void OnDestroy() - { - StopCompetition(); - StopAllCoroutines(); - } - - #region Competition start/stop routines - //Competition mode - public bool competitionStarting = false; - public bool competitionIsActive = false; - Coroutine competitionRoutine; - public CompetitionStartFailureReason competitionStartFailureReason; - - public class CompetitionStatus - { - private List> status = new List>(); - public void Add(string message) { status.Add(new Tuple(Planetarium.GetUniversalTime(), message)); } - public void Set(string message) { status.Clear(); Add(message); } - public override string ToString() - { - var now = Planetarium.GetUniversalTime(); - status = status.Where(s => now - s.Item1 < 5).ToList(); // Update the list of status messages. Only show messages for 5s. - return string.Join("\n", status.Select(s => s.Item2)); // Join them together to display them. - } - public int Count { get { return status.Count; } } - public string lastActiveVessel = ""; - } - - public CompetitionStatus competitionStatus = new CompetitionStatus(); - - bool startCompetitionNow = false; - public void StartCompetitionNow() // Skip the "Competition: Waiting for teams to get in position." - { - startCompetitionNow = true; - } - - public void StartCompetitionMode(float distance) - { - if (!competitionStarting) - { - DeathCount = 0; - ResetCompetitionScores(); - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Starting Competition"); - startCompetitionNow = false; - if (BDArmorySettings.GRAVITY_HACKS) - { - lastGravityMultiplier = 1f; - gravityMultiplier = 1f; - PhysicsGlobals.GraviticForceMultiplier = (double)gravityMultiplier; - VehiclePhysics.Gravity.Refresh(); - } - RemoveDebrisNow(); - GameEvents.onVesselPartCountChanged.Add(OnVesselModified); - GameEvents.onVesselCreate.Add(OnVesselModified); - if (BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING) - LoadedVesselSwitcher.Instance.EnableAutoVesselSwitching(true); - competitionStartFailureReason = CompetitionStartFailureReason.None; - competitionRoutine = StartCoroutine(DogfightCompetitionModeRoutine(distance)); - } - } - - public void StopCompetition() - { - LogResults(); - if (competitionRoutine != null) - { - StopCoroutine(competitionRoutine); - } - - competitionStarting = false; - competitionIsActive = false; - competitionStartTime = -1; - competitionShouldBeRunning = false; - GameEvents.onCollision.Remove(AnalyseCollision); - GameEvents.onVesselPartCountChanged.Remove(OnVesselModified); - GameEvents.onVesselCreate.Remove(OnVesselModified); - GameEvents.onVesselCreate.Remove(DebrisDelayedCleanUp); - rammingInformation = null; // Reset the ramming information. - } - - void CompetitionStarted() - { - competitionIsActive = true; //start logging ramming now that the competition has officially started - competitionStarting = false; - GameEvents.onCollision.Add(AnalyseCollision); // Start collision detection - GameEvents.onVesselCreate.Add(DebrisDelayedCleanUp); - 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 - finalGracePeriodStart = -1; - lastTagUpdateTime = competitionStartTime; - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Competition Started"); - } - - public void ResetCompetitionScores() - { - // reinitilize everything when the button get hit. - CompetitionID = (int)DateTime.UtcNow.Subtract(new DateTime(2020, 1, 1)).TotalSeconds; - DoPreflightChecks(); - Scores.Clear(); - DeathOrder.Clear(); - whoCleanShotWho.Clear(); - whoCleanShotWhoWithMissiles.Clear(); - whoCleanRammedWho.Clear(); - KillTimer.Clear(); - nonCompetitorsToRemove.Clear(); - pilotActions.Clear(); // Clear the pilotActions, so we don't get " is Dead" on the next round of the competition. - finalGracePeriodStart = -1; - 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 - // now find all vessels with weapons managers - using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) - while (loadedVessels.MoveNext()) - { - if (loadedVessels.Current == null || !loadedVessels.Current.loaded) - continue; - IBDAIControl pilot = loadedVessels.Current.FindPartModuleImplementing(); - if (pilot == null || !pilot.weaponManager || pilot.weaponManager.Team.Neutral) - continue; - // put these in the scoring dictionary - these are the active participants - Scores[loadedVessels.Current.GetName()] = new ScoringData { vesselRef = loadedVessels.Current, weaponManagerRef = pilot.weaponManager, lastFiredTime = Planetarium.GetUniversalTime(), previousPartCount = loadedVessels.Current.parts.Count }; - } - } - - IEnumerator DogfightCompetitionModeRoutine(float distance) - { - competitionStarting = true; - startTag = true; // Tag entry condition, should be true even if tag is not currently enabled, so if tag is enabled later in the competition it will function - competitionStatus.Set("Competition: Pilots are taking off."); - var pilots = new Dictionary>(); - HashSet readyToLaunch = new HashSet(); - using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) - while (loadedVessels.MoveNext()) - { - if (loadedVessels.Current == null || !loadedVessels.Current.loaded) - continue; - IBDAIControl pilot = loadedVessels.Current.FindPartModuleImplementing(); - if (pilot == null || !pilot.weaponManager || pilot.weaponManager.Team.Neutral) - continue; - - if (!pilots.TryGetValue(pilot.weaponManager.Team, out List teamPilots)) - { - teamPilots = new List(); - pilots.Add(pilot.weaponManager.Team, teamPilots); - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Adding Team " + pilot.weaponManager.Team.Name); - } - teamPilots.Add(pilot); - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Adding Pilot " + pilot.vessel.GetName()); - readyToLaunch.Add(pilot); - } - - foreach (var pilot in readyToLaunch) - { - pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[10]); // Modular Missiles use lower AGs (1-3) for staging, use a high AG number to not affect them - pilot.ActivatePilot(); - pilot.CommandTakeOff(); - if (pilot.weaponManager.guardMode) - { - pilot.weaponManager.ToggleGuardMode(); - } - if (!pilot.vessel.FindPartModulesImplementing().Any(engine => engine.EngineIgnited)) // Find vessels that didn't activate their engines on AG10 and fire their next stage. - { - Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: " + pilot.vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines."); - foreach (var engine in pilot.vessel.FindPartModulesImplementing()) - engine.Activate(); - } - } - - //clear target database so pilots don't attack yet - BDATargetManager.ClearDatabase(); - - foreach (var vname in Scores.Keys) - { - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Adding Score Tracker For " + vname); - } - - if (pilots.Count < 2) - { - Debug.Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: Unable to start competition mode - one or more teams is empty"); - competitionStatus.Set("Competition: Failed! One or more teams is empty."); - yield return new WaitForSeconds(2); - competitionStarting = false; - competitionIsActive = false; - competitionStartFailureReason = CompetitionStartFailureReason.OnlyOneTeam; - yield break; - } - - var leaders = new List(); - using (var pilotList = pilots.GetEnumerator()) - while (pilotList.MoveNext()) - { - if (pilotList.Current.Value == null) - { - competitionStatus.Set("Competition: Teams got adjusted during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.OnlyOneTeam; - StopCompetition(); - yield break; - } - leaders.Add(pilotList.Current.Value[0]); - } - while (leaders.Any(leader => leader?.weaponManager?.wingCommander?.weaponManager == null)) - { - yield return new WaitForFixedUpdate(); - if (leaders.Any(leader => leader?.weaponManager == null)) - { - competitionStatus.Set("Competition: One of the team leaders disappeared during start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); - yield break; - } - } - foreach (var leader in leaders) - leader.weaponManager.wingCommander.CommandAllFollow(); - - //wait till the leaders are ready to engage (airborne for PilotAI) - bool ready = false; - while (!ready) - { - ready = true; - using (var leader = leaders.GetEnumerator()) - while (leader.MoveNext()) - if (leader.Current != null && !leader.Current.CanEngage()) - { - ready = false; - yield return new WaitForSeconds(1); - break; - } - } - - using (var leader = leaders.GetEnumerator()) - while (leader.MoveNext()) - if (leader.Current == null) - { - competitionStatus.Set("Competition: A leader vessel has disappeared during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); - yield break; - } - - competitionStatus.Set("Competition: Sending pilots to start position."); - Vector3 center = Vector3.zero; - using (var leader = leaders.GetEnumerator()) - 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; - Quaternion directionStep = Quaternion.AngleAxis(360f / leaders.Count, VectorUtils.GetUpDirection(center)); - - for (var i = 0; i < leaders.Count; ++i) - { - leaders[i].CommandFlyTo(VectorUtils.WorldPositionToGeoCoords(startDirection, FlightGlobals.currentMainBody)); - startDirection = directionStep * startDirection; - } - - Vector3 centerGPS = VectorUtils.WorldPositionToGeoCoords(center, FlightGlobals.currentMainBody); - - //wait till everyone is in position - competitionStatus.Set("Competition: Waiting for teams to get in position."); - bool waiting = true; - var sqrDistance = distance * distance; - while (waiting && !startCompetitionNow) - { - waiting = false; - - foreach (var leader in leaders) - if (leader == null) - { - competitionStatus.Set("Competition: A leader vessel has disappeared during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); // A yield has occurred, check that the leaders list hasn't changed in the meantime. - yield break; - } - - - using (var leader = leaders.GetEnumerator()) - while (leader.MoveNext()) - { - using (var otherLeader = leaders.GetEnumerator()) - while (otherLeader.MoveNext()) - { - if (leader.Current == otherLeader.Current) - continue; - try // Somehow, if a vessel gets destroyed during competition start, the following can throw a null reference exception despite checking for nulls! - { - if ((leader.Current.transform.position - otherLeader.Current.transform.position).sqrMagnitude < sqrDistance) - waiting = true; - } - catch - { - competitionStatus.Set("Competition: A leader vessel has disappeared during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); // A yield has occurred, check that the leaders list hasn't changed in the meantime. - yield break; - } - } - - // Increase the distance for large teams - if (!pilots.ContainsKey(leader.Current.weaponManager.Team)) - { - competitionStatus.Set("Competition: The teams were changed during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.TeamsChanged; - StopCompetition(); - yield break; - } - var sqrTeamDistance = (800 + 100 * pilots[leader.Current.weaponManager.Team].Count) * (800 + 100 * pilots[leader.Current.weaponManager.Team].Count); - using (var pilot = pilots[leader.Current.weaponManager.Team].GetEnumerator()) - while (pilot.MoveNext()) - if (pilot.Current != null - && pilot.Current.currentCommand == PilotCommands.Follow - && (pilot.Current.vessel.CoM - pilot.Current.commandLeader.vessel.CoM).sqrMagnitude > 1000f * 1000f) - waiting = true; - - if (waiting) break; - } - - yield return null; - } - previousNumberCompetitive = 2; // For entering into tag mode - - //start the match - foreach (var teamPilots in pilots.Values) - { - if (teamPilots == null) - { - competitionStatus.Set("Competition: Teams have been changed during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.TeamsChanged; - StopCompetition(); - yield break; - } - foreach (var pilot in teamPilots) - if (pilot == null) - { - competitionStatus.Set("Competition: A pilot has disappeared from team during competition start-up, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.PilotDisappeared; - StopCompetition(); // Check that the team pilots haven't been changed during the competition startup. - yield break; - } - } - using (var teamPilots = pilots.GetEnumerator()) - while (teamPilots.MoveNext()) - using (var pilot = teamPilots.Current.Value.GetEnumerator()) - while (pilot.MoveNext()) - { - if (pilot.Current == null) continue; - - if (!pilot.Current.weaponManager.guardMode) - pilot.Current.weaponManager.ToggleGuardMode(); - - using (var leader = leaders.GetEnumerator()) - while (leader.MoveNext()) - BDATargetManager.ReportVessel(pilot.Current.vessel, leader.Current.weaponManager); - - pilot.Current.ReleaseCommand(); - pilot.Current.CommandAttack(centerGPS); - pilot.Current.vessel.altimeterDisplayState = AltimeterDisplayState.AGL; - } - - competitionStatus.Set("Competition starting! Good luck!"); - yield return new WaitForSeconds(2); - CompetitionStarted(); - } - #endregion - - HashSet uniqueVesselNames = new HashSet(); - private List getAllPilots() - { - var pilots = new List(); - uniqueVesselNames.Clear(); - int count = 0; - foreach (var vessel in BDATargetManager.LoadedVessels) - { - if (vessel == null || !vessel.loaded) continue; - if (IsValidVessel(vessel) != InvalidVesselReason.None) continue; - var pilot = vessel.FindPartModuleImplementing(); - if (pilot.weaponManager.Team.Neutral) continue; // Ignore the neutrals. - pilots.Add(pilot); - if (uniqueVesselNames.Contains(vessel.vesselName)) - vessel.vesselName += "_" + (++count); - uniqueVesselNames.Add(vessel.vesselName); - } - return pilots; - } - - #region Vessel validity - public enum InvalidVesselReason { None, NullVessel, NoAI, NoWeaponManager, NoCommand }; - public InvalidVesselReason IsValidVessel(Vessel vessel) - { - if (vessel == null) - return InvalidVesselReason.NullVessel; - var pilot = vessel.FindPartModuleImplementing(); - if (pilot == null) // Check for an AI. - return InvalidVesselReason.NoAI; - if (vessel.FindPartModuleImplementing() == null) // Check for a weapon manager. - return InvalidVesselReason.NoWeaponManager; - if (vessel.FindPartModuleImplementing() == null && vessel.FindPartModuleImplementing() == null) // Check for a cockpit or command seat. - CheckVesselType(vessel); // Attempt to fix it. - if (vessel.FindPartModuleImplementing() == null && vessel.FindPartModuleImplementing() == null) // Check for a cockpit or command seat. - return InvalidVesselReason.NoCommand; - return InvalidVesselReason.None; - } - - public void OnVesselModified(Vessel vessel) - { - CheckVesselType(vessel); - if (!BDArmorySettings.AUTONOMOUS_COMBAT_SEATS) CheckForAutonomousCombatSeat(vessel); - if (BDArmorySettings.DESTROY_UNCONTROLLED_WMS) CheckForUncontrolledVessel(vessel); - } - - HashSet validVesselTypes = new HashSet { VesselType.Plane, VesselType.Ship }; - public void CheckVesselType(Vessel vessel) - { - if (!BDArmorySettings.RUNWAY_PROJECT) return; - if (vessel != null && vessel.vesselName != null) - { - var vesselTypeIsValid = validVesselTypes.Contains(vessel.vesselType); - var hasMissileFire = vessel.FindPartModuleImplementing() != null; - if (!vesselTypeIsValid && hasMissileFire) // Found an invalid vessel type with a weapon manager. - { - var message = "Found weapon manager on " + vessel.vesselName + " of type " + vessel.vesselType; - if (vessel.vesselName.EndsWith(" " + vessel.vesselType.ToString())) - vessel.vesselName = vessel.vesselName.Remove(vessel.vesselName.Length - vessel.vesselType.ToString().Length - 1); - vessel.vesselType = VesselType.Plane; - message += ", changing vessel name and type to " + vessel.vesselName + ", " + vessel.vesselType; - Debug.Log("[BDACompetitionMode]: " + message); - return; - } - if (vesselTypeIsValid && vessel.vesselType == VesselType.Plane && vessel.vesselName.EndsWith(" Plane") && !Scores.ContainsKey(vessel.vesselName) && Scores.ContainsKey(vessel.vesselName.Remove(vessel.vesselName.Length - 6)) && IsValidVessel(vessel) == InvalidVesselReason.None) - { - var message = "Found a valid vessel (" + vessel.vesselName + ") tagged with 'Plane' when it shouldn't be, renaming."; - Debug.Log("[BDACompetitionMode]: " + message); - vessel.vesselName = vessel.vesselName.Remove(vessel.vesselName.Length - 6); - return; - } - } - } - - HashSet chutesToDeploy = new HashSet(); - HashSet seatsToLeave = new HashSet(); - public void CheckForAutonomousCombatSeat(Vessel vessel) - { - if (vessel == null) return; - var kerbalEVA = vessel.FindPartModuleImplementing(); - if (kerbalEVA != null && vessel.parts.Count == 1) // Check for a falling kerbal. - { - var chute = kerbalEVA.vessel.FindPartModuleImplementing(); - if (chute != null && chute.deploymentState != ModuleParachute.deploymentStates.DEPLOYED && !chutesToDeploy.Contains(chute)) - { - chutesToDeploy.Add(chute); - StartCoroutine(DelayedChuteDeployment(chute)); - } - return; - } - var kerbalSeat = vessel.FindPartModuleImplementing(); - if (kerbalSeat != null) - { - if (vessel.parts.Count == 1) // Check for a falling combat seat. - { - Debug.Log("[BDACompetitionMode]: Found a lone combat seat, killing it."); - PartExploderSystem.AddPartToExplode(vessel.parts[0]); - return; - } - if (vessel.parts.Count == 2 && kerbalEVA != null) // Just a kerbal in a combat seat. - { - seatsToLeave.Add(kerbalSeat); - StartCoroutine(DelayedLeaveSeat(kerbalSeat)); - return; - } - // Check for a lack of control. - var AI = vessel.FindPartModuleImplementing(); - if (kerbalEVA == null && AI != null && AI.pilotEnabled) // If not controlled by a kerbalEVA in a KerbalSeat, check the regular ModuleCommand parts. - { - var commandModules = vessel.FindPartModulesImplementing(); - if (commandModules.All(c => c.GetControlSourceState() == CommNet.VesselControlState.None)) - { - Debug.Log("[BDACompetitionMode]: Kerbal has left the seat of " + vessel.vesselName + " and it has no other controls, disabling the AI."); - AI.DeactivatePilot(); - } - } - } - } - - IEnumerator DelayedChuteDeployment(ModuleEvaChute chute, float delay = 1f) - { - yield return new WaitForSeconds(delay); - if (chute != null) - { - Debug.Log("[BDACompetitionMode]: Found a falling kerbal, deploying halo parachute."); - chutesToDeploy.Remove(chute); - if (chute.deploymentState != ModuleParachute.deploymentStates.SEMIDEPLOYED) - chute.deploymentState = ModuleParachute.deploymentStates.STOWED; // Reset the deployment state. - chute.deployAltitude = 30f; - chute.Deploy(); - } - } - - IEnumerator DelayedLeaveSeat(KerbalSeat kerbalSeat, float delay = 3f) - { - yield return new WaitForSeconds(delay); - if (kerbalSeat != null) - { - Debug.Log("[BDACompetitionMode]: Found a kerbal in a combat chair just falling, ejecting."); - seatsToLeave.Remove(kerbalSeat); - kerbalSeat.LeaveSeat(new KSPActionParam(KSPActionGroup.Abort, KSPActionType.Activate)); - } - } - - void CheckForUncontrolledVessel(Vessel vessel) - { - if (vessel == null || vessel.vesselName == null) return; - if (vessel.FindPartModuleImplementing() != null) return; // Check for Kerbals on the inside. - if (vessel.FindPartModuleImplementing() != null) return; // Check for Kerbals on the outside. - // Check for drones - var commandModules = vessel.FindPartModulesImplementing(); - var craftbricked = vessel.FindPartModuleImplementing(); - if (commandModules.All(c => c.GetControlSourceState() == CommNet.VesselControlState.None)) - if (Scores.ContainsKey(vessel.vesselName) && Scores[vessel.vesselName]?.weaponManagerRef != null) - StartCoroutine(DelayedExplodeWM(Scores[vessel.vesselName].weaponManagerRef, 5f)); // Uncontrolled vessel, destroy its weapon manager in 5s. - if (craftbricked.bricked) - { - if (Scores.ContainsKey(vessel.vesselName) && Scores[vessel.vesselName]?.weaponManagerRef != null) - StartCoroutine(DelayedExplodeWM(Scores[vessel.vesselName].weaponManagerRef, 2f)); // vessel fried by EMP, destroy its weapon manager in 2s. - } - } - - IEnumerator DelayedExplodeWM(MissileFire weaponManager, float delay = 1f) - { - yield return new WaitForSeconds(delay); - if (weaponManager != null) - { - Debug.Log("[BDACompetitionMode]: " + weaponManager.vessel.vesselName + " has no form of control, killing the weapon manager."); - PartExploderSystem.AddPartToExplode(weaponManager.part); - } - } - - void CheckForBadlyNamedVessels() - { - foreach (var wm in LoadedVesselSwitcher.Instance.weaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).ToList()) - if (wm != null && wm.vessel != null && wm.vessel.vesselName != null) - { - if (wm.vessel.vesselType == VesselType.Plane && wm.vessel.vesselName.EndsWith(" Plane") && !Scores.ContainsKey(wm.vessel.vesselName) && Scores.ContainsKey(wm.vessel.vesselName.Remove(wm.vessel.vesselName.Length - 6)) && IsValidVessel(wm.vessel) == InvalidVesselReason.None) - { - var message = "Found a valid vessel (" + wm.vessel.vesselName + ") tagged with 'Plane' when it shouldn't be, renaming."; - Debug.Log("[BDACompetitionMode]: " + message); - wm.vessel.vesselName = wm.vessel.vesselName.Remove(wm.vessel.vesselName.Length - 6); - } - } - } - #endregion - - #region Runway Project - public bool killerGMenabled = false; - public bool pinataAlive = false; - public bool OneOfAKind = false; - - public void StartRapidDeployment(float distance) - { - if (!BDArmorySettings.RUNWAY_PROJECT) return; - if (!competitionStarting) - { - ResetCompetitionScores(); - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Starting Rapid Deployment "); - string commandString = "0:SetThrottle:100\n0:ActionGroup:14:0\n0:Stage\n35:ActionGroup:1\n10:ActionGroup:2\n3:RemoveFairings\n0:ActionGroup:3\n0:ActionGroup:12:1\n1:TogglePilot:1\n6:ToggleGuard:1\n0:ActionGroup:16:0\n5:EnableGM\n5:RemoveDebris\n0:ActionGroup:16:0\n"; - competitionRoutine = StartCoroutine(SequencedCompetition(commandString)); - } - } - - private void DoPreflightChecks() - { - if (BDArmorySettings.RUNWAY_PROJECT) - { - var pilots = getAllPilots(); - foreach (var pilot in pilots) - { - if (pilot.vessel == null) continue; - - enforcePartCount(pilot.vessel); - } - } - } - // "JetEngine", "miniJetEngine", "turboFanEngine", "turboJet", "turboFanSize2", "RAPIER" - static string[] allowedEngineList = { "JetEngine", "miniJetEngine", "turboFanEngine", "turboJet", "turboFanSize2", "RAPIER" }; - static HashSet allowedEngines = new HashSet(allowedEngineList); - - // allow duplicate landing gear - static string[] allowedDuplicateList = { "GearLarge", "GearFixed", "GearFree", "GearMedium", "GearSmall", "SmallGearBay", "fuelLine", "strutConnector" }; - static HashSet allowedLandingGear = new HashSet(allowedDuplicateList); - - // don't allow "SaturnAL31" - static string[] bannedPartList = { "SaturnAL31" }; - static HashSet bannedParts = new HashSet(bannedPartList); - - // ammo boxes - static string[] ammoPartList = { "baha20mmAmmo", "baha30mmAmmo", "baha50CalAmmo", "BDAcUniversalAmmoBox", "UniversalAmmoBoxBDA" }; - static HashSet ammoParts = new HashSet(ammoPartList); - - public void enforcePartCount(Vessel vessel) - { - if (!BDArmorySettings.RUNWAY_PROJECT) return; - if (!OneOfAKind) return; - using (List.Enumerator parts = vessel.parts.GetEnumerator()) - { - Dictionary partCounts = new Dictionary(); - List partsToKill = new List(); - List ammoBoxes = new List(); - int engineCount = 0; - while (parts.MoveNext()) - { - if (parts.Current == null) continue; - var partName = parts.Current.name; - //Debug.Log("Part " + vessel.GetName() + " " + partName); - if (partCounts.ContainsKey(partName)) - { - partCounts[partName]++; - } - else - { - partCounts[partName] = 1; - } - if (allowedEngines.Contains(partName)) - { - engineCount++; - } - if (bannedParts.Contains(partName)) - { - partsToKill.Add(parts.Current); - } - if (allowedLandingGear.Contains(partName)) - { - // duplicates allowed - continue; - } - if (ammoParts.Contains(partName)) - { - // can only figure out limits after counting engines. - ammoBoxes.Add(parts.Current); - continue; - } - if (partCounts[partName] > 1) - { - partsToKill.Add(parts.Current); - } - } - if (engineCount == 0) - { - engineCount = 1; - } - - while (ammoBoxes.Count > engineCount * 3) - { - partsToKill.Add(ammoBoxes[ammoBoxes.Count - 1]); - ammoBoxes.RemoveAt(ammoBoxes.Count - 1); - } - if (partsToKill.Count > 0) - { - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "] Vessel Breaking Part Count Rules " + vessel.GetName()); - foreach (var part in partsToKill) - { - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "] KILLPART:" + part.name + ":" + vessel.GetName()); - PartExploderSystem.AddPartToExplode(part); - } - } - } - } - - private void DoRapidDeploymentMassTrim() - { - // in rapid deployment this verified masses etc. - var oreID = PartResourceLibrary.Instance.GetDefinition("Ore").id; - var pilots = getAllPilots(); - var lowestMass = 100000000000000f; - var highestMass = 0f; - foreach (var pilot in pilots) - { - - if (pilot.vessel == null) continue; - - var notShieldedCount = 0; - using (List.Enumerator parts = pilot.vessel.parts.GetEnumerator()) - { - while (parts.MoveNext()) - { - if (parts.Current == null) continue; - // count the unshielded parts - if (!parts.Current.ShieldedFromAirstream) - { - notShieldedCount++; - } - using (IEnumerator resources = parts.Current.Resources.GetEnumerator()) - while (resources.MoveNext()) - { - if (resources.Current == null) continue; - - if (resources.Current.resourceName == "Ore") - { - if (resources.Current.maxAmount == 1500) - { - resources.Current.amount = 0; - } - // oreMass = 10; - // ore to add = difference / 10; - // is mass in tons or KG? - //Debug.Log("[BDACompetitionMode]: RESOURCE:" + parts.Current.partName + ":" + resources.Current.maxAmount); - - } - else if (resources.Current.resourceName == "LiquidFuel") - { - if (resources.Current.maxAmount == 3240) - { - resources.Current.amount = 2160; - } - } - else if (resources.Current.resourceName == "Oxidizer") - { - if (resources.Current.maxAmount == 3960) - { - resources.Current.amount = 2640; - } - } - } - } - } - var mass = pilot.vessel.GetTotalMass(); - - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "] UNSHIELDED:" + notShieldedCount.ToString() + ":" + pilot.vessel.GetName()); - - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "] MASS:" + mass.ToString() + ":" + pilot.vessel.GetName()); - if (mass < lowestMass) - { - lowestMass = mass; - } - if (mass > highestMass) - { - highestMass = mass; - } - } - - var difference = highestMass - lowestMass; - // - foreach (var pilot in pilots) - { - if (pilot.vessel == null) continue; - var mass = pilot.vessel.GetTotalMass(); - var extraMass = highestMass - mass; - using (List.Enumerator parts = pilot.vessel.parts.GetEnumerator()) - while (parts.MoveNext()) - { - bool massAdded = false; - if (parts.Current == null) continue; - using (IEnumerator resources = parts.Current.Resources.GetEnumerator()) - while (resources.MoveNext()) - { - if (resources.Current == null) continue; - if (resources.Current.resourceName == "Ore") - { - // oreMass = 10; - // ore to add = difference / 10; - // is mass in tons or KG? - if (resources.Current.maxAmount == 1500) - { - var oreAmount = extraMass / 0.01; // 10kg per unit of ore - if (oreAmount > 1500) oreAmount = 1500; - resources.Current.amount = oreAmount; - } - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "] RESOURCEUPDATE:" + pilot.vessel.GetName() + ":" + resources.Current.amount); - massAdded = true; - } - } - if (massAdded) break; - } - } - } - - // transmits a bunch of commands to make things happen - // this is a really dumb sequencer with text commands - // 0:ThrottleMax - // 0:Stage - // 30:ActionGroup:1 - // 35:ActionGroup:2 - // 40:ActionGroup:3 - // 41:TogglePilot - // 45:ToggleGuard - IEnumerator SequencedCompetition(string commandList) - { - competitionStarting = true; - double startTime = Planetarium.GetUniversalTime(); - double nextStep = startTime; - // split list of events into lines - var events = commandList.Split('\n'); - - foreach (var cmdEvent in events) - { - // parse the event - competitionStatus.Set(cmdEvent); - var parts = cmdEvent.Split(':'); - if (parts.Count() == 1) - { - Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: Competition Command not parsed correctly " + cmdEvent); - break; - } - var timeStep = int.Parse(parts[0]); - nextStep = Planetarium.GetUniversalTime() + timeStep; - while (Planetarium.GetUniversalTime() < nextStep) - { - yield return null; - } - - List pilots; - var command = parts[1]; - - switch (command) - { - case "Stage": - // activate stage - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - Misc.Misc.fireNextNonEmptyStage(pilot.vessel); - } - break; - case "ActionGroup": - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - if (parts.Count() == 3) - { - pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[int.Parse(parts[2])]); - } - else if (parts.Count() == 4) - { - bool state = false; - if (parts[3] != "0") - { - state = true; - } - pilot.vessel.ActionGroups.SetGroup(KM_dictAG[int.Parse(parts[2])], state); - } - else - { - Debug.Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: Competition Command not parsed correctly " + cmdEvent); - } - } - break; - case "TogglePilot": - if (parts.Count() == 3) - { - var newState = true; - if (parts[2] == "0") - { - newState = false; - } - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - if (newState != pilot.pilotEnabled) - pilot.TogglePilot(); - } - } - else - { - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - pilot.TogglePilot(); - } - } - break; - case "ToggleGuard": - if (parts.Count() == 3) - { - var newState = true; - if (parts[2] == "0") - { - newState = false; - } - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - if (pilot.weaponManager != null && pilot.weaponManager.guardMode != newState) - pilot.weaponManager.ToggleGuardMode(); - } - } - else - { - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - if (pilot.weaponManager != null) pilot.weaponManager.ToggleGuardMode(); - } - } - - break; - case "SetThrottle": - if (parts.Count() == 3) - { - pilots = getAllPilots(); - foreach (var pilot in pilots) - { - var throttle = int.Parse(parts[2]) * 0.01f; - pilot.vessel.ctrlState.mainThrottle = throttle; - pilot.vessel.ctrlState.killRot = true; - } - } - break; - case "RemoveDebris": - // remove anything that doesn't contain BD Armory modules - RemoveNonCompetitors(); - break; - case "RemoveFairings": - // removes the fairings after deplyment to stop the physical objects consuming CPU - var rmObj = new List(); - foreach (var phyObj in FlightGlobals.physicalObjects) - { - if (phyObj.name == "FairingPanel") rmObj.Add(phyObj); - Debug.Log("[RemoveFairings] " + phyObj.name); - } - foreach (var phyObj in rmObj) - { - FlightGlobals.removePhysicalObject(phyObj); - } - - break; - case "EnableGM": - killerGMenabled = true; - decisionTick = BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? -1 : Planetarium.GetUniversalTime() + BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY; - ResetSpeeds(); - break; - } - } - // will need a terminator routine - CompetitionStarted(); - } - - // ask the GM to find a 'victim' which means a slow pilot who's not shooting very much - // obviosly this is evil. - // it's enabled by right clicking the M button. - // I also had it hooked up to the death of the Pinata but that's disconnected right now - private void FindVictim() - { - if (!BDArmorySettings.RUNWAY_PROJECT) return; - if (decisionTick < 0) return; - if (Planetarium.GetUniversalTime() < decisionTick) return; - decisionTick = BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? -1 : Planetarium.GetUniversalTime() + BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY; - if (!killerGMenabled) return; - if (Planetarium.GetUniversalTime() - competitionStartTime < BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD) return; - // arbitrary and capbricious decisions of life and death - - bool hasFired = true; - Vessel worstVessel = null; - double slowestSpeed = 100000; - int vesselCount = 0; - using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) - while (loadedVessels.MoveNext()) - { - if (loadedVessels.Current == null || !loadedVessels.Current.loaded) - continue; - IBDAIControl pilot = loadedVessels.Current.FindPartModuleImplementing(); - if (pilot == null || !pilot.weaponManager || pilot.weaponManager.Team.Neutral) - continue; - - - - var vesselName = loadedVessels.Current.GetName(); - if (!Scores.ContainsKey(vesselName)) - continue; - - vesselCount++; - ScoringData vData = Scores[vesselName]; - - var averageSpeed = vData.AverageSpeed / vData.averageCount; - var averageAltitude = vData.AverageAltitude / vData.averageCount; - averageSpeed = averageAltitude + (averageSpeed * averageSpeed / 200); // kinetic & potential energy - if (pilot.weaponManager != null) - { - if (!pilot.weaponManager.guardMode) averageSpeed *= 0.5; - } - - bool vesselNotFired = (Planetarium.GetUniversalTime() - vData.lastFiredTime) > 120; // if you can't shoot in 2 minutes you're at the front of line - - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "] Victim Check " + vesselName + " " + averageSpeed.ToString() + " " + vesselNotFired.ToString()); - if (hasFired) - { - if (vesselNotFired) - { - // we found a vessel which hasn't fired - worstVessel = loadedVessels.Current; - slowestSpeed = averageSpeed; - hasFired = false; - } - else if (averageSpeed < slowestSpeed) - { - // this vessel fired but is slow - worstVessel = loadedVessels.Current; - slowestSpeed = averageSpeed; - } - } - else - { - if (vesselNotFired) - { - // this vessel was slow and hasn't fired - worstVessel = loadedVessels.Current; - slowestSpeed = averageSpeed; - } - } - } - // if we have 3 or more vessels kill the slowest - if (vesselCount > 2 && worstVessel != null) - { - var vesselName = worstVessel.GetName(); - if (!Scores.ContainsKey(vesselName)) - { - if (Scores[vesselName].lastPersonWhoHitMe == "") - { - Scores[vesselName].lastPersonWhoHitMe = "GM"; - Scores[vesselName].gmKillReason = GMKillReason.GM; // Indicate that it was us who killed it and remove any "clean" kills. - if (whoCleanShotWho.ContainsKey(vesselName)) whoCleanShotWho.Remove(vesselName); - if (whoCleanRammedWho.ContainsKey(vesselName)) whoCleanRammedWho.Remove(vesselName); - if (whoCleanShotWhoWithMissiles.ContainsKey(vesselName)) whoCleanShotWhoWithMissiles.Remove(vesselName); - } - } - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "] killing " + vesselName); - Misc.Misc.ForceDeadVessel(worstVessel); - } - ResetSpeeds(); - } - - // reset all the tracked speeds, and copy the shot clock over, because I wanted 2 minutes of shooting to count - private void ResetSpeeds() - { - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "] resetting kill clock"); - foreach (var vname in Scores.Keys) - { - if (Scores[vname].averageCount == 0) - { - Scores[vname].AverageAltitude = 0; - Scores[vname].AverageSpeed = 0; - } - else - { - // ensures we always have a sensible value in here - Scores[vname].AverageAltitude /= Scores[vname].averageCount; - Scores[vname].AverageSpeed /= Scores[vname].averageCount; - Scores[vname].averageCount = 1; - } - } - } - - #endregion - - #region Debris clean-up - private HashSet nonCompetitorsToRemove = new HashSet(); - public void RemoveNonCompetitors() - { - foreach (var vessel in FlightGlobals.Vessels) - { - if (vessel == null) continue; - if (vessel.vesselType == VesselType.Debris) continue; // Handled by DebrisDelayedCleanUp. - if (nonCompetitorsToRemove.Contains(vessel)) continue; // Already scheduled for removal. - bool activePilot = false; - if (BDArmorySettings.RUNWAY_PROJECT && vessel.GetName() == "Pinata") - { - activePilot = true; - } - else - { - int foundActiveParts = 0; // Note: this checks for exactly one of each part. - using (var wms = vessel.FindPartModulesImplementing().GetEnumerator()) // Has a weapon manager - while (wms.MoveNext()) - if (wms.Current != null) - { - foundActiveParts++; - break; - } - - using (var wms = vessel.FindPartModulesImplementing().GetEnumerator()) // Has an AI - while (wms.MoveNext()) - if (wms.Current != null) - { - foundActiveParts++; - break; - } - - using (var wms = vessel.FindPartModulesImplementing().GetEnumerator()) // Has a command module - while (wms.MoveNext()) - if (wms.Current != null) - { - foundActiveParts++; - break; - } - - if (foundActiveParts != 3) - { - using (var wms = vessel.FindPartModulesImplementing().GetEnumerator()) // Command seats are ok - while (wms.MoveNext()) - if (wms.Current != null) - { - foundActiveParts++; - break; - } - } - activePilot = foundActiveParts == 3; - - using (var wms = vessel.FindPartModulesImplementing().GetEnumerator()) // Allow missiles - while (wms.MoveNext()) - if (wms.Current != null) - { - activePilot = true; - break; - } - } - if (!activePilot) - { - nonCompetitorsToRemove.Add(vessel); - StartCoroutine(DelayedVesselRemovalCoroutine(vessel, BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY)); - } - } - } - - public void RemoveDebrisNow() - { - foreach (var vessel in FlightGlobals.Vessels) - { - if (vessel == null) continue; - if (vessel.vesselType == VesselType.Debris) // Clean up any old debris. - StartCoroutine(DelayedVesselRemovalCoroutine(vessel, 0)); - if (vessel.vesselType == VesselType.SpaceObject) // Remove comets and asteroids to try to avoid null refs. - StartCoroutine(DelayedVesselRemovalCoroutine(vessel, 0)); - } - } - - void DebrisDelayedCleanUp(Vessel debris) - { - try - { - if (debris != null && debris.vesselType == VesselType.Debris) - { - StartCoroutine(DelayedVesselRemovalCoroutine(debris, BDArmorySettings.DEBRIS_CLEANUP_DELAY)); - } - } - catch - { - Debug.Log("DEBUG debris " + debris.vesselName + " is a component? " + (debris is Component) + ", is a monobehaviour? " + (debris is MonoBehaviour)); - } - } - - private IEnumerator DelayedVesselRemovalCoroutine(Vessel vessel, float delay) - { - var vesselType = vessel.vesselType; - yield return new WaitForSeconds(delay); - if (vessel != null && vesselType == VesselType.Debris && vessel.vesselType != VesselType.Debris) - { - Debug.Log("[BDACompetitionMode]: Debris " + vessel.vesselName + " is no longer labelled as debris, not removing."); - yield break; - } - if (vessel != null) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDACompetitionMode]: Removing " + vessel.GetName()); - vessel.Die(); - } - yield return new WaitForFixedUpdate(); - if (vessel != null) - { - var partsToKill = vessel.parts.ToList(); - foreach (var part in partsToKill) - if (part != null) - part.Die(); - } - if (nonCompetitorsToRemove.Contains(vessel)) - { - Debug.Log("[BDACompetitionMode]: " + vessel?.vesselName + " removed."); - nonCompetitorsToRemove.Remove(vessel); - } - } - #endregion - - // This is called every Time.fixedDeltaTime. - void FixedUpdate() - { - if (competitionIsActive) - LogRamming(); - } - - // the competition update system - // cleans up dead vessels, tries to track kills (badly) - // all of these are based on the vessel name which is probably sub optimal - // This is triggered every Time.deltaTime. - HashSet vesselsToKill = new HashSet(); - HashSet alive = new HashSet(); - public void DoUpdate() - { - // should be called every frame during flight scenes - if (competitionStartTime < 0) return; - if (competitionIsActive) - competitionShouldBeRunning = true; - if (competitionShouldBeRunning && !competitionIsActive) - { - Debug.Log("DEBUG Competition stopped unexpectedly!"); - competitionShouldBeRunning = false; - } - // 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."); - var now = Planetarium.GetUniversalTime(); - if (now < nextUpdateTick) - return; - CheckForBadlyNamedVessels(); - double updateTickLength = BDArmorySettings.TAG_MODE ? 0.1 : BDArmorySettings.GRAVITY_HACKS ? 0.5 : 2; - vesselsToKill.Clear(); - nextUpdateTick = nextUpdateTick + updateTickLength; - int numberOfCompetitiveVessels = 0; - alive.Clear(); - string doaUpdate = "ALIVE: "; - // check all the planes - foreach (var vessel in FlightGlobals.Vessels) - { - if (vessel == null || !vessel.loaded) // || vessel.packed) // Allow packed craft to avoid the packed craft being considered dead (e.g., when command seats spawn). - continue; - - var mf = vessel.FindPartModuleImplementing(); - - if (mf != null) - { - // things to check - // does it have fuel? - string vesselName = vessel.GetName(); - ScoringData vData = null; - if (Scores.ContainsKey(vesselName)) - { - vData = Scores[vesselName]; - } - - // this vessel really is alive - if ((vessel.vesselType != VesselType.Debris) && !vesselName.EndsWith("Debris")) // && !vesselName.EndsWith("Plane") && !vesselName.EndsWith("Probe")) - { - if (!VesselSpawner.Instance.vesselsSpawningContinuously && DeathOrder.ContainsKey(vesselName)) // This isn't an issue when continuous spawning is active. - { - Debug.Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: Dead vessel found alive " + vesselName); - //DeathOrder.Remove(vesselName); - } - // vessel is still alive - alive.Add(vesselName); - doaUpdate += " *" + vesselName + "* "; - numberOfCompetitiveVessels++; - } - pilotActions[vesselName] = ""; - - // try to create meaningful activity strings - if (mf.AI != null && mf.AI.currentStatus != null) - { - pilotActions[vesselName] = ""; - if (mf.vessel.LandedOrSplashed) - { - if (mf.vessel.Landed) - { - pilotActions[vesselName] = " is landed"; - } - else - { - pilotActions[vesselName] = " is splashed"; - } - } - var activity = mf.AI.currentStatus; - if (activity == "Taking off") - pilotActions[vesselName] = " is taking off"; - else if (activity == "Follow") - { - if (mf.AI.commandLeader != null && mf.AI.commandLeader.vessel != null) - pilotActions[vesselName] = " is following " + mf.AI.commandLeader.vessel.GetName(); - } - else if (activity.StartsWith("Gain Alt")) - pilotActions[vesselName] = " is gaining altitude"; - else if (activity.StartsWith("Terrain")) - pilotActions[vesselName] = " is avoiding terrain"; - else if (activity == "Orbiting") - pilotActions[vesselName] = " is orbiting"; - else if (activity == "Extending") - pilotActions[vesselName] = " is extending "; - else if (activity == "AvoidCollision") - pilotActions[vesselName] = " is avoiding collision"; - else if (activity == "Evading") - { - if (mf.incomingThreatVessel != null) - pilotActions[vesselName] = " is evading " + mf.incomingThreatVessel.GetName(); - else - pilotActions[vesselName] = " is taking evasive action"; - } - else if (activity == "Attack") - { - if (mf.currentTarget != null && mf.currentTarget.name != null) - pilotActions[vesselName] = " is attacking " + mf.currentTarget.Vessel.GetName(); - else - pilotActions[vesselName] = " is attacking"; - } - else if (activity == "Ramming Speed!") - { - if (mf.currentTarget != null && mf.currentTarget.name != null) - pilotActions[vesselName] = " is trying to ram " + mf.currentTarget.Vessel.GetName(); - else - pilotActions[vesselName] = " is in ramming speed"; - } - } - - // update the vessel scoring structure - if (vData != null) - { - var partCount = vessel.parts.Count(); - if (BDArmorySettings.RUNWAY_PROJECT) - { - if (partCount != vData.previousPartCount) - { - // part count has changed, check for broken stuff - enforcePartCount(vessel); - } - } - if (vData.previousPartCount < vessel.parts.Count) - vData.lastLostPartTime = now; - vData.previousPartCount = vessel.parts.Count; - - if (vessel.LandedOrSplashed) - { - if (!vData.landedState) - { - // was flying, is now landed - vData.lastLandedTime = now; - vData.landedState = true; - if (vData.landedKillTimer == 0) - { - vData.landedKillTimer = now; - } - } - } - else - { - if (vData.landedState) - { - vData.lastLandedTime = now; - vData.landedState = false; - } - if (vData.landedKillTimer != 0) - { - // safely airborne for 15 seconds - if (now - vData.landedKillTimer > 15) - { - vData.landedKillTimer = 0; - } - } - } - } - - // after this point we're checking things that might result in kills. - if (now - competitionStartTime < BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD) continue; - - // keep track if they're shooting for the GM - if (mf.currentGun != null) - { - if (mf.currentGun.recentlyFiring) - { - // keep track that this aircraft was shooting things - if (vData != null) - { - vData.lastFiredTime = now; - } - if (mf.currentTarget != null && mf.currentTarget.Vessel != null) - { - pilotActions[vesselName] = " is shooting at " + mf.currentTarget.Vessel.GetName(); - } - } - } - // does it have ammunition: no ammo => Disable guard mode - if (!BDArmorySettings.INFINITE_AMMO) - { - if (mf.outOfAmmo && !outOfAmmo.Contains(vesselName)) // Report being out of weapons/ammo once. - { - outOfAmmo.Add(vesselName); - if (vData != null && (now - vData.lastHitTime < 2)) - { - competitionStatus.Add(vesselName + " damaged by " + vData.LastPersonWhoDamagedMe() + " and lost weapons"); - } - else - { - competitionStatus.Add(vesselName + " is out of Ammunition"); - } - } - if (mf.guardMode) // If we're in guard mode, check to see if we should disable it. - { - var pilotAI = vessel.FindPartModuleImplementing(); // Get the pilot AI if the vessel has one. - var surfaceAI = vessel.FindPartModuleImplementing(); // Get the surface AI if the vessel has one. - if ((pilotAI == null && surfaceAI == 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; - } - } - - // update the vessel scoring structure - if (vData != null) - { - vData.AverageSpeed += vessel.srfSpeed; - vData.AverageAltitude += vessel.altitude; - vData.averageCount++; - if (vData.landedState && !BDArmorySettings.DISABLE_KILL_TIMER) - { - KillTimer[vesselName] = (int)(now - vData.landedKillTimer); - if (now - vData.landedKillTimer > BDArmorySettings.COMPETITION_KILL_TIMER) - { - vesselsToKill.Add(mf.vessel); - competitionStatus.Add(vesselName + " landed too long."); - } - } - else if (KillTimer.ContainsKey(vesselName)) - KillTimer.Remove(vesselName); - } - } - } - if (BDArmorySettings.TAG_MODE) - { - foreach (var vesselName in Scores.Keys) - UpdateTag(alive.Contains(vesselName) ? Scores[vesselName].weaponManagerRef : null, vesselName, previousNumberCompetitive); - } - string aliveString = string.Join(",", alive.ToArray()); - previousNumberCompetitive = numberOfCompetitiveVessels; - // Log("[BDACompetitionMode:" + CompetitionID.ToString() + "] STILLALIVE: " + aliveString); // This just fills the logs needlessly. - if (BDArmorySettings.RUNWAY_PROJECT) - { - // If we find a vessel named "Pinata" that's a special case object - // this should probably be configurable. - if (!pinataAlive && alive.Contains("Pinata")) - { - Debug.Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: Setting Pinata Flag to Alive!"); - pinataAlive = true; - competitionStatus.Add("Enabling Pinata"); - } - else if (pinataAlive && !alive.Contains("Pinata")) - { - // switch everyone onto separate teams when the Pinata Dies - LoadedVesselSwitcher.Instance.MassTeamSwitch(); - pinataAlive = false; - competitionStatus.Add("Pinata is dead - competition is now a Free for all"); - // start kill clock - if (!killerGMenabled) - { - // disabled for now, should be in a competition settings UI - //killerGMenabled = true; - } - - } - } - doaUpdate += " DEAD: "; - foreach (string key in Scores.Keys) - { - // check everyone who's no longer alive - if (!alive.Contains(key)) - { - if (BDArmorySettings.RUNWAY_PROJECT && key == "Pinata") continue; - if (!DeathOrder.ContainsKey(key)) - { - // adding pilot into death order - DeathOrder[key] = new Tuple(DeathOrder.Count, now - competitionStartTime); - pilotActions[key] = " is Dead"; - var whoKilledMe = ""; - - DeathCount++; - - if (Scores[key].gmKillReason == GMKillReason.None && now - Scores[key].LastDamageTime() < 10) // Recent kills that weren't instigated by the GM (or similar). - { - // if last hit was recent that person gets the kill - whoKilledMe = Scores[key].LastPersonWhoDamagedMe(); - Scores[key].cleanDeath = true; - - var lastDamageWasFrom = Scores[key].LastDamageWasFrom(); - switch (lastDamageWasFrom) - { - case DamageFrom.Bullet: - if (!whoCleanShotWho.ContainsKey(key)) - { - // twice - so 2 points - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":CLEANKILL:" + whoKilledMe); - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":KILLED:" + whoKilledMe); - whoCleanShotWho.Add(key, whoKilledMe); - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - Competition.BDAScoreService.Instance.TrackKill(whoKilledMe, key); - whoKilledMe += " (BOOM! HEADSHOT!)"; - } - break; - case DamageFrom.Missile: - if (!whoCleanShotWhoWithMissiles.ContainsKey(key)) - { - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":CLEANMISSILEKILL:" + whoKilledMe); - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":KILLED:" + whoKilledMe); - whoCleanShotWhoWithMissiles.Add(key, whoKilledMe); - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - Competition.BDAScoreService.Instance.TrackKill(whoKilledMe, key); - whoKilledMe += " (BOOM! HEADSHOT!)"; - } - break; - case DamageFrom.Ram: - if (!whoCleanRammedWho.ContainsKey(key)) - { - // if ram killed - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":CLEANRAMKILL:" + whoKilledMe); - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":KILLED VIA RAMMERY BY:" + whoKilledMe); - whoCleanRammedWho.Add(key, whoKilledMe); - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - Competition.BDAScoreService.Instance.TrackKill(whoKilledMe, key); - whoKilledMe += " (BOOM! HEADSHOT!)"; - } - break; - default: - break; - } - } - else if (Scores[key].everyoneWhoHitMe.Count > 0 || Scores[key].everyoneWhoRammedMe.Count > 0 || Scores[key].everyoneWhoHitMeWithMissiles.Count > 0) - { - List killReasons = new List(); - if (Scores[key].everyoneWhoHitMe.Count > 0) - killReasons.Add("Hits"); - if (Scores[key].everyoneWhoHitMeWithMissiles.Count > 0) - killReasons.Add("Missiles"); - if (Scores[key].everyoneWhoRammedMe.Count > 0) - killReasons.Add("Rams"); - whoKilledMe = String.Join(" ", killReasons) + ": " + String.Join(", ", Scores[key].EveryOneWhoDamagedMe()) + (Scores[key].gmKillReason != GMKillReason.None ? ", " + Scores[key].gmKillReason : ""); - - foreach (var killer in Scores[key].EveryOneWhoDamagedMe()) - { - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":KILLED:" + killer); - } - if (BDArmorySettings.REMOTE_LOGGING_ENABLED && Scores[key].gmKillReason == GMKillReason.None) // Don't count kills by the GM. - Competition.BDAScoreService.Instance.ComputeAssists(key, "", now - competitionStartTime); - } - if (whoKilledMe != "") - { - switch (Scores[key].LastDamageWasFrom()) - { - case DamageFrom.Bullet: - case DamageFrom.Missile: - competitionStatus.Add(key + " was killed by " + whoKilledMe); - break; - case DamageFrom.Ram: - competitionStatus.Add(key + " was rammed by " + whoKilledMe); - break; - default: - break; - } - } - else - { - competitionStatus.Add(key + " was killed"); - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + key + ":KILLED:NOBODY"); - } - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - Competition.BDAScoreService.Instance.TrackDeath(key); - } - doaUpdate += " :" + key + ": "; - } - } - deadOrAlive = doaUpdate; - - var numberOfCompetitiveTeams = LoadedVesselSwitcher.Instance.weaponManagers.Count; - if (now - competitionStartTime > BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD && (numberOfCompetitiveVessels < 2 || (!BDArmorySettings.TAG_MODE && numberOfCompetitiveTeams < 2)) && !VesselSpawner.Instance.vesselsSpawningContinuously) - { - if (finalGracePeriodStart < 0) - finalGracePeriodStart = now; - if (!(BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD > 60) && now - finalGracePeriodStart > BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD) - { - competitionStatus.Add("All Pilots are Dead"); - foreach (string key in alive) - { - competitionStatus.Add(key + " wins the round!"); - } - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]:No viable competitors, Automatically dumping scores"); - StopCompetition(); - } - } - - //Reset gravity - if (BDArmorySettings.GRAVITY_HACKS && competitionIsActive) - { - int maxVesselsActive = (VesselSpawner.Instance.vesselsSpawningContinuously && BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0) ? BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS : Scores.Count; - double time = now - competitionStartTime; - gravityMultiplier = 1f + 7f * (float)(DeathCount % maxVesselsActive) / (float)(maxVesselsActive - 1); // From 1G to 8G. - gravityMultiplier += VesselSpawner.Instance.vesselsSpawningContinuously ? Mathf.Sqrt(5f - 5f * Mathf.Cos((float)time / 600f * Mathf.PI)) : Mathf.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. - { - lastGravityMultiplier = gravityMultiplier; - competitionStatus.Add("Competition: Adjusting gravity to " + gravityMultiplier.ToString("0.0") + "G!"); - } - } - - // use the exploder system to remove vessels that should be nuked - foreach (var vessel in vesselsToKill) - { - var vesselName = vessel.GetName(); - var killerName = ""; - if (Scores.ContainsKey(vesselName)) - { - killerName = Scores[vesselName].LastPersonWhoDamagedMe(); - if (killerName == "") - { - Scores[vesselName].lastPersonWhoHitMe = "Landed Too Long"; // only do this if it's not already damaged - killerName = "Landed Too Long"; - } - } - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + vesselName + ":REMOVED:" + killerName); - if (KillTimer.ContainsKey(vesselName)) KillTimer.Remove(vesselName); - Misc.Misc.ForceDeadVessel(vessel); - } - - if (!(BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY > 60)) - RemoveNonCompetitors(); - - if (BDArmorySettings.RUNWAY_PROJECT) - { - if (BDArmorySettings.COMPETITION_KILLER_GM_MAX_ALTITUDE < 101) // Kill off those flying too high. - { - foreach (var weaponManager in LoadedVesselSwitcher.Instance.weaponManagers.SelectMany(tm => tm.Value).ToList()) - { - if (alive.Contains(weaponManager.vessel.vesselName) && weaponManager.vessel.altitude > BDArmorySettings.COMPETITION_KILLER_GM_MAX_ALTITUDE * 1000) - { - Scores[weaponManager.vessel.vesselName].gmKillReason = GMKillReason.GM; - var killerName = Scores[weaponManager.vessel.vesselName].lastPersonWhoHitMe; - if (killerName == "") - { - killerName = "Flew too high!"; - Scores[weaponManager.vessel.vesselName].lastPersonWhoHitMe = killerName; - } - competitionStatus.Add(weaponManager.vessel.vesselName + " flew too high!"); - Log("[BDACompetitionMode:" + CompetitionID.ToString() + "]: " + weaponManager.vessel.vesselName + ":REMOVED:" + killerName); - if (KillTimer.ContainsKey(weaponManager.vessel.vesselName)) KillTimer.Remove(weaponManager.vessel.vesselName); - Misc.Misc.ForceDeadVessel(weaponManager.vessel); - } - } - } - FindVictim(); - } - // Debug.Log("[BDACompetitionMode" + CompetitionID.ToString() + "]: Done With Update"); - if (BDArmorySettings.TAG_MODE) lastTagUpdateTime = now; - - if (!VesselSpawner.Instance.vesselsSpawningContinuously && BDArmorySettings.COMPETITION_DURATION > 0 && now - competitionStartTime >= BDArmorySettings.COMPETITION_DURATION * 60d) - { - LogResults("due to out-of-time"); - StopCompetition(); - } - } - - // This now also writes the competition logs to GameData/BDArmory/Logs/[-tag].log - public void LogResults(string message = "", string tag = "") - { - if (competitionStartTime < 0) - { - Debug.Log("[BDArmoryCompetition]: No active competition, not dumping results."); - return; - } - CheckMemoryUsage(); - if (VesselSpawner.Instance.vesselsSpawningContinuously) // Dump continuous spawning scores instead. - { - VesselSpawner.Instance.DumpContinuousSpawningScores(tag); - return; - } - - - var logStrings = new List(); - - // get everyone who's still alive - alive.Clear(); - competitionStatus.Add("Dumping scores for competition " + CompetitionID.ToString() + (tag != "" ? " " + tag : "")); - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: Dumping Results" + (message != "" ? " " + message : "") + " after " + (int)(Planetarium.GetUniversalTime() - competitionStartTime) + "s at " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss zzz")); - - - using (var v = FlightGlobals.Vessels.GetEnumerator()) - while (v.MoveNext()) - { - if (v.Current == null || !v.Current.loaded || v.Current.packed) - continue; - using (var wms = v.Current.FindPartModulesImplementing().GetEnumerator()) - while (wms.MoveNext()) - if (wms.Current != null) - { - if (wms.Current.vessel != null) - { - alive.Add(wms.Current.vessel.GetName()); - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: ALIVE:" + wms.Current.vessel.GetName()); - } - break; - } - } - - // find out who's still alive - foreach (string key in Scores.Keys) - { - if (!alive.Contains(key)) - if (DeathOrder.ContainsKey(key)) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: DEAD:" + DeathOrder[key].Item1 + ":" + DeathOrder[key].Item2.ToString("0.0") + ":" + key); // DEAD: :: - else - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: MIA:" + key); - } - - // Who shot who. - foreach (var key in Scores.Keys) - if (Scores[key].hitCounts.Count > 0) - { - string whoShotMe = "[BDArmoryCompetition:" + CompetitionID.ToString() + "]: WHOSHOTWHO:" + key; - foreach (var vesselName in Scores[key].hitCounts.Keys) - whoShotMe += ":" + Scores[key].hitCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoShotMe); - } - - // Damage from bullets - foreach (var key in Scores.Keys) - if (Scores[key].damageFromBullets.Count > 0) - { - string whoDamagedMeWithBullets = "[BDArmoryCompetition:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHBULLETS:" + key; - foreach (var vesselName in Scores[key].damageFromBullets.Keys) - whoDamagedMeWithBullets += ":" + Scores[key].damageFromBullets[vesselName].ToString("0.0") + ":" + vesselName; - logStrings.Add(whoDamagedMeWithBullets); - } - - // Who shot who with missiles. - foreach (var key in Scores.Keys) - if (Scores[key].missilePartDamageCounts.Count > 0) - { - string whoShotMeWithMissiles = "[BDArmoryCompetition:" + CompetitionID.ToString() + "]: WHOSHOTWHOWITHMISSILES:" + key; - foreach (var vesselName in Scores[key].missilePartDamageCounts.Keys) - whoShotMeWithMissiles += ":" + Scores[key].missilePartDamageCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoShotMeWithMissiles); - } - - // Damage from missiles - foreach (var key in Scores.Keys) - if (Scores[key].damageFromMissiles.Count > 0) - { - string whoDamagedMeWithMissiles = "[BDArmoryCompetition:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHMISSILES:" + key; - foreach (var vesselName in Scores[key].damageFromMissiles.Keys) - whoDamagedMeWithMissiles += ":" + Scores[key].damageFromMissiles[vesselName].ToString("0.0") + ":" + vesselName; - logStrings.Add(whoDamagedMeWithMissiles); - } - - // Who rammed who. - foreach (var key in Scores.Keys) - if (Scores[key].rammingPartLossCounts.Count > 0) - { - string whoRammedMe = "[BDArmoryCompetition:" + CompetitionID.ToString() + "]: WHORAMMEDWHO:" + key; - foreach (var vesselName in Scores[key].rammingPartLossCounts.Keys) - whoRammedMe += ":" + Scores[key].rammingPartLossCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoRammedMe); - } - - // Other kill reasons - foreach (var key in Scores.Keys) - if (Scores[key].gmKillReason != GMKillReason.None) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: OTHERKILL:" + key + ":" + Scores[key].gmKillReason); - - // Log clean kills/rams - foreach (var key in whoCleanShotWho.Keys) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: CLEANKILL:" + key + ":" + whoCleanShotWho[key]); - foreach (var key in whoCleanShotWhoWithMissiles.Keys) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: CLEANMISSILEKILL:" + key + ":" + whoCleanShotWhoWithMissiles[key]); - foreach (var key in whoCleanRammedWho.Keys) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: CLEANRAM:" + key + ":" + whoCleanRammedWho[key]); - - // Accuracy - foreach (var key in Scores.Keys) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: ACCURACY:" + key + ":" + Scores[key].Score + "/" + Scores[key].shotsFired); - - // Time "IT" and kills while "IT" logging - if (BDArmorySettings.TAG_MODE) - { - foreach (var key in Scores.Keys) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: TAGSCORE:" + key + ":" + Scores[key].tagScore.ToString("0.0")); - - foreach (var key in Scores.Keys) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: TIMEIT:" + key + ":" + Scores[key].tagTotalTime.ToString("0.0")); - - foreach (var key in Scores.Keys) - if (Scores[key].tagKillsWhileIt > 0) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: KILLSWHILEIT:" + key + ":" + Scores[key].tagKillsWhileIt); - - foreach (var key in Scores.Keys) - if (Scores[key].tagTimesIt > 0) - logStrings.Add("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: TIMESIT:" + key + ":" + Scores[key].tagTimesIt); - } - - // Dump the log results to a file - if (CompetitionID > 0) - { - var folder = Environment.CurrentDirectory + "/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); - File.WriteAllLines(Path.Combine(folder, CompetitionID.ToString() + (tag != "" ? "-" + tag : "") + ".log"), logStrings); - } - // Also dump the results to the normal log. - foreach (var line in logStrings) - Log(line); - } - - #region Ramming - // Ramming Logging - public class RammingTargetInformation - { - public Vessel vessel; // The other vessel involved in a collision. - public double lastUpdateTime = 0; // Last time the timeToCPA was updated. - public float timeToCPA = 0f; // Time to closest point of approach. - public bool potentialCollision = false; // Whether a collision might happen shortly. - public double potentialCollisionDetectionTime = 0; // The latest time the potential collision was detected. - public int partCountJustPriorToCollision; // The part count of the colliding vessel just prior to the collision. - public float sqrDistance; // Distance^2 at the time of collision. - public float angleToCoM = 0f; // The angle from a vessel's velocity direction to the center of mass of the target. - public bool collisionDetected = false; // Whether a collision has actually been detected. - public double collisionDetectedTime; // The time that the collision actually occurs. - public bool ramming = false; // True if a ram was attempted between the detection of a potential ram and the actual collision. - }; - public class RammingInformation - { - public Vessel vessel; // This vessel. - public string vesselName; // The GetName() name of the vessel (in case vessel gets destroyed and we can't get it from there). - public int partCount; // The part count of a vessel. - public float radius; // The vessels "radius" at the time the potential collision was detected. - public Dictionary targetInformation; // Information about the ramming target. - }; - public Dictionary rammingInformation; - - // Initialise the rammingInformation dictionary with the required vessels. - public void InitialiseRammingInformation() - { - double currentTime = Planetarium.GetUniversalTime(); - rammingInformation = new Dictionary(); - var pilots = getAllPilots(); - foreach (var pilot in pilots) - { - var pilotAI = pilot.vessel.FindPartModuleImplementing(); // Get the pilot AI if the vessel has one. - if (pilotAI == null) continue; - var targetRammingInformation = new Dictionary(); - foreach (var otherPilot in pilots) - { - if (otherPilot == pilot) continue; // Don't include same-vessel information. - var otherPilotAI = otherPilot.vessel.FindPartModuleImplementing(); // Get the pilot AI if the vessel has one. - if (otherPilotAI == null) continue; - targetRammingInformation.Add(otherPilot.vessel.vesselName, new RammingTargetInformation { vessel = otherPilot.vessel }); - } - rammingInformation.Add(pilot.vessel.vesselName, new RammingInformation - { - vessel = pilot.vessel, - vesselName = pilot.vessel.GetName(), - partCount = pilot.vessel.parts.Count, - radius = GetRadius(pilot.vessel), - targetInformation = targetRammingInformation, - }); - } - } - - // Update the ramming information dictionary with expected times to closest point of approach. - private float maxTimeToCPA = 5f; // Don't look more than 5s ahead. - public void UpdateTimesToCPAs() - { - double currentTime = Planetarium.GetUniversalTime(); - foreach (var vesselName in rammingInformation.Keys) - { - var vessel = rammingInformation[vesselName].vessel; - var pilotAI = vessel?.FindPartModuleImplementing(); // Get the pilot AI if the vessel has one. - - foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) - { - var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; - var otherPilotAI = otherVessel?.FindPartModuleImplementing(); // Get the pilot AI if the vessel has one. - if (pilotAI == null || otherPilotAI == null) // One of the vessels or pilot AIs has been destroyed. - { - rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA = maxTimeToCPA; // Set the timeToCPA to maxTimeToCPA, so that it's not considered for new potential collisions. - rammingInformation[otherVesselName].targetInformation[vesselName].timeToCPA = maxTimeToCPA; // Set the timeToCPA to maxTimeToCPA, so that it's not considered for new potential collisions. - } - else - { - 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. - 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. - rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA = maxTimeToCPA; - // This is symmetric, so update the symmetric value and set the lastUpdateTime for both so that we don't bother calculating the same thing twice. - rammingInformation[otherVesselName].targetInformation[vesselName].timeToCPA = rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA; - rammingInformation[vesselName].targetInformation[otherVesselName].lastUpdateTime = currentTime; - rammingInformation[otherVesselName].targetInformation[vesselName].lastUpdateTime = currentTime; - } - } - } - } - } - - // Get the upcoming collisions ordered by predicted separation^2 (for Scott to adjust camera views). - public IOrderedEnumerable, Tuple>> UpcomingCollisions(float distanceThreshold, bool sortByDistance = true) - { - var upcomingCollisions = new Dictionary, Tuple>(); - if (rammingInformation != null) - foreach (var vesselName in rammingInformation.Keys) - foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) - if (rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision && rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA < maxTimeToCPA && String.Compare(vesselName, otherVesselName) < 0) - if (rammingInformation[vesselName].vessel != null && rammingInformation[otherVesselName].vessel != null) - { - var predictedSqrSeparation = Vector3.SqrMagnitude(rammingInformation[vesselName].vessel.CoM - rammingInformation[otherVesselName].vessel.CoM); - if (predictedSqrSeparation < distanceThreshold * distanceThreshold) - upcomingCollisions.Add( - new Tuple(vesselName, otherVesselName), - new Tuple(predictedSqrSeparation, rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA) - ); - } - return upcomingCollisions.OrderBy(d => sortByDistance ? d.Value.Item1 : d.Value.Item2); - } - - // Check for potential collisions in the near future and update data structures as necessary. - private float potentialCollisionDetectionTime = 1f; // 1s ought to be plenty. - private void CheckForPotentialCollisions() - { - float collisionMargin = 4f; // Sum of radii is less than this factor times the separation. - double currentTime = Planetarium.GetUniversalTime(); - foreach (var vesselName in rammingInformation.Keys) - { - var vessel = rammingInformation[vesselName].vessel; - foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) - { - if (!rammingInformation.ContainsKey(otherVesselName)) - { - Debug.Log("DEBUG 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!"); - return; - } - if (!rammingInformation[otherVesselName].targetInformation.ContainsKey(vesselName)) - { - Debug.Log("DEBUG vessel (" + vesselName + ") is missing from rammingInformation[otherVessel].targetInformation!"); - return; - } - var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; - if (rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA < potentialCollisionDetectionTime) // Closest point of approach is within the detection time. - { - if (vessel != null && otherVessel != null) // If one of the vessels has been destroyed, don't calculate new potential collisions, but allow the timer on existing potential collisions to run out so that collision analysis can still use it. - { - var separation = Vector3.Magnitude(vessel.transform.position - otherVessel.transform.position); - if (separation < collisionMargin * (GetRadius(vessel) + GetRadius(otherVessel))) // Potential collision detected. - { - if (!rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision) // Register the part counts and angles when the potential collision is first detected. - { // Note: part counts and vessel radii get updated whenever a new potential collision is detected, but not angleToCoM (which is specific to a colliding pair). - rammingInformation[vesselName].partCount = vessel.parts.Count; - rammingInformation[otherVesselName].partCount = otherVessel.parts.Count; - rammingInformation[vesselName].radius = GetRadius(vessel); - rammingInformation[otherVesselName].radius = GetRadius(otherVessel); - rammingInformation[vesselName].targetInformation[otherVesselName].angleToCoM = Vector3.Angle(vessel.srf_vel_direction, otherVessel.CoM - vessel.CoM); - rammingInformation[otherVesselName].targetInformation[vesselName].angleToCoM = Vector3.Angle(otherVessel.srf_vel_direction, vessel.CoM - otherVessel.CoM); - } - - // Update part counts if vessels get shot and potentially lose parts before the collision happens. - if (Scores[rammingInformation[vesselName].vesselName].lastHitTime > rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollisionDetectionTime) - if (rammingInformation[vesselName].partCount != vessel.parts.Count) - { - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] " + vesselName + " lost " + (rammingInformation[vesselName].partCount - vessel.parts.Count) + " parts from getting shot."); - rammingInformation[vesselName].partCount = vessel.parts.Count; - } - if (Scores[rammingInformation[otherVesselName].vesselName].lastHitTime > rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime) - if (rammingInformation[vesselName].partCount != vessel.parts.Count) - { - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] " + otherVesselName + " lost " + (rammingInformation[otherVesselName].partCount - otherVessel.parts.Count) + " parts from getting shot."); - rammingInformation[otherVesselName].partCount = otherVessel.parts.Count; - } - - // Set the potentialCollision flag to true and update the latest potential collision detection time. - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision = true; - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime = currentTime; - rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollision = true; - rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollisionDetectionTime = currentTime; - - // Register intent to ram. - var pilotAI = vessel.FindPartModuleImplementing(); - rammingInformation[vesselName].targetInformation[otherVesselName].ramming |= (pilotAI != null && pilotAI.ramming); // Pilot AI is alive and trying to ram. - var otherPilotAI = otherVessel.FindPartModuleImplementing(); - rammingInformation[otherVesselName].targetInformation[vesselName].ramming |= (otherPilotAI != null && otherPilotAI.ramming); // Other pilot AI is alive and trying to ram. - } - } - } - else if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime > 2f * potentialCollisionDetectionTime) // Potential collision is no longer relevant. - { - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision = false; - rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollision = false; - } - } - } - } - - // Get a vessel's "radius". - public static float GetRadius(Vessel v) - { - //get vessel size - Vector3 size = v.vesselSize; - - //get largest dimension - float radius; - - if (size.x > size.y && size.x > size.z) - { - radius = size.x / 2; - } - else if (size.y > size.x && size.y > size.z) - { - radius = size.y / 2; - } - else if (size.z > size.x && size.z > size.y) - { - radius = size.z / 2; - } - else - { - radius = size.x / 2; - } - - return radius; - } - - // Analyse a collision to figure out if someone rammed someone else and who should get awarded for it. - private void AnalyseCollision(EventReport data) - { - if (data.origin == null) return; // The part is gone. Nothing much we can do about it. - double currentTime = Planetarium.GetUniversalTime(); - float collisionMargin = 2f; // Compare the separation to this factor times the sum of radii to account for inaccuracies in the vessels size and position. Hopefully, this won't include other nearby vessels. - var vessel = data.origin.vessel; - if (vessel == null) // Can vessel be null here? It doesn't appear so. - { - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] in AnalyseCollision the colliding part belonged to a null vessel!"); - return; - } - 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 (!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; - 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.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] Collision detected between " + vesselName + " and " + otherVesselName); - } - 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. - { - rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = true; // register it as involved in the collision. We'll check for damaged parts in CheckForDamagedParts. - hitVessel = true; - } - } - if (!hitVessel) // We didn't hit another vessel, maybe it crashed and died. - { - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] " + vesselName + " hit something else."); - foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) - { - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision = false; // Set potential collisions to false. - rammingInformation[otherVesselName].targetInformation[vesselName].potentialCollision = false; // Set potential collisions to false. - } - } - } - } - - // Check for parts being lost on the various vessels for which collisions have been detected. - private void CheckForDamagedParts() - { - double currentTime = Planetarium.GetUniversalTime(); - float headOnLimit = 20f; - var collidingVesselsBySeparation = new Dictionary>>>(); - - // First, we're going to filter the potentially colliding vessels and sort them by separation. - foreach (var vesselName in rammingInformation.Keys) - { - var vessel = rammingInformation[vesselName].vessel; - var collidingVesselDistances = new Dictionary(); - - // For each potential collision that we've waited long enough for, refine the potential colliding vessels. - foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) - { - if (!rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. - if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime > potentialCollisionDetectionTime) // We've waited long enough for the parts that are going to explode to explode. - { - // First, check the vessels marked as colliding with this vessel for lost parts. If at least one other lost parts or was destroyed, exclude any that didn't lose parts (set collisionDetected to false). - bool someOneElseLostParts = false; - foreach (var tmpVesselName in rammingInformation[vesselName].targetInformation.Keys) - { - if (!rammingInformation[vesselName].targetInformation[tmpVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. - var tmpVessel = rammingInformation[vesselName].targetInformation[tmpVesselName].vessel; - if (tmpVessel == null || rammingInformation[vesselName].targetInformation[tmpVesselName].partCountJustPriorToCollision - tmpVessel.parts.Count > 0) - { - someOneElseLostParts = true; - break; - } - } - if (someOneElseLostParts) // At least one other vessel lost parts or was destroyed. - { - foreach (var tmpVesselName in rammingInformation[vesselName].targetInformation.Keys) - { - if (!rammingInformation[vesselName].targetInformation[tmpVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. - var tmpVessel = rammingInformation[vesselName].targetInformation[tmpVesselName].vessel; - if (tmpVessel != null && rammingInformation[vesselName].targetInformation[tmpVesselName].partCountJustPriorToCollision == tmpVessel.parts.Count) // Other vessel didn't lose parts, mark it as not involved in this collision. - { - rammingInformation[vesselName].targetInformation[tmpVesselName].collisionDetected = false; - rammingInformation[tmpVesselName].targetInformation[vesselName].collisionDetected = false; - } - } - } // Else, the collided with vessels didn't lose any parts, so we don't know who this vessel really collided with. - - // If the other vessel is still a potential collider, add it to the colliding vessels dictionary with its distance to this vessel. - if (rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) - collidingVesselDistances.Add(otherVesselName, rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance); - } - } - - // If multiple vessels are involved in a collision with this vessel, the lost parts counts are going to be skewed towards the first vessel processed. To counteract this, we'll sort the colliding vessels by their distance from this vessel. - var collidingVessels = collidingVesselDistances.OrderBy(d => d.Value); - if (collidingVesselDistances.Count > 0) - collidingVesselsBySeparation.Add(vesselName, new KeyValuePair>>(collidingVessels.First().Value, collidingVessels)); - - if (BDArmorySettings.DEBUG_RAMMING_LOGGING && collidingVesselDistances.Count > 1) // DEBUG - { - foreach (var otherVesselName in collidingVesselDistances.Keys) Debug.Log("[Ram logging] colliding vessel distances^2 from " + vesselName + ": " + otherVesselName + " " + collidingVesselDistances[otherVesselName]); - foreach (var otherVesselName in collidingVessels) Debug.Log("[Ram logging] sorted order: " + otherVesselName.Key); - } - } - var sortedCollidingVesselsBySeparation = collidingVesselsBySeparation.OrderBy(d => d.Value.Key); // Sort the outer dictionary by minimum separation from the nearest colliding vessel. - - // Then we're going to try to figure out who should be awarded the ram. - foreach (var vesselNameKVP in sortedCollidingVesselsBySeparation) - { - var vesselName = vesselNameKVP.Key; - var vessel = rammingInformation[vesselName].vessel; - foreach (var otherVesselNameKVP in vesselNameKVP.Value.Value) - { - var otherVesselName = otherVesselNameKVP.Key; - if (!rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) continue; // Other vessel wasn't involved in a collision with this vessel. - if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollisionDetectionTime > potentialCollisionDetectionTime) // We've waited long enough for the parts that are going to explode to explode. - { - var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; - var pilotAI = vessel?.FindPartModuleImplementing(); - var otherPilotAI = otherVessel?.FindPartModuleImplementing(); - - // 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; - 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. - foreach (var tmpVesselName in rammingInformation[vesselName].targetInformation.Keys) - if (rammingInformation[tmpVesselName].targetInformation[vesselName].collisionDetected) - rammingInformation[tmpVesselName].targetInformation[vesselName].partCountJustPriorToCollision = rammingInformation[vesselName].partCount; - foreach (var tmpVesselName in rammingInformation[otherVesselName].targetInformation.Keys) - if (rammingInformation[tmpVesselName].targetInformation[otherVesselName].collisionDetected) - rammingInformation[tmpVesselName].targetInformation[otherVesselName].partCountJustPriorToCollision = rammingInformation[otherVesselName].partCount; - - // Figure out who should be awarded the ram. - var rammingVessel = rammingInformation[vesselName].vesselName; - var rammedVessel = rammingInformation[otherVesselName].vesselName; - var headOn = false; - if (rammingInformation[vesselName].targetInformation[otherVesselName].ramming ^ rammingInformation[otherVesselName].targetInformation[vesselName].ramming) // Only one of the vessels was ramming. - { - if (!rammingInformation[vesselName].targetInformation[otherVesselName].ramming) // Switch who rammed who if the default is backwards. - { - rammingVessel = rammingInformation[otherVesselName].vesselName; - rammedVessel = rammingInformation[vesselName].vesselName; - var tmp = rammingPartsLost; - rammingPartsLost = rammedPartsLost; - rammedPartsLost = tmp; - } - } - else // Both or neither of the vessels were ramming. - { - if (rammingInformation[vesselName].targetInformation[otherVesselName].angleToCoM < headOnLimit && rammingInformation[otherVesselName].targetInformation[vesselName].angleToCoM < headOnLimit) // Head-on collision detected, both get awarded with ramming the other. - { - headOn = true; - } - else - { - if (rammingInformation[vesselName].targetInformation[otherVesselName].angleToCoM > rammingInformation[otherVesselName].targetInformation[vesselName].angleToCoM) // Other vessel had a better angleToCoM, so switch who rammed who. - { - rammingVessel = rammingInformation[otherVesselName].vesselName; - rammedVessel = rammingInformation[vesselName].vesselName; - var tmp = rammingPartsLost; - rammingPartsLost = rammedPartsLost; - rammedPartsLost = tmp; - } - } - } - - LogRammingVesselScore(rammingVessel, rammedVessel, rammedPartsLost, rammingPartsLost, headOn, true, true, rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetectedTime); // Log the ram. - - // Set the collisionDetected flag to false, since we've now logged this collision. We set both so that the collision only gets logged once. - rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = false; - rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetected = false; - } - } - } - } - - // Actually log the ram to various places. Note: vesselName and targetVesselName need to be those returned by the GetName() function to match the keys in Scores. - public void LogRammingVesselScore(string rammingVesselName, string rammedVesselName, int rammedPartsLost, int rammingPartsLost, bool headOn, bool logToCompetitionStatus, bool logToDebug, double timeOfCollision) - { - if (logToCompetitionStatus) - { - if (!headOn) - competitionStatus.Add(rammedVesselName + " got RAMMED by " + rammingVesselName + " and lost " + rammedPartsLost + " parts (" + rammingVesselName + " lost " + rammingPartsLost + " parts)."); - else - competitionStatus.Add(rammedVesselName + " and " + rammingVesselName + " RAMMED each other and lost " + rammedPartsLost + " and " + rammingPartsLost + " parts, respectively."); - } - if (logToDebug) - { - if (!headOn) - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: " + rammedVesselName + " got RAMMED by " + rammingVesselName + " and lost " + rammedPartsLost + " parts (" + rammingVesselName + " lost " + rammingPartsLost + " parts)."); - else - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: " + rammedVesselName + " and " + rammingVesselName + " RAMMED each other and lost " + rammedPartsLost + " and " + rammingPartsLost + " parts, respectively."); - } - - // Log score information for the ramming vessel. - LogRammingToScoreData(rammingVesselName, rammedVesselName, timeOfCollision, rammedPartsLost); - // If it was a head-on, log scores for the rammed vessel too. - if (headOn) LogRammingToScoreData(rammedVesselName, rammingVesselName, timeOfCollision, rammingPartsLost); - } - - // Write ramming information to the Scores dictionary. - private void LogRammingToScoreData(string rammingVesselName, string rammedVesselName, double timeOfCollision, int partsLost) - { - // Log attributes for the ramming vessel. - if (!Scores.ContainsKey(rammingVesselName)) - { - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "] Scores does not contain the key " + rammingVesselName); - return; - } - var vData = Scores[rammingVesselName]; - vData.totalDamagedPartsDueToRamming += partsLost; - var key = rammingVesselName + ":" + rammedVesselName; - - // Log attributes for the rammed vessel. - if (!Scores.ContainsKey(rammedVesselName)) - { - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "] Scores does not contain the key " + rammedVesselName); - return; - } - var tData = Scores[rammedVesselName]; - tData.lastRammedTime = timeOfCollision; - tData.lastPersonWhoRammedMe = rammingVesselName; - tData.everyoneWhoRammedMe.Add(rammingVesselName); - tData.everyoneWhoDamagedMe.Add(rammingVesselName); - if (tData.rammingPartLossCounts.ContainsKey(rammingVesselName)) - tData.rammingPartLossCounts[rammingVesselName] += partsLost; - else - tData.rammingPartLossCounts.Add(rammingVesselName, partsLost); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - BDAScoreService.Instance.TrackRammedParts(rammingVesselName, rammedVesselName, partsLost); - } - - Dictionary partsCheck; - void CheckForMissingParts() - { - if (partsCheck == null) - { - partsCheck = new Dictionary(); - foreach (var vesselName in rammingInformation.Keys) - { - partsCheck.Add(vesselName, rammingInformation[vesselName].vessel.parts.Count); - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] " + vesselName + " started with " + partsCheck[vesselName] + " parts."); - } - } - foreach (var vesselName in rammingInformation.Keys) - { - var vessel = rammingInformation[vesselName].vessel; - if (vessel != null) - { - if (partsCheck[vesselName] != vessel.parts.Count) - { - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) Debug.Log("[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.DEBUG_RAMMING_LOGGING) Debug.Log("[Ram logging] Parts Check: " + vesselName + " has been destroyed, losing " + partsCheck[vesselName] + " parts."); - partsCheck[vesselName] = 0; - } - } - } - - // Main calling function to control ramming logging. - private void LogRamming() - { - if (!competitionIsActive) return; - if (rammingInformation == null) InitialiseRammingInformation(); - UpdateTimesToCPAs(); - CheckForPotentialCollisions(); - CheckForDamagedParts(); - if (BDArmorySettings.DEBUG_RAMMING_LOGGING) CheckForMissingParts(); // DEBUG - } - #endregion - - #region Tag - public double lastTagUpdateTime; - // Function to update tag - private void UpdateTag(MissileFire mf, string key, int previousNumberCompetitive) - { - var updateTickLength = Planetarium.GetUniversalTime() - lastTagUpdateTime; - var vData = Scores[key]; - if (alive.Contains(key)) // Vessel that is being updated is alive - { - // Update tag mode scoring - if ((mf.Team.Name == "IT") && (previousNumberCompetitive > 1) && (!vData.landedState)) // Don't keep increasing score if we're the only ones left or we're landed - { - vData.tagTotalTime += updateTickLength; - vData.tagScore += updateTickLength * previousNumberCompetitive * (previousNumberCompetitive - 1) / 5; // Rewards craft accruing time with more competitors - } - else if ((vData.tagIsIt) && (previousNumberCompetitive > 1) && (!vData.landedState)) // We need this in case the person who was "IT" died before the updating code ran - { - mf.SetTeam(BDTeam.Get("IT")); - mf.vessel.ActionGroups.ToggleGroup(KM_dictAG[8]); // Trigger AG8 on becoming "IT" - vData.tagTotalTime += updateTickLength; - vData.tagScore += updateTickLength * previousNumberCompetitive * (previousNumberCompetitive - 1) / 5; - } - - // If a vessel is NOT IT, make sure it's on the right team (this is important for continuous spawning - if ((!startTag) && (!vData.tagIsIt) && (mf.Team.Name != "NO")) - mf.SetTeam(BDTeam.Get("NO")); - - // Update Tag Mode! If we're IT (or no one is IT yet) and we get hit, change everyone's teams and update the scoring - double lastDamageTime = vData.LastDamageTime(); - // Be a little more lenient on detecting damage that didn't occur within the last update tick once tag has started, sometimes the update ticks take longer and damage isn't detected otherwise - if (((startTag) && (Planetarium.GetUniversalTime() - lastDamageTime <= updateTickLength)) || ((vData.tagIsIt) && (Planetarium.GetUniversalTime() - lastDamageTime <= (updateTickLength * 5)))) - { - // We've started tag, we don't need the entry condition boolean anymore - if (startTag) - startTag = false; - - // Update teams - var pilots = getAllPilots(); - if (pilots.All(p => p.vessel.GetName() != vData.LastPersonWhoDamagedMe())) // IT was killed off by GM or BRB. - TagResetTeams(); - else - { - foreach (var pilot in pilots) - { - if (!Scores.ContainsKey(pilot.vessel.GetName())) { Debug.Log("DEBUG 1 Scores doesn't contain " + pilot.vessel.GetName()); continue; } // How can this happen? This occurred for a vessel that got labelled as a Rover or Debris! Check that the vessel has the mf attached to the cockpit (e.g. JohnF's plane). - if (pilot.vessel.GetName() == vData.LastPersonWhoDamagedMe()) // Set the person who scored hits as "IT" - { - if (pilot.vessel.GetName() == key) Debug.Log("DEBUG " + key + " tagged themself with " + vData.LastDamageWasFrom() + " at " + vData.LastDamageTime().ToString("G1") + "!"); - competitionStatus.Add(pilot.vessel.GetDisplayName() + " is IT!"); - pilot.weaponManager.SetTeam(BDTeam.Get("IT")); - Scores[pilot.vessel.GetName()].tagIsIt = true; - Scores[pilot.vessel.GetName()].tagTimesIt++; - pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[8]); // Trigger AG8 on becoming "IT" - Scores[pilot.vessel.GetName()].tagTotalTime += Math.Min(Planetarium.GetUniversalTime() - lastDamageTime, updateTickLength); - Scores[pilot.vessel.GetName()].tagScore += Math.Min(Planetarium.GetUniversalTime() - lastDamageTime, updateTickLength) - * previousNumberCompetitive * (previousNumberCompetitive - 1) / 5; - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: " + pilot.vessel.GetDisplayName() + " is IT!"); - } - else // Everyone else is "NOT IT" - { - pilot.weaponManager.SetTeam(BDTeam.Get("NO")); - Scores[pilot.vessel.GetName()].tagIsIt = false; - pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[9]); // Trigger AG9 on becoming "NOT IT" - if (BDArmorySettings.DRAW_DEBUG_LABELS) Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: " + pilot.vessel.GetDisplayName() + " is NOT IT!"); - } - } - foreach (var pilot in pilots) - pilot.weaponManager.ForceScan(); // Update targets. - } - } - } - else // Vessel that is being updated is dead - { - // If the player who was "IT" died declare a new "IT" player - if (Scores[key].tagIsIt) - { - Scores[key].tagIsIt = false; - var tagKillerIs = Scores[key].LastPersonWhoDamagedMe(); - if ((Scores.ContainsKey(tagKillerIs)) && (tagKillerIs != "") && (alive.Contains(tagKillerIs))) // We have a killer who is alive - { - if (tagKillerIs == key) Debug.Log("DEBUG " + tagKillerIs + " tagged themself to death with " + vData.LastDamageWasFrom() + " at " + vData.LastDamageTime().ToString("G1") + "!"); - Scores[tagKillerIs].tagIsIt = true; - Scores[tagKillerIs].tagTimesIt++; - Scores[tagKillerIs].tagTotalTime += Math.Min(Planetarium.GetUniversalTime() - Scores[key].LastDamageTime(), updateTickLength); - Scores[tagKillerIs].tagScore += Math.Min(Planetarium.GetUniversalTime() - Scores[key].LastDamageTime(), updateTickLength) - * previousNumberCompetitive * (previousNumberCompetitive - 1) / 5; - Log("[BDArmoryCompetition:" + CompetitionID.ToString() + "]: " + key + " died, " + tagKillerIs + " is IT!"); // FIXME, killing the IT craft with the GM/BRB breaks this. - competitionStatus.Add(tagKillerIs + " is IT!"); - foreach (var pilot in getAllPilots()) - pilot.weaponManager.ForceScan(); // Update targets. - } - else // We don't have a killer who is alive, reset teams - TagResetTeams(); - } - else - { - if (Scores.ContainsKey(Scores[key].LastPersonWhoDamagedMe()) && Scores[Scores[key].LastPersonWhoDamagedMe()].tagIsIt) // "IT" player got a kill, let's log it - { - Scores[Scores[key].LastPersonWhoDamagedMe()].tagKillsWhileIt++; - } - } - } - } - - void TagResetTeams() - { - char T = 'A'; - var pilots = getAllPilots(); - foreach (var pilot in pilots) - { - if (!Scores.ContainsKey(pilot.vessel.GetName())) { Debug.Log("DEBUG 2 Scores doesn't contain " + pilot.vessel.GetName()); continue; } - pilot.weaponManager.SetTeam(BDTeam.Get(T.ToString())); - Scores[pilot.vessel.GetName()].tagIsIt = false; - pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[9]); // Trigger AG9 on becoming "NOT IT" - T++; - } - foreach (var pilot in pilots) - pilot.weaponManager.ForceScan(); // Update targets. - startTag = true; - } - #endregion - - // A filter for log messages so Scott can do other stuff depending on the content. - public void Log(string message) - { - // Filter stuff based on the message, then log it to the debug log. - Debug.Log(message); - } - - public void CheckMemoryUsage() // DEBUG - { - List strings = new List(); - strings.Add("System memory: " + SystemInfo.systemMemorySize + "MB"); - strings.Add("Reserved: " + UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong() / 1024 / 1024 + "MB"); - strings.Add("Allocated: " + UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024 / 1024 + "MB"); - strings.Add("Mono heap: " + UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong() / 1024 / 1024 + "MB"); - strings.Add("Mono used: " + UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024 / 1024 + "MB"); - strings.Add("plus unspecified runtime (native) memory."); - Debug.Log("DEBUG Memory Usage: " + string.Join(", ", strings)); - } - - public void CheckNumbersOfThings() // DEBUG - { - List strings = new List(); - strings.Add("FlightGlobals.Vessels: " + FlightGlobals.Vessels.Count); - strings.Add("Non-competitors to remove: " + nonCompetitorsToRemove.Count); - strings.Add("EffectBehaviour: " + EffectBehaviour.FindObjectsOfType().Length); - strings.Add("EffectBehaviour: " + EffectBehaviour.FindObjectsOfType().Length); - strings.Add("KSPParticleEmitters: " + FindObjectsOfType().Length); - strings.Add("KSPParticleEmitters including inactive: " + Resources.FindObjectsOfTypeAll(typeof(KSPParticleEmitter)).Length); - Debug.Log("DEBUG " + string.Join(", ", strings)); - Dictionary emitterNames = new Dictionary(); - foreach (var pe in Resources.FindObjectsOfTypeAll(typeof(KSPParticleEmitter)).Cast()) - { - if (!pe.isActiveAndEnabled) - { - if (emitterNames.ContainsKey(pe.gameObject.name)) - ++emitterNames[pe.gameObject.name]; - else - emitterNames.Add(pe.gameObject.name, 1); - } - } - Debug.Log("DEBUG inactive/disabled emitter names: " + string.Join(", ", emitterNames.Select(pe => pe.Key + ":" + pe.Value))); - - strings.Clear(); - strings.Add("Parts: " + FindObjectsOfType().Length + " active of " + Resources.FindObjectsOfTypeAll(typeof(Part)).Length); - strings.Add("Vessels: " + FindObjectsOfType().Length + " active of " + Resources.FindObjectsOfTypeAll(typeof(Vessel)).Length); - strings.Add("CometVessels: " + FindObjectsOfType().Length); - Debug.Log("DEBUG " + string.Join(", ", strings)); - } - - public void RunDebugChecks() - { - CheckMemoryUsage(); - CheckNumbersOfThings(); - } - } -} diff --git a/BDArmory/Control/BDATournament.cs b/BDArmory/Control/BDATournament.cs deleted file mode 100644 index 8cd8b60a0..000000000 --- a/BDArmory/Control/BDATournament.cs +++ /dev/null @@ -1,400 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using UnityEngine; -using BDArmory.Core; -using BDArmory.UI; - -namespace BDArmory.Control -{ - // A serializable configuration for loading and saving the tournament state. - [Serializable] - public class RoundConfig : VesselSpawner.SpawnConfig - { - public RoundConfig(int round, int heat, bool completed, VesselSpawner.SpawnConfig config) : base(config) { this.round = round; this.heat = heat; this.completed = completed; } - public int round; - public int heat; - public bool completed; - } - - [Serializable] - public class TournamentState - { - public uint tournamentID; - public List craftFiles; - [NonSerialized] public Dictionary> rounds; // > - [NonSerialized] public Dictionary> completed = new Dictionary>(); - - /* Generate rounds and heats by shuffling the crafts list and breaking it into groups. - * The last heat in a round will have fewer craft if the number of craft is not divisible by the number of vessels per heat. - * The vessels per heat is limited to the number of available craft. - */ - public bool Generate(string folder, int numberOfRounds, int vesselsPerHeat) - { - tournamentID = (uint)DateTime.UtcNow.Subtract(new DateTime(2020, 1, 1)).TotalSeconds; - var abs_folder = Environment.CurrentDirectory + $"/AutoSpawn/{folder}"; - if (!Directory.Exists(abs_folder)) - { - var message = "Tournament folder (" + folder + ") containing craft files does not exist."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDATournament]: " + message); - return false; - } - craftFiles = Directory.GetFiles(abs_folder).Where(f => f.EndsWith(".craft")).ToList(); - int fullHeatCount; - switch (vesselsPerHeat) - { - case 0: // Auto - var autoVesselsPerHeat = OptimiseVesselsPerHeat(craftFiles.Count); - vesselsPerHeat = autoVesselsPerHeat.Item1; - fullHeatCount = Mathf.CeilToInt(craftFiles.Count / vesselsPerHeat) - autoVesselsPerHeat.Item2; - break; - case 1: // Unlimited (all vessels in one heat). - vesselsPerHeat = craftFiles.Count; - fullHeatCount = 1; - break; - default: - vesselsPerHeat = Mathf.Clamp(vesselsPerHeat, 1, craftFiles.Count); - fullHeatCount = craftFiles.Count / vesselsPerHeat; - break; - } - rounds = new Dictionary>(); - Debug.Log("[BDATournament]: Generating " + numberOfRounds + " rounds for tournament " + tournamentID + ", each with " + vesselsPerHeat + " vessels per heat."); - for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) - { - craftFiles.Shuffle(); - int vesselsThisHeat = vesselsPerHeat; - int count = 0; - List selectedFiles = craftFiles.Take(vesselsThisHeat).ToList(); - rounds.Add(rounds.Count, new Dictionary()); - int heatIndex = 0; - while (selectedFiles.Count > 0) - { - rounds[roundIndex].Add(rounds[roundIndex].Count, new VesselSpawner.SpawnConfig( - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, - BDArmorySettings.VESSEL_SPAWN_DISTANCE, - BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, - true, // Kill everything first. - true, // Assign teams. - null, // No folder, we're going to specify the craft files. - selectedFiles.ToList() // Add a copy of the craft files list. - )); - count += vesselsThisHeat; - vesselsThisHeat = heatIndex++ < fullHeatCount ? vesselsPerHeat : vesselsPerHeat - 1; // Take one less for the remaining heats to distribute the deficit of craft files. - selectedFiles = craftFiles.Skip(count).Take(vesselsThisHeat).ToList(); - } - } - return true; - } - - Tuple OptimiseVesselsPerHeat(int count) - { - var options = new List { 8, 7, 6 }; - foreach (var val in options) - { - if (count % val == 0) - return new Tuple(val, 0); - } - var result = OptimiseVesselsPerHeat(count + 1); - return new Tuple(result.Item1, result.Item2 + 1); - } - - public bool SaveState(string stateFile) - { - try - { - List strings = new List(); - - strings.Add(JsonUtility.ToJson(this)); - foreach (var round in rounds.Keys) - foreach (var heat in rounds[round].Keys) - strings.Add(JsonUtility.ToJson(new RoundConfig(round, heat, completed.ContainsKey(round) && completed[round].Contains(heat), rounds[round][heat]))); - - File.WriteAllLines(Path.Combine(Environment.CurrentDirectory, stateFile), strings); - return true; - } - catch - { - return false; - } - } - - public bool LoadState(string stateFile) - { - try - { - if (!File.Exists(Path.Combine(Environment.CurrentDirectory, stateFile))) return false; - var strings = File.ReadAllLines(Path.Combine(Environment.CurrentDirectory, stateFile)); - var data = JsonUtility.FromJson(strings[0]); - tournamentID = data.tournamentID; - craftFiles = data.craftFiles; - rounds = new Dictionary>(); - completed = new Dictionary>(); - for (int i = 1; i < strings.Length; ++i) - { - if (strings[i].Length > 0) - { - var roundConfig = JsonUtility.FromJson(strings[i]); - if (!rounds.ContainsKey(roundConfig.round)) rounds.Add(roundConfig.round, new Dictionary()); - rounds[roundConfig.round].Add(roundConfig.heat, new VesselSpawner.SpawnConfig(roundConfig.latitude, roundConfig.longitude, roundConfig.altitude, roundConfig.distance, roundConfig.absDistanceOrFactor, roundConfig.easeInSpeed, roundConfig.killEverythingFirst, roundConfig.assignTeams, roundConfig.folder, roundConfig.craftFiles)); - if (roundConfig.completed) - { - if (!completed.ContainsKey(roundConfig.round)) completed.Add(roundConfig.round, new HashSet()); - completed[roundConfig.round].Add(roundConfig.heat); - } - } - } - return true; - } - catch (Exception e) - { - Debug.LogError(e); - return false; - } - } - } - - public enum TournamentStatus { Stopped, Running, Waiting, Completed }; - - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class BDATournament : MonoBehaviour - { - public static BDATournament Instance; - - #region Flags and Variables - TournamentState tournamentState; - string stateFile = "GameData/BDArmory/tournament.state"; - string message; - private Coroutine runTournamentCoroutine; - public TournamentStatus tournamentStatus = TournamentStatus.Stopped; - public uint tournamentID = 0; - public int numberOfRounds = 0; - public int currentRound = 0; - public int numberOfHeats = 0; - public int currentHeat = 0; - public int vesselCount = 0; - public int heatsRemaining = 0; - bool competitionStarted = false; - #endregion - - void Awake() - { - if (Instance) - Destroy(Instance); - Instance = this; - } - - void Start() - { - StartCoroutine(LoadStateWhenReady()); - } - - IEnumerator LoadStateWhenReady() - { - while (BDACompetitionMode.Instance == null) - yield return null; - LoadTournamentState(); // Load the last state. - } - - void OnDestroy() - { - StopTournament(); // Stop any running tournament. - SaveTournamentState(); // Save the last state. - } - - // Load tournament state from disk - bool LoadTournamentState(string stateFile = "") - { - if (stateFile != "") this.stateFile = stateFile; - tournamentState = new TournamentState(); - if (tournamentState.LoadState(this.stateFile)) - { - message = "Tournament state loaded from " + this.stateFile; - tournamentID = tournamentState.tournamentID; - vesselCount = tournamentState.craftFiles.Count; - numberOfRounds = tournamentState.rounds.Count; - numberOfHeats = numberOfRounds > 0 ? tournamentState.rounds[0].Count : 0; - heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum(); - } - else - message = "Failed to load tournament state."; - Debug.Log("[BDATournament]: " + message); - // if (BDACompetitionMode.Instance != null) - // BDACompetitionMode.Instance.competitionStatus.Add(message); - tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; - return true; - } - - // Save tournament state to disk - bool SaveTournamentState() - { - if (tournamentState.SaveState(stateFile)) - message = "Tournament state saved to " + stateFile; - else - message = "Failed to save tournament state."; - Debug.Log("[BDATournament]: " + message); - // if (BDACompetitionMode.Instance != null) - // BDACompetitionMode.Instance.competitionStatus.Add(message); - return true; - } - - public void SetupTournament(string folder, int rounds, int vesselsPerHeat, string stateFile = "") - { - if (stateFile != "") this.stateFile = stateFile; - tournamentState = new TournamentState(); - if (!tournamentState.Generate(folder, rounds, vesselsPerHeat)) return; - tournamentID = tournamentState.tournamentID; - vesselCount = tournamentState.craftFiles.Count; - numberOfRounds = tournamentState.rounds.Count; - numberOfHeats = numberOfRounds > 0 ? tournamentState.rounds[0].Count : 0; - heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum(); - tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; - message = "Tournament generated for " + vesselCount + " craft found in AutoSpawn" + (folder == "" ? "" : "/" + folder); - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDATournament]: " + message); - SaveTournamentState(); - } - - public void RunTournament() - { - BDACompetitionMode.Instance.StopCompetition(); - VesselSpawner.Instance.CancelVesselSpawn(); - if (runTournamentCoroutine != null) - StopCoroutine(runTournamentCoroutine); - runTournamentCoroutine = StartCoroutine(RunTournamentCoroutine()); - } - - public void StopTournament() - { - if (runTournamentCoroutine != null) - { - StopCoroutine(runTournamentCoroutine); - runTournamentCoroutine = null; - } - tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; - } - - IEnumerator RunTournamentCoroutine() - { - yield return new WaitForFixedUpdate(); - foreach (var roundIndex in tournamentState.rounds.Keys) - { - currentRound = roundIndex; - foreach (var heatIndex in tournamentState.rounds[roundIndex].Keys) - { - currentHeat = heatIndex; - if (tournamentState.completed.ContainsKey(roundIndex) && tournamentState.completed[roundIndex].Contains(heatIndex)) continue; // We've done that heat. - - message = "Running heat " + heatIndex + " of round " + roundIndex + " of tournament " + tournamentState.tournamentID; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDATournament]: " + message); - - int attempts = 0; - competitionStarted = false; - while (!competitionStarted && attempts++ < 3) // 3 attempts is plenty - { - tournamentStatus = TournamentStatus.Running; - yield return ExecuteHeat(roundIndex, heatIndex); - if (!competitionStarted) - switch (VesselSpawner.Instance.spawnFailureReason) - { - case VesselSpawner.SpawnFailureReason.None: // Successful spawning, but competition failed to start for some reason. - BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + BDACompetitionMode.Instance.competitionStartFailureReason + ", trying again."); - break; - case VesselSpawner.SpawnFailureReason.VesselLostParts: // Recoverable spawning failure. - case VesselSpawner.SpawnFailureReason.TimedOut: // Recoverable spawning failure. - BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + VesselSpawner.Instance.spawnFailureReason + ", trying again."); - break; - default: // Spawning is unrecoverable. - BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + VesselSpawner.Instance.spawnFailureReason + ", aborting."); - attempts = 3; - break; - } - } - if (!competitionStarted) - { - Debug.Log("[BDATournament]: Failed to run heat, failure reasons: " + VesselSpawner.Instance.spawnFailureReason + ", " + BDACompetitionMode.Instance.competitionStartFailureReason); - tournamentStatus = TournamentStatus.Stopped; - yield break; - } - - // Register the heat as completed. - if (!tournamentState.completed.ContainsKey(roundIndex)) tournamentState.completed.Add(roundIndex, new HashSet()); - tournamentState.completed[roundIndex].Add(heatIndex); - SaveTournamentState(); - heatsRemaining = tournamentState.rounds.Select(r => r.Value.Count).Sum() - tournamentState.completed.Select(c => c.Value.Count).Sum(); - - if (heatsRemaining > 0) - { - // Wait a bit for any user action - tournamentStatus = TournamentStatus.Waiting; - double startTime = Planetarium.GetUniversalTime(); - 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 running next heat."); - yield return new WaitForSeconds(1); - } - } - } - message = "All heats in round " + roundIndex + " have been run."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDATournament]: " + message); - } - message = "All rounds in tournament " + tournamentState.tournamentID + " have been run."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDATournament]: " + message); - tournamentStatus = TournamentStatus.Completed; - } - - IEnumerator ExecuteHeat(int roundIndex, int heatIndex) - { - if (VesselSpawnerWindow.Instance.round4running) // FIXME Round 4 - { - var team1Config = new VesselSpawner.SpawnConfig(tournamentState.rounds[roundIndex][heatIndex]); - team1Config.craftFiles = team1Config.craftFiles.Take(team1Config.craftFiles.Count / 2).ToList(); - var team2Config = new VesselSpawner.SpawnConfig(tournamentState.rounds[roundIndex][heatIndex]); - team2Config.craftFiles = team2Config.craftFiles.Skip(team2Config.craftFiles.Count / 2).ToList(); - team2Config.latitude += 8; - VesselSpawner.Instance.TeamSpawn(new List { team1Config, team2Config }, false); - } - else - VesselSpawner.Instance.SpawnAllVesselsOnce(tournamentState.rounds[roundIndex][heatIndex]); - while (VesselSpawner.Instance.vesselsSpawning) - yield return new WaitForFixedUpdate(); - if (!VesselSpawner.Instance.vesselSpawnSuccess) - { - tournamentStatus = TournamentStatus.Stopped; - yield break; - } - yield return new WaitForFixedUpdate(); - - // NOTE: runs in separate coroutine - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - 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; - message = "Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."; - Debug.Log("[BDATournament]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - while (BDACompetitionMode.Instance.competitionStarting) - yield return new 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("[BDATournament]: " + message); - tournamentStatus = TournamentStatus.Stopped; - yield break; - } - competitionStarted = true; - while (BDACompetitionMode.Instance.competitionIsActive) // Wait for the competition to finish. - yield return new WaitForSeconds(1); - } - } -} \ No newline at end of file diff --git a/BDArmory/Control/BDAirspeedControl.cs b/BDArmory/Control/BDAirspeedControl.cs index 8d37bc371..34ed4ed54 100644 --- a/BDArmory/Control/BDAirspeedControl.cs +++ b/BDArmory/Control/BDAirspeedControl.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using UnityEngine; +using BDArmory.Extensions; +using BDArmory.Utils; + namespace BDArmory.Control { public class BDAirspeedControl : MonoBehaviour //: PartModule @@ -10,7 +13,11 @@ public class BDAirspeedControl : MonoBehaviour //: PartModule public float targetSpeed = 0; public float throttleOverride = -1f; public bool useBrakes = true; + public float brakingPriority = 0.5f; 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)] @@ -18,13 +25,30 @@ public class BDAirspeedControl : MonoBehaviour //: PartModule public Vessel vessel; + AxisGroupsModule axisGroupsModule; + bool hasAxisGroupsModule = false; // To avoid repeated null checks + bool controlEnabled; + float possibleAccel; + private float smoothedAccel = 0; // smoothed acceleration, prevents super fast toggling of afterburner + bool shouldSetAfterburners = false; + bool setAfterburnersEnabled = false; + float geeForce = 9.81f; + float gravAccel = 0; + public float TWR { get; private set; } = 1; // Maximum TWR for the current engine modes. + //[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() { @@ -58,7 +82,7 @@ void AirspeedControl(FlightCtrlState s) { if (useBrakes) vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - s.mainThrottle = 0; + SetThrottle(s, 0); return; } @@ -68,26 +92,30 @@ 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) { - float gravAccel = GravAccel(); + gravAccel = GravAccel(); float requestEngineAccel = accel - gravAccel; possibleAccel = 0; //gravAccel; - float dragAccel = 0; - float engineAccel = MaxEngineAccel(requestEngineAccel, out dragAccel); + float engineAccel = MaxEngineAccel(requestEngineAccel, out float dragAccel); 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; } @@ -95,12 +123,12 @@ 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) { - if (requestThrottle < -0.5f) + if (requestThrottle < brakingPriority - 1f) { vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); } @@ -111,13 +139,27 @@ 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; float finalThrust = 0; multiModeEngines.Clear(); - using (List.Enumerator engines = vessel.FindPartModulesImplementing().GetEnumerator()) + using (var engines = VesselModuleRegistry.GetModuleEngines(vessel).GetEnumerator()) while (engines.MoveNext()) { if (engines.Current == null) continue; @@ -136,7 +178,7 @@ float MaxEngineAccel(float requestAccel, out float dragAccel) { engineThrust *= engines.Current.flowMultiplier; } - maxThrust += engineThrust * (engines.Current.thrustPercentage / 100f); + maxThrust += Mathf.Max(0f, engineThrust * (engines.Current.thrustPercentage / 100f)); // Don't include negative thrust percentage drives (Danny2462 drives) as they don't contribute to the thrust. finalThrust += engines.Current.finalThrust; } @@ -146,61 +188,88 @@ float MaxEngineAccel(float requestAccel, out float dragAccel) float vesselMass = vessel.GetTotalMass(); float accel = maxThrust / vesselMass; // This assumes that all thrust is in the same direction. + TWR = accel / geeForce; // GravAccel gets called before this. + + float alpha = 0.05f; // Approx 25 frame (0.5s) lag (similar to 50 frames moving average, but with more weight on recent values and much faster to calculate). + smoothedAccel = smoothedAccel * (1f - alpha) + alpha * accel; //estimate drag - float estimatedCurrentAccel = finalThrust / vesselMass - GravAccel(); + float estimatedCurrentAccel = finalThrust / vesselMass - gravAccel; Vector3 vesselAccelProjected = Vector3.Project(vessel.acceleration_immediate, vessel.velocityD.normalized); float actualCurrentAccel = vesselAccelProjected.magnitude * Mathf.Sign(Vector3.Dot(vesselAccelProjected, vessel.velocityD.normalized)); float accelError = (actualCurrentAccel - estimatedCurrentAccel); // /2 -- why divide by 2 here? dragAccel = accelError; possibleAccel += accel; // This assumes that the acceleration from engines is in the same direction as the original possibleAccel. + forceAfterburner = forceAfterburner || (afterburnerPriority == 100f); + 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 && accel < requestAccel * 0.2f) + + bool afterburnerHasFuel = true; + if (!CheatOptions.InfinitePropellant) + { + using var fuel = mmes.Current.SecondaryEngine.propellants.GetEnumerator(); + while (fuel.MoveNext()) + { + if (!GetABresources(fuel.Current.id)) { afterburnerHasFuel = false; break; } + } + } + if (enable && afterburnerHasFuel) { if (mmes.Current.runningPrimary) { - mmes.Current.Events["ModeEvent"].Invoke(); + if (afterburnerHasFuel) mmes.Current.Events["ModeEvent"].Invoke(); } } - else if (!allowAfterburner || accel > requestAccel * 1.5f) + else { if (!mmes.Current.runningPrimary) { mmes.Current.Events["ModeEvent"].Invoke(); } } + } - return accel; } - + public bool GetABresources(int fuelID) + { + vessel.GetConnectedResourceTotals(fuelID, out double fuelCurrent, out double fuelMax); + return fuelCurrent > 0; + } private static bool IsAfterBurnerEngine(MultiModeEngine engine) { if (engine == null) { return false; } - if (!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() { Vector3 geeVector = FlightGlobals.getGeeForceAtPosition(vessel.CoM); - float gravAccel = geeVector.magnitude * Mathf.Cos(Mathf.Deg2Rad * Vector3.Angle(-geeVector, vessel.velocityD)); // -g.v/|v| ??? - return gravAccel; + geeForce = geeVector.magnitude; + return geeForce * Mathf.Cos(Mathf.Deg2Rad * VectorUtils.Angle(-geeVector, vessel.velocityD)); // -g.v/|v| ??? } - float possibleAccel; - public float GetPossibleAccel() { return possibleAccel; @@ -210,15 +279,25 @@ public float GetPossibleAccel() public class BDLandSpeedControl : MonoBehaviour { public float targetSpeed; + public float signedSrfSpeed; 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; @@ -235,21 +314,412 @@ 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 { - float throttle = zeroPoint + (targetSpeed - (float)vessel.srfSpeed) * gain; + float throttle = zeroPoint + (targetSpeed - signedSrfSpeed) * gain; 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, (targetSpeed * signedSrfSpeed < -5)); + } + } + + /// + /// Set the wheel throttle and the corresponding axis group. + /// + /// The flight control state + /// The throttle value + public void SetThrottle(FlightCtrlState s, float value) + { + s.mainThrottle = 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 + { + public float targetAltitude; + public Vessel vessel; + public bool preventNegativeZeroPoint = false; + + public float targetSpeed = 0; + + AxisGroupsModule axisGroupsModule; + bool hasAxisGroupsModule = false; // To avoid repeated null checks + + private float altIntegral; + public float zeroPoint { get; private set; } + + private const float Kp = 0.5f; + 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() + { + vessel.OnFlyByWire -= AltitudeControl; + vessel.OnFlyByWire += AltitudeControl; + altIntegral = 0; + } + + public void Deactivate() + { + vessel.OnFlyByWire -= AltitudeControl; + } + + void AltitudeControl(FlightCtrlState s) + { + + if (targetAltitude == 0) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + SetThrottle(s, 0); + } + else + { + float altError = (targetAltitude - (float)vessel.radarAltitude); //this could use the MaxEngineAccel/AB stuff from speedController for VTOLS using AB capable engines for flight + 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; + SetThrottle(s, Mathf.Clamp01(throttle)); + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, throttle < -5f); } + if (targetSpeed == 0) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + SetSecondaryThrottle(0); + return; + } + else + { + float currentSpeed = (float)vessel.srfSpeed; + float speedError = targetSpeed - currentSpeed; + + float setAccel = speedError * 2; + + SetAcceleration(setAccel); + } } + void SetAcceleration(float accel) + { + float maxThrust = 0; + using (var engines = VesselModuleRegistry.GetModuleEngines(vessel).GetEnumerator()) //very very simplified cutdown version of MaxEngineAccel from AirspeedController; could also use AB stuff + while (engines.MoveNext()) //Turn the MaxEngineAccel chunk into a static utility class accessible by the various biome speed controllers? + { + if (engines.Current == null) continue; + if (!engines.Current.independentThrottle) continue; + if (!engines.Current.EngineIgnited) continue; + + float engineThrust = engines.Current.maxThrust; + if (engines.Current.atmChangeFlow) + { + engineThrust *= engines.Current.flowMultiplier; + } + maxThrust += Mathf.Max(0f, engineThrust * (engines.Current.thrustPercentage / 100f)); // Don't include negative thrust percentage drives (Danny2462 drives) as they don't contribute to the thrust. + } + + float vesselMass = vessel.GetTotalMass(); + + float engineAccel = maxThrust / vesselMass; // This assumes that all thrust is in the same direction. + + if (engineAccel == 0) + { + SetSecondaryThrottle(accel > 0 ? 1 : 0); + return; + } + + accel = Mathf.Clamp(accel, -engineAccel, engineAccel); + + float requestThrottle = accel / engineAccel; + + SetSecondaryThrottle(Mathf.Clamp01(requestThrottle)); + + //use brakes if overspeeding too much + + if (requestThrottle < 0.5f - 1f) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + } + else + { + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); + } + } + /// + /// 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. + } + } + public void SetSecondaryThrottle(float value) + { + using (var engines = VesselModuleRegistry.GetModuleEngines(vessel).GetEnumerator()) //allow VTOL AI to have standard horizontal engines for thrust, using engines set to independent throttle so normal engines usable for altitude + while (engines.MoveNext()) + { + if (engines.Current == null) continue; + if (!engines.Current.EngineIgnited) continue; + if (!engines.Current.independentThrottle) continue; + engines.Current.independentThrottlePercentage = value * 100; + } + } + } + + public class BDOrbitalControl : MonoBehaviour //: PartModule + { + + // ///////////////////////////////////////////////////// + public Vessel vessel; + public Vector3 attitude = Vector3.zero; + private Vector3 attitudeLerped; + private float error; + private float angleLerp; + public bool lerpAttitude = true; + private float lerpRate; + private bool lockAttitude = false; + public bool PIDActive = false; + public List rcsEngines = new List(); + private bool facingDesiredRotation; + public float throttle; + public float throttleActual; + internal float throttleLerped; + public float throttleLerpRate = 1; + public bool lerpThrottle = true; + public float rcsLerpRate = 5; + public bool rcsRotate = false; + public float alignmentToleranceforBurn = 5; + public bool useReverseThrust = false; + public Vector3 thrustDirection = Vector3.zero; + public bool engineRCSRotation = true; + public bool engineRCSTranslation = true; + + AxisGroupsModule axisGroupsModule; + bool hasAxisGroupsModule = false; // To avoid repeated null checks + + public Vector3 RCSVector; + public Vector3 RCSVectorLerped = Vector3.zero; + public float RCSPower = 3f; + private Vector3 RCSThrust; + private Vector3 up, right, forward; + private float RCSThrottle; + private float lastEpsilon = 0.05f; + + //[KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "ToggleAC")] + + void Start() + { + if (!vessel.IsMissile()) + { + axisGroupsModule = vessel.FindVesselModuleImplementingBDA(); // Look for an axis group module. + if (axisGroupsModule != null) hasAxisGroupsModule = true; + } + } + + public void Activate() + { + vessel.OnFlyByWire -= OrbitalControl; + vessel.OnFlyByWire += OrbitalControl; + } + + public void Deactivate() + { + vessel.OnFlyByWire -= OrbitalControl; + } + + void OrbitalControl(FlightCtrlState s) + { + error = VectorUtils.Angle(vessel.ReferenceTransform.up, attitude); + + if (!PIDActive) + UpdateSAS(s); + UpdateThrottle(s); + UpdateRCS(s); + } + + private void UpdateThrottle(FlightCtrlState s) + { + facingDesiredRotation = VectorUtils.Angle((useReverseThrust ? -1 : 1) * vessel.ReferenceTransform.up, thrustDirection) < alignmentToleranceforBurn; + + throttleActual = facingDesiredRotation ? throttle : 0; + + // Move actual throttle towards throttle target gradually. + throttleLerped = Mathf.MoveTowards(throttleLerped, throttleActual, throttleLerpRate * Time.fixedDeltaTime); + + SetThrottle(s, lerpThrottle ? throttleLerped : throttleActual); + + } + + /// + /// 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. + } + } + + void UpdateRCS(FlightCtrlState s) + { + // When firing, adjust the minimum RCS thrust based on angle to target to allow minute adjustments using RCS + float rcsEpsilon = lerpAttitude == false ? 0.05f : Mathf.Lerp(0.05f, 0.01f, Mathf.Clamp01((Vector3.Dot(attitude, vessel.ReferenceTransform.up) - 0.999f) / 0.001f)); // 0.05 at greater than ~2.5 deg, 0.01 at 0 deg, default Epsilon is 0.05f + if (rcsEpsilon != lastEpsilon) + { + foreach (ModuleRCS thruster in VesselModuleRegistry.GetModules(vessel)) + thruster.EPSILON = rcsEpsilon; + lastEpsilon = rcsEpsilon; + } + + if (RCSVector == Vector3.zero) + { + RCSEngineControl(s); + return; + } + + if (RCSVectorLerped == Vector3.zero) + RCSVectorLerped = RCSVector; + + float rcsLerpMag = RCSVectorLerped.magnitude; + float rcsLerpT = rcsLerpRate * Time.fixedDeltaTime * Mathf.Clamp01(rcsLerpMag / RCSPower); + + if (rcsRotate) // Quickly rotate RCS thrust towards commanded RCSVector + RCSVectorLerped = Vector3.Slerp(RCSVectorLerped, RCSVector, rcsLerpT); + else // Gradually lerp RCS thrust towards commanded RCSVector + RCSVectorLerped = Vector3.Lerp(RCSVectorLerped, RCSVector, rcsLerpT); + RCSThrottle = Mathf.Lerp(0, 1.732f, Mathf.InverseLerp(0, RCSPower, rcsLerpMag)); + RCSThrust = RCSVectorLerped.normalized * RCSThrottle; + + up = -vessel.ReferenceTransform.forward; + forward = -vessel.ReferenceTransform.up; + right = Vector3.Cross(up, forward); + + SetAxisControlState(s, + Mathf.Clamp(Vector3.Dot(RCSThrust, right), -1, 1), + Mathf.Clamp(Vector3.Dot(RCSThrust, up), -1, 1), + Mathf.Clamp(Vector3.Dot(RCSThrust, forward), -1, 1)); + + RCSEngineControl(s); + } + + void UpdateSAS(FlightCtrlState s) + { + if (attitude == Vector3.zero || lockAttitude) return; + + // SAS must be turned off. Don't know why. + if (vessel.ActionGroups[KSPActionGroup.SAS]) + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, false); + + var ap = vessel.Autopilot; + if (ap == null) return; + + // The offline SAS must not be on stability assist. Normal seems to work on most probes. + if (ap.Mode != VesselAutopilot.AutopilotMode.Normal) + ap.SetMode(VesselAutopilot.AutopilotMode.Normal); + + // Lerp attitude while burning to reduce instability. + if (lerpAttitude) + { + angleLerp = Mathf.InverseLerp(0, 10, error); + lerpRate = Mathf.Lerp(1, 10, angleLerp); + attitudeLerped = Vector3.Lerp(attitudeLerped, attitude, lerpRate * Time.deltaTime); + } + + ap.SAS.SetTargetOrientation(throttleLerped > 0 && lerpAttitude ? attitudeLerped : attitude, false); + } + + public void RCSEngineControl(FlightCtrlState s) + { + // Control engines perpendicular to longitudinal axis to act as RCS thrusters using FlightCtrlState s.pitch/s.yaw/s.yaw/s.X/s.Y/s.Z inputs + // Call this last of all control methods so FlightCtrlState is finalized + Vector3 rcsTranslation = s.X * right + s.Y * up + s.Z * forward; + Vector3 vesselRad = new( // vesselSize is width, height, length + (vessel.vesselSize.y + vessel.vesselSize.z) / 4f, // Pitch: average of height and length, halved + (vessel.vesselSize.x + vessel.vesselSize.z) / 4f, // Yaw: average of width and length, halved + (vessel.vesselSize.x + vessel.vesselSize.y) / 4f); // Roll: average of width and height, halved + float giveThrustMin = Mathf.Lerp(0.13f, 0f, Mathf.Clamp01(Vector3.Dot(attitude, vessel.ReferenceTransform.up))); + for (int i = 0; i < rcsEngines.Count; i++) + { + if (rcsEngines[i] == null) continue; + + // RCS translation + float giveThrust = engineRCSTranslation ? Mathf.Clamp(Vector3.Dot(-rcsEngines[i].thrustTransforms[0].forward, rcsTranslation), -1f, 1f) : 0f; + + // RCS Rotation using Moments + if (engineRCSRotation) + { + float pitchMoment = s.pitch * Vector3.Dot(vessel.ReferenceTransform.right, Vector3.Cross(rcsEngines[i].transform.position - vessel.CoM, rcsEngines[i].thrustTransforms[0].forward)) / vesselRad.x; + float yawMoment = s.yaw * Vector3.Dot(vessel.ReferenceTransform.forward, Vector3.Cross(rcsEngines[i].transform.position - vessel.CoM, rcsEngines[i].thrustTransforms[0].forward)) / vesselRad.y; + float rollMoment = s.roll * Vector3.Dot(vessel.ReferenceTransform.up, Vector3.Cross(rcsEngines[i].transform.position - vessel.CoM, rcsEngines[i].thrustTransforms[0].forward)) / vesselRad.z; + giveThrust += pitchMoment + yawMoment + rollMoment; // Modify any translation to allow rotation + } + + if (giveThrust >= giveThrustMin) + rcsEngines[i].thrustPercentage = Mathf.Clamp01(giveThrust) * 100; + else + rcsEngines[i].thrustPercentage = 0; + } + } + + public void Stability(bool enable) + { + if (lockAttitude == enable) return; + lockAttitude = enable; + + var ap = vessel.Autopilot; + if (ap == null) return; + + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, enable); + ap.SetMode(enable ? VesselAutopilot.AutopilotMode.StabilityAssist : VesselAutopilot.AutopilotMode.Normal); + } + + /// + /// Set the axis control state and also the corresponding axis groups. + /// + /// The flight control state + /// x + /// y + /// z + protected virtual void SetAxisControlState(FlightCtrlState s, float X, float Y, float Z) + { + s.X = X; + s.Y = Y; + s.Z = Z; + if (hasAxisGroupsModule) + { + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.TranslateX, X); + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.TranslateY, Y); + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.TranslateZ, Z); + } + } + } } diff --git a/BDArmory/Control/BDGenericAIBase.cs b/BDArmory/Control/BDGenericAIBase.cs new file mode 100644 index 000000000..1a6d89e4d --- /dev/null +++ b/BDArmory/Control/BDGenericAIBase.cs @@ -0,0 +1,716 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ModuleWheels; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Control +{ + /// + /// A base class for implementing AI. + /// Note: You do not have to use it, it is just for convenience, all the game cares about is that you implement the IBDAIControl interface. + /// + public abstract class BDGenericAIBase : PartModule, IBDAIControl, IBDWMModule + { + #region declarations + public virtual AIType aiType => AIType.GenericAI; + public bool pilotEnabled => pilotOn; + + // separate private field for pilot On, because properties cannot be KSPFields + [KSPField(isPersistant = true)] + public bool pilotOn; + protected Vessel activeVessel; + + public MissileFire WeaponManager + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; + } + } + MissileFire _weaponManager; + + /// + /// The default is BDAirspeedControl. If you want to use something else, just override ActivatePilot (and, potentially, DeactivatePilot), and make it use something else. + /// + protected BDAirspeedControl speedController; + public float originalMaxSpeed = -1; + protected bool hasAxisGroupsModule = false; + protected AxisGroupsModule axisGroupsModule; + + protected Transform vesselTransform => vessel.ReferenceTransform; + + protected StringBuilder debugString = new StringBuilder(); + + protected Vessel targetVessel; + + protected virtual Vector3d assignedPositionGeo { get; set; } + + public Vector3d assignedPositionWorld + { + get + { + return VectorUtils.GetWorldSurfacePostion(assignedPositionGeo, vessel.mainBody); + } + protected set + { + assignedPositionGeo = VectorUtils.WorldPositionToGeoCoords(value, vessel.mainBody); + } + } + + //wing commander + public ModuleWingCommander commandLeader + { + get + { + if (_commandLeader == null || _commandLeader.vessel == null || !_commandLeader.vessel.isActiveAndEnabled) return null; // Vessel's don't immediately become null on dying if they're the active vessel. + return _commandLeader; + } + protected set { _commandLeader = value; } + } + ModuleWingCommander _commandLeader; + + protected PilotCommands command; + PilotCommands previousCommand; + public string currentStatus { get; protected set; } = "Free"; + public int commandFollowIndex { get; protected set; } = -1; + + public PilotCommands currentCommand => command; + public virtual Vector3d commandGPS => assignedPositionGeo; + + #endregion declarations + + public abstract bool CanEngage(); + + public abstract bool IsValidFixedWeaponTarget(Vessel target); + + /// + /// This will be called every update and should run the autopilot logic. + /// + /// For simple use cases: + /// 1. Engage your target (get in position to engage, shooting is done by guard mode) + /// 2. If no target, check command, and follow it + /// Do this by setting s.pitch, s.yaw and s.roll. + /// + /// For advanced use cases you probably know what you're doing :P + /// + /// current flight control state + protected abstract void AutoPilot(FlightCtrlState s); + + // A small wrapper to make sure the autopilot does not do anything when it shouldn't + private void autoPilot(FlightCtrlState s) + { + debugString.Length = 0; + if (!vessel || !vessel.transform || vessel.packed || !vessel.mainBody) + return; + //vessel lost command parts from damage? + if (!vessel.isCommandable) //isCommandable is only false when there is *no* command parts on the vessel (cockpits/probecores/etc) + { + DeactivatePilot(); + debugString.AppendLine($"Vessel: No Command parts!"); + if (vessel.Autopilot.Enabled) Debug.Log("[BDArmory.BDGenericAIBase]: " + vessel.vesselName + " is not commandable, disabling autopilot."); + s.NeutralizeStick(); + vessel.Autopilot.Disable(); + return; + } + // nobody is controlling any more possibly due to G forces? + if (!vessel.IsControllable) //false when probes out of EC/cockpits don't have pilots + { + debugString.AppendLine($"Vessel: No Control!"); + if (vessel.Autopilot.Enabled) Debug.Log("[BDArmory.BDGenericAIBase]: " + vessel.vesselName + " is not controllable, disabling autopilot."); + s.NeutralizeStick(); + vessel.Autopilot.Disable(); + return; + } + + // generally other AI and guard mode expects this target to be engaged + GetGuardTarget(); // get the guard target from weapon manager + GetNonGuardTarget(); // if guard mode is off, get the UI target + GetGuardNonTarget(); // pick a target if guard mode is on, but no target is selected, + // though really targeting should be managed by the weaponManager, what if we pick an airplane while having only abrams cannons? :P + // (this is another reason why target selection is hardcoded into the base class, so changing this later is less of a mess :) ) + + 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 + + /// + /// Activate the AI. + /// + /// true if successfully activated, false otherwise. + public virtual bool ActivatePilot() + { + pilotOn = true; + vessel.ActiveController().UpdateAIModules(); // Update the active AI module. + if (!pilotOn) return false; // If it's not us, abort. + if (activeVessel) + activeVessel.OnFlyByWire -= autoPilot; + activeVessel = vessel; + activeVessel.OnFlyByWire += autoPilot; + + if (!speedController) + { + speedController = gameObject.AddComponent(); + speedController.vessel = vessel; + } + + speedController.Activate(); + + GameEvents.onVesselDestroy.Remove(RemoveAutopilot); + GameEvents.onVesselDestroy.Add(RemoveAutopilot); + + if (command == PilotCommands.Free) assignedPositionWorld = vessel.ReferenceTransform.position; + try // Sometimes the FSM breaks trying to set the gear action group + { + // Make sure the FSM is started for deployable wheels. (This should hopefully fix the FSM errors.) + foreach (var part in VesselModuleRegistry.GetModules(vessel).Where(part => part != null && part.fsm != null && !part.fsm.Started)) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDAGenericAIBase]: Starting FSM with state {(string.IsNullOrEmpty(part.fsm.currentStateName) ? "Retracted" : part.fsm.currentStateName)} on {part.name} of {part.vessel.vesselName}"); + part.fsm.StartFSM(part.fsm.CurrentState ?? new KFSMState("Retracted")); + } + // 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 on {vessel.vesselName}: {e.Message}"); + } + RefreshPartWindow(); + return true; + } + + public virtual void DeactivatePilot() + { + pilotOn = false; + if (activeVessel) + activeVessel.OnFlyByWire -= autoPilot; + RefreshPartWindow(); + + if (speedController) + { + speedController.Deactivate(); + } + } + + protected void RemoveAutopilot(Vessel v) + { + if (v == vessel) + { + v.OnFlyByWire -= autoPilot; + } + } + + protected void RefreshPartWindow() + { + 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 + public void TogglePilot() + { + if (pilotEnabled) + { + DeactivatePilot(); + } + else + { + vessel.ActiveController().SetActiveAI(this); + } + } + + [KSPAction("Activate Pilot")] + public void AGActivatePilot(KSPActionParam param) => vessel.ActiveController().SetActiveAI(this); + + [KSPAction("Deactivate Pilot")] + public void AGDeactivatePilot(KSPActionParam param) => DeactivatePilot(); + + [KSPAction("Toggle Pilot")] + public void AGTogglePilot(KSPActionParam param) => TogglePilot(); + + public virtual string Name { get; } = "AI Control"; + public bool Enabled => pilotEnabled; + + public bool TakingOff = true; + + public void Toggle() => TogglePilot(); + + #endregion Pilot on/off + + #region events + + protected virtual void Start() + { + if (HighLogic.LoadedSceneIsFlight) + { + part.OnJustAboutToBeDestroyed += DeactivatePilot; + GameEvents.onVesselWasModified.Add(onVesselWasModified); + MissileFire.OnChangeTeam += OnToggleTeam; + GameEvents.onPartDie.Add(OnPartDie); + + activeVessel = vessel; + 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) + { + ActivatePilot(); + } + } + + RefreshPartWindow(); + } + + void OnPartDie() { OnPartDie(part); } + protected virtual void OnPartDie(Part p) + { + if (part == p) + { + Destroy(this); // Force this module to be removed from the gameObject as something is holding onto part references and causing a memory leak. + } + } + + protected virtual void OnDestroy() + { + part.OnJustAboutToBeDestroyed -= DeactivatePilot; + GameEvents.onVesselWasModified.Remove(onVesselWasModified); + GameEvents.onVesselDestroy.Remove(RemoveAutopilot); + MissileFire.OnChangeTeam -= OnToggleTeam; + GameEvents.onPartDie.Remove(OnPartDie); + } + + protected virtual void OnGUI() + { + if (!pilotEnabled || !vessel.isActiveVessel) return; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + GUI.Label(new Rect(200, Screen.height - 350, 600, 350), $"{vessel.name}\n{debugString.ToString()}"); + } + } + + protected virtual void OnToggleTeam(MissileFire mf, BDTeam team) + { + if (mf.vessel == vessel || (commandLeader && commandLeader.vessel == mf.vessel)) + { + ReleaseCommand(); + } + } + + protected virtual void onVesselWasModified(Vessel v) + { + if (v != activeVessel) + return; + + activeVessel = vessel; + } + + #endregion events + + #region utilities + + protected void GetGuardTarget() + { + var weaponManager = WeaponManager; + if (weaponManager == null) return; + if (weaponManager.guardMode && weaponManager.currentTarget != null) + { + targetVessel = weaponManager.currentTarget.Vessel; + } + else + { + targetVessel = null; + } + } + + /// + /// If guard mode is set but no target is selected, pick something + /// + protected virtual void GetGuardNonTarget() + { + var weaponManager = WeaponManager; + if (weaponManager && weaponManager.guardMode && !targetVessel) + { + // select target based on competition style + TargetInfo potentialTarget = BDArmorySettings.DEFAULT_FFA_TARGETING ? BDATargetManager.GetClosestTargetWithBiasAndHysteresis(weaponManager) : BDATargetManager.GetLeastEngagedTarget(weaponManager); + if (potentialTarget && potentialTarget.Vessel) + { + targetVessel = potentialTarget.Vessel; + } + } + } + + /// + /// If guard mode off, and UI target is of the opposing team, set it as target + /// + protected void GetNonGuardTarget() + { + var weaponManager = WeaponManager; + if (weaponManager != null && !weaponManager.guardMode) + { + if (vessel.targetObject != null) + { + var nonGuardTargetVessel = vessel.targetObject.GetVessel(); + if (nonGuardTargetVessel != null) + { + var targetWeaponManager = nonGuardTargetVessel.ActiveController().WM; + if (targetWeaponManager != null && weaponManager.Team.IsEnemy(targetWeaponManager.Team)) + targetVessel = (Vessel)vessel.targetObject; + } + } + } + } + + /// + /// Write some text to the debug field (the one on lower left when debug labels are on), followed by a newline. + /// + /// text to write + protected void DebugLine(string text) + { + debugString.AppendLine(text); + } + + protected virtual void SetStatus(string text) + { + currentStatus = text; + // DebugLine(text); + } + + #endregion utilities + + #region WingCommander + + public virtual void ReleaseCommand(bool resetAssignedPosition = true, bool storeCommand = true) + { + if (!vessel || command == PilotCommands.Free) return; + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was released from command."); + previousCommand = command; + command = PilotCommands.Free; + + if (!storeCommand) // Clear the previous command. + { + commandLeader = null; + commandFollowIndex = -1; + 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 is null || leader == vessel || followerIndex < 0) return; + + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to follow."); + previousCommand = command; + command = PilotCommands.Follow; + commandLeader = leader; + commandFollowIndex = followerIndex; + } + + public virtual void CommandAG(KSPActionGroup ag) + { + if (!pilotEnabled) return; + vessel.ActionGroups.ToggleGroup(ag); + } + + public virtual void CommandFlyTo(Vector3 gpsCoords) + { + if (!pilotEnabled) return; + + if (BDArmorySettings.DEBUG_AI && (command != PilotCommands.FlyTo || (gpsCoords - assignedPositionGeo).sqrMagnitude > 0.1)) Debug.Log($"[BDArmory.BDGenericAIBase]: {vessel.vesselName} was commanded to go to {gpsCoords}."); + assignedPositionGeo = gpsCoords; + previousCommand = command; + command = PilotCommands.FlyTo; + } + + public virtual void CommandAttack(Vector3 gpsCoords) + { + if (!pilotEnabled) return; + + if (BDArmorySettings.DEBUG_AI && (command != PilotCommands.Attack || (gpsCoords - assignedPositionGeo).sqrMagnitude > 0.1)) Debug.Log($"[BDArmory.BDGenericAIBase]: {vessel.vesselName} was commanded to attack {gpsCoords}."); + assignedPositionGeo = gpsCoords; + previousCommand = command; + command = PilotCommands.Attack; + } + + public virtual void CommandTakeOff() + { + ActivatePilot(); + } + + public virtual void CommandFollowWaypoints() + { + if (!pilotEnabled) return; // Do nothing if we haven't taken off (or activated with airspawn) yet. + + 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 < (BDArmorySettings.WAYPOINTS_SCALE > 0 ? BDArmorySettings.WAYPOINTS_SCALE : (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}; active index{2} of {3}", activeWaypointIndex, deviation, activeWaypointIndex * (activeWaypointLap - 1), waypoints.Count * waypointLapLimit)); + BDACompetitionMode.Instance.Scores.RegisterWaypointReached(vessel.vesselName, waypointCourseIndex, activeWaypointIndex, activeWaypointLap, waypointLapLimit, deviation); + + var weaponManager = WeaponManager; + if (BDArmorySettings.WAYPOINT_GUARD_INDEX >= 0 && !weaponManager.guardMode && activeWaypointIndex + (waypoints.Count * (activeWaypointLap - 1)) >= Mathf.Min(BDArmorySettings.WAYPOINT_GUARD_INDEX, waypoints.Count * waypointLapLimit)) //allow guard activating, i.e. halfway through lap2), guarantee guard activation after last guate + { + // 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(); + if (BDArmorySettings.WAYPOINT_GUARD_INDEX >= 0 && !weaponManager.guardMode) weaponManager.guardMode = true; + return; + } + else if (activeWaypointIndex >= waypoints.Count && activeWaypointLap <= waypointLapLimit) + { + activeWaypointIndex = 0; + activeWaypointLap++; + } + UpdateWaypoint(); // Call ourselves again for the new waypoint to follow. + //Modify AI maxSpeed if the gate we just pased has a speed limit + float mSpeed = WaypointCourses.CourseLocations[waypointCourseIndex].waypoints[activeWaypointIndex].maxSpeed; + var ai = vessel.ActiveController().AI; + if (ai != null) switch (ai.aiType) + { + case AIType.PilotAI: + var pilotAI = ai as BDModulePilotAI; + pilotAI.maxSpeed = mSpeed > 0 ? mSpeed : originalMaxSpeed; + pilotAI.OnMaxSpeedChanged(); + break; + case AIType.SurfaceAI: + var surfaceAI = ai as BDModuleSurfaceAI; + surfaceAI.MaxSpeed = mSpeed > 0 ? mSpeed : originalMaxSpeed; + break; + case AIType.VTOLAI: + var vtolAI = ai as BDModuleVTOLAI; + vtolAI.MaxSpeed = mSpeed > 0 ? mSpeed : originalMaxSpeed; + break; + case AIType.OrbitalAI: + // var orbitalAI = ai as BDModuleOrbitalAI; + // orbitalAI.ManeuverSpeed = mSpeed > 0 ? mSpeed : originalMaxSpeed; //Don' think WPs would work, period, with an orbital reference frame? + break; + } + } + } + + Coroutine maintainingFuelLevelsCoroutine; + Coroutine maintainingWaypointFuelLevelsCoroutine; + /// + /// Prevent fuel resource drain until the next waypoint. + /// + public void MaintainFuelLevelsUntilWaypoint() + { + if (maintainingWaypointFuelLevelsCoroutine != null) StopCoroutine(maintainingWaypointFuelLevelsCoroutine); + maintainingWaypointFuelLevelsCoroutine = 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; + } + */ + MaintainFuelLevels(true); + var wait = new WaitForFixedUpdate(); + var currentWaypointIndex = CurrentWaypointIndex; + while (vessel != null && IsRunningWaypoints && CurrentWaypointIndex == currentWaypointIndex) + { + yield return wait; + } + MaintainFuelLevels(false); + } + #endregion + /// + /// Prevent fuel drain (control function). + /// Note: CheatOptions.InfinitePropellant doesn't work for FS helicopter engines, so we need to maintain fuel manually. + /// + /// Activate or deactive fuel preservation. + public void MaintainFuelLevels(bool active) + { + if (maintainingFuelLevelsCoroutine != null) StopCoroutine(maintainingFuelLevelsCoroutine); + if (active) maintainingFuelLevelsCoroutine = StartCoroutine(MaintainFuelLevelsCoroutine()); + } + /// + /// Prevent fuel drain (coroutine). + /// + /// + IEnumerator MaintainFuelLevelsCoroutine() + { + if (vessel == null) yield break; + var wait = new WaitForFixedUpdate(); + var fuelResourceParts = new Dictionary>(); + 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) + { + foreach (var fuelResource in fuelResources.Values) + { + foreach (var partResource in fuelResource.Keys) + { partResource.amount = fuelResource[partResource]; } + } + yield return wait; + } + } + + Coroutine disableBattleDamageCoroutine = null; + public void DisableBattleDamage(bool active) + { + if (!active && disableBattleDamageCoroutine != null) + { + StopCoroutine(disableBattleDamageCoroutine); + disableBattleDamageCoroutine = null; + BDArmorySettings.BATTLEDAMAGE = true; + } + if (active && BDArmorySettings.BATTLEDAMAGE) + { + BDArmorySettings.BATTLEDAMAGE = false; + disableBattleDamageCoroutine = StartCoroutine(new WaitWhile(() => true)); + } + } + } +} diff --git a/BDArmory/Control/BDModuleOrbitalAI.cs b/BDArmory/Control/BDModuleOrbitalAI.cs new file mode 100644 index 000000000..13a0cb8fd --- /dev/null +++ b/BDArmory/Control/BDModuleOrbitalAI.cs @@ -0,0 +1,2087 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +using BDArmory.Extensions; +using BDArmory.Targeting; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Guidances; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Control +{ + public class BDModuleOrbitalAI : BDGenericAIBase, IBDAIControl + { + // Code contained within this file is adapted from Hatbat, Spartwo and MiffedStarfish's Kerbal Combat Systems Mod https://github.com/Halbann/StockCombatAI/tree/dev/Source/KerbalCombatSystems. + // Code is distributed under CC-BY-SA 4.0: https://creativecommons.org/licenses/by-sa/4.0/ + + public override AIType aiType => AIType.OrbitalAI; + #region Declarations + + // Orbiter AI variables. + public float updateInterval; + public float emergencyUpdateInterval = 0.5f; + public float combatUpdateInterval = 2.5f; + + private BDOrbitalControl fc; + private bool PIDActive; + private int ECID; + + public IBDWeapon currentWeapon; + + private float trackedDeltaV; + private Vector3 attitudeCommand; + private PilotCommands lastUpdateCommand = PilotCommands.Free; + private float maneuverTime; + private float minManeuverTime; + private bool maneuverStateChanged = false; + enum OrbitCorrectionReason { None, FallingInsideAtmosphere, ApoapsisLow, PeriapsisLow, Escaping }; + private OrbitCorrectionReason ongoingOrbitCorrectionDueTo = OrbitCorrectionReason.None; + private float missileTryLaunchTime = 0f; + private bool wasDescendingUnsafe = false; + private bool hasPropulsion; + private bool hasRCS; + private bool hasWeapons; + private bool hasEC; + private float maxAcceleration; + private float maxThrust; + + private float reverseForwardThrustRatio = 0f; + private List forwardEngines = new List(); + private List reverseEngines = new List(); + private List rcsEngines = new List(); + private bool currentForwardThrust; + private bool engineListsRequireUpdating = true; + private Dictionary> engineIndependentThrottleState = new Dictionary>(); + + private Vector3 maxAngularAcceleration; + private float maxAngularAccelerationMag; + private Vector3 availableTorque; + private double minSafeAltitude; + private CelestialBody safeAltBody = null; + public Vector3 interceptRanges = Vector3.one; + private Vector3 lastFiringSolution; + const float interceptMargin = 0.25f; + + // Evading + bool evadingGunfire = false; + float evasiveTimer; + Vector3 threatRelativePosition; + Vector3 evasionNonLinearityDirection; + string evasionString = " & Evading Gunfire"; + + //collision detection (for other vessels). + const int vesselCollisionAvoidanceTickerFreq = 10; // Number of fixedDeltaTime steps between vessel-vessel collision checks. + int collisionDetectionTicker = 0; + Vector3 collisionAvoidDirection; + public Vessel currentlyAvoidedVessel; + + public enum PIDModeTypes + { + Inactive, // Stock autopilot always used + Firing, // PID used for firing weapons + Everything // PID used for firing weapons and maneuvers + } + + public enum RollModeTypes + { + Port_Starboard, // Roll to port or starboard, whichever is closer + Dorsal_Ventral, // Roll to dorsal or ventral, whichever is closer + Port, // Always roll port to target + Starboard, // Always roll starboard to target + Dorsal, // Always roll dorsal to target + Ventral, // Always roll ventral to target + } + + // User parameters changed via UI. + #region PID + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_OrbitalPIDActive"),//PID active mode + UI_ChooseOption(options = new string[3] { "Inactive", "Firing", "Everything" })] + public string pidMode = "Firing"; + public readonly string[] pidModes = new string[3] { "Inactive", "Firing", "Everything" }; + + public PIDModeTypes PIDMode + => (PIDModeTypes)Enum.Parse(typeof(PIDModeTypes), pidMode); + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerPower"),//Steer Factor + UI_FloatRange(minValue = 0.2f, maxValue = 20f, stepIncrement = .1f, scene = UI_Scene.All)] + public float steerMult = 14; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerKi"), //Steer Ki + UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float steerKiAdjust = 0.1f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDamping"),//Steer Damping + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] + public float steerDamping = 5; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerMaxError"),//Steer Max Error + UI_FloatRange(minValue = 25f, maxValue = 180f, stepIncrement = 5f, scene = UI_Scene.All)] + public float steerMaxError = 45f; + #endregion + + #region Combat + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BroadsideAttack"),//Attack vector + UI_Toggle(enabledText = "#LOC_BDArmory_AI_BroadsideAttack_enabledText", disabledText = "#LOC_BDArmory_AI_BroadsideAttack_disabledText")]//Broadside--Bow + public bool BroadsideAttack = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_RollMode"),// Preferred roll direction of ship towards target + UI_ChooseOption(options = new string[6] { "Port_Starboard", "Dorsal_Ventral", "Port", "Starboard", "Dorsal", "Ventral" })] + public string rollTowards = "Port_Starboard"; + public readonly string[] rollTowardsModes = new string[6] { "Port_Starboard", "Dorsal_Ventral", "Port", "Starboard", "Dorsal", "Ventral" }; + + public RollModeTypes rollMode + => (RollModeTypes)Enum.Parse(typeof(RollModeTypes), rollTowards); + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinEngagementRange"),//Min engagement range + UI_FloatSemiLogRange(minValue = 10f, maxValue = 10000f, sigFig = 1, withZero = true)] + public float MinEngagementRange = 100; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ForceFiringRange"),//Force firing range + UI_FloatSemiLogRange(minValue = 10f, maxValue = 10000f, sigFig = 1, withZero = true)] + public float ForceFiringRange = 100f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_AllowRamming", advancedTweakable = true), //Toggle Allow Ramming + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool allowRamming = true; // Allow switching to ramming mode. + #endregion + + #region Control + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_FiringRCS"),//Use RCS to kill relative velocity when firing + UI_Toggle(enabledText = "#LOC_BDArmory_AI_FiringRCS_enabledText", disabledText = "#LOC_BDArmory_AI_FiringRCS_disabledText", scene = UI_Scene.All),]//Manage Velocity--Maneuvers Only + public bool FiringRCS = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ManeuverRCS"),//Use RCS for all maneuvering + UI_Toggle(enabledText = "#LOC_BDArmory_AI_ManeuverRCS_enabledText", disabledText = "#LOC_BDArmory_AI_ManeuverRCS_disabledText", scene = UI_Scene.All),]//Always--Combat Only + public bool ManeuverRCS = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ReverseEngines"),//Use reverse engines + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool ReverseThrust = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EngineRCSRotation"),//Use engines as RCS for rotation + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool EngineRCSRotation = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EngineRCSTranslation"),//Use engines as RCS for translation + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool EngineRCSTranslation = true; + #endregion + + #region Speeds + [KSPField(isPersistant = true, + guiActive = true, + guiActiveEditor = true, + guiName = "#LOC_BDArmory_AI_ManeuverSpeed", + guiUnits = " m/s"), + UI_FloatSemiLogRange( + minValue = 10f, + maxValue = 10000f, + stepIncrement = 10f, + scene = UI_Scene.All + )] + public float ManeuverSpeed = 100f; + + [KSPField(isPersistant = true, + guiActive = true, + guiActiveEditor = true, + guiName = "#LOC_BDArmory_AI_FiringSpeedMin", + guiUnits = " m/s"), + UI_FloatSemiLogRange( + minValue = 2f, + maxValue = 1000f, + scene = UI_Scene.All, + withZero = true + )] + public float minFiringSpeed = 0f; + + [KSPField(isPersistant = true, + guiActive = true, + guiActiveEditor = true, + guiName = "#LOC_BDArmory_AI_FiringSpeedLimit", + guiUnits = " m/s"), + UI_FloatSemiLogRange( + minValue = 1f, + maxValue = 10000f, + reducedPrecisionAtMin = true, + scene = UI_Scene.All + )] + public float firingSpeed = 50f; + + [KSPField(isPersistant = true, + guiActive = true, + guiActiveEditor = true, + guiName = "#LOC_BDArmory_AI_AngularSpeedLimit", + guiUnits = " m/s"), + UI_FloatSemiLogRange( + minValue = 1f, + maxValue = 1000f, + scene = UI_Scene.All + )] + public float firingAngularVelocityLimit = 10f; + #endregion + + #region Evade + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinEvasionTime", advancedTweakable = true, // Min Evasion Time + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + 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_AI_EvasionThreshold", advancedTweakable = true, //Evasion Distance Threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float evasionThreshold = 25f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionTimeThreshold", advancedTweakable = true, // Evasion Time Threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + 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_AI_EvasionErraticness", advancedTweakable = true, // Evasion Erraticness + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float evasionErraticness = 0.1f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionMinRangeThreshold", advancedTweakable = true, // Evasion Min Range Threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatSemiLogRange(minValue = 10f, maxValue = 10000f, sigFig = 1, withZero = true)] + public float evasionMinRangeThreshold = 10f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionRCS", advancedTweakable = true,//Use RCS for gun evasion + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),]//Enabled--Disabled + public bool evasionRCS = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionEngines", advancedTweakable = true,//Use engines for gun evasion + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),]//Enabled--Disabled + public bool evasionEngines = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe", advancedTweakable = true,//Ignore my target targeting me + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool evasionIgnoreMyTargetTargetingMe = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CollisionAvoidanceThreshold", advancedTweakable = true, //Vessel collision avoidance threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float collisionAvoidanceThreshold = 50f; // 20m + target's average radius. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod", advancedTweakable = true, //Vessel collision avoidance look ahead period + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 30f, stepIncrement = 0.5f, scene = UI_Scene.All)] + public float vesselCollisionAvoidanceLookAheadPeriod = 10f; // Look 10s ahead for potential collisions. + #endregion + + + // Debugging + internal float distToCPA; + internal float timeToCPA; + internal string timeToCPAString; + internal float stoppingDist; + internal Vector3 debugTargetPosition; + internal Vector3 debugTargetDirection; + internal Vector3 debugRollTarget; + + // Dynamic measurements + float dynAngAccel = 1f; // Start at reasonable value. + float lastAngVel = 1f; // Start at reasonable value. + float dynDecayRate = 1f; // Decay rate for dynamic measurements. Set to a half-life of 60s in ActivatePilot. + + /// + /// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + + Vector3 upDir; + + #endregion + + #region Status Mode + public enum StatusMode { Idle, AvoidingCollision, Evading, CorrectingOrbit, Withdrawing, Ramming, Firing, Maneuvering, Stranded, Commanded, Custom } + public StatusMode currentStatusMode = StatusMode.Idle; + StatusMode lastStatusMode = StatusMode.Idle; + protected override void SetStatus(string status) + { + if (evadingGunfire && (evasionRCS || evasionEngines)) + status += evasionString; + + base.SetStatus(status); + if (status.StartsWith("Idle")) currentStatusMode = StatusMode.Idle; + else if (status.StartsWith("Avoiding Collision")) currentStatusMode = StatusMode.AvoidingCollision; + else if (status.StartsWith("Correcting Orbit")) currentStatusMode = StatusMode.CorrectingOrbit; + else if (status.StartsWith("Evading")) currentStatusMode = StatusMode.Evading; + else if (status.StartsWith("Withdrawing")) currentStatusMode = StatusMode.Withdrawing; + else if (status.StartsWith("Ramming")) currentStatusMode = StatusMode.Ramming; + else if (status.StartsWith("Firing")) currentStatusMode = StatusMode.Firing; + else if (status.StartsWith("Maneuvering")) currentStatusMode = StatusMode.Maneuvering; + else if (status.StartsWith("Stranded")) currentStatusMode = StatusMode.Stranded; + else if (status.StartsWith("Commanded")) currentStatusMode = StatusMode.Commanded; + else currentStatusMode = StatusMode.Custom; + } + #endregion + + #region RMB info in editor + + // Yes + public override string GetInfo() + { + // known bug - the game caches the RMB info, changing the variable after checking the info + // does not update the info. :( No idea how to force an update. + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Available settings:"); + sb.AppendLine($"- Min Engagement Range - AI will try to move away from oponents if closer than this range"); + sb.AppendLine($"- RCS Active - Use RCS during any maneuvers, or only in combat"); + sb.AppendLine($"- Maneuver Speed - Max speed relative to target during intercept maneuvers"); + sb.AppendLine($"- Strafing Speed - Max speed relative to target during gun firing"); + if (GameSettings.ADVANCED_TWEAKABLES) + { + sb.AppendLine($"- Min Evasion Time - Minimum seconds AI will evade for"); + sb.AppendLine($"- Evasion Distance Threshold - How close incoming gunfire needs to come to trigger evasion"); + sb.AppendLine($"- Evasion Time Threshold - How many seconds the AI needs to be under fire to begin evading"); + sb.AppendLine($"- Evasion Min Range Threshold - Attacker needs to be beyond this range to trigger evasion"); + sb.AppendLine($"- Don't Evade My Target - Whether gunfire from the current target is ignored for evasion"); + } + return sb.ToString(); + } + + #endregion RMB info in editor + + #region UI Initialisers and Callbacks + protected void SetSliderPairClamps(string fieldNameMin, string fieldNameMax) + { + // Enforce min <= max for pairs of sliders + UI_FloatRange field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields[fieldNameMin].uiControlFlight : Fields[fieldNameMin].uiControlEditor); + field.onFieldChanged = OnMinUpdated; + field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields[fieldNameMax].uiControlFlight : Fields[fieldNameMax].uiControlEditor); + field.onFieldChanged = OnMaxUpdated; + } + + public void OnMinUpdated(BaseField field = null, object obj = null) + { + if (firingSpeed < minFiringSpeed) { firingSpeed = minFiringSpeed; } // Enforce min < max for firing speeds. + if (ManeuverSpeed < firingSpeed) { ManeuverSpeed = firingSpeed; } // Enforce firing < maneuver for firing/maneuver speeds. + if (ForceFiringRange < MinEngagementRange) { ForceFiringRange = MinEngagementRange; } // Enforce MinEngagementRange < ForceFiringRange + } + + public void OnMaxUpdated(BaseField field = null, object obj = null) + { + if (minFiringSpeed > firingSpeed) { minFiringSpeed = firingSpeed; } // Enforce min < max for firing speeds. + if (firingSpeed > ManeuverSpeed) { firingSpeed = ManeuverSpeed; } // Enforce firing < maneuver for firing/maneuver speeds. + if (MinEngagementRange > ForceFiringRange) { MinEngagementRange = ForceFiringRange; } // Enforce MinEngagementRange < ForceFiringRange + } + #endregion + + #region events + + public override void OnStart(StartState state) + { + base.OnStart(state); + if (!(HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor)) return; + SetChooseOptions(); + SetSliderPairClamps("minFiringSpeed", "firingSpeed"); + SetSliderPairClamps("firingSpeed", "ManeuverSpeed"); + SetSliderPairClamps("MinEngagementRange", "ForceFiringRange"); + if (HighLogic.LoadedSceneIsFlight) + { + GameEvents.onVesselPartCountChanged.Add(CalculateAvailableTorque); + GameEvents.onVesselPartCountChanged.Add(CheckEngineLists); + } + CalculateAvailableTorque(vessel); + ECID = PartResourceLibrary.Instance.GetDefinition("ElectricCharge").id; // This should always be found. + } + + protected override void OnDestroy() + { + GameEvents.onVesselPartCountChanged.Remove(CalculateAvailableTorque); + GameEvents.onVesselPartCountChanged.Remove(CheckEngineLists); + base.OnDestroy(); + } + + public override bool ActivatePilot() + { + if (!base.ActivatePilot()) return false; + TakingOff = false; + dynDecayRate = Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime / 60f); // Decay rate for a half-life of 60s. + //originalMaxSpeed = ManeuverSpeed; + if (!fc) + { + fc = gameObject.AddComponent(); + fc.vessel = vessel; + + fc.alignmentToleranceforBurn = 7.5f; + fc.throttleLerpRate = 3; + } + fc.Activate(); + foreach (var engine in VesselModuleRegistry.GetModuleEngines(vessel)) // Save indepedent throttle settings + { + if (engine.throttleLocked || !engine.allowShutdown || !engine.allowRestart) continue; // Ignore engines that can't be throttled, shutdown, or restart + if (VesselSpawning.SpawnUtils.IsModularMissilePart(engine.part)) continue; // Ignore modular missile engines. + if (engineIndependentThrottleState.ContainsKey(engine.part.persistentId)) continue; // don't re-add engines + engineIndependentThrottleState.Add(engine.part.persistentId, new(engine.independentThrottle, engine.independentThrottlePercentage, engine.thrustPercentage)); + } + UpdateEngineLists(true); // Update engine list, turn off reverse engines if active + return true; + } + + public override void DeactivatePilot() + { + base.DeactivatePilot(); + + if (fc) + { + fc.Deactivate(); + fc = null; + } + foreach (var engine in VesselModuleRegistry.GetModuleEngines(vessel)) // Restore indepedent throttle settings + { + if (engine.throttleLocked || !engine.allowShutdown || !engine.allowRestart) continue; // Ignore engines that can't be throttled, shutdown, or restart + if (VesselSpawning.SpawnUtils.IsModularMissilePart(engine.part)) continue; // Ignore modular missile engines. + if (engineIndependentThrottleState.ContainsKey(engine.part.persistentId)) + { + engine.independentThrottle = engineIndependentThrottleState[engine.part.persistentId].Item1; + engine.independentThrottlePercentage = engineIndependentThrottleState[engine.part.persistentId].Item2; + engine.thrustPercentage = engineIndependentThrottleState[engine.part.persistentId].Item3; + } + } + engineIndependentThrottleState.Clear(); + evadingGunfire = false; + SetStatus(""); + } + + public void SetChooseOptions() + { + UI_ChooseOption pidmode = (UI_ChooseOption)(HighLogic.LoadedSceneIsFlight ? Fields["pidMode"].uiControlFlight : Fields["pidMode"].uiControlEditor); + pidmode.onFieldChanged = ChooseOptionsUpdated; + } + + public void ChooseOptionsUpdated(BaseField field, object obj) + { + this.part.RefreshAssociatedWindows(); + if (BDArmoryAIGUI.Instance != null) + { + BDArmoryAIGUI.Instance.SetChooseOptionSliders(); + } + } + + protected override void OnGUI() + { + base.OnGUI(); + + if (!pilotEnabled || !vessel.isActiveVessel) return; + + if (!BDArmorySettings.DEBUG_LINES) return; + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, PIDActive ? debugTargetPosition : fc.attitude * 1000, 5, Color.red); // The point we're asked to turn to + if (fc.thrustDirection != fc.attitude) GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, fc.thrustDirection * 100, 5, Color.yellow); // Thrust direction + if (PIDActive) GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugTargetDirection, 5, Color.green); // The direction PID control will actually turn to + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, fc.RCSVector * 100, 5, Color.cyan); // RCS command + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, fc.RCSVectorLerped * 100, 5, Color.magenta); // RCS lerped command + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + debugRollTarget, 2, Color.blue); // Roll target + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vesselTransform.up * 1000, 3, Color.white); + if (currentStatusMode == StatusMode.AvoidingCollision) GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, currentlyAvoidedVessel.transform.position, 8, Color.grey); // Collision avoidance + } + + #endregion events + + #region Actual AI Pilot + protected override void AutoPilot(FlightCtrlState s) + { + // Update vars + InitialFrameUpdates(); + UpdateStatus(); // Combat decisions, evasion, maneuverStateChanged = true and set new statusMode, etc. + + maneuverTime += Time.fixedDeltaTime; + if (maneuverStateChanged || maneuverTime > minManeuverTime) + { + maneuverTime = 0; + evasionNonLinearityDirection = UnityEngine.Random.onUnitSphere; + fc.lerpAttitude = true; + minManeuverTime = combatUpdateInterval; + switch (currentStatusMode) + { + case StatusMode.AvoidingCollision: + minManeuverTime = emergencyUpdateInterval; + break; + case StatusMode.Evading: + minManeuverTime = emergencyUpdateInterval; + break; + case StatusMode.CorrectingOrbit: + break; + case StatusMode.Withdrawing: + { + var weaponManager = WeaponManager; + // Determine the direction. + Vector3 averagePos = Vector3.zero; + using (List.Enumerator target = BDATargetManager.TargetList(weaponManager.Team).GetEnumerator()) + while (target.MoveNext()) + { + if (target.Current == null) continue; + if (target.Current && target.Current.Vessel && weaponManager.CanSeeTarget(target.Current)) + { + averagePos += FromTo(vessel, target.Current.Vessel).normalized; + } + } + + Vector3 direction = -averagePos.normalized; + Vector3 orbitNormal = vessel.orbit.Normal(Planetarium.GetUniversalTime()); + bool facingNorth = Vector3.Dot(direction, orbitNormal) > 0; + trackedDeltaV = 200; + attitudeCommand = (orbitNormal * (facingNorth ? 1 : -1)).normalized; + } + break; + case StatusMode.Ramming: + break; + case StatusMode.Commanded: + { + lastUpdateCommand = currentCommand; + if (maneuverStateChanged) + { + if (currentCommand == PilotCommands.Follow) + attitudeCommand = commandLeader.transform.up; + else + attitudeCommand = (assignedPositionWorld - vessel.transform.position).normalized; + } + minManeuverTime = 30f; + trackedDeltaV = 200; + } + break; + case StatusMode.Firing: + fc.lerpAttitude = false; + break; + case StatusMode.Maneuvering: + break; + case StatusMode.Stranded: + break; + default: // Idle + break; + } + } + Maneuver(); // Set attitude, alignment tolerance, throttle, update RCS if needed + if (PIDActive) + AttitudeControl(s); + AddDebugMessages(); + } + + void InitialFrameUpdates() + { + upDir = vessel.up; + UpdateBody(); + UpdateEngineLists(); + CalculateAngularAcceleration(); + maxAcceleration = GetMaxAcceleration(); + fc.alignmentToleranceforBurn = 7.5f; + if (fc.throttle > 0) + lastFiringSolution = Vector3.zero; // Forget prior firing solution if we recently used engines + fc.throttle = 0; + fc.lerpThrottle = true; + fc.useReverseThrust = false; + fc.thrustDirection = Vector3.zero; + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, ManeuverRCS); + maneuverStateChanged = false; + } + + void Maneuver() + { + Vector3 rcsVector = Vector3.zero; + var weaponManager = WeaponManager; + + switch (currentStatusMode) + { + case StatusMode.AvoidingCollision: + { + SetStatus("Avoiding Collision"); + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + + Vector3 safeDirection = -collisionAvoidDirection; + + fc.attitude = safeDirection; + fc.alignmentToleranceforBurn = 70; + fc.throttle = 1; + fc.lerpThrottle = false; + rcsVector = safeDirection; + } + break; + case StatusMode.Evading: + { + SetStatus("Evading Missile"); + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + + Vector3 incomingVector = FromTo(vessel, weaponManager.incomingMissileVessel); + Vector3 dodgeVector = Vector3.ProjectOnPlane(vessel.ReferenceTransform.up, incomingVector.normalized); + + fc.attitude = dodgeVector; + fc.alignmentToleranceforBurn = 70; + fc.throttle = 1; + fc.lerpThrottle = false; + rcsVector = dodgeVector; + } + break; + case StatusMode.CorrectingOrbit: + { + Orbit o = vessel.orbit; + double UT = Planetarium.GetUniversalTime(); + fc.alignmentToleranceforBurn = 15f; + var descending = o.timeToPe > 0 && o.timeToPe < o.timeToAp; + if (o.altitude > minSafeAltitude && ( + (ongoingOrbitCorrectionDueTo == OrbitCorrectionReason.None && EscapingOrbit()) || + (ongoingOrbitCorrectionDueTo == OrbitCorrectionReason.Escaping && (EscapingOrbit() || (o.ApA > 0.1f * safeAltBody.sphereOfInfluence))))) + { + // Vessel is on an escape orbit and has passed the periapsis by over 60s, burn retrograde + SetStatus("Correcting Orbit (On escape trajectory)"); + ongoingOrbitCorrectionDueTo = OrbitCorrectionReason.Escaping; + + fc.attitude = -o.GetPrograde(UT); + fc.throttle = 1; + } + else if (descending && o.PeA < minSafeAltitude && ( + (ongoingOrbitCorrectionDueTo == OrbitCorrectionReason.None && o.ApA >= minSafeAltitude && o.altitude >= minSafeAltitude) || + (ongoingOrbitCorrectionDueTo == OrbitCorrectionReason.PeriapsisLow && o.altitude > minSafeAltitude * 1.1f))) + { + // We are outside the atmosphere but our periapsis is inside the atmosphere. + // Execute a burn to circularize our orbit at the current altitude. + SetStatus("Correcting Orbit (Circularizing)"); + ongoingOrbitCorrectionDueTo = OrbitCorrectionReason.PeriapsisLow; + + Vector3d fvel = Math.Sqrt(o.referenceBody.gravParameter / o.GetRadiusAtUT(UT)) * o.Horizontal(UT); + Vector3d deltaV = fvel - vessel.GetObtVelocity(); + fc.attitude = deltaV.normalized; + fc.throttle = Mathf.Lerp(0, 1, (float)(deltaV.sqrMagnitude / 100)); + } + else + { + if (o.ApA < minSafeAltitude * 1.1f) + { + // Entirety of orbit is inside atmosphere, perform gravity turn burn until apoapsis is outside atmosphere by a 10% margin. + SetStatus("Correcting Orbit (Apoapsis too low)"); + ongoingOrbitCorrectionDueTo = OrbitCorrectionReason.ApoapsisLow; + + double gravTurnAlt = 0.1; + float turn; + + if (o.altitude < gravTurnAlt * minSafeAltitude || descending || wasDescendingUnsafe) // At low alts or when descending, burn straight up + { + turn = 1f; + fc.alignmentToleranceforBurn = 45f; // Use a wide tolerance as aero forces could make it difficult to align otherwise. + wasDescendingUnsafe = descending || o.timeToAp < 10; // Hysteresis for upwards vs gravity turn burns. + } + else // At higher alts, gravity turn towards horizontal orbit vector + { + turn = Mathf.Clamp((float)((1.1 * minSafeAltitude - o.ApA) / (minSafeAltitude * (1.1 - gravTurnAlt))), 0.1f, 1f); + turn = Mathf.Clamp(Mathf.Log10(turn) + 1f, 0.33f, 1f); + fc.alignmentToleranceforBurn = Mathf.Clamp(15f * turn, 5f, 15f); + wasDescendingUnsafe = false; + } + + fc.attitude = Vector3.Lerp(o.Horizontal(UT), upDir, turn); + fc.throttle = 1; + } + else if (descending && o.altitude < minSafeAltitude * 1.1f) + { + // Our apoapsis is outside the atmosphere but we are inside the atmosphere and descending. + // Burn up until we are ascending and our apoapsis is outside the atmosphere by a 10% margin. + SetStatus("Correcting Orbit (Falling inside atmo)"); + ongoingOrbitCorrectionDueTo = OrbitCorrectionReason.FallingInsideAtmosphere; + + fc.attitude = o.Radial(UT); + fc.alignmentToleranceforBurn = 45f; // Use a wide tolerance as aero forces could make it difficult to align otherwise. + fc.throttle = 1; + } + else + { + SetStatus("Correcting Orbit (Drifting)"); + ongoingOrbitCorrectionDueTo = OrbitCorrectionReason.None; + } + } + } + break; + case StatusMode.Commanded: + { + // We have been given a command from the WingCommander to fly/follow/attack in a general direction + // Burn for 200 m/s then coast remainder of 30s period + switch (currentCommand) + { + case PilotCommands.Follow: + SetStatus("Commanded to Follow Leader"); + break; + case PilotCommands.Attack: + SetStatus("Commanded to Attack"); + break; + default: // Fly To + SetStatus("Commanded to Position"); + break; + } + trackedDeltaV -= Vector3.Project(vessel.acceleration, attitudeCommand).magnitude * TimeWarp.fixedDeltaTime; + fc.attitude = attitudeCommand; + fc.throttle = (trackedDeltaV > 10) ? 1 : 0; + } + break; + case StatusMode.Withdrawing: + { + SetStatus("Withdrawing"); + + // Withdraw sequence. Locks behaviour while burning 200 m/s of delta-v either north or south. + trackedDeltaV -= Vector3.Project(vessel.acceleration, attitudeCommand).magnitude * TimeWarp.fixedDeltaTime; + fc.attitude = attitudeCommand; + fc.throttle = (trackedDeltaV > 10) ? 1 : 0; + fc.alignmentToleranceforBurn = 70; + } + break; + case StatusMode.Ramming: + { + SetStatus("Ramming Speed!"); + + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + + // Target information + Vector3 targetPosition = targetVessel.CoM; + Vector3 targetVector = targetPosition - vessel.CoM; + Vector3 relVel = vessel.GetObtVelocity() - targetVessel.GetObtVelocity(); + Vector3 relVelNrm = relVel.normalized; + Vector3 interceptVector; + float relVelmag = relVel.magnitude; + + float timeToImpact = BDAMath.SolveTime(targetVector.magnitude, maxAcceleration, Vector3.Dot(relVel, targetVector.normalized)); + Vector3 lead = -timeToImpact * relVelmag * relVelNrm; + interceptVector = (targetPosition + lead) - vessel.CoM; + + fc.attitude = interceptVector; + fc.thrustDirection = interceptVector; + fc.throttle = 1f; + fc.alignmentToleranceforBurn = 25f; + + rcsVector = -Vector3.ProjectOnPlane(relVel, vesselTransform.up); + } + break; + case StatusMode.Firing: + { + // Aim at appropriate point to fire guns/launch missiles + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + + fc.lerpAttitude = false; + Vector3 firingSolution = BroadsideAttack ? BroadsideAttitude(vessel, targetVessel) : FromTo(vessel, targetVessel).normalized; + Vector3 relVel = RelVel(vessel, targetVessel); + if (FiringRCS) + { + float targetSpeed = FiringTargetSpeed(); + float margin = Mathf.Max(Mathf.Abs(firingSpeed - minFiringSpeed) * 0.1f, 2f); + float minSpeed = Mathf.Clamp(targetSpeed - margin, minFiringSpeed, firingSpeed); + float maxSpeed = Mathf.Clamp(targetSpeed + margin, minFiringSpeed, firingSpeed); + if (minFiringSpeed == 0 || relVel.sqrMagnitude > maxSpeed * maxSpeed) + rcsVector = -Vector3.ProjectOnPlane(relVel, FromTo(vessel, targetVessel)); + else if (relVel.sqrMagnitude < minSpeed * minSpeed) + rcsVector = Vector3.ProjectOnPlane(relVel, FromTo(vessel, targetVessel)); + } + + if (weaponManager.currentGun && GunReady(weaponManager.currentGun)) + { + SetStatus("Firing Guns"); + firingSolution = GunFiringSolution(weaponManager.currentGun); + } + else if (weaponManager.CurrentMissile && !weaponManager.GetLaunchAuthorization(targetVessel, weaponManager, weaponManager.CurrentMissile)) + { + SetStatus("Firing Missiles"); + firingSolution = MissileGuidance.GetAirToAirFireSolution(weaponManager.CurrentMissile, targetVessel); + firingSolution = (firingSolution - vessel.transform.position).normalized; + } + else + SetStatus("Firing"); + + lastFiringSolution = firingSolution; + fc.attitude = firingSolution; + fc.throttle = 0; + } + break; + case StatusMode.Maneuvering: + { + Vector3 toTarget = FromTo(vessel, targetVessel).normalized; + + TimeSpan t = TimeSpan.FromSeconds(Mathf.Min(timeToCPA, 86400f)); // Clamp at one day 24*60*60s + timeToCPAString = string.Format((t.Hours > 0 ? "{0:D2}h:" : "") + (t.Minutes > 0 ? "{1:D2}m:" : "") + "{2:D2}s", t.Hours, t.Minutes, t.Seconds); + + float minRange = interceptRanges.x; + float maxRange = interceptRanges.y; + float interceptRange = interceptRanges.z; + + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + + float currentRange = VesselDistance(vessel, targetVessel); + Vector3 relVel = RelVel(vessel, targetVessel); + + float speedTarget = KillVelocityTargetSpeed(); + bool killVelOngoing = currentStatus.Contains("Kill Velocity"); + bool interceptOngoing = currentStatus.Contains("Intercept Target") && !OnIntercept(0.05f) && !ApproachingIntercept(); + + if (currentRange < minRange && AwayCheck(minRange)) // Too close, maneuever away + { + SetStatus("Maneuvering (Away)"); + fc.throttle = 1; + fc.alignmentToleranceforBurn = 135; + fc.thrustDirection = -toTarget; + if (UseForwardThrust(fc.thrustDirection)) + fc.attitude = -toTarget; + else + fc.attitude = toTarget; + fc.throttle = Vector3.Dot(RelVel(vessel, targetVessel), fc.thrustDirection) < ManeuverSpeed ? 1 : 0; + } + else if (hasPropulsion && (relVel.sqrMagnitude > speedTarget * speedTarget) && (ApproachingIntercept(currentStatus.Contains("Kill Velocity") ? 1.5f : 0f) || killVelOngoing)) // Approaching intercept point, kill velocity + KillVelocity(true); + else if (hasPropulsion && interceptOngoing || (currentRange > maxRange && CanInterceptShip(targetVessel) && !OnIntercept(currentStatus.Contains("Intercept Target") ? 0.05f : interceptMargin))) // Too far away, intercept target + InterceptTarget(); + else if (currentRange > interceptRange && OnIntercept(interceptMargin)) + { + fc.throttle = 0; + fc.thrustDirection = -toTarget; + if (ApproachingIntercept(3f)) + { + Vector3 killVel = (relVel + targetVessel.perturbation).normalized; + fc.thrustDirection = killVel; + if (UseForwardThrust(fc.thrustDirection)) + fc.attitude = killVel; + else + fc.attitude = -killVel; + } + else + fc.attitude = toTarget; + SetStatus($"Maneuvering (On Intercept), {timeToCPAString}"); + } + else // Within weapons range, adjust velocity and attitude for targeting + { + bool killAngOngoing = currentStatus.Contains("Kill Angular Velocity") && (AngularVelocity(vessel, targetVessel, 5f) < firingAngularVelocityLimit / 2); + bool increaseVelOngoing = currentStatus.Contains("Increasing Velocity") && relVel.sqrMagnitude < FiringTargetSpeed() * FiringTargetSpeed(); + if (hasPropulsion && (relVel.sqrMagnitude > firingSpeed * firingSpeed || killVelOngoing)) + { + KillVelocity(); + } + else if (hasPropulsion && targetVessel != null && (AngularVelocity(vessel, targetVessel, 5f) > firingAngularVelocityLimit || killAngOngoing)) + { + SetStatus("Maneuvering (Kill Angular Velocity)"); + fc.attitude = -Vector3.ProjectOnPlane(RelVel(vessel, targetVessel), vessel.PredictPosition(timeToCPA)).normalized; + fc.throttle = 1; + fc.alignmentToleranceforBurn = 45f; + } + else if (hasPropulsion && targetVessel != null && (relVel.sqrMagnitude < minFiringSpeed * minFiringSpeed || increaseVelOngoing)) + { + SetStatus("Maneuvering (Increasing Velocity)"); + Vector3 relPos = targetVessel.CoM - vessel.CoM; + float r = Mathf.Clamp01(currentRange / interceptRanges.y); + Vector3 lateralOffset = Vector3.ProjectOnPlane(relVel, relPos).normalized * Mathf.Max(interceptRanges.z, currentRange); + fc.thrustDirection = Vector3.Lerp(relVel - targetVessel.perturbation, relPos + lateralOffset, r).normalized; + if (UseForwardThrust(fc.thrustDirection)) + fc.attitude = fc.thrustDirection; + else + fc.attitude = -fc.thrustDirection; + fc.throttle = 1; + fc.alignmentToleranceforBurn = 45f; + } + else // Drifting + { + fc.throttle = 0; + fc.attitude = toTarget; + + if (RecentFiringSolution(out Vector3 recentSolution)) // If we had a valid firing solution recently, use it to continue pointing toward target + fc.attitude = recentSolution; + else if (BroadsideAttack) + fc.attitude = BroadsideAttitude(vessel, targetVessel); + + if (currentRange < minRange) + SetStatus("Maneuvering (Drift Away)"); + else + SetStatus("Maneuvering (Drift)"); + } + } + } + break; + case StatusMode.Stranded: + { + SetStatus("Stranded"); + + fc.attitude = GunFiringSolution(weaponManager.previousGun); + fc.throttle = 0; + } + break; + default: // Idle + { + if (hasWeapons) + SetStatus("Idle"); + else + SetStatus("Idle (Unarmed)"); + + fc.attitude = Vector3.zero; + fc.throttle = 0; + } + break; + } + GunEngineEvasion(); + AlignEnginesWithThrust(); + UpdateRCSVector(rcsVector); + UpdateBurnAlignmentTolerance(); + } + + void KillVelocity(bool onIntercept = false) + { + // Slow down to KillVelocityTargetSpeed(). If on an intercept to within gun range, try to slow down as late as possible + // Otherwise, keep burning until at target speed + + Vector3 relPos = targetVessel.CoM - vessel.CoM; + Vector3 relVel = targetVessel.GetObtVelocity() - vessel.GetObtVelocity(); + float targetSpeed = KillVelocityTargetSpeed(); + bool maintainThrottle = (relVel.sqrMagnitude > targetSpeed * targetSpeed); + fc.thrustDirection = (relVel + targetVessel.perturbation).normalized; + bool useReverseThrust = !UseForwardThrust(fc.thrustDirection) && (Vector3.Dot(relPos, relVel) < 0f); + if (onIntercept) + { + Vector3 relAccel = targetVessel.perturbation - vessel.perturbation; + Vector3 toIntercept = Intercept(relPos, relVel); + float distanceToIntercept = toIntercept.magnitude; + float timeToIntercept = vessel.TimeToCPA(toIntercept, targetVessel.GetObtVelocity(), targetVessel.perturbation, (float)vessel.orbit.period / 4f); // Avoid checking too far ahead (in case of multiple intercepts) + float cpaDistSqr = AIUtils.PredictPosition(relPos, relVel, relAccel, timeToIntercept).sqrMagnitude; + float interceptRangeMargin = Mathf.Min(interceptRanges.z * (1f + interceptMargin + (useReverseThrust ? 0.25f : 0f)), interceptRanges.y); // Extra margin when reverse thrusting since still facing target + var weaponManager = WeaponManager; + if (cpaDistSqr < weaponManager.gunRange * weaponManager.gunRange) // Gun range intercept, balance between throttle actions and intercept accuracy + maintainThrottle = relPos.sqrMagnitude < (interceptRangeMargin * interceptRangeMargin) || // Within intercept range margin + (maintainThrottle && Vector3.Dot(relPos, relVel) > 0f) || // Moving away from target faster than target speed, timeToIntecept == 0 does not always work here because it's possible we are on an orbit with two intersection points + ApproachingIntercept(); // Stopping distance > distance to target + //else missile range intercept, exact positioning matters less + + SetStatus($"Maneuvering (Kill Velocity), {timeToCPAString}, {distanceToIntercept:N0}m"); + } + else + { + SetStatus($"Maneuvering (Kill Velocity)"); + } + + if (useReverseThrust) // Use reverse thrust if possible and we are closing to target + fc.attitude = -fc.thrustDirection; + else + fc.attitude = fc.thrustDirection; + fc.throttle = maintainThrottle ? 1f : 0f; + fc.alignmentToleranceforBurn = 45f; + } + + void InterceptTarget() + { + SetStatus($"Maneuvering (Intercept Target), {timeToCPAString}, {distToCPA:N0}m"); + Vector3 relPos = targetVessel.CoM - vessel.CoM; + Vector3 relVel = targetVessel.GetObtVelocity() - vessel.GetObtVelocity(); + + // Burn the difference between the target and current velocities. + Vector3 toIntercept = Intercept(relPos, relVel); + Vector3 burn = toIntercept.normalized * ManeuverSpeed + relVel; + fc.thrustDirection = burn.normalized; + + // Use reverse thrust if it is necessary to face target during intercept + bool useReverseThrust = ReverseThrust && (Vector3.Dot(relPos, fc.thrustDirection) < 0f); + if (useReverseThrust) + fc.attitude = -fc.thrustDirection; + else + fc.attitude = fc.thrustDirection; + fc.throttle = 1f; + } + + void AddDebugMessages() + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + debugString.AppendLine($"Current Status: {currentStatus}"); + debugString.AppendLine($"Propulsion:{hasPropulsion}; RCS:{hasRCS}; EC:{hasEC}; Weapons:{hasWeapons}"); + debugString.AppendLine($"Max Acceleration:{maxAcceleration:G3}; Intercept Velocity:{KillVelocityTargetSpeed():G3}"); + if (targetVessel) + { + debugString.AppendLine($"Target Vessel: {targetVessel.GetDisplayName()}"); + debugString.AppendLine($"Can Intercept: {CanInterceptShip(targetVessel)}, On Intercept: {OnIntercept(currentStatus.Contains("Intercept Target") ? 0.05f : 0.25f)}"); + debugString.AppendLine($"Target Range: {VesselDistance(vessel, targetVessel):G3}"); + debugString.AppendLine($"Min/Max/Intercept Range: {interceptRanges.x}/{interceptRanges.y}/{interceptRanges.z}"); + debugString.AppendLine($"Time to CPA: {timeToCPA:G3}"); + debugString.AppendLine($"Distance to CPA: {distToCPA:G3}"); + debugString.AppendLine($"Stopping Distance: {stoppingDist:G3}"); + debugString.AppendLine($"Apoapsis: {vessel.orbit.ApA / 1000:G2}km / {vessel.orbit.timeToAp:G2}s"); + debugString.AppendLine($"Periapsis: {vessel.orbit.PeA / 1000:G2}km / {vessel.orbit.timeToPe:G2}s"); + debugString.AppendLine($"Missile Launch Fail Timer: {missileTryLaunchTime:G2}s"); + } + debugString.AppendLine($"Evasive {evasiveTimer}s"); + var weaponManager = WeaponManager; + if (weaponManager) debugString.AppendLine($"Threat Sqr Distance: {weaponManager.incomingThreatDistanceSqr}"); + } + } + + void UpdateStatus() + { + // Update propulsion and weapon status + hasRCS = VesselModuleRegistry.GetModules(vessel).Any(e => e.rcsEnabled && !e.flameout) || (rcsEngines.Count > 0 && rcsEngines.Any(e => e != null && e.EngineIgnited && e.isOperational && !e.flameout)); + hasPropulsion = VesselModuleRegistry.GetModules(vessel).Any(e => e.rcsEnabled && !e.flameout && e.useThrottle) || + forwardEngines.Any(e => e != null && e.EngineIgnited && e.isOperational && !e.flameout) || + reverseEngines.Any(e => e != null && e.EngineIgnited && e.isOperational && !e.flameout); + vessel.GetConnectedResourceTotals(ECID, out double EcCurrent, out double ecMax); + hasEC = EcCurrent > 0 || CheatOptions.InfiniteElectricity; + var weaponManager = WeaponManager; + hasWeapons = (weaponManager != null) && weaponManager.HasWeaponsAndAmmo(); + + // Check on command status + UpdateCommand(); + + // Update intercept ranges and time to CPA + interceptRanges = InterceptionRanges(); //.x = minRange, .y = maxRange, .z = interceptRange + float targetSqrDist = 0f; + float forceFiringRangeSqr = Mathf.Min(ForceFiringRange, interceptRanges.y); + forceFiringRangeSqr *= forceFiringRangeSqr; + if (targetVessel != null) + { + timeToCPA = vessel.TimeToCPA(targetVessel); + targetSqrDist = FromTo(vessel, targetVessel).sqrMagnitude; + } + + // Prioritize safe orbits over combat outside of weapon range + bool fixOrbitNow = hasPropulsion && (CheckOrbitDangerous() || ongoingOrbitCorrectionDueTo != OrbitCorrectionReason.None) && currentStatusMode != StatusMode.Ramming; + bool fixOrbitLater = false; + if (hasPropulsion && !fixOrbitNow && CheckOrbitUnsafe()) + { + fixOrbitLater = true; + if (weaponManager && targetVessel != null && currentStatusMode != StatusMode.Ramming) + fixOrbitNow = ((vessel.CoM - targetVessel.CoM).sqrMagnitude > interceptRanges.y * interceptRanges.y) && (timeToCPA > 10f); + } + + // Update status mode + if (currentStatusMode != StatusMode.Ramming && FlyAvoidOthers()) + currentStatusMode = StatusMode.AvoidingCollision; + else if (weaponManager && weaponManager.missileIsIncoming && weaponManager.incomingMissileVessel && weaponManager.incomingMissileTime <= weaponManager.evadeThreshold) // Needs to start evading an incoming missile. + currentStatusMode = StatusMode.Evading; + else if (fixOrbitNow) + currentStatusMode = StatusMode.CorrectingOrbit; + else if (currentCommand == PilotCommands.FlyTo || currentCommand == PilotCommands.Follow || currentCommand == PilotCommands.Attack) + { + currentStatusMode = StatusMode.Commanded; + if (currentCommand != lastUpdateCommand) + maneuverStateChanged = true; + } + else if (weaponManager) + { + if (hasPropulsion && !hasWeapons && (allowRamming && !BDArmorySettings.DISABLE_RAMMING) && targetVessel != null) + currentStatusMode = StatusMode.Ramming; + else if (hasPropulsion && !hasWeapons && CheckWithdraw()) + currentStatusMode = StatusMode.Withdrawing; + else if (targetVessel != null && weaponManager.currentGun && GunReady(weaponManager.currentGun)) + currentStatusMode = StatusMode.Firing; // Guns + else if (targetVessel != null && weaponManager.CurrentMissile && !weaponManager.GetLaunchAuthorization(targetVessel, weaponManager, weaponManager.CurrentMissile)) + currentStatusMode = StatusMode.Firing; // Missiles + else if (targetVessel != null && weaponManager.CurrentMissile && weaponManager.guardFiringMissile && currentStatusMode == StatusMode.Firing) + currentStatusMode = StatusMode.Firing; // Post-launch authorization missile firing underway, don't change status from Firing + else if (targetVessel != null && hasWeapons) + { + if (hasPropulsion) + if (targetSqrDist < MinEngagementRange * MinEngagementRange || targetSqrDist > forceFiringRangeSqr) + currentStatusMode = StatusMode.Maneuvering; // Maneuver if outside MinEngagementRange - ForceFiringRange + else + currentStatusMode = StatusMode.Firing; // Else fire with zero throttle (thrust evasion can override this) + else + currentStatusMode = StatusMode.Stranded; + } + else if (fixOrbitLater) + currentStatusMode = StatusMode.CorrectingOrbit; + else + currentStatusMode = StatusMode.Idle; + } + else if (fixOrbitLater) + currentStatusMode = StatusMode.CorrectingOrbit; + else + currentStatusMode = StatusMode.Idle; + + // Flag changed status if necessary + if (lastStatusMode != currentStatusMode || maneuverStateChanged) + { + maneuverStateChanged = true; + lastStatusMode = currentStatusMode; + if (BDArmorySettings.DEBUG_AI) + Debug.Log("[BDArmory.BDModuleOrbitalAI]: Status of " + vessel.vesselName + " changed from " + lastStatusMode + " to " + currentStatus); + } + + // Switch on PID Mode + switch (PIDMode) + { + case PIDModeTypes.Inactive: + PIDActive = false; + break; + case PIDModeTypes.Firing: + PIDActive = currentStatusMode == StatusMode.Firing; + break; + case PIDModeTypes.Everything: + PIDActive = true; + break; + } + + // Temporarily inhibit maneuvers if not evading a missile and waiting for a launched missile to fly to a safe distance + if (currentStatusMode != StatusMode.Evading && weaponManager && weaponManager.PreviousMissile) + { + if ((vessel.CoM - weaponManager.PreviousMissile.vessel.transform.position).sqrMagnitude < vessel.vesselSize.sqrMagnitude) + { + fc.Stability(true); + PIDActive = false; + } + else + fc.Stability(false); + } + else + fc.Stability(false); + + // Disable PID when ramming to prevent vessels from going haywire - FIXME - Figure out why this is happening and fix + if (PIDActive && currentStatusMode == StatusMode.Ramming) + PIDActive = false; + + // Set PID Mode + fc.PIDActive = PIDActive; + if (PIDActive) + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); + // Check for incoming gunfire + EvasionStatus(); + + // Set target as UI target + if (vessel.isActiveVessel && targetVessel && !targetVessel.IsMissile() && (vessel.targetObject == null || vessel.targetObject.GetVessel() != targetVessel)) + { + FlightGlobals.fetch.SetVesselTarget(targetVessel, true); + } + } + + void UpdateCommand() + { + if (command == PilotCommands.Follow && commandLeader is null) + { + ReleaseCommand(); + return; + } + else if (command == PilotCommands.Attack) + { + if (targetVessel != null) + { + ReleaseCommand(false); + return; + } + else + { + var weaponManager = WeaponManager; + if (weaponManager.underAttack || weaponManager.underFire) + { + ReleaseCommand(false); + return; + } + } + } + } + + void EvasionStatus() + { + evadingGunfire = false; + var weaponManager = WeaponManager; + + // Return if evading missile or ramming + if (weaponManager == null || currentStatusMode == StatusMode.Evading || currentStatusMode == StatusMode.Ramming) + { + evasiveTimer = 0; + return; + } + + // Check if we should be evading gunfire, missile evasion is handled separately + float threatRating = evasionThreshold + 1f; // Don't evade by default + if (weaponManager.underFire) + { + if (weaponManager.incomingMissTime >= evasionTimeThreshold && weaponManager.incomingThreatDistanceSqr >= evasionMinRangeThreshold * evasionMinRangeThreshold) // If we haven't been under fire long enough or they're too close, ignore gunfire + threatRating = weaponManager.incomingMissDistance; + } + // If we're currently evading or a threat is significant + if ((evasiveTimer < minEvasionTime && evasiveTimer != 0) || threatRating < evasionThreshold) + { + if (evasiveTimer < minEvasionTime) + { + threatRelativePosition = vessel.GetObtVelocity().normalized + vesselTransform.right; + if (weaponManager) + { + if (weaponManager.underFire) + threatRelativePosition = weaponManager.incomingThreatPosition - vesselTransform.position; + } + } + evadingGunfire = true; + evasionNonLinearityDirection = (evasionNonLinearityDirection + evasionErraticness * UnityEngine.Random.onUnitSphere).normalized; + evasiveTimer += Time.fixedDeltaTime; + + if (evasiveTimer >= minEvasionTime) + evasiveTimer = 0; + } + } + #endregion Actual AI Pilot + + #region Utility Functions + + private bool CheckWithdraw() + { + var nearest = BDATargetManager.GetClosestTarget(WeaponManager); + if (nearest == null) return false; + + return RelVel(vessel, nearest.Vessel).sqrMagnitude < 200 * 200; + } + + private bool CheckOrbitDangerous() + { + Orbit o = vessel.orbit; + bool descending = o.timeToPe > 0 && o.timeToPe < o.timeToAp; + bool fallingInsideAtmo = descending && o.altitude < minSafeAltitude; // Descending inside atmo, OrbitCorrectionReason.FallingInsideAtmosphere + bool dangerousPeriapsis = descending && o.PeA < 0.8f * minSafeAltitude; // Descending & periapsis suggests we are close to falling inside atmosphere, OrbitCorrectionReason.PeriapsisLow + bool entirelyInsideAtmo = o.ApA < minSafeAltitude && o.ApA >= 0; // Entirety of orbit is inside atmosphere, OrbitCorrectionReason.ApoapsisLow + return (fallingInsideAtmo || dangerousPeriapsis || entirelyInsideAtmo); + } + + private bool CheckOrbitUnsafe() + { + Orbit o = vessel.orbit; + bool descending = o.timeToPe > 0 && o.timeToPe < o.timeToAp; + bool escaping = EscapingOrbit(); // Vessel is on an escape orbit and has passed the periapsis by over 60s, OrbitCorrectionReason.Escaping + bool periapsisLow = descending && o.PeA < minSafeAltitude && o.ApA >= minSafeAltitude && o.altitude >= minSafeAltitude; // We are outside the atmosphere but our periapsis is inside the atmosphere, OrbitCorrectionReason.PeriapsisLow + return (escaping || periapsisLow); // Match conditions in PilotLogic + } + + private void UpdateBody() + { + if (vessel.orbit.referenceBody != safeAltBody) // Body has been updated, update min safe alt + { + minSafeAltitude = vessel.orbit.referenceBody.MinSafeAltitude(); + safeAltBody = vessel.orbit.referenceBody; + } + } + + private bool EscapingOrbit() + { + return (vessel.orbit.ApA < 0 && vessel.orbit.timeToPe < -60); + } + + private bool CanInterceptShip(Vessel target) + { + bool canIntercept = false; + + // Is it worth us chasing a withdrawing ship? + BDModuleOrbitalAI targetAI = target.ActiveController().OrbitalAI; + + if (targetAI && targetAI.pilotEnabled) + { + Vector3 toTarget = target.CoM - vessel.CoM; + bool escaping = targetAI.currentStatusMode == StatusMode.Withdrawing; + var weaponManager = WeaponManager; + + canIntercept = !escaping || // It is not trying to escape. + toTarget.sqrMagnitude < weaponManager.gunRange * weaponManager.gunRange || // It is already in range. + maxAcceleration * maxAcceleration > targetAI.vessel.acceleration_immediate.sqrMagnitude || // We are faster (currently). + Vector3.Dot(target.GetObtVelocity() - vessel.GetObtVelocity(), toTarget) < 0; // It is getting closer. + } + return canIntercept; + } + public float BurnTime(float deltaV, float totalConsumption, bool useReverseThrust) + { + float thrust = maxThrust * ((useReverseThrust && currentForwardThrust) ? reverseForwardThrustRatio : 1f); + if (totalConsumption == 0f) + return ((float)vessel.totalMass * deltaV / thrust); + else + { + float isp = thrust / totalConsumption; + return ((float)vessel.totalMass * (1.0f - 1.0f / Mathf.Exp(deltaV / isp)) / totalConsumption); + } + } + public float StoppingDistance(float speed, bool useReverseThrust) + { + float consumptionRate = GetConsumptionRate(useReverseThrust); + float time = BurnTime(speed, consumptionRate, useReverseThrust); + float f = ((useReverseThrust && currentForwardThrust) ? reverseForwardThrustRatio : 1f); + float jerk = (float)(f * maxThrust / (vessel.totalMass - consumptionRate)) - f * maxAcceleration; + return speed * time + 0.5f * -(f * maxAcceleration) * time * time + 1 / 6 * -jerk * time * time * time; + } + + private bool ApproachingIntercept(float margin = 0.0f) + { + Vector3 relPos = targetVessel.CoM - vessel.CoM; + Vector3 relVel = targetVessel.GetObtVelocity() - vessel.GetObtVelocity(); + if (Vector3.Dot(relVel, relPos.normalized) > -10f) + return false; + Vector3 toIntercept = Intercept(relPos, relVel); + bool useReverseThrust = (!UseForwardThrust(-toIntercept) && (Vector3.Dot(relPos, relVel) < 0f)) || (ReverseThrust && forwardEngines.Count == 0); + margin += useReverseThrust ? 1.5f : 0f; // Stop earlier when reverse thrusting to keep facing target + float angleToRotate = VectorUtils.Angle((useReverseThrust ? -1 : 1) * vessel.ReferenceTransform.up, relVel) * Mathf.Deg2Rad * 0.75f; + float timeToRotate = BDAMath.SolveTime(angleToRotate, maxAngularAccelerationMag) / 0.75f; + float relSpeed = relVel.magnitude; + float interceptStoppingDistance = StoppingDistance(relSpeed, useReverseThrust) + relSpeed * (margin + timeToRotate * 3f); + float distanceToIntercept = toIntercept.magnitude; + distToCPA = distanceToIntercept; + stoppingDist = interceptStoppingDistance; + + return distanceToIntercept < interceptStoppingDistance; + } + + private bool OnIntercept(float tolerance) + { + if (targetVessel is null) + return false; + Vector3 relPos = targetVessel.CoM - vessel.CoM; + Vector3 relVel = targetVessel.GetObtVelocity() - vessel.GetObtVelocity(); + if (Vector3.Dot(relPos, relVel) >= 0f) + return false; + Vector3 cpa = vessel.orbit.getPositionAtUT(Planetarium.GetUniversalTime() + timeToCPA) - targetVessel.orbit.getPositionAtUT(Planetarium.GetUniversalTime() + timeToCPA); + float interceptRange = interceptRanges.z; + float interceptRangeTolSqr = (interceptRange * (tolerance + 1f)) * (interceptRange * (tolerance + 1f)); + + bool speedWithinLimits = Mathf.Abs(relVel.magnitude - ManeuverSpeed) < ManeuverSpeed * tolerance; + if (!speedWithinLimits) + { + BDModuleOrbitalAI targetAI = targetVessel.ActiveController().OrbitalAI; + if (targetAI && targetAI.pilotEnabled) + speedWithinLimits = targetAI.ManeuverSpeed > ManeuverSpeed && targetAI.targetVessel == vessel && RelVel(targetVessel, vessel).sqrMagnitude > ManeuverSpeed * ManeuverSpeed; // Target is targeting us, set to maneuver faster, and is maneuvering faster + } + return cpa.sqrMagnitude < interceptRangeTolSqr && speedWithinLimits; + } + + private Vector3 Intercept(Vector3 relPos, Vector3 relVel) + { + Vector3 lateralVel = Vector3.ProjectOnPlane(-relVel, relPos); + Vector3 lateralOffset = lateralVel.normalized * interceptRanges.z; + return relPos + lateralOffset; + } + + private Vector3 InterceptionRanges() + { + Vector3 interceptRanges = Vector3.zero; + float minRange = MinEngagementRange; + float maxRange = Mathf.Max(minRange * 1.2f, ForceFiringRange); + bool usingProjectile = true; + var weaponManager = WeaponManager; + if (weaponManager != null) + { + bool checkAllWeapons = false; + if (weaponManager.selectedWeapon != null) + { + currentWeapon = weaponManager.selectedWeapon; + EngageableWeapon engageableWeapon = currentWeapon as EngageableWeapon; + minRange = Mathf.Max(engageableWeapon.GetEngagementRangeMin(), minRange); + maxRange = engageableWeapon.GetEngagementRangeMax(); + usingProjectile = weaponManager.selectedWeapon.GetWeaponClass() != WeaponClasses.Missile; + if (usingProjectile) + { + missileTryLaunchTime = 0f; + if (weaponManager.CheckAmmo(currentWeapon as ModuleWeapon)) + maxRange = Mathf.Min(maxRange, weaponManager.gunRange); + else + checkAllWeapons = true; + } + else + { + MissileBase ml = currentWeapon as MissileBase; + missileTryLaunchTime = weaponManager.missilesAway.Any() ? 0f : missileTryLaunchTime; + maxRange = weaponManager.MaxMissileRange(ml, weaponManager.UnguidedMissile(ml, maxRange)); + maxRange = Mathf.Max(maxRange * (1 - 0.15f * Mathf.Floor(missileTryLaunchTime / 20f)), + Mathf.Min(weaponManager.gunRange, minRange * 1.2f)); + // If trying to fire a missile and within range, gradually decrease max range by 15% every 20s outside of range and unable to fire + if (targetVessel != null && (targetVessel.CoM - vessel.CoM).sqrMagnitude < maxRange * maxRange) + missileTryLaunchTime += Time.fixedDeltaTime; + } + } + else + checkAllWeapons = true; + + if (checkAllWeapons) + { + missileTryLaunchTime = 0f; + + foreach (var weapon in VesselModuleRegistry.GetModules(vessel)) + { + if (weapon == null) continue; + if (!((EngageableWeapon)weapon).engageAir) continue; + float maxEngageRange = ((EngageableWeapon)weapon).GetEngagementRangeMax(); + if (weapon.GetWeaponClass() == WeaponClasses.Missile) + { + MissileBase ml = weapon as MissileBase; + maxRange = Mathf.Max(weaponManager.MaxMissileRange(ml, weaponManager.UnguidedMissile(ml, maxRange)), maxRange); + usingProjectile = false; + } + else if (weaponManager.CheckAmmo(weapon as ModuleWeapon)) + maxRange = Mathf.Max(Mathf.Min(maxEngageRange, weaponManager.gunRange), maxRange); + } + } + if (targetVessel != null) + minRange = Mathf.Max(minRange, targetVessel.GetRadius()); + } + float interceptRange = minRange + (maxRange - minRange) * (usingProjectile ? 0.25f : 0.75f); + interceptRanges.x = minRange; + interceptRanges.y = maxRange; + interceptRanges.z = interceptRange; + return interceptRanges; + } + + private bool GunReady(ModuleWeapon gun) + { + if (gun == null) return false; + + // Check gun/laser can fire soon, we are within guard and weapon engagement ranges, and we are under the firing speed + float targetSqrDist = FromTo(vessel, targetVessel).sqrMagnitude; + var weaponManager = WeaponManager; + return GunFiringSpeedCheck() && gun.CanFireSoon() && + (targetSqrDist <= gun.GetEngagementRangeMax() * gun.GetEngagementRangeMax()) && + (targetSqrDist <= weaponManager.gunRange * weaponManager.gunRange); + } + + private bool GunFiringSpeedCheck() + { + // See if we are under firing speed for firing, or if killing velocity, under kill velocity speed target + float relVelSqrMag = RelVel(vessel, targetVessel).sqrMagnitude; + if (currentStatus.Contains("Kill Velocity")) + { + float speedTarget = KillVelocityTargetSpeed(); + return relVelSqrMag < speedTarget * speedTarget; + } + else + return minFiringSpeed * minFiringSpeed < relVelSqrMag && relVelSqrMag < firingSpeed * firingSpeed; + } + private Vector3 GunFiringSolution(ModuleWeapon weapon) + { + // For fixed weapons, returns attitude that puts fixed weapon on target, even if not aligned with vesselTransform.up + // For turreted weapons, returns attitude toward target or broadside attitude depending on BroadsideAttack setting + + Vector3 firingSolution; + if (weapon != null && !weapon.turret) + { + Vector3 leadOffset = weapon.GetLeadOffset(); + Vector3 target = targetVessel.CoM; + target -= leadOffset; // Lead offset from aiming assuming the gun is forward aligned and centred. + // Note: depending on the airframe, there is an island of stability around -2°—30° in pitch and ±10° in yaw where the vessel can stably aim with offset weapons. + Vector3 weaponPosition = weapon.offsetWeaponPosition + vessel.ReferenceTransform.position; + Vector3 weaponDirection = vessel.ReferenceTransform.TransformDirection(weapon.offsetWeaponDirection); + + target = Quaternion.FromToRotation(weaponDirection, vesselTransform.up) * (target - vesselTransform.position) + vesselTransform.position; // correctly account for angular offset guns/schrage Musik + var weaponOffset = vessel.ReferenceTransform.position - weaponPosition; + + debugString.AppendLine($"WeaponOffset ({targetVessel.vesselName}): {weaponOffset.x}x m; {weaponOffset.y}y m; {weaponOffset.z}z m"); + target += weaponOffset; //account for weapons with translational offset from longitudinal axis + firingSolution = (target - vessel.CoM).normalized; + } + else // Null weapon or firing turrets, just point in general direction + { + firingSolution = BroadsideAttack ? BroadsideAttitude(vessel, targetVessel) : FromTo(vessel, targetVessel).normalized; + } + return firingSolution; + } + + private bool RecentFiringSolution(out Vector3 recentFiringSolution) + { + // Return true if a valid recent firing solution exists, if the solution exists, also return that value + bool validRecentSolution = false; + var weaponManager = WeaponManager; + + if (lastFiringSolution == Vector3.zero) // Throttle was used recently, no valid firing solution exists, even if previousGun != null + { + recentFiringSolution = Vector3.zero; + } + else if (weaponManager && weaponManager.previousGun != null) // If we had a gun recently selected, use it to continue pointing toward target + { + recentFiringSolution = GunFiringSolution(weaponManager.previousGun); + validRecentSolution = true; + } + else // (lastFiringSolution != Vector3.zero) + { + recentFiringSolution = lastFiringSolution; + validRecentSolution = true; + } + + return validRecentSolution; + } + + private float KillVelocityTargetSpeed() + { + float speedTarget = maxAcceleration * 0.15f; + + if (targetVessel != null) + { + float speedTargetHigh = Mathf.Abs(firingSpeed - minFiringSpeed) * 0.75f + minFiringSpeed; + + // If below speedTargetHigh and enemy is still decelerating, set the speed target as speedTargetHigh + if (speedTarget < speedTargetHigh && targetVessel.acceleration_immediate.sqrMagnitude > 0.1f && + Vector3.Dot(targetVessel.acceleration_immediate, FromTo(vessel, targetVessel)) > 0 && + RelVel(targetVessel, vessel).sqrMagnitude < speedTargetHigh * speedTargetHigh) + speedTarget = speedTargetHigh; + + // If our target is targeting us and is maneuvering at a speed slower than our fire speed, set speed target as slighly above their maneuver speed + BDModuleOrbitalAI targetAI = targetVessel.ActiveController().OrbitalAI; + if (targetAI && targetAI.pilotEnabled && targetAI.targetVessel == vessel && targetAI.ManeuverSpeed < firingSpeed) + speedTarget = Mathf.Max(1.05f * targetAI.ManeuverSpeed, speedTarget); + } + + return Mathf.Clamp(speedTarget, FiringTargetSpeed(), firingSpeed); + } + + private float FiringTargetSpeed() + { + return Mathf.Abs(firingSpeed - minFiringSpeed) * 0.2f + minFiringSpeed; + } + + private bool AwayCheck(float minRange) + { + // Check if we need to manually burn away from an enemy that's too close or + // if it would be better to drift away. + + Vector3 toTarget = FromTo(vessel, targetVessel); + Vector3 toEscape = -toTarget.normalized; + Vector3 relVel = targetVessel.GetObtVelocity() - vessel.GetObtVelocity(); + if (relVel.sqrMagnitude < minFiringSpeed * minFiringSpeed) return true; + bool useForwardThrust = UseForwardThrust(toEscape); + Vector3 thrustDir = useForwardThrust ? vessel.ReferenceTransform.up : -vessel.ReferenceTransform.up; + float rotDistance = VectorUtils.Angle(thrustDir, toEscape) * Mathf.Deg2Rad; + float timeToRotate = BDAMath.SolveTime(rotDistance / 2, maxAngularAccelerationMag) * 2; + float timeToDisplace = BDAMath.SolveTime(minRange - toTarget.magnitude, (!useForwardThrust && currentForwardThrust ? reverseForwardThrustRatio : 1f) * maxAcceleration, Vector3.Dot(-relVel, toEscape)); + float timeToEscape = timeToRotate * 2 + timeToDisplace; + + Vector3 drift = AIUtils.PredictPosition(toTarget, relVel, Vector3.zero, timeToEscape); + bool manualEscape = drift.sqrMagnitude < minRange * minRange; + + return manualEscape; + } + + bool PredictCollisionWithVessel(Vessel v, float maxTime, out Vector3 badDirection) + { + var weaponManager = WeaponManager; + if (vessel == null || v == null || + (weaponManager != null && v == weaponManager.incomingMissileVessel) || + (v.rootPart != null && v.rootPart.FindModuleImplementing() != null) || //evasive will handle avoiding missiles + Vector3.Dot(v.GetObtVelocity() - vessel.GetObtVelocity(), v.CoM - vessel.CoM) >= 0f) // Don't bother if vessels are not approaching each other + { + badDirection = Vector3.zero; + return false; + } + + // Adjust some values for asteroids. + 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. + { + maxTime += targetRadius * targetRadius / (v.GetObtVelocity() - vessel.GetObtVelocity()).sqrMagnitude; + } + + // 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.TimeToCPA(v, maxTime); // This uses the same kinematics as AIUtils.PredictPosition. + if (timeToCPA > 0 && timeToCPA < maxTime) + { + Vector3 tPos = AIUtils.PredictPosition(v, timeToCPA); + Vector3 myPos = AIUtils.PredictPosition(vessel, timeToCPA); + if (Vector3.SqrMagnitude(tPos - myPos) < threshold * threshold) // Within collisionAvoidanceThreshold of each other. Danger Will Robinson! + { + badDirection = tPos - vesselTransform.position; + return true; + } + } + + badDirection = Vector3.zero; + return false; + } + + bool FlyAvoidOthers() // Check for collisions with other vessels and try to avoid them. + { + if (collisionAvoidanceThreshold == 0) return false; + if (currentlyAvoidedVessel != null) // Avoidance has been triggered. + { + 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. + return true; + else // Stop avoiding, but immediately check again for new collisions. + { + currentlyAvoidedVessel = null; + collisionDetectionTicker = vesselCollisionAvoidanceTickerFreq + 1; + return FlyAvoidOthers(); + } + } + else if (collisionDetectionTicker > vesselCollisionAvoidanceTickerFreq) // Only check every vesselCollisionAvoidanceTickerFreq frames. + { + collisionDetectionTicker = 0; + + // Check for collisions with other vessels. + bool vesselCollision = false; + VesselType collisionVesselType = VesselType.Unknown; // Start as not debris. + float collisionTargetLargestSize = -1f; + collisionAvoidDirection = vessel.srf_vel_direction; + // First pass, only consider valid vessels. + using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) + while (vs.MoveNext()) + { + if (vs.Current == null) continue; + if (vs.Current.vesselType == VesselType.Debris) continue; // Ignore debris on the first pass. + if (vs.Current == vessel || vs.Current.Landed) continue; + if (!PredictCollisionWithVessel(vs.Current, vesselCollisionAvoidanceLookAheadPeriod, out Vector3 collisionAvoidDir)) continue; + if (!VesselModuleRegistry.IgnoredVesselTypes.Contains(vs.Current.vesselType)) + { + var ibdaiControl = vs.Current.ActiveController().AI; + if (ibdaiControl != null && ibdaiControl.currentCommand == PilotCommands.Follow && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel) continue; + } + var collisionTargetSize = vs.Current.vesselSize.sqrMagnitude; // We're only interested in sorting by size, which is much faster than sorting by mass. + if (collisionVesselType == vs.Current.vesselType && collisionTargetSize < collisionTargetLargestSize) continue; // Avoid the largest object. + vesselCollision = true; + currentlyAvoidedVessel = vs.Current; + collisionAvoidDirection = collisionAvoidDir; + collisionVesselType = vs.Current.vesselType; + collisionTargetLargestSize = collisionTargetSize; + } + // Second pass, only consider debris. + if (!vesselCollision) + { + using var vs = BDATargetManager.LoadedVessels.GetEnumerator(); + while (vs.MoveNext()) + { + if (vs.Current == null) continue; + if (vs.Current.vesselType != VesselType.Debris) continue; // Only consider debris on the second pass. + if (vs.Current == vessel || vs.Current.Landed) continue; + if (!PredictCollisionWithVessel(vs.Current, vesselCollisionAvoidanceLookAheadPeriod, out Vector3 collisionAvoidDir)) continue; + var collisionTargetSize = vs.Current.vesselSize.sqrMagnitude; + if (collisionTargetSize < collisionTargetLargestSize) continue; // Avoid the largest debris object. + vesselCollision = true; + currentlyAvoidedVessel = vs.Current; + collisionAvoidDirection = collisionAvoidDir; + collisionVesselType = vs.Current.vesselType; + collisionTargetLargestSize = collisionTargetSize; + } + } + if (vesselCollision) + return true; + else + { currentlyAvoidedVessel = null; } + } + else + { ++collisionDetectionTicker; } + return false; + } + + Vector3 broadsideAttitudeLerp; + private Vector3 BroadsideAttitude(Vessel self, Vessel target) + { + // Return lerped attitude for broadside attack. Lerp attitude to reduce oscillation. + Vector3 toTarget = FromTo(self, target).normalized; + Vector3 up = self.vesselTransform.up; + Vector3 broadsideAttitude = up.ProjectOnPlanePreNormalized(toTarget); + float error = VectorUtils.Angle(up, broadsideAttitude); + float angleLerp = Mathf.InverseLerp(0, 10, error); + float lerpRate = Mathf.Lerp(1, 10, angleLerp); + broadsideAttitudeLerp = Vector3.Lerp(broadsideAttitudeLerp, broadsideAttitude, lerpRate * Time.deltaTime); //Lerp has reduced oscillation compared to Slerp + return broadsideAttitudeLerp; + } + + private void GunEngineEvasion() + { + if (!((evadingGunfire && evasionEngines) && (currentStatusMode == StatusMode.Maneuvering || currentStatusMode == StatusMode.Firing))) return; + + var weaponManager = WeaponManager; + Vector3 relVelProjected = Vector3.ProjectOnPlane(weaponManager.incomingThreatVessel ? RelVel(vessel, weaponManager.incomingThreatVessel) : vessel.GetObtVelocity(), threatRelativePosition); + Vector3 evasionDir = evasionErraticness * Vector3.Project(evasionNonLinearityDirection, relVelProjected).normalized; + Vector3 thrustDir = (fc.thrustDirection == Vector3.zero ? fc.attitude : fc.thrustDirection); + evasionDir = evasionErraticness == 0 ? thrustDir : Vector3.Lerp(evasionDir, thrustDir + evasionDir, fc.throttle).normalized; + if (fc.thrustDirection == fc.attitude || fc.thrustDirection == Vector3.zero) + { + fc.thrustDirection = evasionDir; + fc.attitude = evasionDir; + } + else + { + fc.thrustDirection = evasionDir; + fc.attitude = -evasionDir; + } + fc.alignmentToleranceforBurn = 70; + fc.throttle = Mathf.Clamp01(fc.throttle + Mathf.Clamp01(1f - threatRelativePosition.sqrMagnitude / 1e8f)); + fc.lerpThrottle = false; + } + + private void UpdateRCSVector(Vector3 inputVec = default(Vector3)) + { + if (currentStatusMode == StatusMode.AvoidingCollision) + { + fc.rcsLerpRate = 100f / Time.fixedDeltaTime; // instant changes, don't Lerp + fc.rcsRotate = false; + } + else if (currentStatusMode == StatusMode.Ramming) + { + fc.RCSPower = 20f; + fc.rcsLerpRate = 5f; + fc.rcsRotate = false; + } + else if (evadingGunfire && evasionRCS) // Quickly move RCS vector + { + var weaponManager = WeaponManager; + Vector3 relVelProjected = Vector3.ProjectOnPlane(weaponManager.incomingThreatVessel ? RelVel(vessel, weaponManager.incomingThreatVessel) : vessel.GetObtVelocity(), threatRelativePosition); + inputVec = Vector3.Cross(Vector3.Project(evasionNonLinearityDirection, relVelProjected), threatRelativePosition).normalized; + fc.rcsLerpRate = 100f / Time.fixedDeltaTime; // instant changes, don't Lerp + fc.rcsRotate = false; + } + else // Slowly lerp RCS vector + { + fc.rcsLerpRate = 5f; + fc.rcsRotate = false; + } + + fc.RCSVector = inputVec; + + // Update engine RCS + fc.engineRCSTranslation = EngineRCSTranslation; + fc.engineRCSRotation = EngineRCSRotation; + } + + private void UpdateBurnAlignmentTolerance() + { + if (!hasEC && !hasRCS) + fc.alignmentToleranceforBurn = 180f; + } + + public bool HasPropulsion => hasPropulsion; + #endregion + + #region Utils + public static Vector3 FromTo(Vessel v1, Vessel v2) + { + return v2.transform.position - v1.transform.position; + } + + public static Vector3 RelVel(Vessel v1, Vessel v2) + { + return v1.GetObtVelocity() - v2.GetObtVelocity(); + } + + public static Vector3 AngularAcceleration(Vector3 torque, Vector3 MoI) + { + return new Vector3(MoI.x.Equals(0) ? float.MaxValue : torque.x / MoI.x, + MoI.y.Equals(0) ? float.MaxValue : torque.y / MoI.y, + MoI.z.Equals(0) ? float.MaxValue : torque.z / MoI.z); + } + + public static float AngularVelocity(Vessel v, Vessel t, float window) + { + Vector3 tv1 = FromTo(v, t); + Vector3 tv2 = tv1 + window * RelVel(v, t); + return VectorUtils.Angle(tv1, tv2) / window; + } + + public static float VesselDistance(Vessel v1, Vessel v2) + { + return (v1.transform.position - v2.transform.position).magnitude; + } + + public static Vector3 Displacement(Vector3 velocity, Vector3 acceleration, float time) + { + return velocity * time + 0.5f * acceleration * time * time; + } + + private void CalculateAngularAcceleration() + { + maxAngularAcceleration = AngularAcceleration(availableTorque, vessel.MOI); + maxAngularAccelerationMag = maxAngularAcceleration.magnitude; + + float angVel = vessel.angularVelocity.magnitude; + float angAccel = Mathf.Abs(angVel - lastAngVel) / Time.fixedDeltaTime / 3f; + dynAngAccel *= dynDecayRate; // Decay the highest observed angular acceleration (we want a fairly recent value in case the craft's dynamics have changed). + dynAngAccel = Mathf.Max(dynAngAccel, angAccel); + maxAngularAccelerationMag = Mathf.Clamp(((1f - 0.1f) * maxAngularAccelerationMag + 0.1f * dynAngAccel), maxAngularAccelerationMag, dynAngAccel); + lastAngVel = angVel; + } + + private void CalculateAvailableTorque(Vessel v) + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (v != vessel) return; + + availableTorque = Vector3.zero; + var reactionWheels = VesselModuleRegistry.GetModules(v); + foreach (var wheel in reactionWheels) + { + wheel.GetPotentialTorque(out Vector3 pos, out pos); + availableTorque += pos; + } + } + + private void CheckEngineLists(Vessel v) + { + if (v != vessel) return; + if ( + rcsEngines.Any(e => e == null || e.vessel != vessel) || + reverseEngines.Any(e => e == null || e.vessel != vessel) || + forwardEngines.Any(e => e == null || e.vessel != vessel) + ) engineListsRequireUpdating = true; + } + + private float GetMaxAcceleration() + { + maxThrust = GetMaxThrust(); + return maxThrust / vessel.GetTotalMass(); + } + + private float GetMaxThrust() + { + float thrust = VesselModuleRegistry.GetModuleEngines(vessel).Where(e => e != null && e.EngineIgnited && e.isOperational && !rcsEngines.Contains(e)).Sum(e => e.MaxThrustOutputVac(true)); + thrust += VesselModuleRegistry.GetModules(vessel).Where(rcs => rcs != null && rcs.useThrottle).Sum(rcs => rcs.thrusterPower); + return thrust; + } + private void UpdateEngineLists(bool forceUpdate = false) + { + // Update lists of engines that can provide forward, reverse and rcs thrust + if (!(engineListsRequireUpdating || forceUpdate)) return; + engineListsRequireUpdating = false; + forwardEngines.Clear(); + reverseEngines.Clear(); + rcsEngines.Clear(); + float forwardThrust = 0f; + float reverseThrust = 0f; + foreach (var engine in VesselModuleRegistry.GetModuleEngines(vessel)) + { + if (engine.throttleLocked || !engine.allowShutdown || !engine.allowRestart) continue; // Ignore engines that can't be throttled, shutdown, or restart + if (VesselSpawning.SpawnUtils.IsModularMissilePart(engine.part)) continue; // Ignore modular missile engines. + if (Vector3.Dot(-engine.thrustTransforms[0].forward, vesselTransform.up) > 0.1f && engine.MaxThrustOutputVac(true) > 0) + { + forwardEngines.Add(engine); + forwardThrust += engine.MaxThrustOutputVac(true); + } + else if (Vector3.Dot(-engine.thrustTransforms[0].forward, -vesselTransform.up) > 0.1f && engine.MaxThrustOutputVac(true) > 0) + { + reverseEngines.Add(engine); + reverseThrust += engine.MaxThrustOutputVac(true); + var gimbal = engine.part.FindModuleImplementing(); + if (gimbal != null) // Disable gimbal on reverse/RCS engines since they don't work for directions other than forward + { + gimbal.gimbalRange = 0; + gimbal.gimbalLock = true; + } + } + else if (EngineRCSRotation || EngineRCSTranslation) + { + if (Vector3.Dot(-engine.thrustTransforms[0].forward, vesselTransform.right) > 0.5f || + Vector3.Dot(-engine.thrustTransforms[0].forward, vesselTransform.right) < -0.5f || + Vector3.Dot(-engine.thrustTransforms[0].forward, vesselTransform.forward) > 0.5f || + Vector3.Dot(-engine.thrustTransforms[0].forward, vesselTransform.forward) < -0.5f) + rcsEngines.Add(engine); //grab engines pointing sideways. Not grabbing fore/aft engines since while those would impart some torque, + //ship design is generally going to be long and anrrow, so torque imparted would be minimal while adding noticable forward/reverse vel + if (engine.MaxThrustOutputVac(true) > 0) //just in case someone has mounted jets to the side of their space cruiser for some reason + { + engine.Activate(); + if (!engine.independentThrottle) engine.independentThrottle = true; //using independent throttle so these can fire while main engines are off but not shudown + engine.thrustPercentage = 0; //activate and set to 0 thrust so they're ready when needed + if (engine.independentThrottlePercentage == 0) engine.independentThrottlePercentage = 100; + } + var gimbal = engine.part.FindModuleImplementing(); + if (gimbal != null) // Disable gimbal on reverse/RCS engines since they don't work for directions other than forward + { + gimbal.gimbalRange = 0; + gimbal.gimbalLock = true; + } + fc.rcsEngines = rcsEngines; + } + } + if (ReverseThrust && reverseEngines.Count == 0) // Disable reverse thrust if no reverse engines available + { + ReverseThrust = false; + reverseForwardThrustRatio = 1f; + fc.thrustDirection = vesselTransform.up; + currentForwardThrust = false; + AlignEnginesWithThrust(true); + } + else + reverseForwardThrustRatio = (forwardThrust == 0) ? 1 : reverseThrust / forwardThrust; + // Debug.Log($"DEBUG {vessel.vesselName} has forward engines: {string.Join(", ", forwardEngines.Select(e => e.part.partInfo.name))}"); + // Debug.Log($"DEBUG {vessel.vesselName} has reverse engines: {string.Join(", ", reverseEngines.Select(e => e.part.partInfo.name))}"); + // Debug.Log($"DEBUG {vessel.vesselName} has rcs engines: {string.Join(", ", rcsEngines.Select(e => e.part.partInfo.name))}"); + } + + private void AlignEnginesWithThrust(bool forceUpdate = false) + { + // Activate or shutdown forward or reverse engines based on fc.thrustDirection + fc.thrustDirection = (fc.thrustDirection == Vector3.zero) ? fc.attitude : fc.thrustDirection; // Set thrust direction to attitude if not already set + + if (!ReverseThrust && !forceUpdate) return; // Don't continue if reverse thrust is disabled + bool forwardThrust = UseForwardThrust(fc.thrustDirection); // See if it makes sense to use forward thrust for this thrustDirection + + if (forwardEngines.Count == 0 && reverseEngines.Count > 0 && fc.throttle > 0) // Edge case of no forward engines, but we have reverse engines + { + forwardThrust = false; + fc.attitude = -fc.thrustDirection; + } + + fc.useReverseThrust = !forwardThrust; // Tell fc what thrust mode to use + + if (forwardThrust == currentForwardThrust) return; // Don't bother toggling engines if desired direction matches current + if (forwardThrust) // Activate forward engines, shutdown reverse engines + { + foreach (var engine in forwardEngines.Where(e => e != null)) + engine.Activate(); + foreach (var engine in reverseEngines.Where(e => e != null)) + engine.Shutdown(); + currentForwardThrust = true; + } + else // Activate reverse engines, shutdown forward engines + { + foreach (var engine in reverseEngines.Where(e => e != null)) + engine.Activate(); + foreach (var engine in forwardEngines.Where(e => e != null)) + engine.Shutdown(); + currentForwardThrust = false; + } + } + + private bool UseForwardThrust(Vector3 thrustDir) + { + if (!ReverseThrust) return true; + else + return Vector3.Dot(thrustDir, vesselTransform.up) > -0.3f; // Use forward thrust for directions within ~110 deg of vesselTransform.up + } + + private float GetConsumptionRate(bool useReverseThrust) + { + if (BDArmorySettings.INFINITE_FUEL || CheatOptions.InfinitePropellant) return 0f; + float consumptionRate = 0.0f; + List engines = !ReverseThrust ? VesselModuleRegistry.GetModuleEngines(vessel).FindAll(e => (e.EngineIgnited && e.isOperational)) : // No reverse thrust capability, use active engines + (useReverseThrust ? reverseEngines : forwardEngines); // Reverse thrust capability, use appropriate thrusters to evaluate ISP + foreach (var engine in engines) + consumptionRate += Mathf.Lerp(engine.minFuelFlow, engine.maxFuelFlow, 0.01f * engine.thrustPercentage) * engine.flowMultiplier; + return consumptionRate; + } + + //Controller Integral + Vector3 directionIntegral; + float pitchIntegral; + float yawIntegral; + float rollIntegral; + Vector3 prevTargetDir; + void AttitudeControl(FlightCtrlState s) + { + Vector3 targetDirection = fc.attitude; + Vector3 currentRoll = -vesselTransform.forward; + Vector3 rollTarget = currentRoll; + debugRollTarget = Vector3.zero; + if (targetVessel != null) // If we have a target, adjust roll orientation relative to target based on rollMode setting + { + // Determine toTarget direction for roll command + Vector3 toTarget; + if (currentStatusMode == StatusMode.Firing) + toTarget = targetDirection; + else if (RecentFiringSolution(out Vector3 recentSolution)) // If we valid firing solution recently, use it to continue pointing toward target + toTarget = recentSolution; + else + toTarget = FromTo(vessel, targetVessel); + toTarget = toTarget.normalized; + + // Determine roll target + if (Vector3.Dot(vesselTransform.up, toTarget) < 0.999) // Only roll if we are not aligned with target/firing solution + { + switch (rollMode) + { + case RollModeTypes.Port_Starboard: + { + if (Vector3.Dot(toTarget, vesselTransform.right) > 0f) + rollTarget = Vector3.Cross(vesselTransform.up, toTarget).ProjectOnPlanePreNormalized(vesselTransform.up); + else + rollTarget = Vector3.Cross(-vesselTransform.up, toTarget).ProjectOnPlanePreNormalized(vesselTransform.up); + } + break; + case RollModeTypes.Dorsal_Ventral: + { + if (Vector3.Dot(toTarget, vesselTransform.forward) < 0f) + rollTarget = toTarget.ProjectOnPlanePreNormalized(vesselTransform.up); + else + rollTarget = -toTarget.ProjectOnPlanePreNormalized(vesselTransform.up); + } + break; + case RollModeTypes.Port: + rollTarget = Vector3.Cross(-vesselTransform.up, toTarget).ProjectOnPlanePreNormalized(vesselTransform.up); + break; + case RollModeTypes.Starboard: + rollTarget = Vector3.Cross(vesselTransform.up, toTarget).ProjectOnPlanePreNormalized(vesselTransform.up); + break; + case RollModeTypes.Dorsal: + rollTarget = toTarget.ProjectOnPlanePreNormalized(vesselTransform.up); + break; + case RollModeTypes.Ventral: + rollTarget = -toTarget.ProjectOnPlanePreNormalized(vesselTransform.up); + break; + } + debugRollTarget = rollTarget * 100f; + } + } + + Vector3 localTargetDirection = vesselTransform.InverseTransformDirection(targetDirection).normalized; + float rotationPerFrame = (currentStatusMode == StatusMode.Firing && Vector3.Dot(vesselTransform.up, targetDirection) > 0.94) ? 25f : steerMaxError; // Reduce rotation rate if firing and within ~20 deg of target + localTargetDirection = Vector3.RotateTowards(Vector3.up, localTargetDirection, rotationPerFrame * Mathf.Deg2Rad, 0); + + float pitchError = VectorUtils.GetAngleOnPlane(localTargetDirection, Vector3.up, Vector3.back); + float yawError = VectorUtils.GetAngleOnPlane(localTargetDirection, Vector3.up, Vector3.right); + float rollError = Mathf.Clamp(VectorUtils.GetAngleOnPlane(rollTarget, currentRoll, vesselTransform.right), -steerMaxError, steerMaxError); + + Vector3 localAngVel = vessel.angularVelocity; + Vector3 targetAngVel = Vector3.Cross(prevTargetDir, targetDirection) / Time.fixedDeltaTime; + Vector3 localTargetAngVel = vesselTransform.InverseTransformVector(targetAngVel); + localAngVel -= localTargetAngVel; + prevTargetDir = targetDirection; + + #region PID calculations + float pitchProportional = 0.005f * steerMult * pitchError; + float yawProportional = 0.005f * steerMult * yawError; + float rollProportional = 0.005f * steerMult * rollError; + + float pitchDamping = steerDamping * -localAngVel.x; + float yawDamping = steerDamping * -localAngVel.z; + float rollDamping = 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 = (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 = steerKiAdjust * Vector3.Dot(directionIntegral, vesselTransform.right); + rollIntegral = steerKiAdjust * Mathf.Clamp(rollIntegral + rollError * Time.deltaTime, -1f, 1f); + + var steerPitch = pitchProportional + pitchIntegral - pitchDamping; + var steerYaw = yawProportional + yawIntegral - yawDamping; + var steerRoll = rollProportional + rollIntegral - rollDamping; + + float maxSteer = 1; + + if (BDArmorySettings.DEBUG_LINES) + { + debugTargetPosition = vessel.transform.position + targetDirection * 1000; // The asked for target position's direction + debugTargetDirection = vessel.transform.position + vesselTransform.TransformDirection(localTargetDirection) * 200; // The actual direction to match the "up" direction of the craft with for pitch (used for PID calculations). + } + + SetFlightControlState(s, + Mathf.Clamp(steerPitch, -maxSteer, maxSteer), // pitch + Mathf.Clamp(steerYaw, -maxSteer, maxSteer), // yaw + Mathf.Clamp(steerRoll, -maxSteer, maxSteer)); // roll + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + debugString.AppendLine(string.Format("rollError: {0,7:F4}, pitchError: {1,7:F4}, yawError: {2,7:F4}", rollError, pitchError, yawError)); + debugString.AppendLine(string.Format("Pitch: P: {0,7:F4}, I: {1,7:F4}, D: {2,7:F4}", pitchProportional, pitchIntegral, pitchDamping)); + debugString.AppendLine(string.Format("Yaw: P: {0,7:F4}, I: {1,7:F4}, D: {2,7:F4}", yawProportional, yawIntegral, yawDamping)); + debugString.AppendLine(string.Format("Roll: P: {0,7:F4}, I: {1,7:F4}, D: {2,7:F4}", rollProportional, rollIntegral, rollDamping)); + } + #endregion + } + #endregion + + #region Autopilot helper functions + + public override bool CanEngage() + { + return !vessel.LandedOrSplashed && vessel.InOrbit(); + } + + public override bool IsValidFixedWeaponTarget(Vessel target) + { + if (!vessel) return false; + + return true; + } + + #endregion Autopilot helper functions + + #region WingCommander + + Vector3 GetFormationPosition() + { + return commandLeader.vessel.CoM + Quaternion.LookRotation(commandLeader.vessel.up, upDir) * this.GetLocalFormationPosition(commandFollowIndex); + } + + #endregion WingCommander + } +} \ No newline at end of file diff --git a/BDArmory/Control/BDModulePilotAI.cs b/BDArmory/Control/BDModulePilotAI.cs new file mode 100644 index 000000000..8c9005d1c --- /dev/null +++ b/BDArmory/Control/BDModulePilotAI.cs @@ -0,0 +1,5417 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Guidances; +using BDArmory.Settings; +using BDArmory.VesselSpawning; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using BDArmory.Radar; +using BDArmory.GameModes.Waypoints; + +namespace BDArmory.Control +{ + public class BDModulePilotAI : BDGenericAIBase, IBDAIControl + { + public override AIType aiType => AIType.PilotAI; + #region Pilot AI Settings GUI + #region PID + enum Axis { Pitch, Yaw, Roll } + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerPower", //Steer Factor + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float steerMult = 14f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerKi", //Steer Ki + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.01f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float steerKiAdjust = 0.4f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDamping", //Steer Damping + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float steerDamping = 5f; + + #region Toggles for 3-axis and dynamic damping (before the sliders to keep them together) + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPID", advancedTweakable = true, // 3-axis PID toggle + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, disabledText = "#LOC_BDArmory_Disabled", enabledText = "#LOC_BDArmory_Enabled")] + public bool threeAxisPID = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisSteerDamping", advancedTweakable = true, // 3-axis damping toggle + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, disabledText = "#LOC_BDArmory_Disabled", enabledText = "#LOC_BDArmory_Enabled")] + public bool threeAxisSteerDamping = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_DynamicSteerDamping", advancedTweakable = true, // Dynamic damping toggle + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, disabledText = "#LOC_BDArmory_Disabled", enabledText = "#LOC_BDArmory_Enabled")] + public bool dynamicSteerDamping = false; + #endregion + + #region 3-axis Steer Damping (static) + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDampingPitch", //Steer Damping Pitch + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float steerDampingPitch = 5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDampingYaw", //Steer Damping Yaw + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float steerDampingYaw = 5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDampingRoll", //Steer Damping Roll + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float steerDampingRoll = 5f; + #endregion + + #region Dynamic Damping + // Deprecated in favor of independent toggles. + // UpgradeDamping() takes care of upgrading craft that used this. + // Reverting to an older version of BDA and loading a craft will cause this setting to be lost. + [KSPField(isPersistant = false)] + public bool CustomDynamicAxisFields = false; + + // 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_AI_DynamicDamping", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + private string DynamicDampingLabel = ""; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_DynamicDampingMin", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + 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_AI_DynamicDampingMax", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + 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_AI_DynamicDampingFactor", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float dynamicSteerDampingFactor = 5f; + + // Dynamic Pitch + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingPitch", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] + public bool dynamicDampingPitch = true; + + [KSPField(guiName = "#LOC_BDArmory_AI_DynamicDampingPitch", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + private string PitchLabel = ""; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingPitchMin", advancedTweakable = true, //Dynamic steer damping Clamp min + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float DynamicDampingPitchMin = 6f; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingPitchMax", advancedTweakable = true, //Dynamic steer damping Clamp max + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + 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_AI_DynamicDampingPitchFactor", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float dynamicSteerDampingPitchFactor = 8f; + + // Dynamic Yaw + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingYaw", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] + public bool dynamicDampingYaw = true; + + [KSPField(guiName = "#LOC_BDArmory_AI_DynamicDampingYaw", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + private string YawLabel = ""; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingYawMin", advancedTweakable = true, //Dynamic steer damping Clamp min + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float DynamicDampingYawMin = 6f; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingYawMax", advancedTweakable = true, //Dynamic steer damping Clamp max + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + 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_AI_DynamicDampingYawFactor", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float dynamicSteerDampingYawFactor = 8f; + + // Dynamic Roll + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingRoll", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] + public bool dynamicDampingRoll = true; + + [KSPField(guiName = "#LOC_BDArmory_AI_DynamicDampingRoll", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + private string RollLabel = ""; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingRollMin", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float DynamicDampingRollMin = 6f; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_AI_DynamicDampingRollMax", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + 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_AI_DynamicDampingRollFactor", advancedTweakable = true, //Dynamic steer dampening Factor + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float dynamicSteerDampingRollFactor = 8f; + #endregion + + #region 3-axis PID + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDPitchMult", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float threeAxisPIDPitchMult = 14f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDPitchKi", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.01f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float threeAxisPIDPitchKi = 0.4f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDPitchDamping", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float threeAxisPIDPitchDamping = 5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDYawMult", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float threeAxisPIDYawMult = 14f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDYawKi", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.01f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float threeAxisPIDYawKi = 0.4f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDYawDamping", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float threeAxisPIDYawDamping = 5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDRollMult", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float threeAxisPIDRollMult = 14f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDRollKi", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.01f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float threeAxisPIDRollKi = 0.4f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_3AxisPIDRollDamping", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float threeAxisPIDRollDamping = 5f; + #endregion + + #region AutoTuning + //Toggle AutoTuning + [KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_PID_AutoTune", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + 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(); } } + public PIDAutoTuning pidAutoTuning; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_Loss", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_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_AI_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_AI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + public string autoTuningLossLabel3 = ""; + public string autoTuningSummary = ""; + + //AutoTuning Number Of Samples + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_NumSamples", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 1f, maxValue = 10f, stepIncrement = 1f, scene = UI_Scene.All)] + public float autoTuningOptionNumSamples = 5f; + + //AutoTuning Fast Response Relevance + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_FastResponseRelevance", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_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_AI_PID_AutoTuning_InitialLearningRate", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatLogRange(minValue = 0.001f, maxValue = 1f, steps = 6, scene = UI_Scene.All)] + public float autoTuningOptionInitialLearningRate = 1f; + + //AutoTuning Initial Roll Relevance + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_InitialRollRelevance", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float autoTuningOptionInitialRollRelevance = 0.5f; + + //AutoTuning Altitude + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_Altitude", //Auto-tuning Altitude + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_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_AI_PID_AutoTuning_Speed", //Auto-tuning Speed + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 50f, maxValue = 800f, stepIncrement = 5f, scene = UI_Scene.All)] + public float autoTuningSpeed = 200f; + + // Re-centering Distance + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_RecenteringDistance", + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 5f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float autoTuningRecenteringDistance = 15f; + public float autoTuningRecenteringDistanceSqr { get; private set; } + + // 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 autoTuningOptionFixedDP = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDY = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDR = 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; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedPp = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedIp = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDp = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedPy = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedIy = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDy = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedPr = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedIr = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDr = false; + + //Clamp Maximums + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AI_PID_AutoTuning_ClampMaximums", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_AI_PID", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool autoTuningOptionClampMaximums = false; + #endregion + #endregion + + #region Altitudes + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_DefaultAltitude", //Default Alt. + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_FloatRange(minValue = 50f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] + public float defaultAltitude = 2000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinAltitude", //Min Altitude + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 1000, stepIncrement = 10f, scene = UI_Scene.All)] + public float minAltitude = 200f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_HardMinAltitude", advancedTweakable = true, + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool hardMinAltitude = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxAltitude", //Max Altitude + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_FloatRange(minValue = 100f, maxValue = 10000, stepIncrement = 100f, scene = UI_Scene.All)] + public float maxAltitude = 7500f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxAltitude", advancedTweakable = true, + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool maxAltitudeToggle = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BombingAltitude", //Min Altitude + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_FloatRange(minValue = 100f, maxValue = 2000, stepIncrement = 10f, scene = UI_Scene.All)] + public float bombingAltitude = 500; + + public float finalBombingAlt; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_DiveBombing", advancedTweakable = true, + groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_AI_Altitudes", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool divebombing = false; + #endregion + + #region Speeds + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxSpeed", //Max Speed + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 50f, maxValue = 800f, stepIncrement = 5f, scene = UI_Scene.All)] + public float maxSpeed = 350; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_TakeOffSpeed", //TakeOff Speed + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 200f, stepIncrement = 1f, scene = UI_Scene.All)] + public float takeOffSpeed = 60; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinSpeed", //MinCombatSpeed + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 200, stepIncrement = 1f, scene = UI_Scene.All)] + public float minSpeed = 60f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_StrafingSpeed", //Strafing Speed + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 200, stepIncrement = 1f, scene = UI_Scene.All)] + public float strafingSpeed = 100f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_IdleSpeed", //Idle Speed + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 200f, stepIncrement = 1f, scene = UI_Scene.All)] + public float idleSpeed = 200f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ABPriority", advancedTweakable = true, //Afterburner Priority + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float ABPriority = 50f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ABOverrideThreshold", advancedTweakable = true, //Afterburner Override Threshold + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 200f, stepIncrement = 1f, scene = UI_Scene.All)] + public float ABOverrideThreshold = 0f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BrakingPriority", advancedTweakable = true, //Afterburner Priority + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_AI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float brakingPriority = 50f; + #endregion + + #region Control Limits + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_LowSpeedSteerLimiter", advancedTweakable = true, // Low-Speed Steer Limiter + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = .1f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.All)] + public float maxSteer = 1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_LowSpeedLimiterSpeed", advancedTweakable = true, // Low-Speed Limiter Switch Speed + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 500f, stepIncrement = 1.0f, scene = UI_Scene.All)] + public float lowSpeedSwitch = 100f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_HighSpeedSteerLimiter", advancedTweakable = true, // High-Speed Steer Limiter + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = .1f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.All)] + public float maxSteerAtMaxSpeed = 1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_HighSpeedLimiterSpeed", advancedTweakable = true, // High-Speed Limiter Switch Speed + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + 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_AI_AltitudeSteerLimiterFactor", advancedTweakable = true, // Altitude Steer Limiter Factor + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_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_AI_AltitudeSteerLimiterAltitude", advancedTweakable = true, // Altitude Steer Limiter Altitude + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_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_AI_AttitudeLimiter", advancedTweakable = true, //Attitude Limiter, not currently functional + // groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + // UI_FloatRange(minValue = 10f, maxValue = 90f, stepIncrement = 5f, scene = UI_Scene.All)] + //public float maxAttitude = 90f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BankLimiter", advancedTweakable = true, //Bank Angle Limiter + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 10f, maxValue = 180f, stepIncrement = 5f, scene = UI_Scene.All)] + public float maxBank = 180f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_WaypointPreRollTime", advancedTweakable = true, //Waypoint Pre-Roll Time + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = 0.05f, scene = UI_Scene.All)] + public float waypointPreRollTime = 0.5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_WaypointYawAuthorityTime", advancedTweakable = true, //Waypoint Yaw Authority Time + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float waypointYawAuthorityTime = 5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxAllowedGForce", //Max G + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 2f, maxValue = 45f, stepIncrement = 0.5f, scene = UI_Scene.All)] + public float maxAllowedGForce = 25; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxAllowedAoA", //Max AoA + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + 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_AI_PostStallAoA", //Post-stall AoA + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_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_AI_ImmelmannTurnAngle", advancedTweakable = true, // Immelmann Turn Angle + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_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; + float BankedTurnDistance = 2800f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ImmelmannPitchUpBias", advancedTweakable = true, // Immelmann Pitch-Up Bias + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_AI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = -90f, maxValue = 90f, stepIncrement = 5f, scene = UI_Scene.All)] + public float ImmelmannPitchUpBias = 10f; // °/s + #endregion + + #region EvadeExtend + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinEvasionTime", advancedTweakable = true, // Min Evasion Time + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + 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_AI_EvasionThreshold", advancedTweakable = true, //Evasion Distance Threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float evasionThreshold = 25f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionTimeThreshold", advancedTweakable = true, // Evasion Time Threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + 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_AI_EvasionMinRangeThreshold", advancedTweakable = true, // Evasion Min Range Threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatSemiLogRange(minValue = 10f, maxValue = 10000f, sigFig = 1, withZero = true)] + public float evasionMinRangeThreshold = 0f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionNonlinearity", advancedTweakable = true, // Evasion/Extension Nonlinearity + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_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_AI_EvasionIgnoreMyTargetTargetingMe", advancedTweakable = true,//Ignore my target targeting me + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool evasionIgnoreMyTargetTargetingMe = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EvasionMissileKinematic", advancedTweakable = true,//Kinematic missile evasion + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool evasionMissileKinematic = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CollisionAvoidanceThreshold", advancedTweakable = true, //Vessel collision avoidance threshold + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 50f, stepIncrement = 1f, scene = UI_Scene.All)] + public float collisionAvoidanceThreshold = 20f; // 20m + target's average radius. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod", advancedTweakable = true, //Vessel collision avoidance look ahead period + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 3f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float vesselCollisionAvoidanceLookAheadPeriod = 1.5f; // Look 1.5s ahead for potential collisions. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CollisionAvoidanceStrength", advancedTweakable = true, //Vessel collision avoidance strength + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 4f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float vesselCollisionAvoidanceStrength = 2f; // 2° per frame (100°/s). + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_StandoffDistance", advancedTweakable = true, //Min Approach Distance + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 1000f, stepIncrement = 50f, scene = UI_Scene.All)] + + public float vesselStandoffDistance = 200f; // try to avoid getting closer than 200m + + // [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendMultiplier", advancedTweakable = true, //Extend Distance Multiplier + // groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + // UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = .1f, scene = UI_Scene.All)] + // public float extendMult = 1f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendDistanceAirToAir", advancedTweakable = true, //Extend Distance Air-To-Air + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + 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_AI_ExtendAngleAirToAir", advancedTweakable = true, //Extend Angle Air-To-Air + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_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_AI_ExtendDistanceAirToGroundGuns", advancedTweakable = true, //Extend Distance Air-To-Ground (Guns) + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] + public float extendDistanceAirToGroundGuns = 1500f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendDistanceAirToGround", advancedTweakable = true, //Extend Distance Air-To-Ground + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] + public float extendDistanceAirToGround = 2000f; // Doesn't include expected (unguided) bomb drop distance. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendTargetVel", advancedTweakable = true, //Extend Target Velocity Factor + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = .1f, scene = UI_Scene.All)] + public float extendTargetVel = 0.8f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendTargetAngle", advancedTweakable = true, //Extend Target Angle + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 180f, stepIncrement = 1f, scene = UI_Scene.All)] + public float extendTargetAngle = 78f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendTargetDist", advancedTweakable = true, //Extend Target Distance + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + 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_AI_ExtendAbortTime", advancedTweakable = true, //Extend Abort Time + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 1f, maxValue = 30f, stepIncrement = 1f, scene = UI_Scene.All)] + public float extendAbortTime = 10f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendMinGainRate", advancedTweakable = true, //Extend Min Gain Rate + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1, scene = UI_Scene.All)] + public float extendMinGainRate = 10f; + + float extensionCutoffTimer = 0; //For FJRT/P:S extension termination to prevent overly long extensions from poorly tuned extension settings + public float extensionCutoffTime = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ExtendToggle", advancedTweakable = true,//Extend Toggle + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_AI_EvadeExtend", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool canExtend = true; + #endregion + + #region Terrain + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, category = "DoubleSlider", guiName = "#LOC_BDArmory_AI_TurnRadiusTwiddleFactorMin", advancedTweakable = true,//Turn radius twiddle factors (category seems to have no effect) + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_AI_Terrain", groupStartCollapsed = true), + UI_FloatRange(minValue = 0.1f, maxValue = 5f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float turnRadiusTwiddleFactorMin = 2.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, category = "DoubleSlider", guiName = "#LOC_BDArmory_AI_TurnRadiusTwiddleFactorMax", advancedTweakable = true,//Turn radius twiddle factors (category seems to have no effect) + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_AI_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_AI_TerrainAvoidanceCriticalAngle", advancedTweakable = true, // Critical angle for inverted terrain avoidance. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_AI_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_AI_TerrainAvoidanceVesselReactionTime", advancedTweakable = true, // Vessel reaction time. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_AI_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_AI_TerrainAvoidancePostAvoidanceCoolDown", advancedTweakable = true, // Post-avoidance cool-down. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_AI_Terrain", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = 0.02f, scene = UI_Scene.All)] + public float postTerrainAvoidanceCoolDownDuration = 1f; // Duration after exiting terrain avoidance to ease out of pulling away from terrain. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_WaypointTerrainAvoidance", advancedTweakable = true,//Waypoint terrain avoidance. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_AI_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 + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_AllowRamming", advancedTweakable = true, //Toggle Allow Ramming + groupName = "pilotAI_Ramming", groupDisplayName = "#LOC_BDArmory_AI_Ramming", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool allowRamming = true; // Allow switching to ramming mode. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_AllowRammingGroundTargets", advancedTweakable = true, //Toggle Allow Ramming Ground Targets + groupName = "pilotAI_Ramming", groupDisplayName = "#LOC_BDArmory_AI_Ramming", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool allowRammingGroundTargets = true; // Allow ramming ground targets. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ControlSurfaceLag", advancedTweakable = true,//Control surface lag (for getting an accurate intercept for ramming). + groupName = "pilotAI_Ramming", groupDisplayName = "#LOC_BDArmory_AI_Ramming", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 0.2f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float controlSurfaceLag = 0.01f; // Lag time in response of control surfaces. + #endregion + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SliderResolution", advancedTweakable = true), // Slider Resolution + UI_ChooseOption(options = new string[4] { "Low", "Normal", "High", "Insane" }, scene = UI_Scene.All)] + public string sliderResolution = "Normal"; + string previousSliderResolution = "Normal"; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_Orbit", advancedTweakable = true),//Orbit + UI_Toggle(enabledText = "#LOC_BDArmory_AI_Orbit_Starboard", disabledText = "#LOC_BDArmory_AI_Orbit_Port", scene = UI_Scene.All),]//Starboard (CW)--Port (CCW) + public bool ClockwiseOrbit = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_UnclampTuning", advancedTweakable = true),//Unclamp tuning + UI_Toggle(enabledText = "#LOC_BDArmory_AI_UnclampTuning_enabledText", disabledText = "#LOC_BDArmory_AI_UnclampTuning_disabledText", scene = UI_Scene.All),]//Unclamped--Clamped + bool upToEleven = false; + public bool UpToEleven { get { return upToEleven; } set { if (upToEleven != value) { upToEleven = value; TurnItUpToEleven(); } } } + + Dictionary altMaxValues = new Dictionary + { + { nameof(defaultAltitude), 100000f }, + { nameof(minAltitude), 100000f }, + { nameof(maxAltitude), 150000f }, + { nameof(bombingAltitude), 10000f}, + { nameof(steerMult), 200f }, + { nameof(steerKiAdjust), 20f }, + { nameof(steerDamping), 100f }, + { nameof(maxSteer), 1f}, + { 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(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), 30f }, + { nameof(vesselStandoffDistance), 5000f }, + { nameof(turnRadiusTwiddleFactorMin), 10f}, + { nameof(turnRadiusTwiddleFactorMax), 10f}, + { nameof(controlSurfaceDeploymentTime), 10f }, + { nameof(controlSurfaceLag), 1f}, + { nameof(steerDampingPitch), 100f}, + { nameof(steerDampingYaw), 100f}, + { nameof(steerDampingRoll), 100f}, + { nameof(DynamicDampingMin), 100f }, + { nameof(DynamicDampingMax), 100f }, + { nameof(dynamicSteerDampingFactor), 100f }, + { nameof(DynamicDampingPitchMin), 100f }, + { nameof(DynamicDampingPitchMax), 100f }, + { nameof(dynamicSteerDampingPitchFactor), 100f }, + { nameof(DynamicDampingYawMin), 100f }, + { nameof(DynamicDampingYawMax), 100f }, + { nameof(dynamicSteerDampingYawFactor), 100f }, + { nameof(DynamicDampingRollMin), 100f }, + { nameof(DynamicDampingRollMax), 100f }, + { nameof(dynamicSteerDampingRollFactor), 100f }, + { nameof(threeAxisPIDPitchMult), 200f }, + { nameof(threeAxisPIDPitchKi), 20f }, + { nameof(threeAxisPIDPitchDamping), 100f }, + { nameof(threeAxisPIDYawMult), 200f }, + { nameof(threeAxisPIDYawKi), 20f }, + { nameof(threeAxisPIDYawDamping), 100f }, + { nameof(threeAxisPIDRollMult), 200f }, + { nameof(threeAxisPIDRollKi), 20f }, + { nameof(threeAxisPIDRollDamping), 100f }, + { nameof(autoTuningAltitude), 100000f }, + { nameof(autoTuningSpeed), 3000f } + }; + Dictionary altMinValues = new Dictionary { + { nameof(extendAngleAirToAir), -90f }, + { nameof(altitudeSteerLimiterFactor), -10f }, + }; + Dictionary altSemiLogValues = new Dictionary { + { nameof(evasionMinRangeThreshold), (1f, 1000000f, 1f) }, + }; + + void TurnItUpToEleven(BaseField _field = null, object _obj = null) + { + if (AutoTune && 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()) + { + var field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields[s.Current].uiControlFlight : Fields[s.Current].uiControlEditor); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Swapping max value of {s.Current} from {field.maxValue} to {altMaxValues[s.Current]}, current value is {(float)typeof(BDModulePilotAI).GetField(s.Current).GetValue(this)}"); + (altMaxValues[s.Current], field.maxValue) = (field.maxValue, altMaxValues[s.Current]); + // 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()) + { + var field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields[s.Current].uiControlFlight : Fields[s.Current].uiControlEditor); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Swapping min value of {s.Current} from {field.minValue} to {altMinValues[s.Current]}, current value is {(float)typeof(BDModulePilotAI).GetField(s.Current).GetValue(this)}"); + (altMinValues[s.Current], field.minValue) = (field.minValue, altMinValues[s.Current]); + // 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))); + } + foreach (var fieldName in altSemiLogValues.Keys.ToList()) + { + var field = (UI_FloatSemiLogRange)(HighLogic.LoadedSceneIsFlight ? Fields[fieldName].uiControlFlight : Fields[fieldName].uiControlEditor); + var temp = (field.minValue, field.maxValue, field.sigFig); + var altValues = altSemiLogValues[fieldName]; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Swapping semiLog limits of {fieldName} from {temp} to {altValues}"); + field.UpdateLimits(altValues.Item1, altValues.Item2, altValues.Item3); + altSemiLogValues[fieldName] = temp; + } + OnAutoTuneOptionsChanged(); // 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_AI_Standby"),//Standby Mode + UI_Toggle(enabledText = "#LOC_BDArmory_On", disabledText = "#LOC_BDArmory_Off")]//On--Off + public bool standbyMode = false; + bool standbyModeEnabled = 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() + { + StoreSettings(null); + } + void StoreSettings(string vesselName) + { + if (vesselName is null) + vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetName() : EditorLogic.fetch.ship.shipName; + if (storedSettings == null) + { + storedSettings = new Dictionary>>(); + } + if (storedSettings.ContainsKey(vesselName)) + { + if (storedSettings[vesselName] == null) + { + storedSettings[vesselName] = new List>(); + } + else + { + storedSettings[vesselName].Clear(); + } + } + else + { + storedSettings.Add(vesselName, new List>()); + } + 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() + { + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetName() : EditorLogic.fetch.ship.shipName; + if (storedSettings == null || !storedSettings.ContainsKey(vesselName) || storedSettings[vesselName] == null || storedSettings[vesselName].Count == 0) + { + Debug.Log("[BDArmory.BDModulePilotAI]: No stored settings found for vessel " + vesselName + "."); + return; + } + foreach (var setting in storedSettings[vesselName]) + { + var field = typeof(BDModulePilotAI).GetField(setting.Item1, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (field != null) + { + 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.GetName() : 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.GetName() : 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.GetName() : 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.GetName() : 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 + Vector3 upDirection = Vector3.up; + + #region Status / Steer Mode + enum StatusMode { Free, Orbiting, Engaging, Evading, Extending, TerrainAvoidance, CollisionAvoidance, RammingSpeed, TakingOff, GainingAltitude, Custom } + StatusMode currentStatusMode = StatusMode.Free; + StatusMode lastStatusMode = StatusMode.Free; + protected override void SetStatus(string status) + { + base.SetStatus(status); + if (status.StartsWith("Free")) currentStatusMode = StatusMode.Free; + else if (status.StartsWith("Engaging")) currentStatusMode = StatusMode.Engaging; + else if (status.StartsWith("Evading")) currentStatusMode = StatusMode.Evading; + else if (status.StartsWith("Orbiting")) currentStatusMode = StatusMode.Orbiting; + else if (status.StartsWith("Extending")) currentStatusMode = StatusMode.Extending; + else if (status.StartsWith("Ramming")) currentStatusMode = StatusMode.RammingSpeed; + else if (status.StartsWith("Taking off")) currentStatusMode = StatusMode.TakingOff; + else if (status.StartsWith("Gain Alt")) currentStatusMode = StatusMode.GainingAltitude; + else if (status.StartsWith("Terrain")) currentStatusMode = StatusMode.TerrainAvoidance; + else if (status.StartsWith("AvoidCollision")) currentStatusMode = StatusMode.CollisionAvoidance; + else currentStatusMode = StatusMode.Custom; + } + + public enum SteerModes + { + NormalFlight, // For most flight situations where the velocity direction is more important. + Manoeuvering, // For high-speed manoeuvering (e.g., evading, avoiding collisions), between NormalFlight and Aiming (less incentive to "roll up"). + Aiming // For actually aiming or for when the orientation of the plane is preferable to use instead of the velocity, e.g., regain energy, PSM. + } + SteerModes steerMode = SteerModes.NormalFlight; + #endregion + + #region PID Internals + //Controller Integral + Vector3 directionIntegral; + float pitchIntegral; + float yawIntegral; + float rollIntegral; + + //Dynamic Steer Damping values for the AI GUI + public float dynSteerDampingValue; + public float dynSteerDampingPitchValue; + public float dynSteerDampingYawValue; + public float dynSteerDampingRollValue; + + bool dirtyPAW_PID = false; // Flag for when the PID part of the PAW needs fixing. + #endregion + + #region Manoeuvrability and G-loading + //manueuverability and g loading data + // float maxDynPresGRecorded; + float dynDynPresGRecorded = 1f; // Start at reasonable non-zero value. + float dynVelocityMagSqr = 1f; // Start at reasonable non-zero value. + 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 dynUserSteerLimitMax = 1f; // Track the recently used max user steer limit. + + float maxAllowedSinAoA; + float lastAllowedAoA; + + float maxPosG; + float sinAoAAtMaxPosG; + + float maxNegG; + float sinAoAAtMaxNegG; + + // float[] gLoadMovingAvgArray = new float[32]; + // float[] cosAoAMovingAvgArray = new float[32]; + // int movingAvgIndex; + // float gLoadMovingAvg; + // float cosAoAMovingAvg; + SmoothingF smoothedGLoad; + SmoothingF smoothedSinAoA; + + 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 lastSinAoA; + float lastPitchInput; + + //instantaneous turn radius and possible acceleration from lift + //properties can be used so that other AI modules can read this for future maneuverability comparisons between craft + float turnRadius; + float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration; + + public float TurnRadius + { + get { return turnRadius; } + private set { turnRadius = value; } + } + + float maxLiftAcceleration; + + public float MaxLiftAcceleration + { + get { return maxLiftAcceleration; } + private set { maxLiftAcceleration = value; } + } + #endregion + + #region Ramming / Extending / Evading + // Ramming + public bool ramming = false; // Whether or not we're currently trying to ram someone. + + // Extending + bool extending; + bool extendParametersSet = false; + bool extendingForBombing = false; + float extendDistance; + float lastExtendDistance = 0; + bool extendHorizontally = true; // Measure the extendDistance horizonally (for A2G) or not (for A2A). + float extendDesiredMinAltitude; + public string extendingReason = ""; + public Vessel extendTarget = null; + Vector3 lastExtendTargetPosition; + float turningTimer; + + bool requestedExtend; + Vector3 requestedExtendTpos; + float extendRequestMinDistance = 0; + MissileBase extendForMissile = null; + float extendAbortTimer = 0; + + public bool IsExtending + { + get { return extending || requestedExtend; } + } + + public void StopExtending(string reason, bool cooldown = false) + { + if (!extending) return; + extending = false; + requestedExtend = false; + extendingReason = ""; + extendTarget = null; + extendRequestMinDistance = 0; + extendAbortTimer = cooldown ? -5f : 0f; + lastExtendDistance = 0; + extensionCutoffTimer = 0; + extendForMissile = null; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} stopped extending due to {reason}."); + } + + /// + /// Request extending away from a target position or vessel. + /// If a vessel is specified, it overrides the specified position. + /// + /// Reason for extending + /// The target to extend from + /// The minimum distance to extend for + /// The position to extend from if the target is null + /// 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 - lastExtendTargetPosition).ProjectOnPlanePreNormalized(upDirection) : vessel.transform.position - lastExtendTargetPosition; + 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}"); + } + + // Evading + bool evading = false; + bool wasEvading = false; + public bool IsEvading => evading; + + float evasiveTimer; + float threatRating; + Vector3 threatRelativePosition; + Vessel incomingMissileVessel; + enum KinematicEvasionStates { None, ToTarget, Crank, Notch, TurnAway, NotchDive } + KinematicEvasionStates kinematicEvasionState = KinematicEvasionStates.None; + #endregion + + #region Speed Controller / Steering / Altitude + bool useAB = true; + bool useBrakes = true; + bool regainEnergy = false; + + bool maxAltitudeEnabled = false; + bool belowMinAltitude; // True when below minAltitude or avoiding terrain. + bool gainAltInhibited = false; // Inhibit gain altitude to minimum altitude when chasing or evading someone as long as we're pointing upwards. + bool gainingAlt = false, wasGainingAlt = false; // Flags for tracking when we're gaining altitude. + Vector3 gainAltSmoothedForwardPoint = default; // Smoothing for the terrain adjustments of gaining altitude. + bool isBombing = false; // Flag for changing altitude behaviour when bombing. + + Vector3 prevTargetDir; + bool useVelRollTarget; + float finalMaxSteer = 1; + float userSteerLimit = 1; + + float targetStalenessTimer = 0; + Vector3 staleTargetPosition = Vector3.zero; + Vector3 staleTargetVelocity = Vector3.zero; + #endregion + + #region Flat-spin / PSM Detection + 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 invertRollTarget = false; // Invert the roll target under some conditions. + #endregion + + #region Collision Detection (between vessels) + //collision detection (for other vessels). + const int vesselCollisionAvoidanceTickerFreq = 10; // Number of fixedDeltaTime steps between vessel-vessel collision checks. + int collisionDetectionTicker = 0; + Vector3 collisionAvoidDirection; + public Vessel currentlyAvoidedVessel; + #endregion + + #region Terrain Avoidance + // Terrain avoidance and below minimum altitude globals. + bool avoidingTerrain = false; // True when avoiding terrain. + int terrainAlertTicker = 0; // A ticker to reduce the frequency of terrain alert checks. + float terrainAlertDetectionRadius = 30.0f; // Sphere radius that the vessel occupies. Should cover most vessels. FIXME This could be based on the vessel's maximum width/height. + float terrainAlertThreatRange; // The distance to the terrain to consider (based on turn radius). + float terrainAlertThreshold; // The current threshold for triggering terrain avoidance based on various factors. + 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 relativeVelocityRightDirection; // Right relative to current velocity and upDirection. + Vector3 relativeVelocityDownDirection; // Down relative to current velocity and upDirection. + Vector3 terrainAlertDebugPos, terrainAlertDebugDir; // Debug vector3's for drawing lines. + Color terrainAlertNormalColour = Color.green; // Color of terrain alert normal indicator. + List terrainAlertDebugRays = []; // Adjusted normals of terrain alerts used to get the final terrain alert normal. + RaycastHit[] terrainAvoidanceHits = new RaycastHit[10]; + float postTerrainAvoidanceCoolDownTimer = -1; // Timer to track how long since exiting terrain avoidance. + #endregion + + #region Debug Lines + LineRenderer lr; + Vector3 debugTargetPosition; + Vector3 debugTargetDirection; + Vector3 flyingToPosition; + Vector3 rollTarget; +#if DEBUG + Vector3 debugSquigglySquidDirection; +#endif + Vector3 angVelRollTarget; + Vector3 debugBreakDirection = default; + #endregion + + #region Wing Command + bool useFollowHints = false; + float followHintDistance = 0, followHintThreshold = 500; + float followSpeedI = 0, followSpeedD = 0; + private Vector3d debugFollowPosition; + #endregion + + GameObject vobj; + Transform velocityTransform + { + get + { + if (!vobj) + { + vobj = new GameObject("velObject"); + vobj.transform.position = vessel.ReferenceTransform.position; + vobj.transform.parent = vessel.ReferenceTransform; + } + + return vobj.transform; + } + } + + public override bool CanEngage() + { + return !vessel.LandedOrSplashed; + } + #endregion + + #region RMB info in editor + + // Yes + public override string GetInfo() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Available settings:"); + sb.AppendLine($"- Default Alt. - altitude to fly at when cruising/idle"); + sb.AppendLine($"- Min Altitude - below this altitude AI will prioritize gaining altitude over combat"); + sb.AppendLine($"- Steer Factor - higher will make the AI apply more control input for the same desired rotation"); + sb.AppendLine($"- Steer Ki - higher will make the AI apply control trim faster"); + sb.AppendLine($"- Steer Damping - higher will make the AI apply more control input when it wants to stop rotation"); + if (GameSettings.ADVANCED_TWEAKABLES) + sb.AppendLine($"- Steer Limiter - limit AI from applying full control input"); + sb.AppendLine($"- Max Speed - AI will not fly faster than this"); + sb.AppendLine($"- TakeOff Speed - speed at which to start pitching up when taking off"); + sb.AppendLine($"- MinCombat Speed - AI will prioritize regaining speed over combat below this"); + sb.AppendLine($"- Idle Speed - Cruising speed when not in combat"); + sb.AppendLine($"- Max G - AI will try not to perform maneuvers at higher G than this"); + sb.AppendLine($"- Max AoA - AI will try not to exceed this angle of attack"); + if (GameSettings.ADVANCED_TWEAKABLES) + { + sb.AppendLine($"- Extend Multiplier - scale the time spent extending"); + sb.AppendLine($"- Evasion Multiplier - scale the time spent evading"); + sb.AppendLine($"- Dynamic Steer Damping (min/max) - Dynamically adjust the steer damping factor based on angle to target"); + sb.AppendLine($"- Dyn Steer Damping Factor - Strength of dynamic steer damping adjustment"); + sb.AppendLine($"- Turn Radius Tuning (min/max) - Compensating factor for not being able to perform the perfect turn when oriented correctly/incorrectly"); + sb.AppendLine($"- Control Surface Lag - Lag time in response of control surfaces"); + sb.AppendLine($"- Orbit - Which direction to orbit when idling over a location"); + sb.AppendLine($"- Extend Toggle - Toggle extending multiplier behaviour"); + sb.AppendLine($"- Dynamic Steer Damping - Toggle dynamic steer damping"); + sb.AppendLine($"- Allow Ramming - Toggle ramming behaviour when out of guns/ammo"); + sb.AppendLine($"- Unclamp tuning - Increases variable limits, no direct effect on behaviour"); + } + sb.AppendLine($"- Standby Mode - AI will not take off until an enemy is detected"); + + return sb.ToString(); + } + + #endregion RMB info in editor + + #region UI Initialisers and Callbacks + protected void SetSliderPairClamps(string fieldNameMin, string fieldNameMax) + { + // Enforce min <= max for pairs of sliders + UI_FloatRange field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields[fieldNameMin].uiControlFlight : Fields[fieldNameMin].uiControlEditor); + field.onFieldChanged = OnMinUpdated; + field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields[fieldNameMax].uiControlFlight : Fields[fieldNameMax].uiControlEditor); + field.onFieldChanged = OnMaxUpdated; + } + + public void OnMinUpdated(BaseField field = null, object obj = null) + { + if (turnRadiusTwiddleFactorMax < turnRadiusTwiddleFactorMin) { turnRadiusTwiddleFactorMax = turnRadiusTwiddleFactorMin; } // Enforce min < max for turn radius twiddle factor. + // if (DynamicDampingMax < DynamicDampingMin) { DynamicDampingMax = DynamicDampingMin; } // Enforce min < max for dynamic steer damping. + // if (DynamicDampingPitchMax < DynamicDampingPitchMin) { DynamicDampingPitchMax = DynamicDampingPitchMin; } + // if (DynamicDampingYawMax < DynamicDampingYawMin) { DynamicDampingYawMax = DynamicDampingYawMin; } + // if (DynamicDampingRollMax < DynamicDampingRollMin) { DynamicDampingRollMax = DynamicDampingRollMin; } // reversed roll dynamic damp behavior + } + + public void OnMaxUpdated(BaseField field = null, object obj = null) + { + if (turnRadiusTwiddleFactorMin > turnRadiusTwiddleFactorMax) { turnRadiusTwiddleFactorMin = turnRadiusTwiddleFactorMax; } // Enforce min < max for turn radius twiddle factor. + // if (DynamicDampingMin > DynamicDampingMax) { DynamicDampingMin = DynamicDampingMax; } // Enforce min < max for dynamic steer damping. + // if (DynamicDampingPitchMin > DynamicDampingPitchMax) { DynamicDampingPitchMin = DynamicDampingPitchMax; } + // if (DynamicDampingYawMin > DynamicDampingYawMax) { DynamicDampingYawMin = DynamicDampingYawMax; } + // if (DynamicDampingRollMin > DynamicDampingRollMax) { DynamicDampingRollMin = DynamicDampingRollMax; } // reversed roll dynamic damp behavior + } + + void SetFieldClamps() + { + var minAltField = (UI_FloatRange)Fields["minAltitude"].uiControlEditor; + minAltField.onFieldChanged = ClampFields; + minAltField = (UI_FloatRange)Fields["minAltitude"].uiControlFlight; + minAltField.onFieldChanged = ClampFields; + var defaultAltField = (UI_FloatRange)Fields["defaultAltitude"].uiControlEditor; + defaultAltField.onFieldChanged = ClampFields; + defaultAltField = (UI_FloatRange)Fields["defaultAltitude"].uiControlFlight; + defaultAltField.onFieldChanged = ClampFields; + var maxAltField = (UI_FloatRange)Fields["maxAltitude"].uiControlEditor; + maxAltField.onFieldChanged = ClampFields; + maxAltField = (UI_FloatRange)Fields["maxAltitude"].uiControlFlight; + maxAltField.onFieldChanged = ClampFields; + var autoTuningAltField = (UI_FloatRange)Fields["autoTuningAltitude"].uiControlFlight; + autoTuningAltField.onFieldChanged = ClampFields; + var autoTuningSpeedField = (UI_FloatRange)Fields["autoTuningSpeed"].uiControlFlight; + autoTuningSpeedField.onFieldChanged = ClampFields; + } + + void ClampFields(BaseField field, object obj) + { + ClampFields(field.name); + } + 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; } + if (minAltitude > defaultAltitude) { minAltitude = defaultAltitude; } + break; + case "maxAltitude": + 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 field name {fieldName} in ClampFields."); + break; + } + } + + [KSPAction("Toggle Max Altitude (AGL)")] + public void ToggleMaxAltitudeAG(KSPActionParam param) + { + maxAltitudeToggle = !maxAltitudeEnabled; + ToggleMaxAltitude(); + } + [KSPAction("Enable Max Altitude (AGL)")] + public void EnableMaxAltitudeAG(KSPActionParam param) + { + maxAltitudeToggle = true; + ToggleMaxAltitude(); + } + [KSPAction("Disable Max Altitude (AGL)")] + public void DisableMaxAltitudeAG(KSPActionParam param) + { + maxAltitudeToggle = false; + ToggleMaxAltitude(); + } + + void SetOnMaxAltitudeChanged() + { + UI_Toggle field = (UI_Toggle)(HighLogic.LoadedSceneIsFlight ? Fields["maxAltitudeToggle"].uiControlFlight : Fields["maxAltitudeToggle"].uiControlEditor); + field.onFieldChanged = ToggleMaxAltitude; + ToggleMaxAltitude(); + } + void ToggleMaxAltitude(BaseField field = null, object obj = null) + { + maxAltitudeEnabled = maxAltitudeToggle; + var maxAltitudeField = Fields["maxAltitude"]; + maxAltitudeField.guiActive = maxAltitudeToggle; + maxAltitudeField.guiActiveEditor = maxAltitudeToggle; + if (!maxAltitudeToggle) + StartCoroutine(FixAltitudesSectionLayout()); + } + void SetMinCollisionAvoidanceLookAheadPeriod() + { + var minCollisionAvoidanceLookAheadPeriod = (UI_FloatRange)Fields["vesselCollisionAvoidanceLookAheadPeriod"].uiControlEditor; + minCollisionAvoidanceLookAheadPeriod.minValue = vesselCollisionAvoidanceTickerFreq * Time.fixedDeltaTime; + minCollisionAvoidanceLookAheadPeriod = (UI_FloatRange)Fields["vesselCollisionAvoidanceLookAheadPeriod"].uiControlFlight; + minCollisionAvoidanceLookAheadPeriod.minValue = vesselCollisionAvoidanceTickerFreq * Time.fixedDeltaTime; + } + + void SetOnExtendAngleA2AChanged() + { + UI_FloatRange field = (UI_FloatRange)Fields["extendAngleAirToAir"].uiControlEditor; + field.onFieldChanged = OnExtendAngleA2AChanged; + field = (UI_FloatRange)Fields["extendAngleAirToAir"].uiControlFlight; + field.onFieldChanged = OnExtendAngleA2AChanged; + OnExtendAngleA2AChanged(); + } + void OnExtendAngleA2AChanged(BaseField field = null, object obj = null) + { + _extendAngleAirToAir = Mathf.Sin(extendAngleAirToAir * Mathf.Deg2Rad); + } + + void SetOnTerrainAvoidanceCriticalAngleChanged() + { + UI_FloatRange field = (UI_FloatRange)Fields["terrainAvoidanceCriticalAngle"].uiControlEditor; + field.onFieldChanged = OnTerrainAvoidanceCriticalAngleChanged; + field = (UI_FloatRange)Fields["terrainAvoidanceCriticalAngle"].uiControlFlight; + field.onFieldChanged = OnTerrainAvoidanceCriticalAngleChanged; + OnTerrainAvoidanceCriticalAngleChanged(); + } + public void OnTerrainAvoidanceCriticalAngleChanged(BaseField field = null, object obj = null) + { + terrainAvoidanceCriticalCosAngle = Mathf.Cos(terrainAvoidanceCriticalAngle * Mathf.Deg2Rad); + } + + void SetOnImmelmannTurnAngleChanged() + { + var field = (UI_FloatRange)Fields["ImmelmannTurnAngle"].uiControlFlight; + field.onFieldChanged = OnImmelmannTurnAngleChanged; + OnImmelmannTurnAngleChanged(); + } + void OnImmelmannTurnAngleChanged(BaseField field = null, object obj = null) + { + ImmelmannTurnCosAngle = -Mathf.Cos(ImmelmannTurnAngle * Mathf.Deg2Rad); + } + + void SetOnBrakingPriorityChanged() + { + var field = (UI_FloatRange)Fields["brakingPriority"].uiControlFlight; + field.onFieldChanged = OnBrakingPriorityChanged; + OnBrakingPriorityChanged(); + } + void OnBrakingPriorityChanged(BaseField field = null, object obj = null) + { + speedController.brakingPriority = brakingPriority / 100f; + } + + void SetOnMaxSpeedChanged() + { + UI_FloatRange field = (UI_FloatRange)Fields["maxSpeed"].uiControlFlight; + field.onFieldChanged = OnMaxSpeedChanged; + OnMaxSpeedChanged(); + } + public void OnMaxSpeedChanged(BaseField field = null, object obj = null) + { + BankedTurnDistance = Mathf.Clamp(8f * maxSpeed, 1000f, 4000f); + } + + /// + /// Upgrade the damping settings to two independent toggles for dynamic and 3-axis. + /// This allows non-dynamic 3-axis damping. + /// + /// Non-persistent KSPField get loaded from craft files, but not saved. + /// + void UpgradeDamping() + { + threeAxisSteerDamping |= dynamicSteerDamping && CustomDynamicAxisFields; // Enable 3-axis if it was enabled previously for 3-axis dynamic damping. + if (dynamicSteerDamping && CustomDynamicAxisFields) // Upgrade non-dynamic axes in mixed setups to the static one, which was used before. + { + if (!dynamicDampingPitch) steerDampingPitch = steerDamping; + if (!dynamicDampingYaw) steerDampingYaw = steerDamping; + if (!dynamicDampingRoll) steerDampingRoll = steerDamping; + } + } + void SetOnDampingTogglesChanged() + { + foreach (var fieldName in new List { "dynamicSteerDamping", "threeAxisSteerDamping", "dynamicDampingPitch", "dynamicDampingRoll", "dynamicDampingYaw", "threeAxisPID" }) + { + var field = (UI_Toggle)(HighLogic.LoadedSceneIsFlight ? Fields[fieldName].uiControlFlight : Fields[fieldName].uiControlEditor); + field.onFieldChanged = OnPIDTogglesChanged; + } + OnPIDTogglesChanged(); + } + public void OnPIDTogglesChanged(BaseField field = null, object obj = null) + { + ToggleDynamicPIDFields(); // Reconfigure the dynamic PID fields in the PAW. + } + public void ToggleDynamicPIDFields() + { + if (HighLogic.LoadedSceneIsFlight && AutoTune) pidAutoTuning.RevertPIDValues(); // Revert to the PID values to best/base values. + + // Static damping + { + var field = Fields["steerDamping"]; + field.guiActive = field.guiActiveEditor = !dynamicSteerDamping && !threeAxisSteerDamping && !threeAxisPID; + } + + // 3-axis static damping + foreach (var fieldName in new List { "steerDampingPitch", "steerDampingYaw", "steerDampingRoll" }) + { + var field = Fields[fieldName]; + field.guiActive = field.guiActiveEditor = !threeAxisPID && threeAxisSteerDamping && ( + !dynamicSteerDamping || // 3-axis static damping + dynamicSteerDamping && ( // 3-axis mixed damping + !dynamicDampingPitch && fieldName == "steerDampingPitch" + || !dynamicDampingYaw && fieldName == "steerDampingYaw" + || !dynamicDampingRoll && fieldName == "steerDampingRoll" + ) + ); + } + + // Dynamic damping + foreach (var fieldName in new List { "DynamicDampingLabel", "DynamicDampingMin", "DynamicDampingMax", "dynamicSteerDampingFactor" }) + { + var field = Fields[fieldName]; + field.guiActive = field.guiActiveEditor = dynamicSteerDamping && !threeAxisSteerDamping && !threeAxisPID; + } + + // 3-axis dynamic damping + foreach (var axis in new List { "Pitch", "Yaw", "Roll" }) + { + var axisField = Fields[$"dynamicDamping{axis}"]; + axisField.guiActive = axisField.guiActiveEditor = dynamicSteerDamping && threeAxisSteerDamping && !threeAxisPID; + var enabled = (bool)axisField.GetValue(this); + foreach (var fieldName in new List { $"{axis}Label", $"DynamicDamping{axis}Max", $"DynamicDamping{axis}Min", $"dynamicSteerDamping{axis}Factor" }) + { + var field = Fields[fieldName]; + field.guiActive = field.guiActiveEditor = dynamicSteerDamping && threeAxisSteerDamping && !threeAxisPID && enabled; + } + } + + // Full 3-axis PID (overrides other modes) + foreach (var fieldName in new List { "dynamicSteerDamping", "threeAxisSteerDamping", "steerMult", "steerKiAdjust" }) + { + var field = Fields[fieldName]; + field.guiActive = field.guiActiveEditor = !threeAxisPID; + } + foreach (var axis in new List { "Pitch", "Yaw", "Roll" }) + { + foreach (var pid in new List { "Mult", "Ki", "Damping" }) + { + var field = Fields[$"threeAxisPID{axis}{pid}"]; + field.guiActive = field.guiActiveEditor = threeAxisPID; + } + } + + dirtyPAW_PID = true; + if (HighLogic.LoadedSceneIsFlight && AutoTune) pidAutoTuning.ResetGradient(); // Reset the auto-tuning if we were autotuning. + } + + void SetOnAutoTuningRecenteringDistanceChanged() + { + UI_FloatRange field = (UI_FloatRange)(HighLogic.LoadedSceneIsFlight ? Fields["autoTuningRecenteringDistance"].uiControlFlight : Fields["autoTuningRecenteringDistance"].uiControlEditor); + field.onFieldChanged = OnAutoTuningRecenteringDistanceChanged; + OnAutoTuningRecenteringDistanceChanged(); + } + void OnAutoTuningRecenteringDistanceChanged(BaseField field = null, object ob = null) + { + autoTuningRecenteringDistanceSqr = autoTuningRecenteringDistance * autoTuningRecenteringDistance * 1e6f; + } + + IEnumerator FixAltitudesSectionLayout() // Fix the layout of the Altitudes section by briefly disabling the fields underneath the one that was removed. + { + var maxAltitudeToggleField = Fields["maxAltitudeToggle"]; + maxAltitudeToggleField.guiActive = false; + maxAltitudeToggleField.guiActiveEditor = false; + yield return null; + maxAltitudeToggleField.guiActive = true; + maxAltitudeToggleField.guiActiveEditor = true; + } + + void SetupSliderResolution() + { + var sliderResolutionField = (UI_ChooseOption)(HighLogic.LoadedSceneIsFlight ? Fields["sliderResolution"].uiControlFlight : Fields["sliderResolution"].uiControlEditor); + sliderResolutionField.onFieldChanged = OnSliderResolutionUpdated; + OnSliderResolutionUpdated(); + } + float sliderResolutionAsFloat(string res, float factor = 10f) + { + switch (res) + { + case "Low": return factor; + case "High": return 1f / factor; + case "Insane": return 1f / factor / factor; + default: return 1f; + } + } + void OnSliderResolutionUpdated(BaseField field = null, object obj = null) + { + if (previousSliderResolution != sliderResolution) + { + var factor = Mathf.Pow(10f, Mathf.Round(Mathf.Log10(sliderResolutionAsFloat(sliderResolution) / sliderResolutionAsFloat(previousSliderResolution)))); + foreach (var PIDField in Fields) + { + 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 = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); + if (alsoMinValue) slider.minValue = slider.stepIncrement; + } + } + if (PIDField.group.name == "pilotAI_Altitudes") + { + 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 = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); + if (alsoMinValue) slider.minValue = slider.stepIncrement; + } + } + if (PIDField.group.name == "pilotAI_Speeds") + { + var uiControl = HighLogic.LoadedSceneIsFlight ? PIDField.uiControlFlight : PIDField.uiControlEditor; + if (uiControl.GetType() == typeof(UI_FloatRange)) + { + var slider = (UI_FloatRange)uiControl; + slider.stepIncrement *= factor; + slider.stepIncrement = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); + } + } + if (PIDField.group.name == "pilotAI_EvadeExtend") + { + if (PIDField.name.StartsWith("extendDistance")) + { + var uiControl = HighLogic.LoadedSceneIsFlight ? PIDField.uiControlFlight : PIDField.uiControlEditor; + if (uiControl.GetType() == typeof(UI_FloatRange)) + { + var slider = (UI_FloatRange)uiControl; + slider.stepIncrement *= factor; + slider.stepIncrement = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); + } + } + } + } + previousSliderResolution = sliderResolution; + } + } + + 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(); + } + void OnAutoTuneChanged(BaseField field = null, object obj = null) + { + 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(); + if (FlightInputHandler.fetch.precisionMode) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Precision input mode is enabled, disabling it."); + FlightInputHandler.fetch.precisionMode = false; // If precision control mode is enabled, disable it. + } + + SetAutoTuneFields(); + MaintainFuelLevels(autoTune); // Prevent fuel drain while auto-tuning. + DisableBattleDamage(autoTune); // Disable battle damage 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 = null, object obj = null) + { + if (!AutoTune || pidAutoTuning is null) return; + pidAutoTuning.RevertPIDValues(); + pidAutoTuning.ResetMeasurements(); + pidAutoTuning.ResetGradient(); + } + + void SetOnUpToElevenChanged() + { + var field = (UI_Toggle)(HighLogic.LoadedSceneIsFlight ? Fields["upToEleven"].uiControlFlight : Fields["upToEleven"].uiControlEditor); + field.onFieldChanged = TurnItUpToEleven; // Only triggered on UI interaction. + if (upToEleven) TurnItUpToEleven(); // The initially loaded values are not the alternate ones. + } + + 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 || HighLogic.LoadedSceneIsEditor)) return; + + if (HighLogic.LoadedSceneIsFlight) + { + 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); // 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(); + SetSliderPairClamps("turnRadiusTwiddleFactorMin", "turnRadiusTwiddleFactorMax"); + // SetSliderClamps("DynamicDampingMin", "DynamicDampingMax"); + // SetSliderClamps("DynamicDampingPitchMin", "DynamicDampingPitchMax"); + // SetSliderClamps("DynamicDampingYawMin", "DynamicDampingYawMax"); + // SetSliderClamps("DynamicDampingRollMin", "DynamicDampingRollMax"); + SetFieldClamps(); + SetMinCollisionAvoidanceLookAheadPeriod(); + SetWaypointTerrainAvoidance(); + UpgradeDamping(); // Upgrade old configurations to the new independent dynamic and 3-axis toggles. + SetOnDampingTogglesChanged(); + SetOnMaxAltitudeChanged(); + SetOnExtendAngleA2AChanged(); + SetOnTerrainAvoidanceCriticalAngleChanged(); + SetOnImmelmannTurnAngleChanged(); + SetOnMaxSpeedChanged(); + SetOnAutoTuningRecenteringDistanceChanged(); + SetupAutoTuneSliders(); + SetOnUpToElevenChanged(); + if ((HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) && storedSettings != null && storedSettings.ContainsKey(HighLogic.LoadedSceneIsFlight ? vessel.GetName() : EditorLogic.fetch.ship.shipName)) + { + Events["RestoreSettings"].active = true; + } + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) + { + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetName() : 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(); + } + + public override bool ActivatePilot() + { + if (!base.ActivatePilot()) return false; + originalMaxSpeed = maxSpeed; + belowMinAltitude = vessel.LandedOrSplashed; + prevTargetDir = vesselTransform.up; + if (TakingOff && !vessel.LandedOrSplashed) // In case we activate pilot after taking off manually. + { + TakingOff = false; + } + + SetOnBrakingPriorityChanged(); // Has to be set after the speed controller exists. + + bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)vessel.orbit.referenceBody.GeeASL; // Set gravity for calculations; + return true; + } + + void Update() + { + if (BDArmorySettings.DEBUG_LINES && pilotEnabled) + { + 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 != null) { lr.enabled = false; } } + + if (dirtyPAW_PID) StartCoroutine(FixFieldOrdering("pilotAI_PID")); + } + + IEnumerator SetVar(string name, float value) + { + yield return null; + typeof(BDModulePilotAI).GetField(name).SetValue(this, value); + } + + void FixedUpdate() + { + //floating origin and velocity offloading corrections + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDKrakensbane.IsActive) + { + if (lastExtendTargetPosition != null) lastExtendTargetPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + var weaponManager = WeaponManager; + 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; + } + } + + // This is triggered every Time.fixedDeltaTime. + protected override void AutoPilot(FlightCtrlState s) + { + // Reset and update various internal values and checks. Then update the pilot logic for the physics frame. + + //default brakes off full throttle + //s.mainThrottle = 1; + + //vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); + AdjustThrottle(maxSpeed, true); + useAB = true; + useBrakes = true; + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); + if (vessel.InNearVacuum()) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + } + + if (!ramming) steerMode = SteerModes.NormalFlight; // Reset the steer mode, unless we're ramming. + useVelRollTarget = false; + + // landed and still, chill out + var weaponManager = WeaponManager; + if (vessel.LandedOrSplashed && standbyMode && weaponManager && (BDATargetManager.GetClosestTarget(weaponManager) == null || BDArmorySettings.PEACE_MODE)) + { + standbyModeEnabled = true; + //s.mainThrottle = 0; + //vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + AdjustThrottle(0, true); + return; + } + if (standbyModeEnabled && standbyMode) // Was in standby, but now there's something to engage, disable standby and engage. + { + CommandTakeOff(); + if (SpawnUtils.CountActiveEngines(vessel) == 0) // If no engines are active, trigger AG10 and then activate all engines if necessary. + { + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); + if (SpawnUtils.CountActiveEngines(vessel) == 0) + { + SpawnUtils.ActivateAllEngines(vessel); + } + } + } + + upDirection = vessel.up; + + finalMaxSteer = 1f; // Reset finalMaxSteer, is adjusted in subsequent methods + userSteerLimit = GetUserDefinedSteerLimit(); // Get the current user-defined steer limit. + CalculateAccelerationAndTurningCircle(); + CheckFlatSpin(); + + if ((float)vessel.radarAltitude < minAltitude)// && !isBombing) //TODO - refinement of torp bombing temporary minAlt ignore needed + { belowMinAltitude = true; } + + if (gainAltInhibited && (!belowMinAltitude || !(isBombing || currentStatusMode == StatusMode.Engaging || currentStatusMode == StatusMode.Evading || currentStatusMode == StatusMode.RammingSpeed || currentStatusMode == StatusMode.GainingAltitude))) + { // Allow switching between "Engaging", "Bombing", "Evading", "Ramming speed!" and "Gain Alt." while below minimum altitude without disabling the gain altitude inhibitor. + gainAltInhibited = false; + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDModulePilotAI]: " + vessel.vesselName + " is no longer inhibiting gain alt"); + } + + if (!hardMinAltitude && !gainAltInhibited && belowMinAltitude && (isBombing || currentStatusMode == StatusMode.Engaging || currentStatusMode == StatusMode.Evading || currentStatusMode == StatusMode.RammingSpeed) && !vessel.InNearVacuum()) + { // Vessel went below minimum altitude while "Engaging", "Bombing", "Evading" or "Ramming speed!", enable the gain altitude inhibitor. + gainAltInhibited = true; + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDModulePilotAI]: " + vessel.vesselName + " was " + currentStatus + " and went below min altitude, inhibiting gain alt."); + } + + if ((vessel.srfSpeed < minSpeed) || (FlatSpin != 0)) + { regainEnergy = true; } + else if (!belowMinAltitude && vessel.srfSpeed > Mathf.Min(minSpeed + 20f, idleSpeed)) + { regainEnergy = false; } + + UpdateVelocityRelativeDirections(); + CheckLandingGear(); + if (IsRunningWaypoints) UpdateWaypoint(); // Update the waypoint state. + + wasGainingAlt = gainingAlt; gainingAlt = false; + if (!vessel.LandedOrSplashed && ((!(ramming && steerMode == SteerModes.Manoeuvering) && FlyAvoidTerrain(s)) || (!ramming && FlyAvoidOthers(s)))) // Avoid terrain and other planes, unless we're trying to ram stuff. + { turningTimer = 0; } + else if (TakingOff) // Take off. + { + TakeOff(s); + turningTimer = 0; + } + 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); + AdjustPitchForGAndAoALimits(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. + { + belowMinAltitude = false; + } + + if (BDArmorySettings.DEBUG_AI) + { + if (lastStatusMode != currentStatusMode) + { + Debug.Log("[BDArmory.BDModulePilotAI]: Status of " + vessel.vesselName + " changed from " + lastStatusMode + " to " + currentStatus); + } + lastStatusMode = currentStatusMode; + } + } + + void UpdateAI(FlightCtrlState s) + { + SetStatus("Free"); + + CheckExtend(ExtendChecks.RequestsOnly); + + // Calculate threat rating from any threats + float minimumEvasionTime = minEvasionTime; + threatRating = evasionThreshold + 1f; // Don't evade by default + wasEvading = evading; + evading = false; + isBombing = false; + if (extendAbortTimer < 0) // Extending is in cooldown. + { + extendAbortTimer += TimeWarp.fixedDeltaTime; + if (extendAbortTimer > 0) extendAbortTimer = 0; + } + var weaponManager = WeaponManager; + if (weaponManager != null) + { + bool evadeMissile = weaponManager.incomingMissileTime <= weaponManager.evadeThreshold; + if (evadeMissile && evasionMissileKinematic && weaponManager.incomingMissileVessel) // Ignore missiles when they are post-thrust and we are turning back towards target + { + MissileBase mb = VesselModuleRegistry.GetMissileBase(weaponManager.incomingMissileVessel); + if (mb != null) + evadeMissile = !(kinematicEvasionState == KinematicEvasionStates.ToTarget && incomingMissileVessel == weaponManager.incomingMissileVessel && mb.MissileState == MissileBase.MissileStates.PostThrust); + } + else + kinematicEvasionState = KinematicEvasionStates.None; // Reset missile kinematic evasion state + + if (evadeMissile) + { + 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 + incomingMissileVessel = weaponManager.incomingMissileVessel; + } + else if (weaponManager.underFire && !ramming) // If we're ramming, ignore gunfire. + { + if (weaponManager.incomingMissTime >= evasionTimeThreshold && weaponManager.incomingThreatDistanceSqr >= evasionMinRangeThreshold * evasionMinRangeThreshold) // If we haven't been under fire long enough or they're too close, ignore gunfire + { + threatRating = weaponManager.incomingMissDistance; + } + } + } + + 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) + { + if (evasiveTimer < minimumEvasionTime) + { + threatRelativePosition = vessel.Velocity().normalized + vesselTransform.right; + + if (weaponManager) + { + if (weaponManager.incomingMissileVessel)//switch to weaponManager.missileisIncoming? + { + threatRelativePosition = weaponManager.incomingThreatPosition - vesselTransform.position; + if (extending) + StopExtending("missile threat"); // Don't keep trying to extend if under fire from missiles + } + + if (weaponManager.underFire) + { + threatRelativePosition = weaponManager.incomingThreatPosition - vesselTransform.position; + } + } + } + Evasive(s); + evasiveTimer += Time.fixedDeltaTime; + turningTimer = 0; + + if (evasiveTimer >= minimumEvasionTime) + { + evasiveTimer = 0; + collisionDetectionTicker = vesselCollisionAvoidanceTickerFreq + 1; //check for collision again after exiting evasion routine + } + if (evading) return; + } + 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) + { + evasiveTimer = 0; + if (!targetVessel.LandedOrSplashed) + { + Vector3 targetVesselRelPos = targetVessel.vesselTransform.position - vesselTransform.position; + if (canExtend && vessel.radarAltitude < defaultAltitude && VectorUtils.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); + } + + if (VectorUtils.Angle(targetVessel.vesselTransform.position - vesselTransform.position, vesselTransform.up) > 35) // If target is outside of 35° cone ahead of us then keep flying straight. + { + turningTimer += Time.fixedDeltaTime; + } + else + { + turningTimer = 0; + } + + 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 + + float extendTargetDot = Mathf.Cos(extendTargetAngle * Mathf.Deg2Rad); + if (canExtend && targetVelFrac < extendTargetVel && targetForwardDot < extendTargetDot && targetVesselRelPos.sqrMagnitude < extendTargetDist * extendTargetDist) // Default values: Target is outside of ~78° cone ahead, closer than 400m and slower than us, so we won't be able to turn to attack it now. + { + RequestExtend("can't turn fast enough", targetVessel); + weaponManager.ForceScan(); + } + if (canExtend && turningTimer > 15) + { + RequestExtend("turning too long", targetVessel); //extend if turning circles for too long + turningTimer = 0; + weaponManager.ForceScan(); + } + } + else //extend if too close for an air-to-ground attack + { + CheckExtend(ExtendChecks.AirToGroundOnly); + } + + if (!extending) + { + if (weaponManager.HasWeaponsAndAmmo() || !RamTarget(s, targetVessel)) // If we're out of ammo, see if we can ram someone, otherwise, behave as normal. + { + ramming = false; + SetStatus("Engaging"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Flying to target " + targetVessel.vesselName); + FlyToTargetVessel(s, targetVessel); + return; + } + } + } + else + { + evasiveTimer = 0; + if (!extending) + { + if (ResumeCommand()) + { + UpdateCommand(s); + return; + } + SetStatus("Orbiting"); + FlyOrbit(s, assignedPositionGeo, 2000, idleSpeed, ClockwiseOrbit); + return; + } + } + + if (CheckExtend()) + { + weaponManager.ForceScan(); + evasiveTimer = 0; + FlyExtend(s, lastExtendTargetPosition); + return; + } + } + + bool PredictCollisionWithVessel(Vessel v, float maxTime, out Vector3 badDirection) + { + var weaponManager = WeaponManager; + if (vessel == null || v == null || v == (weaponManager != null ? weaponManager.incomingMissileVessel : null) + || (v.rootPart != null && v.rootPart.FindModuleImplementing() != null)) //evasive will handle avoiding missiles + { + badDirection = Vector3.zero; + return false; + } + + // Adjust some values for asteroids. + 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. + { + maxTime += targetRadius / (float)vessel.srfSpeed * (turnRadiusTwiddleFactorMin + turnRadiusTwiddleFactorMax); + } + + // 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.TimeToCPA(v, maxTime); // This uses the same kinematics as AIUtils.PredictPosition. + if (timeToCPA > 0 && timeToCPA < maxTime) + { + Vector3 tPos = AIUtils.PredictPosition(v, timeToCPA); + Vector3 myPos = AIUtils.PredictPosition(vessel, timeToCPA); + if (Vector3.SqrMagnitude(tPos - myPos) < threshold * threshold) // Within collisionAvoidanceThreshold of each other. Danger Will Robinson! + { + badDirection = tPos - vesselTransform.position; + return true; + } + } + + badDirection = Vector3.zero; + return false; + } + + bool RamTarget(FlightCtrlState s, Vessel v) + { + if (BDArmorySettings.DISABLE_RAMMING || !allowRamming || (!allowRammingGroundTargets && v.LandedOrSplashed)) return false; // Override from BDArmory settings and local config. + if (v == null) return false; // We don't have a target. + if (Vector3.Dot(vessel.srf_vel_direction, v.srf_vel_direction) * (float)v.srfSpeed / (float)vessel.srfSpeed > 0.95f) return false; // We're not approaching them fast enough. + float timeToCPA = vessel.TimeToCPA(v, 16f); + + // Set steer mode to manoeuvering for less than 8s left, we're trying to collide, not aim. + if (timeToCPA < 8f) + steerMode = SteerModes.Manoeuvering; + else + steerMode = SteerModes.NormalFlight; + + // Let's try to ram someone! + if (!ramming) + ramming = true; + SetStatus("Ramming speed!"); + + // If they're also in ramming speed and trying to ram us, then just aim straight for them until the last moment. + var targetAI = v.ActiveController().PilotAI; + if (timeToCPA > 1f && targetAI != null && targetAI.pilotEnabled && targetAI.ramming) + { + var targetWM = v.ActiveController().WM; + if (targetWM != null && targetWM.currentTarget != null && targetWM.currentTarget.Vessel == vessel && Vector3.Dot(vessel.srf_vel_direction, v.srf_vel_direction) < -0.866f) // They're trying to ram us and are mostly head-on! Two can play at that game! + { + FlyToPosition(s, AIUtils.PredictPosition(v.transform.position, v.Velocity(), v.acceleration, TimeWarp.fixedDeltaTime)); // Ultimate Chicken!!! + AdjustThrottle(maxSpeed, false, true); + return true; + } + } + + // 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); + float easeVel = Mathf.Clamp01(2f - timeToCPA / 8f); + Vector3 predictedPosition = AIUtils.PredictPosition(v.transform.position, v.Velocity() * easeVel, v.acceleration * easeAccel, timeToCPA + TimeWarp.fixedDeltaTime); // Compensate for the off-by-one frame issue. + + if (controlSurfaceLag > 0) + predictedPosition += -1 * controlSurfaceLag * controlSurfaceLag * (timeToCPA / controlSurfaceLag - 1f + Mathf.Exp(-timeToCPA / controlSurfaceLag)) * vessel.acceleration * easeAccel; // Compensation for control surface lag. + FlyToPosition(s, predictedPosition); + AdjustThrottle(maxSpeed, false, true); // Ramming speed! + + return true; + } + 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 = vectorToTarget.ProjectOnPlanePreNormalized(upDirection).magnitude; + float angleToTarget = VectorUtils.Angle(target - vesselTransform.position, vesselTransform.up); + float strafingDistance = -1f; + float relativeVelocity = (float)(vessel.srf_velocity - v.srf_velocity).magnitude; + + var weaponManager = WeaponManager; + if (weaponManager) + { + if (!weaponManager.staleTarget) staleTargetVelocity = Vector3.zero; //if actively tracking target, reset last known velocity vector + missile = weaponManager.CurrentMissile; + if (missile != null) + { + if (missile.GetWeaponClass() == WeaponClasses.Missile) + { + if (distanceToTarget > 5500f) //why 5.5km? + { + finalMaxSteer = GetSteerLimiterForSpeedAndPower(); + } + + if (missile.TargetingMode == MissileBase.TargetingModes.Heat && !weaponManager.heatTarget.exists) + { + 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 + { + target = MissileGuidance.GetAirToAirFireSolution(missile, v); + } + //Vector3 leadOffset = (missile.MissileReferenceTransform.position + (missile.MissileReferenceTransform.forward * distanceToTarget)) - (vesselTransform.position + (vesselTransform.up * distanceToTarget)); + //target -= leadOffset; //correctly account for missiles mounted at an angle (important if heater to keep them pointed at heatsource and/or keep target within boresight) + if (!missile.isMMG) target = Quaternion.FromToRotation(missile.MissileReferenceTransform.forward, vesselTransform.up) * (target - vesselTransform.position) + vesselTransform.position; + angleToTarget = VectorUtils.Angle(vesselTransform.up, target - vesselTransform.position); + if (angleToTarget < 20f) + { + steerMode = SteerModes.Aiming; + } + } + else //bombing + { + target = GetSurfacePosition(target); //set submerged targets to surface for future bombingAlt vectoring + finalBombingAlt = (weaponManager.currentTarget != null && weaponManager.currentTarget.Vessel != null) && weaponManager.currentTarget.Vessel.LandedOrSplashed ? (missile.GetWeaponClass() == WeaponClasses.SLW ? 200 : //drop to the deck for torpedo run // sources suggest torp drop height varies (based on torp) from ~15m to ~260m. 200 seems a decent mid ground. + bombingAltitude) : //else commence level bombing + (float)v.altitude + (divebombing ? bombingAltitude : missile.GetBlastRadius() * 2); //else target flying; get close for bombing airships to try and ensure hits + if (distanceToTarget > Mathf.Max(4500f, extendDistanceAirToGround + ((float)vessel.horizontalSrfSpeed * BDAMath.Sqrt(2 * finalBombingAlt / bodyGravity)) + finalBombingAlt)) //lead based on estimate of fall time at desired alt, regardless if we're there yet + { + finalMaxSteer = GetSteerLimiterForSpeedAndPower(); + //target = target + (finalBombingAlt * upDirection); //aim for target alt while still out of range + if (missile.GetWeaponClass() != WeaponClasses.SLW) //semi-aggressively get to desired bombing alt before we get into range + { + //if (Mathf.Abs((float)vessel.altitude - finalBombingAlt) > 100) target = transform.position + (target - transform.position).normalized * 2000; //get to bombing alt if not yet there. + //target += (finalBombingAlt - (float)FlightGlobals.getAltitudeAtPos(target)) * upDirection; + + var (distance, direction) = (vessel.CoM - target).ProjectOnPlanePreNormalized(upDirection).MagNorm(); + target += 0.5f * distance * direction + finalBombingAlt * upDirection; // Aim for the bombing altitude at half-way to the target + } + else + target += finalBombingAlt * upDirection; //leisurely aim for target alt while still out of range, AI might be above coastline so don't go low early + } + else + { + if (missile.GetWeaponClass() == WeaponClasses.SLW) + { + if (distanceToTarget < missile.engageRangeMax + relativeVelocity) // Distance until starting to strafe plus 1s for changing speed. + { + if (weaponManager.firedMissiles < weaponManager.maxMissilesOnTarget) + strafingDistance = Mathf.Max(0f, distanceToTarget - missile.engageRangeMax); //slow to strafing speed so torps survive hitting the water + } + } + if (weaponManager.firedMissiles >= weaponManager.maxMissilesOnTarget) finalBombingAlt = bombingAltitude; //have craft break off as soon as torps away so AI doesn't continue to fly towards enemy guns + if (!divebombing || missile.GetWeaponClass() == WeaponClasses.SLW) //don't divebomb w/ torpedoes + { + steerMode = SteerModes.Manoeuvering; //steer to aim bombs(not guns). Manoeuvering is a lot more stable. + if (angleToTarget < 45f) + { + if (missile.GetWeaponClass() == WeaponClasses.SLW) + { + target = MissileGuidance.GetAirToAirFireSolution(missile, v); + //if (Mathf.Abs((float)vessel.altitude - finalBombingAlt) > 40) target = transform.position + (target - transform.position).normalized * 400; //dive to the deck to get to torpbombing alt + //target += (finalBombingAlt - (float)FlightGlobals.getAltitudeAtPos(target)) * upDirection; + } + else + { + target = AIUtils.PredictPosition(v, weaponManager.bombAirTime); //make AI properly lead bombs vs moving targets, also why AI didn't like dropping them before. Should be at altitude, so use correct timing + //Look at averaged velocity of target? SrfAI weave behavior throws off bombing targting, due to 10+ sec drop time (ofc, this is true to life - point me to one instance of level bombing with UGBs vs moving targets being accurate) + //could just accept that UGBs need low bombingAlts/divebombing and higher bombing should be with JDAMs + } + var (distance, direction) = (vessel.CoM - target).ProjectOnPlanePreNormalized(upDirection).MagNorm(); + target += (missile.GetWeaponClass() == WeaponClasses.SLW ? 0.85f : 0.5f) * distance * direction + finalBombingAlt * upDirection; //get to target alt semi-aggressively. 0.75 is a bit too leisurely for torp bombing, but 0.85 seems to do reasonably well. + } + else //probably overshot the target at this point + { + if (missile.GetWeaponClass() == WeaponClasses.SLW) + { + target = MissileGuidance.GetAirToAirFireSolution(missile, v); + } + target += finalBombingAlt * upDirection; + } + } + else + { + target = AIUtils.PredictPosition(v, weaponManager.bombAirTime); //actively diving towards target, use real-Time drop time vs estimate for static alt + if (distanceToTarget < defaultAltitude * 2) finalBombingAlt = (v.LandedOrSplashed ? minAltitude : (float)v.altitude + missile.GetBlastRadius() * 2); //dive towards target. Distance trigger in MissileFire may need some tweaking; currently must be under this + 500 to drop bombs + if (weaponManager.firedMissiles >= weaponManager.maxMissilesOnTarget) finalBombingAlt = bombingAltitude; //have craft break off as soon as bombs away so AI doesn't continue to fly towards enemy guns/ground + target += finalBombingAlt * upDirection; + } + } + debugString.AppendLine($"bombingAlt: {finalBombingAlt}"); + isBombing = true; + } + } + else if (weaponManager.currentGun) + { + ModuleWeapon weapon = weaponManager.currentGun; + if (weapon != null) + { + Vector3 weaponPosition, weaponDirection; + if (weapon.turret && (weapon.yawRange > 0 || weapon.maxPitch > weapon.minPitch)) // Don't apply lead offset and weapon offsets for turrets. + { + weaponPosition = vessel.ReferenceTransform.position; + weaponDirection = vesselTransform.up; + } + else + { + Vector3 leadOffset = weapon.GetLeadOffset(); + target -= leadOffset; // Lead offset from aiming assuming the gun is forward aligned and centred. + + // Note: depending on the airframe, there is an island of stability around -2°—30° in pitch and ±10° in yaw where the vessel can stably aim with offset weapons. + weaponPosition = weapon.offsetWeaponPosition + vessel.ReferenceTransform.position; + weaponDirection = vessel.ReferenceTransform.TransformDirection(weapon.offsetWeaponDirection); + + target = Quaternion.FromToRotation(weaponDirection, vesselTransform.up) * (target - vesselTransform.position) + vesselTransform.position; // correctly account for angular offset guns/schrage Musik + var weaponOffset = vessel.ReferenceTransform.position - weaponPosition; + + debugString.AppendLine($"WeaponOffset ({v.vesselName}): {weaponOffset.x}x m; {weaponOffset.y}y m; {weaponOffset.z}z m"); + target += weaponOffset; //account for weapons with translational offset from longitudinal axis + } + + angleToTarget = VectorUtils.Angle(weaponDirection, target - weaponPosition); + 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 + } + else + { + if (distanceToTarget > 3500f || angleToTarget > 90f || vessel.srfSpeed < takeOffSpeed) + { + finalMaxSteer = GetSteerLimiterForSpeedAndPower(); + } + else + { + //figuring how much to lead the target's movement to get there after its movement assuming we can manage a constant speed turn + //this only runs if we're not aiming and not that far from the target and the target is in front of us + float curVesselMaxAccel = Math.Min(dynDynPresGRecorded * (float)vessel.dynamicPressurekPa, maxAllowedGForce * bodyGravity); + if (curVesselMaxAccel > 0) + { + float timeToTurn = (float)vessel.srfSpeed * angleToTarget * Mathf.Deg2Rad / curVesselMaxAccel; + target += timeToTurn * v.Velocity(); + target += 0.5f * timeToTurn * timeToTurn * v.acceleration; + } + } + } + + if (v.LandedOrSplashed) + { + if (distanceToTarget < weapon.engageRangeMax + relativeVelocity) // Distance until starting to strafe plus 1s for changing speed. + { + strafingDistance = Mathf.Max(0f, distanceToTarget - weapon.engageRangeMax); + } + if (distanceToTarget > weapon.engageRangeMax) + { + target = FlightPosition(target, Mathf.Min(defaultAltitude, weapon.engageRangeMax / 2f)); // Clamp target minAlt to give at most a 30° dive slope. + } + else + { + steerMode = SteerModes.Aiming; + } + } + else if (Vector3.Dot(target - weaponPosition, weaponDirection) < 0) //If a gun is selected, craft is probably already within gunrange, or a couple of seconds of being in gunrange + { + // Don't bother with the off-by-one physics frame correction as this doesn't need to be so accurate here. + target = Quaternion.FromToRotation(weaponDirection, vesselTransform.up) * (v.CoM - vesselTransform.position) + vesselTransform.position; + } + } + } + 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 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 (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. + { + if (distanceToTarget > vesselStandoffDistance) // Adjust target speed based on distance from desired stand-off distance. + finalMaxSpeed = (distanceToTarget - vesselStandoffDistance) / 8f + (float)v.srfSpeed; // Beyond stand-off distance, approach a little faster. + else + { + //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. + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Getting too close to Enemy. Braking!"); + } + } + else + { + finalMaxSpeed = strafingSpeed + (float)v.srfSpeed; + } + } + finalMaxSpeed = Mathf.Clamp(finalMaxSpeed, minSpeed, maxSpeed); + AdjustThrottle(finalMaxSpeed, true); + + 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 + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Enemy on tail. Braking!"); + AdjustThrottle(minSpeed, true); + } + + if (missile != null) + { + float boresightFactor = (vessel.LandedOrSplashed || v.LandedOrSplashed || missile.uncagedLock) ? 0.75f : 0.35f; + float minOffBoresight = missile.maxOffBoresight * boresightFactor; + float missileAngleToTarget = VectorUtils.Angle(missile.GetForwardTransform(), v.transform.position - missile.transform.position); + var minDynamicLaunchRange = MissileLaunchParams.GetDynamicLaunchParams( + missile, + v.Velocity(), + v.transform.position, + // minOffBoresight + (180f - minOffBoresight) * Mathf.Clamp01(((missile.transform.position - v.transform.position).magnitude - missile.minStaticLaunchRange) / (Mathf.Max(100f + missile.minStaticLaunchRange * 1.5f, 0.1f * missile.maxStaticLaunchRange) - missile.minStaticLaunchRange)) // Reduce the effect of being off-target while extending to prevent super long extends. + // missileAngleToTarget <= minOffBoresight ? -1 : (missile.transform.position - v.transform.position).sqrMagnitude < (missile.minStaticLaunchRange * missile.minStaticLaunchRange) ? 180 : -1 + missileAngleToTarget <= minOffBoresight ? -1 : minOffBoresight + ).minLaunchRange; + //all we should be concerned about here is: + //1) we're on target, but too close - extend back to min launch range, AI will handle coming about so by time it does so we should still be outside min range; all we need is missile absolute min distance + //2) off target, and too close - extend to min launch range, ditto + //3) on target, and beyond min range - no need to extend + //4) off target, and beyond min launch range - are we within kinematic min range? + //so: the FoV value should then be (missileAngleToTarget <= maxBoresight ? -1 : (missile.transform.position - v.transform.position).sqrmagnitude < (missile.minStaticLaunchRange * missile.minStaticLaunchRange) ? 180 : -1) + + 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) + { + RegainEnergy(s, target - vesselTransform.position); + return; + } + else + { + debugString.AppendLine($"AngleToTarget ({v.vesselName}): {angleToTarget}° Dot: {Vector3.Dot((target - vesselTransform.position).normalized, vesselTransform.up):F6}"); + useVelRollTarget = true; + FlyToPosition(s, target); + return; + } + } + + void RegainEnergy(FlightCtrlState s, Vector3 direction, float throttleOverride = -1f) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Regaining energy"); + + steerMode = SteerModes.Aiming; // Just point the plane in the direction we want to go to minimise drag. + 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 + AdjustThrottle(maxSpeed, false, true); + + FlyToPosition(s, vesselTransform.position + (targetDirection * 100), true); + } + + float GetSteerLimiterForSpeedAndPower() + { + float possibleAccel = speedController.GetPossibleAccel(); + float speed = (float)vessel.srfSpeed; + + 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. + 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 / Mathf.Clamp((float)vessel.atmDensity, 0, 1.225f); // Scale based on atmospheric density relative to sea level Kerbin (since dynamic pressure depends on density) + + return Mathf.Clamp01(limiter); + } + + void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrottle = false) + { + var weaponManager = WeaponManager; + Vector3 vesselPos = vesselTransform.position; + Vector3 vesselForward = vesselTransform.forward; + Vector3 vesselUp = vesselTransform.up; + Vector3 vesselRight = vesselTransform.right; + + //test poststall (before FlightPosition is called so we're using the right steerMode) + float AoA = VectorUtils.Angle(vessel.ReferenceTransform.up, vessel.Velocity()); + if (AoA > postStallAoA) + { + isPSM = true; + steerMode = SteerModes.Aiming; // Too far off-axis for the velocity direction to be relevant. + } + else + { + isPSM = false; + } + Vector3 targetDirection = (targetPosition - vesselPos).normalized; + if (AutoTune && (Vector3.Dot(targetDirection, vesselUp) > 0.9397f)) // <20° + { + steerMode = SteerModes.Aiming; // Pretend to aim when on target. + } + + if (!belowMinAltitude && command != PilotCommands.Follow) // Includes avoidingTerrain + { + if (weaponManager && Time.time - weaponManager.timeBombReleased < 1.5f) + { + targetPosition = vessel.transform.position + vessel.Velocity(); + } + + targetPosition = LongRangeAltitudeCorrection(targetPosition); //have this only trigger in atmo? + targetPosition = FlightPosition(targetPosition, isBombing ? Mathf.Min(finalBombingAlt, minAltitude) : minAltitude); + targetDirection = (targetPosition - vesselPos).normalized; + targetPosition = vesselPos + 100 * targetDirection; + } + + Vector3 srfVel = vessel.Velocity(); + if (srfVel.sqrMagnitude > Vector3.kEpsilon) // vel < 3mm/s + { + velocityTransform.rotation = Quaternion.LookRotation(srfVel, -vesselForward); + } + velocityTransform.rotation = Quaternion.AngleAxis(90, velocityTransform.right) * velocityTransform.rotation; + + //ang vel + Vector3 localAngVel = vessel.angularVelocity; + //test + Vector3 currTargetDir = targetDirection; + if (evasionNonlinearity > 0 && (IsExtending || IsEvading || // If we're extending or evading, add a deviation to the fly-to direction to make us harder to hit. + weaponManager && (((steerMode == SteerModes.NormalFlight || steerMode == SteerModes.Aiming && weaponManager.CurrentMissile != null) || IsRunningWaypoints) && weaponManager.guardMode && // Also, if we know enemies are near, but they're beyond gun or visual range and we're not aiming a gun, or we're running a WP course and standard evasion isn't ideal + BDATargetManager.TargetList(weaponManager.Team).Where(target => + !target.isMissile && + weaponManager.CanSeeTarget(target, true, true) + ).AllAndNotEmpty(target => + (target.Vessel.CoM - vesselPos).sqrMagnitude > weaponManager.maxVisualGunRangeSqr + )))) + { + 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, targetDirection) * upDirection.ProjectOnPlanePreNormalized(targetDirection).normalized; +#if DEBUG + debugSquigglySquidDirection = squigglySquidDirection; +#endif + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Squiggly Squid: {VectorUtils.Angle(targetDirection, Vector3.RotateTowards(targetDirection, squigglySquidDirection, evasionNonlinearity * Mathf.Deg2Rad, 0f))}° at {(squigglySquidTime % 360f).ToString("G3")}°"); + targetDirection = Vector3.RotateTowards(targetDirection, squigglySquidDirection, evasionNonlinearity * Mathf.Deg2Rad, 0f); + } + Vector3 targetAngVel = Vector3.Cross(prevTargetDir, targetDirection) / Time.fixedDeltaTime; + Vector3 localTargetAngVel = vesselTransform.InverseTransformVector(targetAngVel); + prevTargetDir = targetDirection; + targetPosition = vessel.transform.position + 100 * targetDirection; + flyingToPosition = targetPosition; + float angleToTarget = VectorUtils.Angle(targetDirection, vesselUp); + + //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; + // float velAngleToTarget = Mathf.Clamp(VectorUtils.Angle(targetDirection, vessel.Velocity()), 0, 90); + // if (vessel.atmDensity > 0.05f) finalSpeed = Mathf.Min(speedController.targetSpeed, Mathf.Clamp(maxSpeed - (speedReductionFactor * velAngleToTarget), idleSpeed, maxSpeed)); + if (!vessel.InNearVacuum()) 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) + { + AdjustThrottle(finalSpeed, useBrakes, useAB); + } + + if (steerMode == SteerModes.Aiming) + { + localAngVel -= localTargetAngVel; + } + + Vector3 localTargetDirection; + Vector3 localTargetDirectionYaw; + if (steerMode == SteerModes.NormalFlight || steerMode == SteerModes.Manoeuvering) + { + localTargetDirection = velocityTransform.InverseTransformDirection(targetPosition - velocityTransform.position).normalized; + localTargetDirection = Vector3.RotateTowards(Vector3.up, localTargetDirection, 45 * Mathf.Deg2Rad, 0); + + if (useWaypointYawAuthority && IsRunningWaypoints) + { + var refYawDir = Vector3.RotateTowards(Vector3.up, vesselTransform.InverseTransformDirection(targetDirection), 25 * Mathf.Deg2Rad, 0).normalized; + var velYawDir = Vector3.RotateTowards(Vector3.up, vesselTransform.InverseTransformDirection(vessel.Velocity()), 45 * Mathf.Deg2Rad, 0).normalized; + localTargetDirectionYaw = waypointYawAuthorityStrength * refYawDir + (1f - waypointYawAuthorityStrength) * velYawDir; + } + else + { + localTargetDirectionYaw = vesselTransform.InverseTransformDirection(vessel.Velocity()).normalized; + localTargetDirectionYaw = Vector3.RotateTowards(Vector3.up, localTargetDirectionYaw, 45 * Mathf.Deg2Rad, 0); + } + } + else//(steerMode == SteerModes.Aiming) + { + localTargetDirection = vesselTransform.InverseTransformDirection(targetDirection).normalized; + localTargetDirection = Vector3.RotateTowards(Vector3.up, localTargetDirection, 25 * Mathf.Deg2Rad, 0); + localTargetDirectionYaw = localTargetDirection; + } + + //// Adjust targetDirection based on ATTITUDE limits + // 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 = targetDirection.ProjectOnPlanePreNormalized(projectPlane); + //} + //debugString.AppendLine($"Attitude: " + attitude); + + // User-set steer limits + finalMaxSteer *= userSteerLimit; + finalMaxSteer = Mathf.Clamp(finalMaxSteer, 0.1f, 1f); // added just in case to ensure some input is retained no matter what happens + + //roll + Vector3 currentRoll = -vesselForward; + float rollUp = steerMode == SteerModes.NormalFlight ? 10f : 5f; // Reduced roll-up for Aiming and Manoeuvering. + if (steerMode == SteerModes.NormalFlight) + { + rollUp += (1 - finalMaxSteer) * 10f; + } + rollTarget = targetPosition + (rollUp * upDirection) - vesselPos; + + //test + if (steerMode == SteerModes.Aiming && !belowMinAltitude && !invertRollTarget) + { + angVelRollTarget = -140 * vesselTransform.TransformVector(Quaternion.AngleAxis(90f, Vector3.up) * localTargetAngVel); + rollTarget += angVelRollTarget; + } + + if (command == PilotCommands.Follow && useFollowHints) + { + rollTarget *= Mathf.Clamp(followHintDistance / followHintThreshold, 0.2f, 1f); // Reduce our own rollTarget requirements as we get close to the formation position. + rollTarget += Mathf.Clamp01(1 - followHintDistance / followHintThreshold) * rollUp * -commandLeader.vessel.ReferenceTransform.forward; // Use stronger hint from leader as we get closer to being in position. + } + + if (invertRollTarget) rollTarget = -rollTarget; + + bool requiresLowAltitudeRollTargetCorrection = false; + if (avoidingTerrain || postTerrainAvoidanceCoolDownTimer >= 0) + { + rollTarget = terrainAlertNormal * 100; + var terrainAvoidanceRollCosAngle = Vector3.Dot(-vesselForward, terrainAlertNormal.ProjectOnPlanePreNormalized(vesselUp).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. + } + if (postTerrainAvoidanceCoolDownTimer >= 0 && postTerrainAvoidanceCoolDownDuration > 0) + { + localTargetDirection = Vector3.RotateTowards(localTargetDirection, Vector3.forward, (terrainAvoidanceRollCosAngle < terrainAvoidanceCriticalCosAngle ? 30f : -30f) * Mathf.Deg2Rad * Mathf.Clamp01(1f - postTerrainAvoidanceCoolDownTimer / postTerrainAvoidanceCoolDownDuration), 0); + } + } + else if (belowMinAltitude && !gainAltInhibited) + { + rollTarget = Vector3.Lerp(BodyUtils.GetSurfaceNormal(vesselPos), 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 && Vector3.Dot(rollTarget, upDirection) < 0 && Vector3.Dot(rollTarget, vessel.Velocity()) < 0) // If we're not avoiding terrain and the roll target is behind us and downwards, check that a circle arc of radius "turn radius" (scaled by twiddle factor maximum) tilted at angle of rollTarget has enough room to avoid hitting the ground. + { + if (belowMinAltitude) // Never do inverted loops below min altitude. + { requiresLowAltitudeRollTargetCorrection = true; } + else // Otherwise, check the turning circle. + { + // The following calculates the altitude required to turn in the direction of the rollTarget based on the current velocity and turn radius. + // The setup is a circle in the plane of the rollTarget, which is tilted by angle phi from vertical, with the vessel at the point subtending an angle theta as measured from the top of the circle. + var n = Vector3.Cross(vessel.srf_vel_direction, rollTarget).normalized; // Normal of the plane of rollTarget. + 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 = BDAMath.Sqrt(1f - a * a); // cos(phi) = sqrt(1-sin(phi)^2) + var r = turnRadiusTwiddleFactorMax * turnRadius; // Worst-case radius of turning circle. + + var h = r * (1 + Vector3.Dot(m, vessel.srf_vel_direction)) * b; // Required altitude: h = r * (1+cos(theta)) * cos(phi). + if (vessel.radarAltitude + Vector3.Dot(vessel.srf_velocity, upDirection) * controlSurfaceDeploymentTime < h) // Too low for this manoeuvre. + { + 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 (ramming) + { + rollTarget = (targetPosition - vesselPos + rollUp * Mathf.Clamp((targetPosition - vesselPos).magnitude / 500f, 0f, 1f) * upDirection).ProjectOnPlanePreNormalized(vesselUp); + } + else if (useWaypointRollTarget && IsRunningWaypoints) + { + var angle = waypointRollTargetStrength * VectorUtils.Angle(waypointRollTarget, rollTarget); + rollTarget = Vector3.RotateTowards(rollTarget, waypointRollTarget, angle * Mathf.Deg2Rad, 0f).ProjectOnPlane(vessel.Velocity()); + } + else if (useVelRollTarget && !belowMinAltitude) + { + Vector3 normVel = vessel.Velocity().normalized; + rollTarget = rollTarget.ProjectOnPlanePreNormalized(normVel); + currentRoll = currentRoll.ProjectOnPlanePreNormalized(normVel); + } + else + { + rollTarget = rollTarget.ProjectOnPlanePreNormalized(vesselUp); + } + + if (requiresLowAltitudeRollTargetCorrection) // Low altitude downwards loop prevention to avoid triggering terrain avoidance. + { + // Set the roll target to be horizontal. + 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 = (vessel.transform.position - vessel.mainBody.transform.position).ProjectOnPlanePreNormalized(vesselUp); + float bankAngle = Vector3.SignedAngle(horizonNormal, rollTarget, vesselUp); + if ((Mathf.Abs(bankAngle) > maxBank) && (maxBank != 180)) + rollTarget = Vector3.RotateTowards(horizonNormal, rollTarget, maxBank / 180 * Mathf.PI, 0.0f); + bankAngle = Vector3.SignedAngle(horizonNormal, rollTarget, vesselUp); + + float pitchError = VectorUtils.GetAngleOnPlane(localTargetDirection, Vector3.up, Vector3.back); + float yawError = VectorUtils.GetAngleOnPlane(localTargetDirectionYaw, Vector3.up, Vector3.right); + float rollError; + if (useVelRollTarget) + rollError = VectorUtils.SignedAngle(currentRoll, rollTarget, vesselRight); + else + rollError = VectorUtils.GetAngleOnPlane(rollTarget, currentRoll, vesselRight); + + if (BDArmorySettings.DEBUG_LINES) + { + debugTargetPosition = vessel.transform.position + targetDirection * 1000; // The asked for target position's direction + debugTargetDirection = vessel.transform.position + vesselTransform.TransformDirection(localTargetDirection) * 200; // The actual direction to match the "up" direction of the craft with for pitch (used for PID calculations). + } + + #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 + float pitchProportional = 0.015f * SteerPower(Axis.Pitch) * pitchError; + float yawProportional = 0.005f * SteerPower(Axis.Yaw) * yawError; + float rollProportional = 0.0015f * SteerPower(Axis.Roll) * rollError; + + float pitchDamping = SteerDamping(Mathf.Abs(angleToTarget), angleToTarget, Axis.Pitch) * -localAngVel.x; + float yawDamping = 0.33f * SteerDamping(Mathf.Abs(yawError * (steerMode == SteerModes.Aiming ? (180f / 25f) : 4f)), angleToTarget, Axis.Yaw) * -localAngVel.z; + float rollDamping = 0.1f * SteerDamping(Mathf.Abs(rollError), angleToTarget, Axis.Roll) * -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 = (directionIntegral + (pitchError * -vesselForward + yawError * vesselRight) * Time.fixedDeltaTime).ProjectOnPlanePreNormalized(vesselUp); + if (directionIntegral.sqrMagnitude > 1f) directionIntegral = directionIntegral.normalized; + pitchIntegral = SteerCorrection(Axis.Pitch) * Vector3.Dot(directionIntegral, -vesselForward); + yawIntegral = 0.33f * SteerCorrection(Axis.Yaw) * Vector3.Dot(directionIntegral, vesselRight); + rollIntegral = 0.1f * SteerCorrection(Axis.Roll) * Mathf.Clamp(rollIntegral + rollError * Time.fixedDeltaTime, -1f, 1f); + + var steerPitch = pitchProportional + pitchIntegral - pitchDamping; + var steerYaw = yawProportional + yawIntegral - yawDamping; + var steerRoll = rollProportional + rollIntegral - rollDamping; + #endregion + + //v/q + float dynamicAdjustment = Mathf.Clamp(16 * (float)(vessel.srfSpeed / vessel.dynamicPressurekPa), 0, 1.2f); + steerPitch *= dynamicAdjustment; + steerYaw *= dynamicAdjustment; + steerRoll *= dynamicAdjustment; + + SetFlightControlState(s, + Mathf.Clamp(steerPitch, -finalMaxSteer, finalMaxSteer), // pitch + Mathf.Clamp(steerYaw, -finalMaxSteer, finalMaxSteer), // yaw + Mathf.Clamp(steerRoll, -userSteerLimit, userSteerLimit)); // roll + + if (AutoTune) + { pidAutoTuning.Update(pitchError, rollError, yawError); } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + debugString.AppendLine($"steerMode: {steerMode}, rollError: {rollError,7:F4}°, pitchError: {pitchError,7:F4}°, yawError: {yawError,7:F4}°"); + debugString.AppendLine($"finalMaxSteer: {finalMaxSteer:G3}, dynAdj: {dynamicAdjustment:G3}"); + // debugString.AppendLine($"Bank Angle: {bankAngle}"); + debugString.AppendLine($"Pitch: P: {pitchProportional,7:F4}, I: {pitchIntegral,7:F4}, D: {pitchDamping,7:F4}"); + debugString.AppendLine($"Yaw: P: {yawProportional,7:F4}, I: {yawIntegral,7:F4}, D: {yawDamping,7:F4}"); + debugString.AppendLine($"Roll: P: {rollProportional,7:F4}, I: {rollIntegral,7:F4}, D: {rollDamping,7:F4}"); + // debugString.AppendLine($"ω.x: {vessel.angularVelocity.x:F3} rad/s, I.x: {vessel.angularMomentum.x / vessel.angularVelocity.x:F3} kg•m²"); + } + } + + enum ExtendChecks { All, RequestsOnly, AirToGroundOnly }; + bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) + { + // Sanity checks. + var weaponManager = WeaponManager; + if (weaponManager == null) + { + StopExtending("no weapon manager"); + return false; + } + if (weaponManager.TargetOverride) // Target is overridden, follow others' instructions. + { + 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. + extendHorizontally = true; + extendingForBombing = false; + } + if (requestedExtend) + { + requestedExtend = false; + if (CheckRequestedExtendDistance()) + { + extending = true; + lastExtendTargetPosition = requestedExtendTpos; + } + } + // TODO - Extend vectors - for something like a bombing run that fails, or seeking to extend from a enemy plane that's getting too close, makes more sense to continue forward, or with something like a 45deg deflection, vs a full 180, to conserve energy. + + if (checkType == ExtendChecks.RequestsOnly) return extending; + if (extending && extendParametersSet) + { + if (extendTarget != null) // Update the last known target position. + { + lastExtendTargetPosition = 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; + float minOffBoresight = extendForMissile.maxOffBoresight * boresightFactor; + float missileAngleToTarget = VectorUtils.Angle(extendForMissile.GetForwardTransform(), extendTarget.transform.position - extendForMissile.transform.position); + var minDynamicLaunchRange = MissileLaunchParams.GetDynamicLaunchParams( + extendForMissile, + extendTarget.Velocity(), + extendTarget.transform.position, + //minOffBoresight + (180f - minOffBoresight) * Mathf.Clamp01(((missile.transform.position - v.transform.position).magnitude - missile.minStaticLaunchRange) / (Mathf.Max(100f + missile.minStaticLaunchRange * 1.5f, 0.1f * missile.maxStaticLaunchRange) - missile.minStaticLaunchRange)) // Reduce the effect of being off-target while extending to prevent super long extends. + //missileAngleToTarget <= minOffBoresight ? -1 : (extendForMissile.transform.position - extendTarget.transform.position).sqrMagnitude < (extendForMissile.minStaticLaunchRange * extendForMissile.minStaticLaunchRange) ? 180 : -1 + missileAngleToTarget <= minOffBoresight ? -1 : minOffBoresight + ).minLaunchRange; + extendDistance = Mathf.Max(extendDistanceAirToAir, minDynamicLaunchRange); + extendDesiredMinAltitude = Mathf.Min(finalBombingAlt, minAltitude); + //(weaponManager.currentTarget != null && weaponManager.currentTarget.Vessel != null && weaponManager.currentTarget.Vessel.LandedOrSplashed) ? (extendForMissile.GetWeaponClass() == WeaponClasses.SLW ? 10 : //drop to the deck for torpedo run + //Mathf.Max(defaultAltitude - 500f, minAltitude)) : //else commence level bombing + //extendForMissile.GetBlastRadius() * 2; //else target flying; get close for bombing airships to try and ensure hits + } + } + return true; // Already extending. + } + if (!wasEvading) evasionNonlinearityDirection = Mathf.Sign(UnityEngine.Random.Range(-1f, 1f)); // This applies to extending too. + + // Dropping a bomb. + if (extending && (extendingReason == "bombs away!" || extendingReason == "too close to bomb")) + //weaponManager.CurrentMissile && weaponManager.CurrentMissile.GetWeaponClass() == WeaponClasses.Bomb) // Run away from the bomb! + { + extendDistance = extendRequestMinDistance; //4500; //what, are we running from nukes? blast radius * 1.5 should be sufficient + extendDesiredMinAltitude = Mathf.Min(finalBombingAlt, minAltitude); + extendingForBombing = true; + extendParametersSet = true; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} is extending due to dropping a bomb!"); + return true; + } + + // Ground targets. + if (targetVessel != null && targetVessel.LandedOrSplashed) + { + var selectedGun = weaponManager.currentGun; + if (selectedGun == null && weaponManager.selectedWeapon == null) selectedGun = weaponManager.previousGun; + if (selectedGun != null && !selectedGun.engageGround) // Don't extend from ground targets when using a weapon that can't target ground targets. + { + weaponManager.ForceScan(); // Look for another target instead. + return false; + } + if (selectedGun != null) // If using a gun or no weapon is selected, take the extend multiplier into account. + { + // extendDistance = Mathf.Clamp(weaponManager.guardRange - 1800, 500, 4000) * extendMult; // General extending distance. + extendDistance = extendDistanceAirToGroundGuns; + extendDesiredMinAltitude = minAltitude + 0.5f * extendDistance; // Desired minimum altitude after extending. (30° attack vector plus min alt.) + } + else //Bombing + { + // extendDistance = Mathf.Clamp(weaponManager.guardRange - 1800, 2500, 4000); + // desiredMinAltitude = (float)vessel.radarAltitude + (defaultAltitude - (float)vessel.radarAltitude) * extendMult; // Desired minimum altitude after extending. + //extendDistance = extendDistanceAirToGround + ((float)vessel.horizontalSrfSpeed * BDAMath.Sqrt(2 * finalBombingAlt / bodyGravity)); //account for bomb lead distance + extendDistance = Mathf.Max(extendDistanceAirToGround, strafingSpeed * BDAMath.Sqrt(2 * finalBombingAlt / bodyGravity)); //horizontalSrfSpeed is a non-static value, which means extend dist will increase as vessel accelerates during the extend + extendDesiredMinAltitude = Mathf.Min(finalBombingAlt, minAltitude); + //((weaponManager.CurrentMissile && weaponManager.CurrentMissile.GetWeaponClass() == WeaponClasses.SLW) ? 10 : //drop to the deck for torpedo run + // defaultAltitude); //else commence level bombing + extendingForBombing = true; + } + float srfDist = (GetSurfacePosition(targetVessel.transform.position) - GetSurfacePosition(vessel.transform.position)).sqrMagnitude; + if (srfDist < extendDistance * extendDistance && VectorUtils.Angle(vesselTransform.up, targetVessel.transform.position - vessel.transform.position) > 45) + { + extending = true; + extendingReason = "Surface target"; + lastExtendTargetPosition = targetVessel.transform.position; + extendTarget = targetVessel; + extendParametersSet = true; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} is extending due to a ground target."); + return true; + } + } + if (checkType == ExtendChecks.AirToGroundOnly) return false; + + // 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 = Mathf.Max(extendDistanceAirToAir, extendRequestMinDistance); + extendHorizontally = false; + extendDesiredMinAltitude = Mathf.Max((float)vessel.radarAltitude + _extendAngleAirToAir * extendDistance, minAltitude); + extendParametersSet = true; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} is extending due to an air target ({extendingReason})."); + return true; + } + + if (extending) StopExtending("no valid extend reason"); + 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 (currentDistance, currentDirection) = (extendHorizontally ? (vessel.transform.position - tPosition).ProjectOnPlanePreNormalized(upDirection) : vessel.transform.position - tPosition).MagNorm(); + SetStatus($"Extending ({currentDistance:0}m / {extendDistance:0}m)"); + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS && extensionCutoffTime > 0) + { + extensionCutoffTimer += Time.fixedDeltaTime; + if (extensionCutoffTimer > extensionCutoffTime) //there are reasons a hard cutoff for extension is a bad idea, and will probably break any sort of bombing routine, but, well, the customer is always right... + { + StopExtending($"extend time limit exceeded", true); + return; + } + } + if (currentDistance < extendDistance) // Extend from position is closer (horizontally) than the extend distance. + { + if (currentDistance > lastExtendDistance + extendMinGainRate * Time.fixedDeltaTime) // Gaining distance fast enough. + { + 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 fast enough. + { + extendAbortTimer += TimeWarp.fixedDeltaTime; + if (extendAbortTimer > extendAbortTime) // Abort if not gaining enough distance. + { + StopExtending($"extend abort time ({extendAbortTime}s) reached at distance {currentDistance}m of {extendDistance}m", true); + return; + } + } + lastExtendDistance = currentDistance; + + Vector3 targetDirection = extendDistance * currentDirection; + Vector3 target = vessel.transform.position + targetDirection; // Target extend position horizontally. + if (extendingForBombing) isBombing = true; + target += upDirection * (Mathf.Min(extendingForBombing ? finalBombingAlt : defaultAltitude, (float)vessel.radarAltitude) - BodyUtils.GetRadarAltitudeAtPos(target)); // Adjust for terrain changes at target extend position. + target = FlightPosition(target, extendDesiredMinAltitude); // Further adjustments for speed, situation, etc. and desired minimum altitude after extending. + if (regainEnergy) + { + RegainEnergy(s, target - vessel.CoM); + return; + } + else + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Extending: {currentDistance:0}m of {extendDistance:0}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 ({currentDistance}m of {extendDistance}m)"); + } + } + + void FlyOrbit(FlightCtrlState s, Vector3d centerGPS, float radius, float speed, bool clockwise) + { + if (regainEnergy) + { + RegainEnergy(s, vessel.Velocity()); + return; + } + finalMaxSteer = GetSteerLimiterForSpeedAndPower(); + + Vector3 vesselPos = vesselTransform.position; + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Flying orbit"); + Vector3 flightCenter = GetTerrainSurfacePosition(VectorUtils.GetWorldSurfacePostion(centerGPS, vessel.mainBody)) + (defaultAltitude * upDirection); + Vector3 myVectorFromCenter = (vesselPos - flightCenter).ProjectOnPlanePreNormalized(upDirection); + Vector3 myVectorOnOrbit = myVectorFromCenter.normalized * radius; + 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.1f); + 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(vesselPos, (targetPosition - vesselPos).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) * (vesselPos + slope * distance); + } + } + Vector3 vectorToTarget = targetPosition - vesselPos; + // Vector3 planarVel = vessel.Velocity().ProjectOnPlanePreNormalized(upDirection); + //vectorToTarget = Vector3.RotateTowards(planarVel, vectorToTarget, 25f * Mathf.Deg2Rad, 0); + vectorToTarget = GetLimitedClimbDirectionForSpeed(vectorToTarget); + targetPosition = vesselPos + vectorToTarget; + + if (command != PilotCommands.Free && (vessel.transform.position - flightCenter).sqrMagnitude < radius * radius * 1.5f) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDModulePilotAI]: AI Pilot reached command destination."); + ReleaseCommand(false, false); + } + + useVelRollTarget = true; + + AdjustThrottle(speed, false); + FlyToPosition(s, targetPosition); + } + + #region Waypoints + 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. + if (BDArmorySettings.WAYPOINT_LOOP_INDEX > 1) + { + SetStatus($"Lap {activeWaypointLap}, Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + } + else + { + var wpName = WaypointCourses.CourseLocations[waypointCourseIndex].waypoints[activeWaypointIndex].name; + SetStatus($"Waypoint {activeWaypointIndex}{(string.IsNullOrEmpty(wpName) ? "" : $" {wpName}")} ({waypointRange:F0}m)"); + } + var waypointDirection = (waypointPosition - vessel.transform.position).normalized; + if (waypointRange < (BDArmorySettings.WAYPOINTS_SCALE > 0 ? BDArmorySettings.WAYPOINTS_SCALE : (WaypointCourses.CourseLocations[waypointCourseIndex].waypoints[activeWaypointIndex].scale)) / 2) //gate radius + { + //if (VectorUtils.Angle(waypointDirection, vessel.ReferenceTransform.up) > maxAllowedAoA)//as we get closer angle to WP is going to very rapidly increase from ~0 to 90 if not *perfectly* aligned + // waypointDirection = vessel.Velocity(); //so if within [gate radius] distance of the WP, if the angle to the gate exceeds max AOA angle, commit to current direaction to prevent control jerk at the last second as the AI tries to correct off-targetness + waypointDirection = Vector3.RotateTowards(vessel.srf_vel_direction, waypointDirection, Mathf.Deg2Rad * Mathf.Min(maxAllowedAoA, Mathf.Min(0.5f, 200f / (float)vessel.srfSpeed) * waypointRange), 0); //- maxAllowedAoA goes from 0 - 90; at default 35deg, would need to be going 400m/s through a 70m gate before speed and diameter matter; figure out different formula + // + } + 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. + } + + private Vector3 WaypointSpline() // FIXME This doesn't work that well yet. + { + // Note: here we're using distance instead of time as the spline interpolation parameter. + float minDistance = (float)vessel.speed * 2f; // Consider the radius of 2s around the waypoint. + + Vector3 point1 = waypointPosition + (vessel.transform.position - waypointPosition).normalized * minDistance; //waypointsRange > minDistance ? vessel.transform.position : waypointPosition + (vessel.transform.position - waypointPosition).normalized * minDistance; + Vector3 point2 = waypointPosition; + Vector3 point3; + if (activeWaypointIndex < waypoints.Count() - 1) + { + 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); + point3 = waypointPosition + (nextWaypointPosition - waypointPosition).normalized * minDistance; + } + else + { + point3 = waypointPosition + (waypointPosition - vessel.transform.position).normalized * minDistance; // Straight out the other side. + } + var distance1 = (point2 - point1).magnitude; + var distance2 = (point3 - point2).magnitude; + Vector3 slope1 = SplineUtils.EstimateSlope(point1, point2, distance1); + Vector3 slope2 = SplineUtils.EstimateSlope(point1, point2, point3, distance1, distance2); + if (Mathf.Max(minDistance - waypointRange + (float)vessel.speed * 0.1f, 0f) < distance1) + { + return SplineUtils.EvaluateSpline(point1, slope1, point2, slope2, Mathf.Max(minDistance - waypointRange + (float)vessel.speed * 0.1f, 0f), 0f, distance1); // 0.1s ahead along the spline. + } + else + { + var slope3 = SplineUtils.EstimateSlope(point2, point3, distance2); + return SplineUtils.EvaluateSpline(point2, slope2, point3, slope3, Mathf.Max(minDistance - waypointRange + (float)vessel.speed * 0.1f - distance1, 0f), 0f, distance2); // 0.1s ahead along the next section of the spline. + } + } + + private void SetWaypointRollAndYaw() + { + if (waypointPreRollTime > 0) + { + var range = (float)vessel.speed * waypointPreRollTime; // Pre-roll ahead of the waypoint. + if (waypointRange < range && activeWaypointIndex < waypoints.Count() - 1) // Within range of a waypoint and it's not the final one => use the waypoint roll target. + { + 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 = (nextWaypointPosition - waypointPosition).ProjectOnPlane(vessel.Velocity()).normalized; + waypointRollTargetStrength = Mathf.Min(1f, VectorUtils.Angle(nextWaypointPosition - waypointPosition, vessel.Velocity()) / maxAllowedAoA) * Mathf.Max(0, 1f - waypointRange / range); // Full strength at maxAllowedAoA and at the waypoint. + useWaypointRollTarget = true; + } + } + if (waypointYawAuthorityTime > 0) + { + var range = (float)vessel.speed * waypointYawAuthorityTime; + waypointYawAuthorityStrength = Mathf.Clamp01((2f * range - waypointRange) / range); + useWaypointYawAuthority = true; + } + } + + protected override void UpdateWaypoint() + { + 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. + } + + 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 + + //sends target speed to speedController + void AdjustThrottle(float targetSpeed, bool useBrakes, bool allowAfterburner = true, bool forceAfterburner = false, float throttleOverride = -1f) + { + speedController.targetSpeed = targetSpeed; + speedController.useBrakes = useBrakes; + speedController.allowAfterburner = allowAfterburner; + speedController.forceAfterburner = forceAfterburner; + speedController.throttleOverride = throttleOverride; + speedController.afterburnerPriority = ABPriority; + speedController.forceAfterburnerIfMaxThrottle = vessel.srfSpeed < ABOverrideThreshold; + } + + void Evasive(FlightCtrlState s) + { + if (s == null) return; + if (vessel == null) return; + var weaponManager = WeaponManager; + if (weaponManager == null) return; + + SetStatus("Evading"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + debugString.AppendLine($"Evasive {evasiveTimer}s"); + debugString.AppendLine($"Threat Distance: {weaponManager.incomingMissileDistance}"); + } + evading = true; + if (!wasEvading) evasionNonlinearityDirection = Mathf.Sign(UnityEngine.Random.Range(-1f, 1f)); + + bool hasABEngines = speedController.multiModeEngines.Count > 0; + + collisionDetectionTicker += 2; + if (BDArmorySettings.DEBUG_LINES) debugBreakDirection = default; + + steerMode = SteerModes.Manoeuvering; + if (weaponManager.isFlaring) + { + useAB = vessel.srfSpeed < minSpeed; + useBrakes = false; + float targetSpeed = minSpeed; + if (weaponManager.isChaffing) + targetSpeed = maxSpeed; + AdjustThrottle(targetSpeed, false, useAB); + } + + if ( + weaponManager.incomingMissileVessel != null + && VesselModuleRegistry.GetMissileBase(weaponManager.incomingMissileVessel) != null // Modular missiles can lose the MMG part. + && (weaponManager.ThreatClosingTime(weaponManager.incomingMissileVessel) <= weaponManager.evadeThreshold) + ) // Missile evasion + { + Vector3 targetDirection; + bool overrideThrottle = false; + if ((weaponManager.ThreatClosingTime(weaponManager.incomingMissileVessel) <= 1.5f) && (!weaponManager.isChaffing)) // Missile is about to impact, pull a hard turn + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Missile about to impact! pull away!"); + + AdjustThrottle(maxSpeed, false, !weaponManager.isFlaring); + + Vector3 cross = Vector3.Cross(weaponManager.incomingMissileVessel.transform.position - vesselTransform.position, vessel.Velocity()).normalized; + if (Vector3.Dot(cross, -vesselTransform.forward) < 0) + { + cross = -cross; + } + targetDirection = (50 * vessel.Velocity() / vessel.srfSpeed + 100 * cross).normalized; + } + else // Fly at 90 deg to missile to put max distance between ourselves and dispensed flares/chaff + { + // Break off at 90 deg to missile + Vector3 threatDirection = -1f * weaponManager.incomingMissileVessel.Velocity(); + 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); // Break left or right depending on which side the missile is coming in on. + + // Missile kinematics check to see if alternate break directions are better (crank or turn around and run) + bool dive = true; + if (evasionMissileKinematic && !vessel.InNearVacuum()) + { + breakDirection = MissileKinematicEvasion(breakDirection, threatDirection); + if (kinematicEvasionState != KinematicEvasionStates.NotchDive) + dive = false; + } + else + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine("Breaking from missile threat!"); + + // Dive to gain energy and hopefully lead missile into ground when not in space + if (!vessel.InNearVacuum() && dive) + { + float diveScale = Mathf.Max(1000f, 2f * turnRadius); + float angle = Mathf.Clamp((float)vessel.radarAltitude - minAltitude, 0, diveScale) / diveScale * 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); + } + if (BDArmorySettings.DEBUG_LINES) debugBreakDirection = breakDirection; + + // Rotate target direction towards break direction, starting with 15 deg, and increasing to maxAllowedAoA as missile gets closer + float rotAngle = Mathf.Deg2Rad * Mathf.Lerp(Mathf.Min(maxAllowedAoA, 90), 15f, Mathf.Clamp01(weaponManager.incomingMissileTime / weaponManager.evadeThreshold)); + targetDirection = Vector3.RotateTowards(vesselTransform.up, breakDirection, rotAngle, 0).normalized; + + if (weaponManager.isFlaring) + if (!hasABEngines) + AdjustThrottle(maxSpeed, false, useAB, false, 0.66f); + else + AdjustThrottle(maxSpeed, false, useAB); + else + { + useAB = true; + AdjustThrottle(maxSpeed, false, useAB); + } + overrideThrottle = true; + } + if (belowMinAltitude) + { + float rise = 0.5f * Mathf.Max(5f, (float)vessel.srfSpeed * 0.25f) * Mathf.Max(speedController.TWR, 1f); // Add some climb like in TakeOff (at half the rate) to get back above min altitude. + targetDirection += rise * upDirection; + + float verticalComponent = Vector3.Dot(targetDirection, upDirection); + if (verticalComponent < 0) // If we're below minimum altitude, enforce the evade direction to gain altitude. + { + targetDirection += -2f * verticalComponent * upDirection; + } + } + RCSEvade(s, targetDirection);//add spacemode RCS dodging; missile evasion, fire in targetDirection + FlyToPosition(s, vesselTransform.position + targetDirection * 100, overrideThrottle); + return; + } + else if (weaponManager.underFire) + { + 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); + + //for the most part, we want to turn _towards_ the threat in order to increase the rel ang vel and get under its guns + //for Waypoint Race evasion, keep pointing towards next gate; dodging is handled by evasion non-linearity waggle in FlyToPosition + Vector3 breakTarget = (IsRunningWaypoints ? (waypointPosition - vessel.transform.position) : threatRelativePosition) * 2f; + + 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 // Airborne threat. + { + 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"); + 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 + { // 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"); + 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 + { + 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; + + 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 + + if (belowMinAltitude) + { + float rise = 0.5f * Mathf.Max(5f, (float)vessel.srfSpeed * 0.25f) * Mathf.Max(speedController.TWR, 1f); // Add some climb like in TakeOff (at half the rate) to get back above min altitude. + breakTarget += rise * upDirection; + + float breakTargetVerticalComponent = Vector3.Dot(breakTarget, upDirection); + if (breakTargetVerticalComponent < 0) // If we're below minimum altitude, enforce the evade direction to gain altitude. + { + breakTarget += -2f * breakTargetVerticalComponent * upDirection; + } + } + } + + breakTarget = GetLimitedClimbDirectionForSpeed(breakTarget); + breakTarget += vessel.transform.position; + FlyToPosition(s, FlightPosition(breakTarget, minAltitude)); + return; + } + + 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) * vesselTransform.up.ProjectOnPlanePreNormalized(upDirection) * 500f; + //+ (Mathf.Sin (Time.time/3) * upDirection * minAltitude/3); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Evading unknown attacker"); + FlyToPosition(s, target); + } + + Vector3 MissileKinematicEvasion(Vector3 breakDirection, Vector3 threatDirection) + { + breakDirection = breakDirection.normalized; + string missileEvasionStatus; + + // Constants + float boostSpeedMult = 5f; + float safeDistMult = 10f; + + // Get missile information + var weaponManager = WeaponManager; + MissileBase missile = VesselModuleRegistry.GetMissileBase(weaponManager.incomingMissileVessel); + float missileKinematicTime = missile.GetKinematicTime(); + float missileKinematicSpeed = missile.GetKinematicSpeed(); + float missileSpeed = (float)weaponManager.incomingMissileVessel.srfSpeed; + float boostSpeed = boostSpeedMult * missileKinematicSpeed; + if (missile is MissileLauncher) + boostSpeed = Mathf.Max(boostSpeed, ((MissileLauncher)missile).optimumAirspeed); + missileSpeed = (missile.MissileState == MissileBase.MissileStates.Boost && missileSpeed < boostSpeed) ? boostSpeed : missileSpeed; + float missileAccel = (missileKinematicSpeed - missileSpeed) / (missileKinematicTime == 0 ? Mathf.Sign(missileKinematicTime) * 0.001f : missileKinematicTime); + float missileSafeDist = safeDistMult * missile.GetBlastRadius(); // Comfortable safe distance + float missileSafeDistSqr = missileSafeDist * missileSafeDist; + Vector3 missilePos = weaponManager.incomingMissileVessel.transform.position; + Vector3 missileVel = weaponManager.incomingMissileVessel.Velocity(); + Vector3 missileDirNorm = missile.GetForwardTransform(); + Vector3 missileAccelVec = missileAccel * missileDirNorm; + + // Get current vessel information + Vector3 currentPos = vesselTransform.position; + float currentSpeed = (float)vessel.srfSpeed; + + // Future position variables + Vector3 futurePos; + Vector3 futureVel; + Vector3 futureAccel; + + // Set up maneuver directions + Vector3 crankDir = breakDirection; + Vector3 turnDir = breakDirection; + Vector3 targetDir = (targetVessel != null) ? + (targetVessel != missile.vessel ? (targetVessel.CoM - currentPos).normalized : (missile.SourceVessel.CoM - currentPos).normalized) + : (missilePos - currentPos).normalized; + + // Calculate estimated time to impact if we execute no maneuvers + float timeToImpact; + float currentAccel = 0; // FIXME if speedController.GetPossibleAccel() is able to calculate acceleration reliably incorporating drag + float distToMissile = (currentPos - missilePos).magnitude; + + // Turn to target / Turn hot + timeToImpact = distToMissile / (missileSpeed + currentSpeed); + futureVel = currentSpeed * targetDir; + futureAccel = currentAccel * targetDir; + futurePos = AIUtils.PredictPosition(currentPos, futureVel, futureAccel, timeToImpact); + missileDirNorm = (futurePos - missilePos).normalized; + missileVel = missileSpeed * missileDirNorm; + missileAccelVec = missileAccel * missileDirNorm; + float targetTime = AIUtils.TimeToCPA(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, missileKinematicTime + 5f); + //float targetDistSqr = (AIUtils.PredictPosition(currentPos, futureVel, futureAccel, targetTime) - AIUtils.PredictPosition(missilePos, missileVel, missileAccelVec, targetTime)).sqrMagnitude; + float targetDistSqr = AIUtils.PredictPosition(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, targetTime).sqrMagnitude; + + // Crank + float crankTime = 0f; + float crankDistSqr = 0f; + if (kinematicEvasionState <= KinematicEvasionStates.Crank || BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) + { + // Set up maneuver direction + float crankAngle; + VesselRadarData vrd = vessel.gameObject.GetComponent(); + if (vrd) + crankAngle = Mathf.Clamp(vrd.GetCrankFOV() / 2 - 5f, 5f, 85f); + else + crankAngle = 60f; + crankDir = Vector3.RotateTowards(breakDirection, threatDirection, (90f - crankAngle) * Mathf.Deg2Rad, 0).normalized; + + // Calculate time and distance of closest point of approach + timeToImpact = distToMissile / BDAMath.Sqrt(currentSpeed * currentSpeed + missileSpeed * missileSpeed); // Assumes missile/target are perpendicular at impact point, 60% of the time it works everytime + futureVel = currentSpeed * crankDir; + futureAccel = currentAccel * crankDir; + futurePos = AIUtils.PredictPosition(currentPos, futureVel, futureAccel, timeToImpact); + missileDirNorm = (futurePos - missilePos).normalized; + missileVel = missileSpeed * missileDirNorm; + missileAccelVec = missileAccel * missileDirNorm; + crankTime = AIUtils.TimeToCPA(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, missileKinematicTime + 5f); + crankDistSqr = AIUtils.PredictPosition(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, crankTime).sqrMagnitude; + } + + // Notch + float notchTime = 0f; + float notchDistSqr = 0f; + if (kinematicEvasionState <= KinematicEvasionStates.Notch || BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) + { + float v1 = Mathf.Max(missileSpeed, currentSpeed); + float v2 = Mathf.Min(missileSpeed, currentSpeed); + timeToImpact = (v1 != v2) ? distToMissile / BDAMath.Sqrt(v1 * v1 - v2 * v2) : timeToImpact; // Assumes angle between start and impact point is 90 deg, 60% of the time it works everytime + futureVel = currentSpeed * breakDirection; + futureAccel = currentAccel * breakDirection; + futurePos = AIUtils.PredictPosition(currentPos, futureVel, futureAccel, timeToImpact); + missileDirNorm = (futurePos - missilePos).normalized; + missileVel = missileSpeed * missileDirNorm; + missileAccelVec = missileAccel * missileDirNorm; + notchTime = AIUtils.TimeToCPA(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, missileKinematicTime + 5f); + notchDistSqr = AIUtils.PredictPosition(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, notchTime).sqrMagnitude; + } + + // Turn Away / Turn Cold + float turnTime = 0f; + float turnDistSqr = 0f; + if (kinematicEvasionState <= KinematicEvasionStates.TurnAway || BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) + { + turnDir = (currentPos - missilePos).ProjectOnPlanePreNormalized(upDirection).normalized; + futureVel = currentSpeed * turnDir; + futureAccel = currentAccel * turnDir; + futurePos = AIUtils.PredictPosition(currentPos, futureVel, futureAccel, missileKinematicTime); + futurePos = GetTerrainSurfacePosition(futurePos) + (minAltitude * upDirection); // Dive towards deck + turnDir = futurePos - currentPos; + missileDirNorm = (futurePos - missilePos).normalized; + missileVel = missileSpeed * missileDirNorm; + missileAccelVec = missileAccel * missileDirNorm; + turnTime = AIUtils.TimeToCPA(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, missileKinematicTime + 5f); + turnDistSqr = AIUtils.PredictPosition(currentPos - missilePos, futureVel - missileVel, futureAccel - missileAccelVec, turnTime).sqrMagnitude; + } + + if (BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) + { + debugString.AppendLine($"Time to Impact; Notch: {notchTime}s; Crank: {crankTime}s; Flee: {turnTime}s; Target:{targetTime}s"); + debugString.AppendLine($"Dist. @ Impact; Notch: {BDAMath.Sqrt(notchDistSqr)}m; Crank: {BDAMath.Sqrt(crankDistSqr)}m; Flee: {BDAMath.Sqrt(turnDistSqr)}m; Target: {BDAMath.Sqrt(targetDistSqr)}m"); + debugString.AppendLine($"Msl Kin. Speed: {missileKinematicSpeed}m/s; Msl Kin. Time: {missileKinematicTime}s; Msl Safe Dist.: {missileSafeDist}m;"); + } + + float newEvasionStateMult = (kinematicEvasionState == KinematicEvasionStates.None) ? 1f : 3f; + + if (targetDistSqr > (kinematicEvasionState == KinematicEvasionStates.ToTarget ? 1f : newEvasionStateMult) * missileSafeDistSqr) + { + // Missile is defeated or probably won't hit us, we can turn back towards/stay on target to exit evasion + breakDirection = targetDir; + missileEvasionStatus = "Turning back towards target!"; + kinematicEvasionState = KinematicEvasionStates.ToTarget; + } + else if (kinematicEvasionState <= KinematicEvasionStates.Crank && (crankDistSqr > (kinematicEvasionState == KinematicEvasionStates.Crank ? 1f : newEvasionStateMult) * missileSafeDistSqr)) + { + // Cranking will defeat missile, don't start cranking if we are executing a more conservative maneuver + breakDirection = crankDir; + missileEvasionStatus = "Cranking from missile threat!"; + kinematicEvasionState = KinematicEvasionStates.Crank; + } + else if (kinematicEvasionState <= KinematicEvasionStates.Notch && (notchDistSqr > (kinematicEvasionState == KinematicEvasionStates.Notch ? 1f : newEvasionStateMult) * missileSafeDistSqr)) + { + // Notching without a dive will defeat missile, don't start notching if we are executing a more conservative maneuver + missileEvasionStatus = "Notching from missile threat!"; + kinematicEvasionState = KinematicEvasionStates.Notch; + } + else if (kinematicEvasionState <= KinematicEvasionStates.TurnAway && (turnDistSqr > (kinematicEvasionState == KinematicEvasionStates.TurnAway ? 1f : newEvasionStateMult) * missileSafeDistSqr)) + { + // We need to turn away and dive, don't start turning away if we are executing a more conservative maneuver + breakDirection = turnDir; + missileEvasionStatus = "Turning away from missile threat!"; + kinematicEvasionState = KinematicEvasionStates.TurnAway; + } + else //we need to dive and notch to have a chance against the missile + { + missileEvasionStatus = "Notching and diving from missile threat"; + kinematicEvasionState = KinematicEvasionStates.NotchDive; + } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine(missileEvasionStatus); + return breakDirection; + } + + public void RCSEvade(FlightCtrlState s, Vector3 EvadeDir) + { + if (!BDArmorySettings.SPACE_HACKS || !vessel.InNearVacuum()) 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; + relativeVelocityDownDirection = Vector3.Cross(relativeVelocityRightDirection, vessel.srf_vel_direction).normalized; + } + + void CheckLandingGear() + { + if (!vessel.LandedOrSplashed) + { + if (vessel.radarAltitude > Mathf.Min(50f, minAltitude / 2f)) + vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, false); + else + vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, true); + } + } + + void TakeOff(FlightCtrlState s) + { + if (vessel.LandedOrSplashed && vessel.srfSpeed < takeOffSpeed) + { + SetStatus(TakingOff ? "Taking off" : vessel.Splashed ? "Splashed" : "Landed"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Taking off"); + if (vessel.Splashed) + { vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, false); } + assignedPositionWorld = vessel.transform.position; + return; + } + SetStatus("Gain Alt. (" + (int)minAltitude + "m)"); + + steerMode = TakingOff ? SteerModes.Aiming : SteerModes.NormalFlight; + + float radarAlt = vessel.Splashed ? 0 : (float)vessel.radarAltitude; + if (TakingOff && radarAlt > terrainAlertDetectionRadius) + { + TakingOff = false; + } + + Vector3 normalToUse; + Vector3 forwardDirection = (vessel.horizontalSrfSpeed < 10 ? vesselTransform.up : (Vector3)vessel.srf_vel_direction) * 100; // Forward direction not adjusted for terrain. + Vector3 forwardPoint = vessel.transform.position + forwardDirection * 100; // Forward point not adjusted for terrain. + if (vessel.Splashed) // If the vessel is splashed, then the surface is flat. + { + normalToUse = upDirection; + } + else + { + // Get surface normal relative to our velocity direction below the vessel and where the vessel is heading. + Ray ray = new(forwardPoint, relativeVelocityDownDirection); // Check ahead and below. + Vector3 terrainBelowAheadNormal = Physics.Raycast(ray, out RaycastHit rayHit, minAltitude + 1.0f, (int)LayerMasks.Scenery) ? rayHit.normal : upDirection; // Terrain normal below point ahead. + if (BDArmorySettings.SPACE_HACKS && vessel.InNearVacuum()) //no need to worry about stalling in null atmo + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Gaining altitude"); + FlyToPosition(s, vessel.transform.position + terrainBelowAheadNormal * 100); //point nose perpendicular to surface for maximum vertical thrust. + return; + } + + 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. + 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 = forwardDirection.ProjectOnPlanePreNormalized(normalToUse).normalized * 100; // Forward point adjusted for terrain relative to vessel. + var alpha = Mathf.Clamp(0.9f + 0.1f * radarAlt / minAltitude, 0f, 0.99f); + gainAltSmoothedForwardPoint = wasGainingAlt ? alpha * gainAltSmoothedForwardPoint + (1f - alpha) * forwardPoint : forwardPoint; // Adjust the forward point a bit more smoothly to avoid sudden jerks. + gainingAlt = true; + float rise = Mathf.Max(5f, 10f * (float)vessel.srfSpeed / takeOffSpeed) * 0.5f * (1f + Mathf.Max(speedController.TWR * Mathf.Clamp01(radarAlt / terrainAlertDetectionRadius), 1f)); // Scale climb rate by TWR (if >1 and not really close to terrain) to allow more powerful craft to climb faster. + rise = Mathf.Min(rise, 1.5f * (defaultAltitude - radarAlt)); // Aim for at most 50% higher than the default altitude. + if (TakingOff) // During the initial take-off, use a more gentle climb rate. 5°—10° at the take-off speed. + { rise = Mathf.Min(rise, Mathf.Max(5f, 10f * (float)vessel.srfSpeed / takeOffSpeed) * 0.5f * (1f + Mathf.Clamp01(radarAlt / terrainAlertDetectionRadius))); } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Gaining altitude @ {Mathf.Rad2Deg * Mathf.Atan(rise / 100f):0.0}°"); + FlyToPosition(s, vessel.transform.position + gainAltSmoothedForwardPoint + upDirection * rise); + } + + void UpdateTerrainAlertDetectionRadius(Vessel v) + { + 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. + } + + bool FlyAvoidTerrain(FlightCtrlState s) // Check for terrain ahead. + { + if (TakingOff) return false; // Don't do anything during the initial take-off. + 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 + ((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; + + // Reset/initialise some variables. + 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 = 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 * controlSurfaceDeploymentTime; // The distance to the terrain to consider. + terrainAlertThreshold = 0; // Reset the threshold in case no threats are within range. + + // 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(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.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) + { + 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.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) + { + 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.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) + { + 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.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) + { + 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 < float.MaxValue) + { + terrainAlertDirection = vesselSrfVelDir.ProjectOnPlanePreNormalized(terrainAlertNormal).normalized; + avoidingTerrain = true; + } + else + { + // Next, cast a sphere forwards to check for upcoming dangers. + 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) + { + 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()) + { + 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. + { + // 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) + { + normal = Vector3.RotateTowards(normal, upDirection, Mathf.Deg2Rad * 45f * (terrainAlertThreatRange - hitAltitude) / terrainAlertThreatRange, 0f); + } + 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(vesselSrfVelDir, upDirection); // sin(theta) (measured relative to the ocean surface). + if (sinTheta < 0f) // Heading downwards + { + 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, controlSurfaceDeploymentTime) - vesselPosition, upDirection)); + terrainAlertThreshold = turnRadiusTwiddleFactor * turnRadius * oneMinusCosTheta + controlLagCompensation; + + 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; + terrainAlertNormalColour = Color.blue; + terrainAlertDirection = vesselSrfVelDir.ProjectOnPlanePreNormalized(upDirection).normalized; + avoidingTerrain = true; + + if (BDArmorySettings.DEBUG_LINES) + { + terrainAlertDebugPos = vesselPosition + vesselSrfVelDir * (float)vessel.altitude / -sinTheta; + terrainAlertDebugDir = upDirection; + } + } + } + } + } + + if (avoidingTerrain) + { + belowMinAltitude = true; // Inform other parts of the code to behave as if we're below minimum altitude. + + 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, 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 = 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. + FlyToPosition(s, vesselPosition + correctionDirection * 100); + } + if (postTerrainAvoidanceCoolDownDuration > 0) postTerrainAvoidanceCoolDownTimer = 0; + steerMode = SteerModes.Manoeuvering; + // Update status and book keeping. + SetStatus("Terrain (" + (int)terrainAlertDistance + "m)"); + return true; + } + + // Hurray, we've avoided the terrain! + avoidingTerrain = false; + if (postTerrainAvoidanceCoolDownTimer >= 0) + { + postTerrainAvoidanceCoolDownTimer += TimeWarp.fixedDeltaTime; + if (postTerrainAvoidanceCoolDownTimer >= postTerrainAvoidanceCoolDownDuration) + postTerrainAvoidanceCoolDownTimer = -1f; + } + return false; + } + + bool FlyAvoidOthers(FlightCtrlState s) // Check for collisions with other vessels and try to avoid them. + { + if (vesselCollisionAvoidanceStrength == 0 || collisionAvoidanceThreshold == 0) return false; + if (currentlyAvoidedVessel != null) // Avoidance has been triggered. + { + SetStatus("AvoidCollision"); + 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. + { + FlyAvoidVessel(s); + return true; + } + else // Stop avoiding, but immediately check again for new collisions. + { + currentlyAvoidedVessel = null; + collisionDetectionTicker = vesselCollisionAvoidanceTickerFreq + 1; + return FlyAvoidOthers(s); + } + } + else if (collisionDetectionTicker > vesselCollisionAvoidanceTickerFreq) // Only check every vesselCollisionAvoidanceTickerFreq frames. + { + collisionDetectionTicker = 0; + + // Check for collisions with other vessels. + bool vesselCollision = false; + VesselType collisionVesselType = VesselType.Unknown; // Start as not debris. + float collisionTargetLargestSize = -1f; + collisionAvoidDirection = vessel.srf_vel_direction; + // First pass, only consider valid vessels. + using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) + while (vs.MoveNext()) + { + if (vs.Current == null) continue; + if (vs.Current.vesselType == VesselType.Debris) continue; // Ignore debris on the first pass. + if (vs.Current == vessel || vs.Current.Landed) continue; + if (!PredictCollisionWithVessel(vs.Current, vesselCollisionAvoidanceLookAheadPeriod, out Vector3 collisionAvoidDir)) continue; + if (!VesselModuleRegistry.IgnoredVesselTypes.Contains(vs.Current.vesselType)) + { + var ibdaiControl = vs.Current.ActiveController().AI; + if (ibdaiControl != null && ibdaiControl.currentCommand == PilotCommands.Follow && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel) continue; + } + var collisionTargetSize = vs.Current.vesselSize.sqrMagnitude; // We're only interested in sorting by size, which is much faster than sorting by mass. + if (collisionVesselType == vs.Current.vesselType && collisionTargetSize < collisionTargetLargestSize) continue; // Avoid the largest object. + vesselCollision = true; + currentlyAvoidedVessel = vs.Current; + collisionAvoidDirection = collisionAvoidDir; + collisionVesselType = vs.Current.vesselType; + collisionTargetLargestSize = collisionTargetSize; + } + // Second pass, only consider debris. + if (!vesselCollision) + { + using var vs = BDATargetManager.LoadedVessels.GetEnumerator(); + while (vs.MoveNext()) + { + if (vs.Current == null) continue; + if (vs.Current.vesselType != VesselType.Debris) continue; // Only consider debris on the second pass. + if (vs.Current == vessel || vs.Current.Landed) continue; + if (!PredictCollisionWithVessel(vs.Current, vesselCollisionAvoidanceLookAheadPeriod, out Vector3 collisionAvoidDir)) continue; + var collisionTargetSize = vs.Current.vesselSize.sqrMagnitude; + if (collisionTargetSize < collisionTargetLargestSize) continue; // Avoid the largest debris object. + vesselCollision = true; + currentlyAvoidedVessel = vs.Current; + collisionAvoidDirection = collisionAvoidDir; + collisionVesselType = vs.Current.vesselType; + collisionTargetLargestSize = collisionTargetSize; + } + } + if (vesselCollision) + { + FlyAvoidVessel(s); + return true; + } + else + { currentlyAvoidedVessel = null; } + } + else + { ++collisionDetectionTicker; } + return false; + } + + void FlyAvoidVessel(FlightCtrlState s) + { + // Rotate the current flyingToPosition away from the direction to avoid. + Vector3 axis = Vector3.Cross(vessel.srf_vel_direction, collisionAvoidDirection); + steerMode = SteerModes.Manoeuvering; + FlyToPosition(s, vesselTransform.position + Quaternion.AngleAxis(-vesselCollisionAvoidanceStrength, axis) * (flyingToPosition - vesselTransform.position)); // Rotate the flyingToPosition around the axis by the collision avoidance strength (each frame). + } + + Vector3 GetLimitedClimbDirectionForSpeed(Vector3 direction) + { + if (Vector3.Dot(direction, upDirection) < 0) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"climb limit angle: unlimited"); + return direction; //only use this if climbing + } + + Vector3 planarDirection = direction.ProjectOnPlanePreNormalized(upDirection); + + float angle = Mathf.Clamp((float)vessel.srfSpeed * 0.15f * speedController.TWR, 5, 90); + + 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.InNearVacuum() || vessel.LandedOrSplashed) return; // Only measure when airborne and in sufficient atmosphere. + + if (lastAllowedAoA != maxAllowedAoA) + { + lastAllowedAoA = maxAllowedAoA; + 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 curSinAoA = Vector3.Dot(vessel.Velocity().normalized, vessel.ReferenceTransform.forward); + + //adjust moving averages + 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)"); + + 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 (gLoadPred < maxNegG || Math.Abs(sinAoAPred - sinAoAAtMaxNegG) < 0.005f) + { + maxNegG = gLoadPred; + sinAoAAtMaxNegG = sinAoAPred; + } + if (gLoadPred > maxPosG || Math.Abs(sinAoAPred - sinAoAAtMaxPosG) < 0.005f) + { + maxPosG = gLoadPred; + sinAoAAtMaxPosG = sinAoAPred; + } + + if (sinAoAAtMaxNegG >= sinAoAAtMaxPosG) + { + sinAoAAtMaxNegG = sinAoAAtMaxPosG = maxNegG = maxPosG = 0; + gOffsetPerDynPres = gAoASlopePerDynPres = 0; + return; + } + + 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(gLoadPred) > dynDynPresGRecorded) + dynDynPresGRecorded = Math.Abs(gLoadPred); + dynUserSteerLimitMax = Mathf.Max(userSteerLimit, dynDecayRate * dynUserSteerLimitMax, 0.1f); // Recent-ish max user-defined steer limit, clamped to at least 0.1. Decays at the same rate as dynamic pressure for consistency. + + if (!vessel.LandedOrSplashed) + { + dynVelocityMagSqr = dynVelocityMagSqr * dynVelSmoothingCoef + (1f - dynVelSmoothingCoef) * (float)vessel.Velocity().sqrMagnitude; // Smooth the recently measured speed for determining the turn radius. + } + + float AoADiff = Mathf.Max(sinAoAAtMaxPosG - sinAoAAtMaxNegG, 0.001f); // Avoid divide-by-zero. + + gAoASlopePerDynPres = (maxPosG - maxNegG) / AoADiff; + gOffsetPerDynPres = maxPosG - gAoASlopePerDynPres * sinAoAAtMaxPosG; //g force offset + } + + void AdjustPitchForGAndAoALimits(FlightCtrlState s) + { + 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 + { + float speed = Math.Max(takeOffSpeed, minSpeed); + negPitchDynPresLimitIntegrator = -1f * 0.001f * 0.5f * 1.225f * speed * speed; + posPitchDynPresLimitIntegrator = 1f * 0.001f * 0.5f * 1.225f * speed * speed; + return; + } + + float invVesselDynPreskPa = 1f / (float)vessel.dynamicPressurekPa; + + 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 = !vessel.InNearVacuum() ? negPitchDynPresLimitIntegrator * invVesselDynPreskPa + negPitchDynPresLimit : -1; + if (negLim > s.pitch) + { + if (currentG > -(maxAllowedGForce * 0.97f * bodyGravity)) + { + negPitchDynPresLimitIntegrator -= (float)(0.15 * vessel.dynamicPressurekPa); //just an override in case things break + + maxNegG = currentG * invVesselDynPreskPa; + sinAoAAtMaxNegG = curSinAoA; + + negPitchDynPresLimit = 0; + } + + SetFlightControlState(s, negLim, s.yaw, s.roll); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Limiting Neg Gs"); + } + posLim = !vessel.InNearVacuum() ? posPitchDynPresLimitIntegrator * invVesselDynPreskPa + posPitchDynPresLimit : 1; + if (posLim < s.pitch) + { + if (currentG < (maxAllowedGForce * 0.97f * bodyGravity)) + { + posPitchDynPresLimitIntegrator += (float)(0.15 * vessel.dynamicPressurekPa); //just an override in case things break + + maxPosG = currentG * invVesselDynPreskPa; + sinAoAAtMaxPosG = curSinAoA; + + posPitchDynPresLimit = 0; + } + + SetFlightControlState(s, posLim, s.yaw, s.roll); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Limiting Pos Gs"); + } + + lastPitchInput = s.pitch; + lastSinAoA = curSinAoA; + + // 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() + { + maxLiftAcceleration = dynDynPresGRecorded * (float)vessel.dynamicPressurekPa; //maximum acceleration from lift that the vehicle can provide + + 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. + + // Radius that we can turn in assuming constant velocity, assuming simple circular motion (note: this is a terrible assumption, the AI usually turns on afterboosters!) + turnRadius = dynVelocityMagSqr / maxLiftAcceleration / (userSteerLimit / dynUserSteerLimitMax); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + debugString.AppendLine($"Turn Radius: {turnRadius:0}m (max lift acc: {maxLiftAcceleration:0}m/s²), terrain threat range: {turnRadiusTwiddleFactorMax * turnRadius + (float)vessel.srfSpeed * controlSurfaceDeploymentTime:0}m, threshold: {terrainAlertThreshold:0}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() + { + return (vessel.transform.position + (-(float)vessel.altitude * upDirection) + (defaultAltitude * upDirection)); + } + + Vector3 GetSurfacePosition(Vector3 position) + { + return position - ((float)FlightGlobals.getAltitudeAtPos(position) * upDirection); + } + + Vector3 GetTerrainSurfacePosition(Vector3 position) + { + return position - (MissileGuidance.GetRaycastRadarAltitude(position) * upDirection); + } + + Vector3 FlightPosition(Vector3 targetPosition, float minAlt) + { + Vector3 forwardDirection = vesselTransform.up; + Vector3 targetDirection = (targetPosition - vesselTransform.position).normalized; + float targetDistance = (targetPosition - vesselTransform.position).magnitude; + + float vertFactor = 0; + vertFactor += ((float)vessel.srfSpeed / minSpeed - 2f) * 0.3f; //speeds greater than 2x minSpeed encourage going upwards; below encourages downwards + vertFactor += (targetDistance / 1000f - 1f) * 0.3f; //distances greater than 1000m encourage going upwards; closer encourages going downwards + vertFactor -= Mathf.Clamp01(Vector3.Dot(vesselTransform.position - targetPosition, upDirection) / 1600f - 1f) * 0.5f; //being higher than 1600m above a target encourages going downwards + if (targetVessel) + vertFactor += Vector3.Dot(targetVessel.Velocity() / targetVessel.srfSpeed, (targetVessel.ReferenceTransform.position - vesselTransform.position).normalized) * 0.3f; //the target moving away from us encourages upward motion, moving towards us encourages downward motion + else + vertFactor += 0.4f; + var weaponManager = WeaponManager; + if (weaponManager && weaponManager.underFire) vertFactor -= 0.5f; //being under fire encourages going downwards as well, to gain energy + + float alt = (float)vessel.radarAltitude; + vertFactor = Mathf.Clamp(vertFactor, -2, 2); + vertFactor += 0.15f * Mathf.Sin((float)vessel.missionTime * 0.25f); //some randomness in there + + Vector3 projectedDirection = forwardDirection.ProjectOnPlanePreNormalized(upDirection); + Vector3 projectedTargetDirection = targetDirection.ProjectOnPlanePreNormalized(upDirection); + var cosAngle = Vector3.Dot(targetDirection, forwardDirection); + invertRollTarget = false; + if (cosAngle < -1e-8f) + { + if ( + (canExtend && targetDistance > BankedTurnDistance) // For long-range turning, do a banked turn (horizontal) instead to conserve energy, but only if extending is allowed. + || isBombing) // Or for doing a bombing run as height changes mess with the approach. + { + targetPosition = vesselTransform.position + Vector3.Cross(Vector3.Cross(projectedDirection, projectedTargetDirection), projectedDirection).normalized * 200; + } + else + { + if (cosAngle < ImmelmannTurnCosAngle) // Otherwise, if the target is almost directly behind, do an Immelmann turn. + { + bool pitchUp = vessel.radarAltitude < minAltitude + 2f * turnRadiusTwiddleFactorMax * turnRadius ? vessel.angularVelocity.x < 0.05f : // Avoid oscillations at low altitude. + Mathf.Abs(vessel.angularVelocity.x) < Mathf.Abs(Mathf.Deg2Rad * ImmelmannPitchUpBias) ? ImmelmannPitchUpBias > -0.1f : // Otherwise, if not rotating much, pitch up (or down if biased negatively). + vessel.angularVelocity.x < 0; // Otherwise, go with the current pitching direction. + + targetDirection = Vector3.RotateTowards(-vesselTransform.up, pitchUp ? -vesselTransform.forward : vesselTransform.forward, Mathf.Deg2Rad * ImmelmannTurnAngle, 0); // If the target is in our blind spot, just pitch up (or down) to get a better view (initial part of an Immelmann turn). + invertRollTarget = Vector3.Dot(targetDirection, vesselTransform.forward) > 0; // Target is behind and below, pitch down first then roll up. + } + targetPosition = vesselTransform.position + Vector3.Cross(Vector3.Cross(forwardDirection, targetDirection), forwardDirection).normalized * 200; // Make the target position 90° from vesselTransform.up. + } + } + else if (steerMode == SteerModes.NormalFlight) + { + float distance = (targetPosition - vesselTransform.position).magnitude; + if (vertFactor < 0) + distance = Math.Min(distance, Math.Abs((alt - minAlt) / vertFactor)); + + targetPosition += upDirection * Math.Min(distance, 1000) * Mathf.Clamp(vertFactor * Mathf.Clamp01(0.7f - Math.Abs(Vector3.Dot(projectedTargetDirection, projectedDirection))), -0.5f, 0.5f); + if (maxAltitudeEnabled) + { + var targetRadarAlt = BDArmorySettings.COMPETITION_ALTITUDE__LIMIT_ASL ? FlightGlobals.getAltitudeAtPos(targetPosition) : BodyUtils.GetRadarAltitudeAtPos(targetPosition); + if (targetRadarAlt > maxAltitude) + { + targetPosition -= (targetRadarAlt - maxAltitude) * upDirection; + } + } + } + + if ((float)vessel.radarAltitude > minAlt * 1.1f) + { + return targetPosition; + } + + float pointRadarAlt = BodyUtils.GetRadarAltitudeAtPos(targetPosition, true); //return 0 when over water + if (pointRadarAlt < minAlt)// && !isBombing) + { + float adjustment = (minAlt - pointRadarAlt); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Target position is below minAlt. Adjusting by {adjustment}"); + return targetPosition + (adjustment * upDirection); + } + else + { + return targetPosition; + } + } + + Vector3 LongRangeAltitudeCorrection(Vector3 targetPosition) + { + var weaponManager = WeaponManager; + var scale = weaponManager != null ? Mathf.Max(2500f, weaponManager.gunRange) : 2500f; + if (isBombing) scale *= 2; // Double the scale when bombing. + 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 SteerPower(Axis axis) + { + if (threeAxisPID) return axis switch + { + Axis.Pitch => threeAxisPIDPitchMult, + Axis.Yaw => threeAxisPIDYawMult, + Axis.Roll => threeAxisPIDRollMult, + _ => steerMult // Default, shouldn't happen. + }; + else return steerMult; + } + + private float SteerCorrection(Axis axis) + { + if (threeAxisPID) return axis switch + { + Axis.Pitch => threeAxisPIDPitchKi, + Axis.Yaw => threeAxisPIDYawKi, + Axis.Roll => threeAxisPIDRollKi, + _ => steerKiAdjust // Default, shouldn't happen. + }; + else return steerKiAdjust; + } + + private float SteerDamping(float angleToTarget, float defaultTargetPosition, Axis axis) + { + if (!dynamicSteerDamping) + { + if (threeAxisSteerDamping) + { + return axis switch + { + Axis.Pitch => steerDampingPitch, + Axis.Yaw => steerDampingYaw, + Axis.Roll => steerDampingRoll, + _ => steerDamping // Default, shouldn't happen. + }; + } + else + { + return steerDamping; + } + } + + if (angleToTarget >= 180 || angleToTarget < 0) // Check for valid angle to target. This shouldn't happen, but a sanity check is needed for dynamic damping. + { + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) + { + if (threeAxisSteerDamping) + { + switch (axis) + { + case Axis.Pitch: PitchLabel = "N/A"; break; + case Axis.Yaw: YawLabel = "N/A"; break; + case Axis.Roll: RollLabel = "N/A"; break; + } + } + else + { + DynamicDampingLabel = "N/A"; + } + } + if (threeAxisSteerDamping) + { + return axis switch + { + Axis.Pitch => steerDampingPitch, + Axis.Yaw => steerDampingYaw, + Axis.Roll => steerDampingRoll, + _ => steerDamping // Default, shouldn't happen. + }; + } + else + { + return steerDamping; + } + } + + // Dynamic damping + if (threeAxisSteerDamping) + { + float damping = axis switch + { + Axis.Pitch when dynamicDampingPitch => GetDampingFactor(angleToTarget, dynamicSteerDampingPitchFactor, DynamicDampingPitchMin, DynamicDampingPitchMax), + Axis.Pitch when !dynamicDampingPitch => steerDampingPitch, + Axis.Yaw when dynamicDampingYaw => GetDampingFactor(angleToTarget, dynamicSteerDampingYawFactor, DynamicDampingYawMin, DynamicDampingYawMax), + Axis.Yaw when !dynamicDampingYaw => steerDampingYaw, + Axis.Roll when dynamicDampingRoll => GetDampingFactor(angleToTarget, dynamicSteerDampingRollFactor, DynamicDampingRollMin, DynamicDampingRollMax), + Axis.Roll when !dynamicDampingRoll => steerDampingRoll, + _ => steerDamping // Default, shouldn't happen. + }; + switch (axis) + { + case Axis.Pitch when dynamicDampingPitch: + dynSteerDampingPitchValue = damping; + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) + PitchLabel = damping.ToString(); + break; + case Axis.Yaw when dynamicDampingYaw: + dynSteerDampingYawValue = damping; + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) + YawLabel = damping.ToString(); + break; + case Axis.Roll when dynamicDampingRoll: + dynSteerDampingRollValue = damping; + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) + RollLabel = damping.ToString(); + break; + } + return damping; + } + else + { + dynSteerDampingValue = GetDampingFactor(defaultTargetPosition, dynamicSteerDampingFactor, DynamicDampingMin, DynamicDampingMax); + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) DynamicDampingLabel = dynSteerDampingValue.ToString(); + return dynSteerDampingValue; + } + } + + private float GetDampingFactor(float angleToTarget, float dynamicSteerDampingFactorAxis, float DynamicDampingMinAxis, float DynamicDampingMaxAxis) + { + return Mathf.Clamp( + (float)(Math.Pow((180 - angleToTarget) / 175, dynamicSteerDampingFactorAxis) * (DynamicDampingMaxAxis - DynamicDampingMinAxis) + DynamicDampingMinAxis), // Make a 5° dead zone around being on target. + Mathf.Min(DynamicDampingMinAxis, DynamicDampingMaxAxis), + Mathf.Max(DynamicDampingMinAxis, DynamicDampingMaxAxis) + ); + } + + public override bool IsValidFixedWeaponTarget(Vessel target) + { + if (!vessel) return false; + // aircraft can aim at anything + return true; + } + + // Legacy collision avoidance code. + bool DetectCollision(Vector3 direction, out Vector3 badDirection) + { + badDirection = Vector3.zero; + if ((float)vessel.radarAltitude < 20) return false; + + direction = direction.normalized; + Ray ray = new Ray(vesselTransform.position + (50 * vesselTransform.up), direction); + float distance = Mathf.Clamp((float)vessel.srfSpeed * 4f, 125f, 2500); + if (!Physics.SphereCast(ray, 10, out RaycastHit hit, distance, (int)LayerMasks.Scenery)) return false; + Rigidbody otherRb = hit.collider.attachedRigidbody; + if (otherRb) + { + if (!(Vector3.Dot(otherRb.velocity, vessel.Velocity()) < 0)) return false; + badDirection = hit.point - ray.origin; + return true; + } + badDirection = hit.point - ray.origin; + return true; + } + + void UpdateCommand(FlightCtrlState s) + { + if (command == PilotCommands.Follow && commandLeader is null) + { + ReleaseCommand(); + return; + } + + if (command == PilotCommands.Follow) + { + SetStatus("Follow"); + UpdateFollowCommand(s); + } + else if (command == PilotCommands.FlyTo) + { + 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) + { + var weaponManager = WeaponManager; + if (targetVessel != null || weaponManager == null) // Found a target or lost our WM. + { + ReleaseCommand(false); + return; + } + else + { + if (weaponManager.underAttack || weaponManager.underFire) // Switch to Free to allow combat behaviours, but continue flying towards the attack point for now. + ReleaseCommand(false); + SetStatus("Attack"); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 77) + { + AdjustThrottle(maxSpeed, false); + FlyToPosition(s, vesselTransform.position + upDirection * BDArmorySettings.GUARD_MODE_TRIGGER_ALT); + } + else + FlyOrbit(s, assignedPositionGeo, 2500, maxSpeed, ClockwiseOrbit); + } + } + } + + void UpdateFollowCommand(FlightCtrlState s) + { + steerMode = SteerModes.NormalFlight; + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); + + var commandVelocity = commandLeader.vessel.Velocity(); + var (commandSpeed, commandDirection) = commandVelocity.MagNorm(); + var currentVelocity = vessel.Velocity(); + + //formation position + Vector3d commandPosition = GetFormationPosition(); + debugFollowPosition = commandPosition; + + Vector3 flyPos; + float finalMaxSpeed; + var currentPosition = vesselTransform.position; + float distanceToPos = Vector3.Distance(currentPosition, commandPosition); + useFollowHints = distanceToPos < followHintThreshold; + + if (distanceToPos < 1000) + { + // Aim for 1km ahead of the command position, adjusted if we're currently ahead of it. + flyPos = commandPosition + (1000 + Mathf.Max(0, Vector3.Dot(currentPosition - commandPosition, commandDirection))) * commandDirection; + + Vector3 vectorToFlyPos = flyPos - currentPosition; + Vector3 projectedPosOffset = (commandPosition - currentPosition).ProjectOnPlanePreNormalized(commandDirection); + var (offsetMagnitude, offsetDirection) = projectedPosOffset.MagNorm(); + float adjustAngle = Mathf.Clamp(0.5f * (offsetMagnitude < 1 ? 0.5f * offsetMagnitude * offsetMagnitude : offsetMagnitude - 0.5f), 0, 15); + float dbg1 = adjustAngle; // Position component + adjustAngle -= Mathf.Clamp(Vector3.Dot(currentVelocity - commandVelocity, offsetDirection), -5, 5); + float dbg2 = adjustAngle - dbg1; // Velocity component + vectorToFlyPos = Vector3.RotateTowards(vectorToFlyPos, offsetDirection, Mathf.Deg2Rad * adjustAngle, 0); + flyPos = currentPosition + vectorToFlyPos; + + var currentDirection = currentVelocity.normalized; + float dotDistance = Vector3.Dot(commandPosition - currentPosition, currentDirection); + float followSpeedP = (float)commandSpeed + (0.5f + 0.01f * Mathf.Abs(dotDistance)) * (dotDistance > 0 ? dotDistance / 4 : dotDistance / 2); // Adjust for lag amount. Braking needs to be more agressive than accelerating. + float followSpeedError = 0.01f * Time.fixedDeltaTime * dotDistance; + if (followSpeedD != 0) followSpeedD -= dotDistance; + followSpeedI = Mathf.Clamp((1 - Mathf.Clamp01(Mathf.Abs(followSpeedError))) * followSpeedI + followSpeedError, -1, 1); + finalMaxSpeed = followSpeedP + 10 * followSpeedI - 50 * followSpeedD; + finalMaxSpeed = Mathf.Clamp(finalMaxSpeed, 0, maxSpeed); // Don't go over maxSpeed. + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Follow: adj: {adjustAngle:F1}° ({dbg1:F1}°, {dbg2:F1}°), d: {distanceToPos:F1}m, ·d: {dotDistance:F1}m, spdP: {followSpeedP:F0}m/s, spdI: {10 * followSpeedI:F1}, spdD: {50 * followSpeedD:F1}"); + followSpeedD = dotDistance; + + followHintDistance = distanceToPos; + } + else + { + flyPos = commandPosition; + finalMaxSpeed = maxSpeed; + followSpeedD = 0; + followSpeedI = 0; + } + + AdjustThrottle(finalMaxSpeed, true); + + FlyToPosition(s, flyPos); + } + + Vector3d GetFormationPosition() + { + Quaternion origVRot = velocityTransform.rotation; + Vector3 origVLPos = velocityTransform.localPosition; + + velocityTransform.position = commandLeader.vessel.ReferenceTransform.position; + if (commandLeader.vessel.Velocity() != Vector3d.zero) + { + velocityTransform.rotation = Quaternion.LookRotation(commandLeader.vessel.Velocity(), upDirection); + velocityTransform.rotation = Quaternion.AngleAxis(90, velocityTransform.right) * velocityTransform.rotation; + } + else + { + velocityTransform.rotation = commandLeader.vessel.ReferenceTransform.rotation; + } + + Vector3d pos = velocityTransform.TransformPoint(this.GetLocalFormationPosition(commandFollowIndex));// - lateralVelVector - verticalVelVector; + + velocityTransform.localPosition = origVLPos; + velocityTransform.rotation = origVRot; + + return pos; + } + + public override void CommandTakeOff() + { + base.CommandTakeOff(); + standbyMode = false; + } + + public override void CommandFollowWaypoints() + { + if (standbyMode) CommandTakeOff(); + base.CommandFollowWaypoints(); + } + protected override void OnGUI() + { + base.OnGUI(); + + if (!pilotEnabled || !vessel.isActiveVessel) return; + + if (!BDArmorySettings.DEBUG_LINES) return; + if (command == PilotCommands.Follow) + { + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugFollowPosition, 2, Color.red); + } + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugTargetPosition, 5, Color.red); // The point we're asked to fly to + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugTargetDirection, 5, Color.green); // The direction FlyToPosition will actually turn to + 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); + + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + rollTarget, 2, Color.blue); +#if DEBUG + if (IsEvading || IsExtending) GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + debugSquigglySquidDirection.normalized * 10, 1, Color.cyan); +#endif + if (IsEvading && debugBreakDirection != default) GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + debugBreakDirection.normalized * 20, 5, Color.cyan); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right) + angVelRollTarget, 2, Color.green); + if (avoidingTerrain) + { + 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 = AI.vessel.ActiveController().WM; // Set only once, as it shouldn't change during autotuning. + 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 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 _bestLoss = float.MaxValue; // The best result so far for the current learning rate. + public float bestLoss = float.MaxValue; // The best loss result so far. + public float lrAtBest = 1f; // The LR at the time the best loss was found (for logging). + public float rrAtBest = 1f; // The RR at the time the best loss was found (for logging). + + /// + /// Update the learning rate based on the current loss. + /// + /// The current loss, or some other metric. + /// The current roll relevance (for logging). + /// True if the learning rate decreases, False otherwise. + public bool Update(float value, float rollRelevance) + { + if (value < _bestLoss) + { + _bestLoss = value; + count = 0; + if (_bestLoss < bestLoss) + { + bestLoss = _bestLoss; + lrAtBest = current; + rrAtBest = rollRelevance; + } + } + if (++count >= patience) + { + current *= reductionFactor; + count = 0; + _bestLoss = value; // Reset the best loss to avoid unnecessarily reducing the learning rate due to a fluke best score. + return true; + } + return false; + } + + /// + /// Reset everything. + /// + public void Reset(float initial) + { + current = initial; + count = 0; + _bestLoss = float.MaxValue; + bestLoss = _bestLoss; + lrAtBest = initial; + rrAtBest = 1f; + } + } + + /// + /// Optimise various parameters. + /// Currently, this just balances the roll relevance factor. + /// + class Optimiser + { + public float rollRelevance = 1f; // Start high so that the loss decreases as this converges to a fixed value. + float rollRelevanceMomentum = 0.8f; + + public void Update() + { + rollRelevance = rollRelevanceMomentum * rollRelevance + (1f - rollRelevanceMomentum) * Mathf.Clamp01(_rollRelevance.Average()); // Clamp roll relevance to at most 1 in case of freak measurements. + _rollRelevance.Clear(); + } + + public void Reset(float initialRollRelevance = 1f) + { + rollRelevance = initialRollRelevance; + _rollRelevance.Clear(); + } + + List _rollRelevance = []; + 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(); + Optimiser optimiser = new(); + #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. + { + 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. + } + } + else if (recentering) + { + AI.CommandFlyTo((Vector3)startCoords); + if ((FlightGlobals.currentMainBody.GetWorldSurfacePosition(startCoords.x, startCoords.y, startCoords.z) - AI.vessel.transform.position).sqrMagnitude < 0.01f * AI.autoTuningRecenteringDistanceSqr) // Within 10% of recentering distance. + { + 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:G3}, 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 > AI.autoTuningRecenteringDistanceSqr) + { + 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.GetName(); + fieldNames = ["base"]; + fields = []; + fixedFields = []; + baseValues = []; + gradient = []; + limits = []; + lossSamples = []; + baseLossSamples = []; + 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") continue; // Ignore non-PID fields. + if (!field.guiActive) continue; // Ignore inactive fields. Only and all active fields should be relevant. + if (field.uiControlFlight.GetType() != typeof(UI_FloatRange)) continue; // Ignore non-FloatRange fields. + if (field.name.StartsWith("autoTuning")) continue; // Ignore our extra autoTuning fields. + + // 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.autoTuningOptionFixedDP && field.name == "steerDampingPitch") || + (AI.autoTuningOptionFixedDY && field.name == "steerDampingYaw") || + (AI.autoTuningOptionFixedDR && field.name == "steerDampingRoll") || + (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") || + (AI.autoTuningOptionFixedPp && field.name == "threeAxisPIDPitchMult") || + (AI.autoTuningOptionFixedIp && field.name == "threeAxisPIDPitchKi") || + (AI.autoTuningOptionFixedDp && field.name == "threeAxisPIDPitchDamping") || + (AI.autoTuningOptionFixedPy && field.name == "threeAxisPIDYawMult") || + (AI.autoTuningOptionFixedIy && field.name == "threeAxisPIDYawKi") || + (AI.autoTuningOptionFixedDy && field.name == "threeAxisPIDYawDamping") || + (AI.autoTuningOptionFixedPr && field.name == "threeAxisPIDRollMult") || + (AI.autoTuningOptionFixedIr && field.name == "threeAxisPIDRollKi") || + (AI.autoTuningOptionFixedDr && field.name == "threeAxisPIDRollDamping")) + { + 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)); + } + optimiser.Reset(AI.autoTuningOptionInitialRollRelevance); // Reset the optimiser before resetting samples so that the RR is up-to-date in the strings. + ResetSamples(); + lr.Reset(AI.autoTuningOptionInitialLearningRate); + } + + /// + /// 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 / absHeadingChange + optimiser.rollRelevance * 0.0002f * rollOscillationAreaSqr; // This normalisation seems to give a roughly flat distribution over the 30°—120° range for the test craft. + optimiser.Accumulate(pointingOscillationAreaSqr / absHeadingChange / absHeadingChange / (0.0002f * rollOscillationAreaSqr)); + if (currentField == "base") + { + baseLossSamples.Add(lossSample); + if (++sampleNumber >= (int)AI.autoTuningOptionNumSamples) + { + var loss = baseLossSamples.Average(); + if (loss < lr.bestLoss) + { + 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:G3}, RR: {optimiser.rollRelevance}, Loss: {loss}"); + bestValues["rollRelevance"] = optimiser.rollRelevance; // Store the roll relevance for the best PID settings too. + AI.autoTuningOptionInitialRollRelevance = optimiser.rollRelevance; + } + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Current: " + string.Join(", ", baseValues.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value)) + $", LR: {lr.current:G3}, RR: {optimiser.rollRelevance}, Loss: {loss}"); + var lrDecreased = lr.Update(loss, optimiser.rollRelevance); // Update learning rate based on the current loss. + optimiser.Update(); + if (lrDecreased) + { + if (bestValues is not null) RevertPIDValues(); // Revert to the best values when lowering the learning rate. + optimiser.Reset(lr.rrAtBest); // Also, revert to the roll relevance when the best values were found. + } + 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.bestLoss:G6}, completed."; + AI.AutoTune = false; // This also reverts to the best settings and stores them. + return; + } + AI.autoTuningLossLabel = $"{loss:G6} (best: {lr.bestLoss:G6})"; + AI.autoTuningLossLabel2 = $"LR: {lr.current:G3}, Roll rel.: {optimiser.rollRelevance:G3}"; + ++currentFieldIndex; + UpdatePIDValues(false); + sampleNumber = 0; + } + } + else + { + if (firstCFDSample) + { + lossSamples[currentField].Add([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(", ", fields.Keys.Where(fieldName => bestValues.ContainsKey(fieldName)).Select(fieldName => fields[fieldName].guiName + ":" + bestValues[fieldName]))}"); + 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]; + } + if (bestValues.ContainsKey("rollRelevance")) AI.autoTuningOptionInitialRollRelevance = bestValues["rollRelevance"]; // Set the latest roll relevance as the AI's starting roll relevance for next time. + if (lr.current < 9e-4f) + AI.autoTuningSummary = $"Best Loss {lr.bestLoss:G6}, tuning completed, RR: {lr.rrAtBest:G3}."; + else + AI.autoTuningSummary = $"Best Loss {lr.bestLoss:G6}, tuning incomplete, LR: {lr.lrAtBest:G3}, RR: {lr.rrAtBest:G3}."; + } + 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); + AI.autoTuningSummary = ""; + } + } + + 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; + + // Move the vessel to the start position and make sure the AI and engines are active. + if (AI.vessel.LandedOrSplashed) + { + AI.vessel.Landed = false; + AI.vessel.Splashed = false; + VesselMover.Instance.PickUpAndDrop(AI.vessel, AI.autoTuningAltitude); + } + else + { + AI.vessel.SetPosition(FlightGlobals.currentMainBody.GetWorldSurfacePosition(startCoords.x, startCoords.y, startCoords.z)); + } + if (SpawnUtils.CountActiveEngines(AI.vessel) == 0) SpawnUtils.ActivateAllEngines(AI.vessel); + AI.ActivatePilot(); + recentering = true; + } + } +} diff --git a/BDArmory/Control/BDModuleSurfaceAI.cs b/BDArmory/Control/BDModuleSurfaceAI.cs new file mode 100644 index 000000000..379a22230 --- /dev/null +++ b/BDArmory/Control/BDModuleSurfaceAI.cs @@ -0,0 +1,1342 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using BDArmory.GameModes; +using BDArmory.Guidances; + +namespace BDArmory.Control +{ + public class BDModuleSurfaceAI : BDGenericAIBase, IBDAIControl + { + public override AIType aiType => AIType.SurfaceAI; + #region Declarations + Vessel extendingTarget = null; + public bool orderedToExtend = false; + Vessel bypassTarget = null; + Vector3 bypassTargetPos; + + Vector3 targetDirection; // Note: this isn't normalized + float targetVelocity; // the velocity the ship should target, not the velocity of its target + enum AimingMode { Off = 0, Yaw = 1, Pitch = 2, Direct = Yaw | Pitch } + AimingMode aimingMode = AimingMode.Off; + + //Building collision detection stuff + float terrainAlertDetectionRadius; + float terrainAlertThreatRange = 100; //assuming most tanks/ground Vees can manage a 100m turning circle. may need increase for hovercraft + RaycastHit[] terrainAvoidanceHits = new RaycastHit[10]; + int collisionTicker = 100; + int collisionDetectionTicker = 0; + int reverseTicker = 0; + Vector3 dodgeVector = Vector3.zero; + float vehicleWidth; + Vector3 alertNormalAvg = Vector3.zero; + float alertNormalAvgF = 0.95f; + + float weaveAdjustment = 0; + float weaveDirection = 1; + const float weaveLimit = 2.3f; // Scale factor for the limit of the WeaveFactor (original was 6.5 factor and 15 limit). + + Vector3 upDir; + Vector3 terrainNormal; + + AIUtils.TraversabilityMatrix pathingMatrix; + List pathingWaypoints = new List(); + bool leftPath = false; + + bool doExtend = false; + bool doReverse = false; + bool wasReversing = false; + protected override Vector3d assignedPositionGeo + { + get { return intermediatePositionGeo; } + set + { + finalPositionGeo = value; + leftPath = true; + } + } + + Vector3d finalPositionGeo; + Vector3d intermediatePositionGeo; + public override Vector3d commandGPS => finalPositionGeo; + + private BDLandSpeedControl motorControl; + + //settings + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_VehicleType"),//Vehicle type + UI_ChooseOption(options = new string[5] { "Stationary", "Land", "Water", "Amphibious", "Submarine" })] + public string SurfaceTypeName = "Land"; + + bool isHovercraft = false; + + public AIUtils.VehicleMovementType SurfaceType + => (AIUtils.VehicleMovementType)Enum.Parse(typeof(AIUtils.VehicleMovementType), SurfaceTypeName); + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxSlopeAngle"),//Max slope angle + UI_FloatRange(minValue = 1f, maxValue = 30f, stepIncrement = 1f, scene = UI_Scene.All)] + public float MaxSlopeAngle = 10f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CombatAltitude"), //Combat Alt. + UI_FloatRange(minValue = -200, maxValue = -15, stepIncrement = 5, scene = UI_Scene.All)] + public float CombatAltitude = -75; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CruiseSpeed"),//Cruise speed + UI_FloatRange(minValue = 5f, maxValue = 60f, stepIncrement = 1f, scene = UI_Scene.All)] + public float CruiseSpeed = 20; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxSpeed"),//Max speed + UI_FloatRange(minValue = 5f, maxValue = 80f, stepIncrement = 1f, scene = UI_Scene.All)] + public float MaxSpeed = 30; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxDrift"),//Max drift + UI_FloatRange(minValue = 1f, maxValue = 180f, stepIncrement = 1f, scene = UI_Scene.All)] + public float MaxDrift = 10; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_TargetPitch"),//Moving pitch + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] + public float TargetPitch = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BankAngle"),//Bank angle + UI_FloatRange(minValue = -45f, maxValue = 45f, stepIncrement = 1f, scene = UI_Scene.All)] + public float BankAngle = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_WeaveFactor"),//Weave Factor + UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float WeaveFactor = 6.5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerPower"),//Steer Factor + UI_FloatRange(minValue = 0.2f, maxValue = 20f, stepIncrement = .1f, scene = UI_Scene.All)] + public float steerMult = 6; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDamping"),//Steer Damping + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] + public float steerDamping = 3; + + //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Steering"), + // UI_Toggle(enabledText = "Powered", disabledText = "Passive")] + public bool PoweredSteering = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BroadsideAttack"),//Attack vector + UI_Toggle(enabledText = "#LOC_BDArmory_AI_BroadsideAttack_enabledText", disabledText = "#LOC_BDArmory_AI_BroadsideAttack_disabledText")]//Broadside--Bow + public bool BroadsideAttack = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinEngagementRange"),//Min engagement range + UI_FloatRange(minValue = 0f, maxValue = 6000f, stepIncrement = 100f, scene = UI_Scene.All)] + public float MinEngagementRange = 500; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxEngagementRange"),//Max engagement range + UI_FloatRange(minValue = 500f, maxValue = 8000f, stepIncrement = 100f, scene = UI_Scene.All)] + public float MaxEngagementRange = 4000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaintainEngagementRange"),//Maintain min Range + UI_Toggle(enabledText = "#LOC_BDArmory_true", disabledText = "#LOC_BDArmory_false")]//true; false + public bool maintainMinRange = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ManeuverRCS"),//RCS active + UI_Toggle(enabledText = "#LOC_BDArmory_AI_ManeuverRCS_enabledText", disabledText = "#LOC_BDArmory_AI_ManeuverRCS_disabledText", scene = UI_Scene.All),]//Maneuvers--Combat + public bool ManeuverRCS = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinObstacleMass", advancedTweakable = true),//Min obstacle mass + UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All),] + public float AvoidMass = 0f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_PreferredBroadsideDirection", advancedTweakable = true),//Preferred broadside direction + UI_ChooseOption(options = new string[3] { "Port", "Either", "Starboard" }, scene = UI_Scene.All),] + public string OrbitDirectionName = "Either"; + public readonly string[] orbitDirections = new string[3] { "Port", "Either", "Starboard" }; + + [KSPField(isPersistant = true)] + int sideSlipDirection = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_GoesUp", advancedTweakable = true),//Goes up to + UI_Toggle(enabledText = "#LOC_BDArmory_AI_GoesUp_enabledText", disabledText = "#LOC_BDArmory_AI_GoesUp_disabledText", scene = UI_Scene.All),]//eleven--ten + bool upToEleven = false; + public bool UpToEleven { get { return upToEleven; } set { if (upToEleven != value) { upToEleven = value; TurnItUpToEleven(); } } } + + const float AttackAngleAtMaxRange = 30f; + + Dictionary altMaxValues = new Dictionary + { + { nameof(MaxSlopeAngle), 90f }, + { nameof(CruiseSpeed), 300f }, + { nameof(MaxSpeed), 400f }, + { nameof(steerMult), 200f }, + { nameof(steerDamping), 100f }, + { nameof(MinEngagementRange), 20000f }, + { nameof(MaxEngagementRange), 30000f }, + { nameof(AvoidMass), 1000000f }, + }; + + #endregion Declarations + + #region RMB info in editor + + // Yes + public override string GetInfo() + { + // known bug - the game caches the RMB info, changing the variable after checking the info + // does not update the info. :( No idea how to force an update. + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Available settings:"); + sb.AppendLine($"- Vehicle type - can this vessel operate on land/sea/both"); + sb.AppendLine($"- Max slope angle - what is the steepest slope this vessel can negotiate"); + sb.AppendLine($"- Cruise speed - the default speed at which it is safe to maneuver"); + sb.AppendLine($"- Max speed - the maximum combat speed"); + sb.AppendLine($"- Max drift - maximum allowed angle between facing and velocity vector"); + sb.AppendLine($"- Moving pitch - the pitch level to maintain when moving at cruise speed"); + sb.AppendLine($"- Bank angle - the limit on roll when turning, positive rolls into turns"); + sb.AppendLine($"- Steer Factor - higher will make the AI apply more control input for the same desired rotation"); + sb.AppendLine($"- Steer Damping - higher will make the AI apply more control input when it wants to stop rotation"); + sb.AppendLine($"- Attack vector - does the vessel attack from the front or the sides"); + sb.AppendLine($"- Min engagement range - AI will try to move away from oponents if closer than this range"); + sb.AppendLine($"- Max engagement range - AI will prioritize getting closer over attacking when beyond this range"); + sb.AppendLine($"- RCS active - Use RCS during any maneuvers, or only in combat "); + if (GameSettings.ADVANCED_TWEAKABLES) + { + sb.AppendLine($"- Min obstacle mass - Obstacles of a lower mass than this will be ignored instead of avoided"); + sb.AppendLine($"- Goes up to - Increases variable limits, no direct effect on behaviour"); + } + + return sb.ToString(); + } + + #endregion RMB info in editor + + #region events + + public override void OnStart(StartState state) + { + base.OnStart(state); + if (!(HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor)) return; + + SetChooseOptions(); + SetOnUpToElevenChanged(); + } + + public override bool ActivatePilot() + { + if (!base.ActivatePilot()) return false; + TakingOff = false; + originalMaxSpeed = MaxSpeed; + pathingMatrix = new AIUtils.TraversabilityMatrix(); + + if (!motorControl) + { + motorControl = gameObject.AddComponent(); + motorControl.vessel = vessel; + } + speedController.Deactivate(); + motorControl.Activate(); + if (BroadsideAttack && sideSlipDirection == 0) + { + SetBroadsideDirection(OrbitDirectionName); + } + + leftPath = true; + extendingTarget = null; + bypassTarget = null; + collisionDetectionTicker = 6; + terrainAlertDetectionRadius = vessel.GetRadius() * 2; + if (VesselModuleRegistry.GetModules(vessel).Count > 0) isHovercraft = true; + vehicleWidth = vessel.vesselSize.x / 2; + alertNormalAvgF = Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime * 5f); // Decay rate for a half-life of 1/5s. + return true; + } + + public override void DeactivatePilot() + { + base.DeactivatePilot(); + + if (motorControl) + motorControl.Deactivate(); + } + + public void SetChooseOptions() + { + UI_ChooseOption broadside = (UI_ChooseOption)(HighLogic.LoadedSceneIsFlight ? Fields[nameof(OrbitDirectionName)].uiControlFlight : Fields[nameof(OrbitDirectionName)].uiControlEditor); + broadside.onFieldChanged = ChooseOptionsUpdated; + UI_ChooseOption surface = (UI_ChooseOption)(HighLogic.LoadedSceneIsFlight ? Fields[nameof(SurfaceTypeName)].uiControlFlight : Fields[nameof(SurfaceTypeName)].uiControlEditor); + surface.onFieldChanged = ChooseOptionsUpdated; + ChooseOptionsUpdated(null, null); + } + + public void ChooseOptionsUpdated(BaseField field, object obj) + { + // Hide/display the AI fields + var fieldEnabled = SurfaceType != AIUtils.VehicleMovementType.Stationary; + foreach (var fieldName in new List{ + nameof(MaxSlopeAngle), + nameof(CruiseSpeed), + nameof(MaxSpeed), + nameof(MaxDrift), + nameof(TargetPitch), + nameof(BankAngle), + // nameof(steerMult), + // nameof(steerDamping), + nameof(BroadsideAttack), + // nameof(MinEngagementRange), + // nameof(MaxEngagementRange), + // nameof(ManeuverRCS), + nameof(AvoidMass), + nameof(OrbitDirectionName) + }) + { + Fields[fieldName].guiActive = fieldEnabled; + Fields[fieldName].guiActiveEditor = fieldEnabled; + } + Fields[nameof(CombatAltitude)].guiActive = SurfaceType == AIUtils.VehicleMovementType.Submarine; + Fields[nameof(CombatAltitude)].guiActiveEditor = SurfaceType == AIUtils.VehicleMovementType.Submarine; + Fields[nameof(maintainMinRange)].guiActive = SurfaceType == AIUtils.VehicleMovementType.Land; + Fields[nameof(maintainMinRange)].guiActiveEditor = SurfaceType == AIUtils.VehicleMovementType.Land; + part.RefreshAssociatedWindows(); + if (BDArmoryAIGUI.Instance != null) + { + BDArmoryAIGUI.Instance.SetChooseOptionSliders(); + } + } + + public void SetBroadsideDirection(string direction) + { + if (!orbitDirections.Contains(direction)) return; + OrbitDirectionName = direction; + sideSlipDirection = orbitDirections.IndexOf(OrbitDirectionName) - 1; + if (sideSlipDirection == 0) + sideSlipDirection = UnityEngine.Random.value > 0.5f ? 1 : -1; + } + + void SetOnUpToElevenChanged() + { + var field = (UI_Toggle)(HighLogic.LoadedSceneIsFlight ? Fields[nameof(upToEleven)].uiControlFlight : Fields[nameof(upToEleven)].uiControlEditor); + field.onFieldChanged = TurnItUpToEleven; // Only triggered on UI interaction. + if (upToEleven) TurnItUpToEleven(); // The initially loaded values are not the alternate ones. + } + + void TurnItUpToEleven(BaseField _field = null, object _obj = null) + { + 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); + (altMaxValues[s.Current], euic.maxValue) = (euic.maxValue, altMaxValues[s.Current]); + StartCoroutine(SetVar(s.Current, (float)typeof(BDModuleSurfaceAI).GetField(s.Current).GetValue(this))); // change the value back to what it is now after fixed update, because changing the max value will clamp it down + } + } + + IEnumerator SetVar(string name, float value) + { + yield return new WaitForFixedUpdate(); + typeof(BDModuleSurfaceAI).GetField(name).SetValue(this, value); + } + + protected override void OnGUI() + { + base.OnGUI(); + + if (!pilotEnabled || !vessel.isActiveVessel) return; + + if (!BDArmorySettings.DEBUG_LINES) return; + if (command == PilotCommands.Follow) + { + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, assignedPositionWorld, 2, Color.red); + } + foreach (var hit in debugHits) GUIUtils.DrawLineBetweenWorldPositions(hit.Item1, hit.Item1 + 5 * hit.Item2, 5 - 5 / debugHitFadeTime * (Time.time - hit.Item3), Color.magenta); // Collision Avoidance (width fades before they're removed) + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 2, Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(vessel.CoM + vehicleWidth * vesselTransform.right, vessel.CoM + vehicleWidth * vesselTransform.right + (wasReversing ? -vessel.vesselTransform.up : vessel.vesselTransform.up) * (vehicleWidth + terrainAlertDetectionRadius), 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vessel.CoM - vehicleWidth * vesselTransform.right, vessel.CoM - vehicleWidth * vesselTransform.right + (wasReversing ? -vessel.vesselTransform.up : vessel.vesselTransform.up) * (vehicleWidth + terrainAlertDetectionRadius), 2, Color.red); + //GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right), 2, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir) * 10f, 2, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vesselTransform.up * 10f, 5, Color.red); + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + pathingMatrix.DrawDebug(vessel.CoM, pathingWaypoints); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + (Vector3)(dodgeVector != null ? dodgeVector : vessel.srf_vel_direction * 25), 2, Color.white); + } + } + + #endregion events + + #region Status + public enum StatusMode { Free, OnAlert, Engaging, Evading, Extending, Moving, Repositioning, Braking, Reversing, CollisionAvoidance, RammingSpeed, Panic, Custom } + public StatusMode currentStatusMode = StatusMode.Free; + protected override void SetStatus(string status) + { + base.SetStatus(status); + if (status.StartsWith("Free")) currentStatusMode = StatusMode.Free; + else if (status.StartsWith("On Alert")) currentStatusMode = StatusMode.OnAlert; + else if (status.StartsWith("Engaging")) currentStatusMode = StatusMode.Engaging; + else if (status.StartsWith("Evading")) currentStatusMode = StatusMode.Evading; + else if (status.StartsWith("Moving")) currentStatusMode = StatusMode.Moving; + else if (status.StartsWith("Repositioning")) currentStatusMode = StatusMode.Repositioning; + else if (status.StartsWith("Braking")) currentStatusMode = StatusMode.Braking; + else if (status.StartsWith("Reversing")) currentStatusMode = StatusMode.Reversing; + else if (status.StartsWith("Extending")) currentStatusMode = StatusMode.Extending; + else if (status.StartsWith("Avoiding Collision")) currentStatusMode = StatusMode.CollisionAvoidance; + else if (status.StartsWith("Ramming")) currentStatusMode = StatusMode.RammingSpeed; + else if (status.StartsWith("Airtime!") || status.StartsWith("Stranded") || status.StartsWith("Floating") || status.StartsWith("Sunk") || status.StartsWith("Disabled")) currentStatusMode = StatusMode.Panic; + else currentStatusMode = StatusMode.Custom; + } + #endregion + + #region Actual AI Pilot + + protected override void AutoPilot(FlightCtrlState s) + { + if (!vessel.Autopilot.Enabled) + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); + + targetVelocity = 0; + targetDirection = vesselTransform.up; + aimingMode = AimingMode.Off; + upDir = vessel.up; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(""); + if (IsRunningWaypoints) UpdateWaypoint(); // Update the waypoint state. + if (SurfaceType == AIUtils.VehicleMovementType.Stationary) + { + if (!vessel.Splashed && Physics.Raycast(new Ray(vessel.CoM, -upDir), out RaycastHit hit, (float)vessel.radarAltitude + vessel.GetRadius(), (int)LayerMasks.Scenery)) + terrainNormal = hit.normal; + else + terrainNormal = upDir; + } + + // check if we should be panicking + 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(); + // situational awareness modifies the base as best as it can (evasive mainly) + Tactical(); + } + + AttitudeControl(s); // move according to our targets + AdjustThrottle(targetVelocity); // set throttle according to our targets and movement + } + readonly List<(Vector3, Vector3, float)> debugHits = []; + float debugHitFadeTime = 0.5f; + void PilotLogic() + { + wasReversing = doReverse; + doReverse = false; + if (BDArmorySettings.DEBUG_LINES) debugHits.RemoveAll(hit => Time.time - hit.Item3 > debugHitFadeTime); // Clear out those older than the fade time. + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + float alertDistance = terrainAlertThreatRange; + bool vesselCollision = false; + bool reversingTurn = false; + int validHitCount = 0; + Vector3 vesselDir = vessel.srfSpeed > 1 ? vessel.srf_vel_direction : wasReversing ? -vesselTransform.up : vesselTransform.up; + string collidingWith = ""; + + // check for collisions, but not every frame unless we're currently avoiding a collision + if (collisionDetectionTicker == 0 || currentStatusMode == StatusMode.CollisionAvoidance) + { + collisionDetectionTicker = 20; // Every 0.4s when not actively avoiding collisions + + dodgeVector = Vector3.zero; + + { // Vessel-vessel collisions + float predictMult = Mathf.Clamp(10 / MaxDrift, 1, 10); + using var vs = BDATargetManager.LoadedVessels.GetEnumerator(); + while (vs.MoveNext()) + { + if (vs.Current == null || vs.Current == vessel || vs.Current.GetTotalMass() < AvoidMass) continue; //expand for SrfAi ramming implementation? + if (!VesselModuleRegistry.IgnoredVesselTypes.Contains(vs.Current.vesselType)) + { + var ibdaiControl = vs.Current.ActiveController().AI; + if (!vs.Current.LandedOrSplashed || (ibdaiControl != null && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel)) + continue; + } + dodgeVector = PredictCollisionWithVessel(vs.Current, 5f * predictMult, 0.5f); + if (dodgeVector != Vector3.zero) // Dodge the first potential collision (this isn't necessarily the closest, but multi-vessel collisions are unlikely). + + + { + vesselCollision = true; + collidingWith = vs.Current.GetName(); + break; + } + } + } + + { // Terrain/building collisions FIXME We're only checking buildings, should we drop that and check terrain too? + Ray ray = new(vessel.CoM, vesselDir); + terrainAlertThreatRange = Mathf.Clamp((float)vessel.srfSpeed * 10f, 2f * terrainAlertDetectionRadius, Mathf.Max(200f, 10f * terrainAlertDetectionRadius)); // Have threat range scale with speed, but within limits. + + // Check in the direction we're moving up to the threat range + int hitCount = Physics.SphereCastNonAlloc(ray, terrainAlertDetectionRadius, terrainAvoidanceHits, terrainAlertThreatRange, (int)LayerMasks.Scenery); + if (hitCount == terrainAvoidanceHits.Length) + { + terrainAvoidanceHits = Physics.SphereCastAll(ray, terrainAlertDetectionRadius, terrainAlertThreatRange, (int)LayerMasks.Scenery); + hitCount = terrainAvoidanceHits.Length; + } + if (hitCount > 0) // Found something. + { + Vector3 alertNormal = Vector3.zero; + float maxSlopeDot = Mathf.Cos(Mathf.Deg2Rad * MaxSlopeAngle); + bool doProximityCheck = false; + using var hits = terrainAvoidanceHits.Take(hitCount).GetEnumerator(); + while (hits.MoveNext()) + { + if (hits.Current.collider.gameObject.GetComponentUpwards() != null) // Hit a building. + { + if (Vector3.Dot(hits.Current.normal, vesselDir) > 0) continue; // Ignore back-facing hits. + if (Mathf.Abs(Vector3.Dot(hits.Current.normal, vessel.up)) > maxSlopeDot) continue; // Ignore slopes < MaxSlopeAngle. + // Note: for spherecasts, colliders within the starting sphere have distance=0, point=Vector3.zero and normal=-ray.direction ... FFS Unity! + if (hits.Current.distance > 0) + { + alertDistance = Mathf.Min(alertDistance, hits.Current.distance); + var normal = hits.Current.normal; + float collisionAngle = VectorUtils.Angle(vesselDir, -normal); + if (hits.Current.distance < (100 - (100 * Mathf.Cos(collisionAngle - 90))) + (terrainAlertDetectionRadius / 2)) + normal = Vector3.Reflect(vesselDir, hits.Current.normal); // assuming a 100m turning circle, crashing Vee can wait to start turn depending on approach angle + alertNormal += normal / (1 + hits.Current.distance * hits.Current.distance * (collisionAngle < 15 ? 1 : (collisionAngle / 90) * 18)); //weight normals in front of us more heavily than normals to the the vessel's side + //should probably adjust to angle to width of craft at terrainAlertDetectionRadius. ATAN(TAN(vehicleWidth/terrainAlertDetectionRadius * 2)*2)? Since past that, collisions from the spherecast aren't in the way. Test later. + ++validHitCount; + if (BDArmorySettings.DEBUG_LINES) debugHits.Add((hits.Current.point, hits.Current.normal, Time.time)); + } + else // The hit could be anywhere within the sphere centered on CoM, we need to do a short-range proximity check. + { + doProximityCheck = true; + } + } + } + if (doProximityCheck) + { + for (int i = 0; i < 2; ++i) + { + ray.origin = i switch // Just setting the origin avoids re-normalising the direction. + { + 0 => vessel.CoM + vehicleWidth * vesselTransform.right, + 1 => vessel.CoM - vehicleWidth * vesselTransform.right, + _ => vessel.CoM // Dummy to suppress switch complaining about not handling all integer cases + }; + if (Physics.Raycast(ray, out RaycastHit hit, terrainAlertDetectionRadius + vehicleWidth, (int)LayerMasks.Scenery) // Hit something. + && hit.collider.gameObject.GetComponentUpwards() != null // Hit a building. + && Vector3.Dot(hit.normal, ray.direction) < 0 // Ignore back-facing hits. + && Mathf.Abs(Vector3.Dot(hit.normal, vessel.up)) < maxSlopeDot // Ignore slopes < MaxSlopeAngle. + ) + { + alertDistance = Mathf.Min(alertDistance, hit.distance); + alertNormal += hit.normal / (1 + hit.distance * hit.distance); + + ++validHitCount; + if (BDArmorySettings.DEBUG_LINES) debugHits.Add((hit.point, hit.normal, Time.time)); + } + } + } + if (wasReversing) // If reversing, also look directly ahead (but not as far) to keep tracking what we're reversing from. + { + if (Physics.Raycast(new Ray(vessel.CoM, vesselTransform.up), out RaycastHit hit, 5f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery) // Hit something within 5 detection radii. + && hit.collider.gameObject.GetComponentUpwards() != null // Hit a building. + && Vector3.Dot(hit.normal, vesselTransform.up) < 0 // Ignore back-facing hits. + && Mathf.Abs(Vector3.Dot(hit.normal, vessel.up)) < maxSlopeDot // Ignore slopes < MaxSlopeAngle. + ) + { + alertDistance = Mathf.Min(alertDistance, hit.distance); + alertNormal += hit.normal / (1 + hit.distance * hit.distance); + ++validHitCount; + reversingTurn = true; + if (BDArmorySettings.DEBUG_LINES) debugHits.Add((hit.point, hit.normal, Time.time)); + } + } + alertNormalAvg = Vector3.Slerp(alertNormal.normalized, alertNormalAvg, alertNormalAvgF); // Smooth out the alert normal direction. + } + else alertNormalAvg = Vector3.zero; + if (validHitCount > 0) + { + // Smooth out the dodge vector with our current heading to avoid over-correcting for things far away. + alertNormalAvg = Vector3.Slerp(alertNormalAvg.normalized, vesselDir, Mathf.Clamp(alertDistance / terrainAlertThreatRange, 0, 0.5f)); + dodgeVector = vesselCollision ? (dodgeVector + alertNormalAvg).normalized : alertNormalAvg; + // Note, if heading straight at a wall, the yawError in AttitudeControl will handle pulling hard to the left or right. + if (!wasReversing && collisionTicker < 100 && (vessel.srfSpeed < 1 || alertDistance < vessel.srfSpeed) && Vector3.Dot(dodgeVector, vesselTransform.up) < -0.707f) // Close to hitting wall forwards (45°) => trigger reverse early + { + collisionTicker = -1; + } + else if (wasReversing && alertDistance < vessel.srfSpeed && Vector3.Dot(dodgeVector, vesselTransform.up) > 0.866f) // Close to hitting wall in reverse (30°) => abort reverse and delay reverse checks for 1s + collisionTicker = 150; + else if (wasReversing || vessel.srfSpeed < 1 || alertDistance < 1 * vessel.srfSpeed) // Reversing, stuck or about to crash in <1s + --collisionTicker; + else + collisionTicker = Math.Max(100, collisionTicker); + } + else if (collisionTicker < 0 || collisionTicker > 100) // was reversing or had been stuck, but no longer any valid hits => wait for reverse timer to expire or ticker to return to normal range. + { + --collisionTicker; + } + else + collisionTicker = Math.Max(100, collisionTicker); + } + /* collisionTicker thresholds (50 ticks == 1s): + * > 100 recovery from being stuck in reverse, no early reversing checks + * 0 — 100 normal + * -250 — 0 reversing + * < -250 reversing and maybe stuck? + */ + if (collisionTicker < 0) + { + doReverse = true; + // Reversing typically has the dodgeVector pointing backwards relative to the vessel. We want to reverse in an arc peeling away from the normal by up to 90°. + if (reversingTurn && Vector3.Dot(dodgeVector, vesselDir) > 0) + dodgeVector = Vector3.RotateTowards(dodgeVector, -Mathf.Sign(Vector3.Dot(dodgeVector, vesselTransform.right)) * vesselTransform.right, Mathf.Deg2Rad * Mathf.Clamp(alertDistance * 2, 0, 90), 0); // Aim for a 45m arc. + + if (collisionTicker < -250 && vessel.srfSpeed < 1) // Reversing for 5s and we seem to be stuck. + { + collisionTicker = 200; + doReverse = false; + reverseTicker = 0; + } + else if (vessel.srfSpeed > 1 && ++reverseTicker > (validHitCount == 0 ? 150 : 300)) // Have reversed above 1m/s for cumulative 3s with no hits or 6s. + { + collisionTicker = 150; + doReverse = false; + reverseTicker = 0; + } + } + else + { + reverseTicker = 0; + } + } + else + { + --collisionDetectionTicker; + } + // avoid collisions if any are found + if (vesselCollision || validHitCount > 0 || collisionTicker < 0 || collisionTicker > 100) + { + // Lower speed when needing to turn sharply: 25% @ 180°, 75% @ 90°. + targetVelocity = (doReverse && (Vector3.Dot(vessel.vesselTransform.up, vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir)) > 0)) ? -MaxSpeed //we're still moving forward and need to be going backward + : Mathf.Clamp01(0.75f + 0.5f * Vector3.Dot(doReverse ? -vesselTransform.up : vesselTransform.up, dodgeVector)) * (doReverse ? -MaxSpeed : MaxSpeed); + targetDirection = (vesselCollision || validHitCount > 0) ? dodgeVector : vesselDir; + if (vesselCollision) SetStatus($"Avoiding Collision with {collidingWith}"); + else SetStatus($"Avoiding Collision ({alertDistance:0}m)"); + leftPath = true; + DebugLine($"Collision: {alertDistance:0.0}m / {terrainAlertThreatRange:0.0}m ({terrainAlertDetectionRadius:0.0}m, {validHitCount} hits), Reverse {doReverse} ({collisionTicker}), vel: {targetVelocity:0.0}m/s"); + return; + } + } + else { collisionDetectionTicker = 0; } + + // 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 + var weaponManager = WeaponManager; + if (weaponManager != null && 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.selectedWeapon is ModuleWeapon wep) + shotSpeed = wep.bulletVelocity; + var timeToCPA = targetVessel.TimeToCPA(vessel.CoM, vessel.Velocity() + vesselTransform.up * shotSpeed, FlightGlobals.getGeeForceAtPosition(vessel.CoM), MaxEngagementRange / shotSpeed); + vecToTarget = targetVessel.PredictPosition(timeToCPA) - vessel.CoM; + + if (SurfaceType == AIUtils.VehicleMovementType.Stationary) + { + if (distance >= MinEngagementRange && distance <= MaxEngagementRange) + { + targetDirection = vecToTarget; + aimingMode = AimingMode.Direct; + } + 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 + && !pathingMatrix.TraversableStraightLine( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + VectorUtils.WorldPositionToGeoCoords(vessel.PredictPosition(10), vessel.mainBody), + vessel.mainBody, SurfaceType, MaxSlopeAngle, 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) / (CruiseSpeed * 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; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Broadside attack angle {sidestep}"); + } + else // just point at target and go + { + if (!maintainMinRange && (((targetVessel.horizontalSrfSpeed < 10) || Vector3.Dot(targetVessel.vesselTransform.up, vessel.vesselTransform.up) < 0 || orderedToExtend) //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 = SurfaceType == AIUtils.VehicleMovementType.Water ? -vecToTarget + vessel.srf_vel_direction : -vecToTarget; //extend + targetVelocity = MaxSpeed; + if (distance > Mathf.Max(MaxEngagementRange / 2, 2000)) orderedToExtend = false; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Extending: ({distance:F2}/{Mathf.Max(MaxEngagementRange / 2, 2000)})"); + SetStatus($"Extending {distance:0}m/{Mathf.Max(MaxEngagementRange / 2, 2000):0}m"); + return; + } + else + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"velAngle: {VectorUtils.Angle(vessel.srf_vel_direction.ProjectOnPlanePreNormalized(vessel.up), vesselTransform.up):G3}"); + extendingTarget = null; + targetDirection = vecToTarget.ProjectOnPlanePreNormalized(upDir); + if (weaponManager.selectedWeapon != null) + { + switch (weaponManager.selectedWeapon.GetWeaponClass()) + { + case WeaponClasses.Gun: + case WeaponClasses.Rocket: + case WeaponClasses.DefenseLaser: + var gun = (ModuleWeapon)weaponManager.selectedWeapon; + orderedToExtend = false; + if (gun != null && (gun.yawRange == 0 || gun.maxPitch == gun.minPitch) && gun.FiringSolutionVector != null) + { + if (gun.yawRange == 0) aimingMode |= AimingMode.Yaw; + if (gun.maxPitch == gun.minPitch) aimingMode |= AimingMode.Pitch; // FIXME: currently pitch isn't used directly in attitude control for aiming. + if (VectorUtils.Angle((Vector3)gun.FiringSolutionVector, vessel.transform.up) < 20) + targetDirection = (Vector3)gun.FiringSolutionVector; + } + break; + case WeaponClasses.Bomb: //depthcharging subs from a ship + { + if (SurfaceType == AIUtils.VehicleMovementType.Water || SurfaceType == AIUtils.VehicleMovementType.Amphibious) + { + MissileBase bomb = weaponManager.CurrentMissile; + + targetDirection = (AIUtils.PredictPosition(targetVessel, weaponManager.bombAirTime) - vessel.CoM).ProjectOnPlanePreNormalized(upDir); + aimingMode = AimingMode.Yaw; + } + } + break; + case WeaponClasses.SLW: //torpedo boats + { + if (SurfaceType == AIUtils.VehicleMovementType.Water || SurfaceType == AIUtils.VehicleMovementType.Amphibious) + { + MissileBase torpedo = weaponManager.CurrentMissile; + if (torpedo != null) + { + if (distance < torpedo.engageRangeMax + (float)(vessel.srf_velocity - targetVessel.srf_velocity).magnitude) + { + aimingMode = AimingMode.Direct; + targetDirection = (MissileGuidance.GetAirToAirFireSolution(torpedo, targetVessel) - vessel.CoM).ProjectOnPlanePreNormalized(upDir); + } + if (weaponManager.firedMissiles >= weaponManager.maxMissilesOnTarget) + { + targetVelocity = MaxSpeed; //torps away, get out of there + orderedToExtend = true; + } + } + } + } + break; + } + } + if (distance >= MaxEngagementRange || distance <= MinEngagementRange * 1.25f) + { + if (distance >= MaxEngagementRange) + targetVelocity = MaxSpeed;//out of engagement range, engines ahead full + else if (distance <= MinEngagementRange * 1.25f) //coming within minEngagement range + { + if (maintainMinRange) //for some reason ignored if both vessel and targetvessel using Mk2roverCans? + { + //Add LoS provisions if target is behind hill/building? + if (distance <= MinEngagementRange) //rolled to a stop inside minRange/target has encroached + { + //if (Vector3.Dot(vessel.vesselTransform.up, vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir)) > 0) //we're still moving forward + //brakes = true; + //else brakes = false;//come to a stop and reversing, stop braking + if (Vector3.Dot(targetDirection, vesselTransform.up) < 0) + { + targetVelocity = MaxSpeed; + targetDirection = -targetDirection; + extendingTarget = targetVessel; + SetStatus($"Extending {distance:0}m/{MinEngagementRange:0}m"); + } + else + { + doReverse = true; + targetVelocity = -MaxSpeed; + SetStatus($"Reversing"); + } + return; + } + else if (vessel.srfSpeed < 0.1f * MaxSpeed && weaponManager && !weaponManager.recentlyFiring) + { + if (distance < 1.125f * MinEngagementRange) + { + targetVelocity = -0.1f * MaxSpeed; + doReverse = true; + } + else + { + targetVelocity = 0.1f * MaxSpeed; + } + SetStatus($"Adjusting alignment"); + } + else if (targetVessel.srfSpeed < 0.1f * MaxSpeed) + { + targetVelocity = 0; + SetStatus($"Braking"); + } + return; + } + else + { + targetVelocity = MaxSpeed; + if (weaponManager.selectedWeapon != null && (weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb + || weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.SLW)) + orderedToExtend = true; + } + } + } + else //within engagement envelope + { + targetVelocity = !maintainMinRange ? MaxSpeed : CruiseSpeed / 10 + (MaxSpeed - CruiseSpeed / 10) * (distance - MinEngagementRange) / (MaxEngagementRange - MinEngagementRange); //slow down if inside engagement range to extend shooting opportunities + } + targetVelocity = Mathf.Clamp(targetVelocity, PoweredSteering ? CruiseSpeed / 5 : (doReverse ? -MaxSpeed : 0), MaxSpeed); // maintain a bit of speed if using powered steering + } + } + SetStatus($"Engaging target"); + return; + } + + // follow + if (command == PilotCommands.Follow && SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + leftPath = true; + if (collisionDetectionTicker == 5) + checkBypass(commandLeader.vessel); + + Vector3 targetPosition = GetFormationPosition(); + Vector3 targetDistance = targetPosition - vesselTransform.position; + if (Vector3.Dot(targetDistance, vesselTransform.up) < 0 + && targetDistance.ProjectOnPlanePreNormalized(upDir).sqrMagnitude < 250f * 250f + && VectorUtils.Angle(vesselTransform.up, commandLeader.vessel.srf_velocity) < 0.8f) + { + targetDirection = Vector3.RotateTowards(commandLeader.vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), targetDistance, 0.2f, 0); + } + else + { + 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; + } + } + + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + // goto + if (command == PilotCommands.Waypoints) + { + Pathfind(VectorUtils.WorldPositionToGeoCoords(waypointPosition, vessel.mainBody)); + } + else if (leftPath && bypassTarget == null) + { + Pathfind(finalPositionGeo); + leftPath = false; + } + + const float targetRadius = 250f; + targetDirection = (assignedPositionWorld - vesselTransform.position).ProjectOnPlanePreNormalized(upDir); + + if (targetDirection.sqrMagnitude > targetRadius * targetRadius) + { + if (bypassTarget != null) + targetVelocity = MaxSpeed; + else if (pathingWaypoints.Count > 1) + targetVelocity = (command == PilotCommands.Attack || command == PilotCommands.Waypoints) ? MaxSpeed : CruiseSpeed; + else + targetVelocity = command == PilotCommands.Waypoints ? MaxSpeed : Mathf.Clamp((targetDirection.magnitude - targetRadius / 2) / 5f, + 0, command == PilotCommands.Attack ? MaxSpeed : CruiseSpeed); + //if targetDirection > VesselTurnRate reduce speed until vessel is slow enough to make turn ? + if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; + SetStatus(bypassTarget ? "Repositioning" : "Moving"); + if (IsRunningWaypoints) + { + if (BDArmorySettings.WAYPOINT_LOOP_INDEX > 1) + SetStatus($"Lap {activeWaypointLap}, Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + else + SetStatus($"Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + } + return; + } + + cycleWaypoint(); + } + + SetStatus($"Not doing anything in particular"); + targetDirection = vesselTransform.up; + } + + void Tactical() + { + var weaponManager = WeaponManager; + // enable RCS if we're in combat + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, weaponManager != null && ( + targetVessel && !BDArmorySettings.PEACE_MODE && ( + weaponManager.selectedWeapon != null || (vessel.CoM - targetVessel.CoM).sqrMagnitude < MaxEngagementRange * MaxEngagementRange + ) || weaponManager.underFire || weaponManager.missileIsIncoming)); + + // if weaponManager thinks we're under fire, do the evasive dance + if (SurfaceType != AIUtils.VehicleMovementType.Stationary && weaponManager != null && (weaponManager.underFire || weaponManager.missileIsIncoming)) + { + if (!maintainMinRange) targetVelocity = doReverse ? -MaxSpeed : MaxSpeed; + if (weaponManager.underFire || weaponManager.incomingMissileDistance < 2500) + { + if (Mathf.Abs(weaveAdjustment) + Time.deltaTime * WeaveFactor > weaveLimit * WeaveFactor) weaveDirection *= -1; + weaveAdjustment += WeaveFactor * weaveDirection * Time.deltaTime; + } + else + { + weaveAdjustment = 0; + } + } + else + { + weaveAdjustment = 0; + } + if ((BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) && weaponManager) DebugLine($"underFire {weaponManager.underFire}, weaveAdjustment {weaveAdjustment:G3}"); + } + + bool PanicModes() + { + var weaponManager = WeaponManager; + if (!vessel.LandedOrSplashed && (!isHovercraft || isHovercraft && vessel.radarAltitude > MaxSlopeAngle * 3)) //FIXME - unlink hoverAlt from maxSlope, else low hover alt may prevent navigating steeper terrain + { + targetVelocity = 0; + targetDirection = vessel.srf_velocity.ProjectOnPlanePreNormalized(upDir); + SetStatus("Airtime!"); + return true; + } + else if (vessel.Landed + && !vessel.Splashed // I'm looking at you, Kerbal Konstructs. (When launching directly into water, KK seems to set both vessel.Landed and vessel.Splashed to true.) + && (SurfaceType & AIUtils.VehicleMovementType.Land) == 0) + { + targetVelocity = 0; + SetStatus("Stranded"); + return true; + } + else if ( + (SurfaceType & AIUtils.VehicleMovementType.Land) != 0 && vessel.Landed //land Vee on land + && weaponManager != null && weaponManager.guardMode && targetVessel != null //and under AI control + && ( + currentStatusMode == StatusMode.RammingSpeed || !weaponManager.HasWeaponsAndAmmo() //and have been told to ram or doesn't have weapons + || !WeaponCanEngage(weaponManager.currentGun) //or have no guns, or only fixed guns/turrets unable to traverse to target, or out of range + ) + && (Mathf.Abs(targetVelocity) > 0 && vessel.horizontalSrfSpeed < 1 && vessel.angularVelocity.sqrMagnitude < 4) //and engaging but immobilized and can't rotate to bring guns to bear. TODO: angularVel threshold value? Or is 2 ?deg/s? sufficient cutoff? + ) + { + //not setting targetVel to 0, since a craft at rest will take a few moments to accel to > 1m/s + SetStatus("Disabled"); + return true; + } + else if (vessel.Splashed && !vessel.Landed && (SurfaceType & AIUtils.VehicleMovementType.Water) == 0) + { + targetVelocity = 0; + SetStatus("Floating"); + return true; + } + else if (vessel.IsUnderwater() && SurfaceType != AIUtils.VehicleMovementType.Submarine // Only surface vessels. + && !((SurfaceType & AIUtils.VehicleMovementType.Land) != 0 && vessel.Landed) // Unless they're driving on the bottom. FIXME Maybe add a hasPropulsion check to these? Note: this prevents panicking, but the pathing logic should try to get out of the water. + && !(SurfaceType == AIUtils.VehicleMovementType.Water && !(vessel.Landed || vessel.verticalSpeed < 1)) // Or boats actively regaining the surface. FIXME Should this be allowed? + ) + { + targetVelocity = 0; + SetStatus("Sunk"); + return true; + } + return false; + } + + bool WeaponCanEngage(ModuleWeapon weapon) + { + if (!weapon) return false; + if (weapon.turret) + { + return weapon.turret.TargetInRange(targetVessel.CoM - weapon.GetLeadOffset(), weapon.engageRangeMax); + } + else + { + Transform weaponTransform = weapon.fireTransforms[0]; + Vector3 vectorToTarget = (targetVessel.CoM - weapon.GetLeadOffset()) - weaponTransform.position; + bool withinView = Vector3.Dot(weaponTransform.forward, vectorToTarget) >= weapon.targetAdjustedMaxCosAngle; // Fixed weapon within firing angle + bool withinDistance = vectorToTarget.sqrMagnitude < weapon.engageRangeMax * weapon.engageRangeMax; + return withinView && withinDistance; + } + } + + + void AdjustThrottle(float targetSpeed) + { + targetSpeed = Mathf.Clamp(targetSpeed, doReverse ? -MaxSpeed : 0, MaxSpeed); + float velocitySignedSrfSpeed = VectorUtils.Angle(vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), vesselTransform.up) < 110 ? (float)vessel.srfSpeed : -(float)vessel.srfSpeed; + + if (float.IsNaN(targetSpeed)) //because yeah, I might have left division by zero in there somewhere + { + targetSpeed = CruiseSpeed; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine("Target velocity NaN, set to CruiseSpeed."); + } + else + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Target velocity: {targetSpeed}; signed Velocity: {velocitySignedSrfSpeed}; brakeVel: {targetSpeed * velocitySignedSrfSpeed}; use brakes: {(targetSpeed * velocitySignedSrfSpeed < -5)}"); + } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"engine thrust: {speedController.debugThrust}, motor zero: {motorControl.zeroPoint}"); + + speedController.targetSpeed = motorControl.targetSpeed = targetSpeed; + motorControl.signedSrfSpeed = velocitySignedSrfSpeed; + //speedController.useBrakes = motorControl.preventNegativeZeroPoint = speedController.debugThrust > 0; + speedController.useBrakes = targetSpeed * velocitySignedSrfSpeed < -5; + } + + Vector3 directionIntegral; + float pitchIntegral = 0; + + void AttitudeControl(FlightCtrlState s) + { + const float terrainOffset = 5; + + Vector3 yawTarget = targetDirection.ProjectOnPlanePreNormalized(vesselTransform.forward); + + // Invert the yawTarget only if we're deliberately reversing and the target direction is behind us. + // This puts the yawTarget ahead of us when we're deliberately reversing no matter the targetDirection. + if (doReverse && Vector3.Dot(yawTarget, vesselTransform.up) < 0) + yawTarget = -yawTarget; + + // limit "aoa" if we're moving + float driftMult = 1; + if (SurfaceType != AIUtils.VehicleMovementType.Stationary && vessel.horizontalSrfSpeed * 10 > CruiseSpeed) + { + Vector3d tempSrfVel = doReverse ? -vessel.srf_velocity : vessel.srf_velocity; + if (Vector3.Dot(tempSrfVel, yawTarget) < 0) tempSrfVel = -tempSrfVel; // Avoid wrenching sideways when switching from forward to reverse and vice-versa. + driftMult = Mathf.Max(VectorUtils.Angle(tempSrfVel, yawTarget) / MaxDrift, 1); + yawTarget = Vector3.RotateTowards(tempSrfVel, yawTarget, MaxDrift * Mathf.Deg2Rad, 0); + } + + float yawError = VectorUtils.GetAngleOnPlane(yawTarget, vesselTransform.up, vesselTransform.right); + + // Reverse the angle if we're going backwards to steer correctly. + bool invertCtrlPoint = SurfaceType != AIUtils.VehicleMovementType.Stationary && vessel.srfSpeed > 0.1f && Vector3.Dot(vessel.srf_vel_direction.ProjectOnPlanePreNormalized(vessel.up), vesselTransform.up) < 0; + if (invertCtrlPoint) yawError = -yawError; + + // If we don't have a fixed-yaw weapon, add in some weaving. + if ((aimingMode & AimingMode.Yaw) == 0) yawError += weaveAdjustment; + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + DebugLine($"yaw target: {yawTarget}, yaw error: {yawError:G3}, invertCtrlPoint: {invertCtrlPoint}"); + DebugLine($"drift multiplier: {driftMult}"); + } + + float pitchError; + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + if (SurfaceType == AIUtils.VehicleMovementType.Submarine) + { + float targetAlt = CombatAltitude; + var weaponManager = WeaponManager; + if (weaponManager != null && weaponManager.currentTarget != null && weaponManager.selectedWeapon != null) + { + switch (weaponManager.selectedWeapon.GetWeaponClass()) + { + case WeaponClasses.Missile: + { + targetAlt = -10; //come to periscope depth for missile launch + break; + } + case WeaponClasses.Gun: + { + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if (weaponManager.currentTarget.isSplashed || ((weaponManager.currentTarget.isFlying || weaponManager.currentTarget.Vessel.situation == Vessel.Situations.LANDED) && weaponManager.currentGun.turret)) + { + if (vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, weaponManager.selectedWeapon.GetEngageRange())) + targetAlt = -10; //come to periscope depth in preparation for surface attack when in range + else + targetAlt = 1;//in range, surface to engage with deck guns + } + } + else + { + if (weaponManager.currentTarget.Vessel.situation == Vessel.Situations.LANDED && weaponManager.currentGun.turret) //surface for shooting land targets with turrets + { + if (vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, weaponManager.selectedWeapon.GetEngageRange())) + targetAlt = -10; //come to periscope depth in preparation for surface attack when in range + else + targetAlt = 1;//in range, surface to engage with deck guns + } + if (!weaponManager.currentGun.turret && weaponManager.currentTarget.isSplashed) + { + if (!doExtend) + { + if (weaponManager.currentTarget.Vessel.altitude < CombatAltitude / 4 && vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, 200)) //200m + { + targetAlt = (float)weaponManager.currentTarget.Vessel.altitude; //engaging enemy sub or ship, but break off when too close to target or surface + } + else + doExtend = true; + } + else + { + if (vessel.altitude < (CombatAltitude * .66f) || vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, 1000)) doExtend = false; + } + } + //else remain at combat depth and engage with turrets. + } + break; + } + case WeaponClasses.Rocket: + case WeaponClasses.DefenseLaser: + { + if (weaponManager.currentTarget.Vessel.situation == Vessel.Situations.LANDED || weaponManager.currentTarget.isFlying && weaponManager.currentGun.turret) + { + if (vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, weaponManager.selectedWeapon.GetEngageRange())) + targetAlt = -10; //come to periscope depth in preparation for surface attack when in range + else + targetAlt = 1; //surface to engage with turrets + } + if (weaponManager.currentTarget.isSplashed) + { + if (!doExtend) + { + if (weaponManager.currentTarget.Vessel.altitude < CombatAltitude / 4 && vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, 200)) + { + targetAlt = (float)weaponManager.currentTarget.Vessel.altitude; //engaging enemy sub or ship, but break off when too close + } + else + doExtend = true; + } + else + { + if (vessel.altitude < (CombatAltitude * .66f) || vessel.CoM.FurtherFromThan(weaponManager.currentTarget.Vessel.CoM, 1000)) doExtend = false; + } + } + break; + } + default: //SLW + break; + } + //if (weaponManager.missileIsIncoming && !weaponManager.incomingMissileVessel.LandedOrSplashed && targetAlt > -10) targetAlt = -10; //this might make subs too hard to kill? + } + //look into some sort of crash dive routine if under fire from enemies dropping depthcharges/air-dropped torps? + float pitchAngle; + if ((float)vessel.altitude > targetAlt) pitchAngle = -MaxSlopeAngle * (1 - ((float)vessel.altitude / targetAlt)); //may result in not reaching target depth, depending on how neutrally buoyant the sub is. Clamp to maxSlopeAngle if Dist(vessel.altitude, targetAlt) > combatAlt * 0.25 or similar? + else pitchAngle = MaxSlopeAngle * (1 - (targetAlt / (float)vessel.altitude)); + float pitch = 90 - VectorUtils.Angle(vesselTransform.up, upDir); + + pitchError = pitchAngle - pitch; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Target Alt: {targetAlt.ToString("F3")}: PitchAngle: {pitchAngle.ToString("F3")}, Pitch: {pitch.ToString("F3")}, PitchError: {pitchError.ToString("F3")}"); + + directionIntegral = (directionIntegral + (pitchError * -vesselTransform.forward + yawError * vesselTransform.right) * Time.deltaTime).ProjectOnPlanePreNormalized(vesselTransform.up); + if (directionIntegral.sqrMagnitude > 1f) directionIntegral = directionIntegral.normalized; + pitchIntegral = 0.4f * Vector3.Dot(directionIntegral, -vesselTransform.forward); + } + else + { + pitchError = Vector3.SignedAngle(upDir.ProjectOnPlanePreNormalized(vesselTransform.right), -vesselTransform.forward, vesselTransform.right); + if (pitchError > 0) pitchError = Mathf.Max(pitchError - MaxSlopeAngle, 0); + else pitchError = Mathf.Min(pitchError + MaxSlopeAngle, 0); + if ((BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) && pitchError != 0) DebugLine($"pitch error: {pitchError}"); + } + } + else + { + // Stationary vessels should align with the terrain to avoid constantly running reaction wheels. + pitchError = VectorUtils.GetAngleOnPlane(terrainNormal, -vesselTransform.forward, -vesselTransform.up); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"pitch error: {pitchError}"); + } + float rollError; + 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.GetAngleOnPlane(vessel.GetSrfVelocity(), vesselTransform.up, vesselTransform.right); + float bank = -VectorUtils.GetAngleOnPlane(upDir, -vesselTransform.forward, 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 + { + // Stationary vessels should align with the terrain to avoid constantly running reaction wheels. + rollError = VectorUtils.GetAngleOnPlane(terrainNormal, -vesselTransform.forward, vesselTransform.right); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"roll error: {rollError}"); + } + + Vector3 localAngVel = vessel.angularVelocity; + SetFlightControlState(s, + Mathf.Clamp((((aimingMode & AimingMode.Pitch) > 0 ? 0.02f : 0.015f) * steerMult * pitchError) + pitchIntegral - (steerDamping * -localAngVel.x), -2, 2), // pitch + Mathf.Clamp(((((aimingMode & AimingMode.Yaw) > 0 ? 0.007f : 0.005f) * steerMult * yawError) - (steerDamping * 0.2f * -localAngVel.z)) * driftMult, -2, 2), // yaw + steerMult * 0.006f * rollError - 0.4f * steerDamping * -localAngVel.y, // roll + -Mathf.Clamp((((aimingMode & AimingMode.Yaw) > 0 ? 0.005f : 0.003f) * steerMult * yawError) - (steerDamping * 0.1f * -localAngVel.z), -2, 2) // 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 (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 || (SurfaceType & AIUtils.VehicleMovementType.Submarine) == 0)) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: land vehicle in water"); + } + else if (vessel.Landed && (SurfaceType & AIUtils.VehicleMovementType.Land) == 0) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: water vehicle on land"); + } + else if (!vessel.LandedOrSplashed && !isHovercraft) + { + 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) + // { + // if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: no engine power"); + // } + else + return true; + return false; + } + + public override bool IsValidFixedWeaponTarget(Vessel target) + => !BroadsideAttack && + (((target != null && target.Splashed) && (SurfaceType & AIUtils.VehicleMovementType.Water) != 0) //boat targeting boat + || ((target != null && target.Landed) && (SurfaceType & AIUtils.VehicleMovementType.Land) != 0) //vee targeting vee + || (((target != null && !target.LandedOrSplashed) && (SurfaceType & AIUtils.VehicleMovementType.Amphibious) != 0) && isHovercraft)) //repulsorcraft targeting repulsorcraft + ; //valid if can traverse the same medium and using bow fire + + /// null if no collision, dodge vector if one detected + Vector3 PredictCollisionWithVessel(Vessel v, float maxTime, float interval) + { + //evasive will handle avoiding missiles + var weaponManager = WeaponManager; + if ((weaponManager != null && v == weaponManager.incomingMissileVessel) + || v.rootPart.FindModuleImplementing() != null) + return Vector3.zero; + + float time = Mathf.Min(0.5f, maxTime); + while (time < maxTime) + { + Vector3 tPos = v.PredictPosition(time); + Vector3 myPos = vessel.PredictPosition(time); + float radii = v.GetRadius() + vessel.GetRadius(); + if ((tPos - myPos).sqrMagnitude < 2 * radii * radii) + { + return Vector3.Dot(tPos - myPos, vesselTransform.right) > 0 ? -vesselTransform.right : vesselTransform.right; + } + + time = Mathf.MoveTowards(time, maxTime, interval); + } + + return Vector3.zero; + } + + void checkBypass(Vessel target) + { + if (!pathingMatrix.TraversableStraightLine( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), + vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass)) + { + bypassTarget = target; + bypassTargetPos = VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody); + pathingWaypoints = pathingMatrix.Pathfind( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), + vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass); + 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; + } + } + + private void Pathfind(Vector3 destination) + { + pathingWaypoints = pathingMatrix.Pathfind( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + destination, vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass); + intermediatePositionGeo = pathingWaypoints[0]; + //any sort of spline stuff would need to modify this value. + //Spline calc would likely also need to determine if the first couple pathingWaypoints are multiple points beween the craft and + //the next WP Gate (sloping/uneven terrain), or a straight shot between WP Gates. if (pathingWaypoints.count > 1)? + } + + void cycleWaypoint() + { + if (pathingWaypoints.Count > 1) + { + pathingWaypoints.RemoveAt(0); + intermediatePositionGeo = pathingWaypoints[0]; + } + else if (bypassTarget != null) + { + pathingWaypoints.Clear(); + bypassTarget = null; + leftPath = true; + } + } + + #endregion Autopilot helper functions + + #region WingCommander + + Vector3 GetFormationPosition() + { + return commandLeader.vessel.CoM + Quaternion.LookRotation(commandLeader.vessel.up, upDir) * this.GetLocalFormationPosition(commandFollowIndex); + } + + #endregion WingCommander + } +} \ No newline at end of file diff --git a/BDArmory/Control/BDModuleVTOLAI.cs b/BDArmory/Control/BDModuleVTOLAI.cs new file mode 100644 index 000000000..1c4e12076 --- /dev/null +++ b/BDArmory/Control/BDModuleVTOLAI.cs @@ -0,0 +1,931 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +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 + { + public override AIType aiType => AIType.VTOLAI; + #region Declarations + Vessel extendingTarget = null; + public bool orderedToExtend = false; + Vessel bypassTarget = null; + Vector3 bypassTargetPos; + + Vector3 targetDirection; + float targetVelocity; // the forward/reverse velocity the craft should target, not the velocity of its target + float targetLatVelocity; // the left/right velocity the craft should target, not the velocity of its target + float targetAltitude; // the altitude the craft should hold, not the altitude of its target + Vector3 rollTarget; + enum AimingMode { Off = 0, Yaw = 1, Pitch = 2, Direct = Yaw | Pitch } + AimingMode aimingMode = AimingMode.Off; + + int collisionDetectionTicker = 0; + Vector3? dodgeVector; + float weaveAdjustment = 0; + float weaveDirection = 1; + const float weaveLimit = 2.3f; + + Vector3 upDir; + + AIUtils.TraversabilityMatrix pathingMatrix; + List pathingWaypoints = new List(); + bool leftPath = false; + + bool doReverse = false; + + protected override Vector3d assignedPositionGeo + { + get { return intermediatePositionGeo; } + set + { + finalPositionGeo = value; + leftPath = true; + } + } + + Vector3d finalPositionGeo; + Vector3d intermediatePositionGeo; + public override Vector3d commandGPS => finalPositionGeo; + + private BDVTOLSpeedControl altitudeControl; // Throttle is used to control altitude in most quadcopter control systems (position error feeds pitch/roll control), this works decently well for helicopters in BDA + + // Terrain avoidance and below minimum altitude globals. + bool belowMinAltitude; // True when below minAltitude or avoiding terrain. + bool avoidingTerrain = false; // True when avoiding terrain. + bool initialTakeOff = true; // False after the initial take-off + Vector3 terrainAlertNormal; // Approximate surface normal at the terrain intercept. + + // values currently hard-coded since VTOL AI is adapted from surface AI, but should be removed/changed as AI behavior is improved + public string SurfaceTypeName = "Amphibious"; // hard code this for the moment until we have something better + public bool PoweredSteering = true; + public float MaxDrift = 180; + public float AvoidMass = 0f; + + public AIUtils.VehicleMovementType SurfaceType + => (AIUtils.VehicleMovementType)Enum.Parse(typeof(AIUtils.VehicleMovementType), SurfaceTypeName); + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerPower"),//Steer Factor + UI_FloatRange(minValue = 0.2f, maxValue = 20f, stepIncrement = .1f, scene = UI_Scene.All)] + public float steerMult = 6; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerKi"), //Steer Ki + UI_FloatRange(minValue = 0.01f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float steerKiAdjust = 0.4f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDamping"),//Steer Damping + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] + public float steerDamping = 3; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_DefaultAltitude"), //Default Alt. + UI_FloatRange(minValue = 25f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] + public float defaultAltitude = 300; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CombatAltitude"), //Combat Alt. + UI_FloatRange(minValue = 25f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] + public float CombatAltitude = 150; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BombingAltitude"), //bombing Altitude + UI_FloatRange(minValue = 100f, maxValue = 2000, stepIncrement = 10f, scene = UI_Scene.All)] + public float bombingAltitude = 300; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinAltitude"), //Min Altitude + UI_FloatRange(minValue = 10f, maxValue = 1000, stepIncrement = 10f, scene = UI_Scene.All)] + public float minAltitude = 100f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxSpeed"),//Max speed + UI_FloatRange(minValue = 5f, maxValue = 200f, stepIncrement = 1f, scene = UI_Scene.All)] + public float MaxSpeed = 80; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_CombatSpeed"),//Combat speed + UI_FloatRange(minValue = 5f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] + public float CombatSpeed = 40; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxPitchAngle"),//Max pitch angle + UI_FloatRange(minValue = 1f, maxValue = 90f, stepIncrement = 1f, scene = UI_Scene.All)] + public float MaxPitchAngle = 30f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxBankAngle"),// Max Bank angle + UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 1f, scene = UI_Scene.All)] + public float MaxBankAngle = 30; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_WeaveFactor"),//Weave Factor + UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float WeaveFactor = 6.5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinEngagementRange"),//Min engagement range + UI_FloatRange(minValue = 0f, maxValue = 6000f, stepIncrement = 100f, scene = UI_Scene.All)] + public float MinEngagementRange = 500; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxEngagementRange"),//Max engagement range + UI_FloatRange(minValue = 500f, maxValue = 8000f, stepIncrement = 100f, scene = UI_Scene.All)] + public float MaxEngagementRange = 4000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaintainEngagementRange"),//Maintain min Range +UI_Toggle(enabledText = "#LOC_BDArmory_true", disabledText = "#LOC_BDArmory_false")]//true; false + public bool maintainMinRange = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_BroadsideAttack"),//Attack vector + UI_Toggle(enabledText = "#LOC_BDArmory_AI_BroadsideAttack_enabledText", disabledText = "#LOC_BDArmory_AI_BroadsideAttack_disabledText")]//Broadside--Bow + public bool BroadsideAttack = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_ManeuverRCS"),//RCS active + UI_Toggle(enabledText = "#LOC_BDArmory_AI_ManeuverRCS_enabledText", disabledText = "#LOC_BDArmory_AI_ManeuverRCS_disabledText", scene = UI_Scene.All),]//Maneuvers--Combat + public bool ManeuverRCS = false; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_PreferredBroadsideDirection", advancedTweakable = true),//Preferred broadside direction + UI_ChooseOption(options = new string[3] { "Port", "Either", "Starboard" }, scene = UI_Scene.All),] + public string OrbitDirectionName = "Either"; + public readonly string[] orbitDirections = new string[3] { "Port", "Either", "Starboard" }; + + [KSPField(isPersistant = true)] + int sideSlipDirection = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_GoesUp", advancedTweakable = true),//Goes up to + UI_Toggle(enabledText = "#LOC_BDArmory_AI_GoesUp_enabledText", disabledText = "#LOC_BDArmory_AI_GoesUp_disabledText", scene = UI_Scene.All),]//eleven--ten + bool upToEleven = false; + public bool UpToEleven { get { return upToEleven; } set { if (upToEleven != value) { upToEleven = value; TurnItUpToEleven(); } } } + + const float AttackAngleAtMaxRange = 30f; + + Dictionary altMaxValues = new Dictionary + { + { nameof(defaultAltitude), 100000f }, + { nameof(CombatAltitude), 100000f }, + { nameof(minAltitude), 100000f }, + { nameof(steerMult), 200f }, + { nameof(steerKiAdjust), 20f }, + { nameof(steerDamping), 100f }, + { nameof(MaxPitchAngle), 90f }, + { nameof(CombatSpeed), 300f }, + { nameof(MaxSpeed), 400f }, + { nameof(MinEngagementRange), 20000f }, + { nameof(MaxEngagementRange), 30000f }, + }; + + #endregion Declarations + + #region RMB info in editor + + // Yes + public override string GetInfo() + { + // known bug - the game caches the RMB info, changing the variable after checking the info + // does not update the info. :( No idea how to force an update. + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Available settings:"); + sb.AppendLine($"- Steer Factor - higher will make the AI apply more control input for the same desired rotation"); + sb.AppendLine($"- Steer Ki - higher will make the AI apply control trim faster"); + sb.AppendLine($"- Steer Damping - higher will make the AI apply more control input when it wants to stop rotation"); + sb.AppendLine($"- Default Alt. - AI will fly at this altitude outside of combat"); + sb.AppendLine($"- Combat Altitude - AI will fly at this altitude during combat"); + sb.AppendLine($"- Min Altitude - below this altitude AI will prioritize gaining altitude over combat"); + sb.AppendLine($"- Max Speed - the maximum combat speed"); + sb.AppendLine($"- Combat Speed - the default speed at which it is safe to maneuver"); + sb.AppendLine($"- Max pitch angle - the limit on pitch when moving"); + sb.AppendLine($"- Bank angle - the limit on roll when turning, positive rolls into turns"); + sb.AppendLine($"- Attack vector - does the vessel attack from the front or the sides"); + sb.AppendLine($"- Min engagement range - AI will try to move away from oponents if closer than this range"); + sb.AppendLine($"- Max engagement range - AI will prioritize getting closer over attacking when beyond this range"); + sb.AppendLine($"- RCS active - Use RCS during any maneuvers, or only in combat "); + if (GameSettings.ADVANCED_TWEAKABLES) + { + sb.AppendLine($"- Goes up to - Increases variable limits, no direct effect on behaviour"); + } + + return sb.ToString(); + } + + #endregion RMB info in editor + + #region events + + public override void OnStart(StartState state) + { + base.OnStart(state); + if (!(HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor)) return; + + SetChooseOptions(); + SetOnUpToElevenChanged(); + } + + public override bool ActivatePilot() + { + if (!base.ActivatePilot()) return false; + //originalMaxSpeed = MaxSpeed; + pathingMatrix = new AIUtils.TraversabilityMatrix(); + + if (!altitudeControl) + { + altitudeControl = gameObject.AddComponent(); + altitudeControl.vessel = vessel; + } + altitudeControl.Activate(); + + if (initialTakeOff && !vessel.LandedOrSplashed) // In case we activate pilot after taking off manually. + { + initialTakeOff = false; + TakingOff = false; + } + + if (BroadsideAttack && sideSlipDirection == 0) + { + SetBroadsideDirection(OrbitDirectionName); + } + + leftPath = true; + extendingTarget = null; + bypassTarget = null; + collisionDetectionTicker = 6; + return true; + } + + public override void DeactivatePilot() + { + base.DeactivatePilot(); + + if (altitudeControl) + altitudeControl.Deactivate(); + } + + public void SetChooseOptions() + { + UI_ChooseOption broadside = (UI_ChooseOption)(HighLogic.LoadedSceneIsFlight ? Fields["OrbitDirectionName"].uiControlFlight : Fields["OrbitDirectionName"].uiControlEditor); + broadside.onFieldChanged = ChooseOptionsUpdated; + // UI_ChooseOption surface = (UI_ChooseOption)(HighLogic.LoadedSceneIsFlight ? Fields["SurfaceTypeName"].uiControlFlight : Fields["SurfaceTypeName"].uiControlEditor); + // surface.onFieldChanged = ChooseOptionsUpdated; + } + + public void ChooseOptionsUpdated(BaseField field, object obj) + { + this.part.RefreshAssociatedWindows(); + if (BDArmoryAIGUI.Instance != null) + { + BDArmoryAIGUI.Instance.SetChooseOptionSliders(); + } + } + + public void SetBroadsideDirection(string direction) + { + if (!orbitDirections.Contains(direction)) return; + OrbitDirectionName = direction; + sideSlipDirection = orbitDirections.IndexOf(OrbitDirectionName) - 1; + if (sideSlipDirection == 0) + sideSlipDirection = UnityEngine.Random.value > 0.5f ? 1 : -1; + } + + void SetOnUpToElevenChanged() + { + var field = (UI_Toggle)(HighLogic.LoadedSceneIsFlight ? Fields["upToEleven"].uiControlFlight : Fields["upToEleven"].uiControlEditor); + field.onFieldChanged = TurnItUpToEleven; // Only triggered on UI interaction. + if (upToEleven) TurnItUpToEleven(); // The initially loaded values are not the alternate ones. + } + + void TurnItUpToEleven(BaseField _field = null, object _obj = null) + { + 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); + (altMaxValues[s.Current], euic.maxValue) = (euic.maxValue, altMaxValues[s.Current]); + StartCoroutine(SetVar(s.Current, (float)typeof(BDModuleVTOLAI).GetField(s.Current).GetValue(this))); // change the value back to what it is now after fixed update, because changing the max value will clamp it down + } + } + + IEnumerator SetVar(string name, float value) + { + yield return new WaitForFixedUpdate(); + typeof(BDModuleVTOLAI).GetField(name).SetValue(this, value); + } + + protected override void OnGUI() + { + base.OnGUI(); + + if (!pilotEnabled || !vessel.isActiveVessel) return; + + if (!BDArmorySettings.DEBUG_LINES) return; + if (command == PilotCommands.Follow) + { + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, assignedPositionWorld, 2, Color.red); + } + + //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 + 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 + + + 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); + + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + rollTarget, 2, Color.blue); + + pathingMatrix.DrawDebug(vessel.CoM, pathingWaypoints); + } + + #endregion events + + #region Actual AI Pilot + + protected override void AutoPilot(FlightCtrlState s) + { + if (!vessel.Autopilot.Enabled) + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); + + targetVelocity = 0; + targetLatVelocity = 0; + targetDirection = vesselTransform.up; + targetAltitude = defaultAltitude; + aimingMode = AimingMode.Off; + upDir = vessel.up; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(""); + if (IsRunningWaypoints) UpdateWaypoint(); // Update the waypoint state. + if (initialTakeOff) + { + Takeoff(); + } + // pilot logic figures out what we're supposed to be doing, and sets the base state + PilotLogic(); // TODO: pitch based on targetVelocity, roll always 0 + // situational awareness modifies the base as best as it can (evasive mainly) + Tactical(); + CheckLandingGear(); + //CommandAttitude(); // Determine pitch/roll/yaw for movement + AttitudeControl(s); // move according to commanded movement + AdjustThrottle(targetVelocity); // set throttle according to our targets and movement + } + + void PilotLogic() // Surface AI-based with byass target disabled + { + // check for belowMinAlt + belowMinAltitude = (float)vessel.radarAltitude < minAltitude; + + // VTOL ram capability? have it match alt with target flier and engines ahead full? + // ramming ground targets would be iffy, though. + + // 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 = vs.Current.ActiveController().AI; + 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; + } + + // 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 + var weaponManager = WeaponManager; + if (weaponManager && targetVessel != null && !BDArmorySettings.PEACE_MODE) + { + leftPath = true; + + 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; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Broadside attack angle {sidestep}"); + } + else // just point at target and go + { + targetAltitude = CombatAltitude; + if (!maintainMinRange && (((targetVessel.horizontalSrfSpeed < 10) || Vector3.Dot(targetVessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), vessel.up) < 0 || orderedToExtend) //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 + if (distance > Mathf.Max(MaxEngagementRange / 2, 2000)) orderedToExtend = false; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Extending: ({distance:F2}/{Mathf.Max(MaxEngagementRange / 2, 2000)})"); + targetDirection = -vecToTarget + vessel.srf_vel_direction; //extend perpendicular to target to maintain some forward vel + targetVelocity = MaxSpeed; + targetAltitude = CombatAltitude; + SetStatus($"Extending"); + return; + } + else + { + extendingTarget = null; + 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 * 1.25f) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Adjusting Range and speed"); + if (distance >= MaxEngagementRange) + targetVelocity = MaxSpeed;//out of engagement range, engines ahead full + if (distance <= MinEngagementRange * 1.25f) //coming within minEngagement range + { + if (maintainMinRange) //for some reason ignored if both vessel and targetvessel using Mk2roverCans? + { + if (targetVessel.srfSpeed < 10) + { + targetVelocity = 0; + SetStatus($"Braking"); + } + if (distance <= MinEngagementRange) //rolled to a stop inside minRange/target has encroached + { + doReverse = true; + targetVelocity = -MaxSpeed; + SetStatus($"Reversing"); + return; + } + return; + } + else + { + targetVelocity = MaxSpeed; + if (weaponManager != null && weaponManager.selectedWeapon != null && ( + weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb || + weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.SLW + )) + orderedToExtend = true; + } + } + } + else + { + targetVelocity = !maintainMinRange ? MaxSpeed : 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.Missile: + MissileBase missile = weaponManager.CurrentMissile; + orderedToExtend = false; + if (missile != null) + { + if (missile.TargetingMode == MissileBase.TargetingModes.Heat && !weaponManager.heatTarget.exists) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Attempting heat lock"); + aimingMode = AimingMode.Direct; + targetDirection = MissileGuidance.GetAirToAirFireSolution(missile, targetVessel) - vessel.CoM; + } + else + { + if (!weaponManager.GetLaunchAuthorization(targetVessel, weaponManager, missile) && (Vector3.SqrMagnitude(targetVessel.vesselTransform.position - vesselTransform.position) < (missile.engageRangeMax * missile.engageRangeMax))) + { + aimingMode = AimingMode.Direct; + targetDirection = MissileGuidance.GetAirToAirFireSolution(missile, targetVessel) - vessel.CoM; + } + } + } + break; + case WeaponClasses.Bomb: + { + MissileBase bomb = weaponManager.CurrentMissile; + targetAltitude = bombingAltitude; + + targetDirection = (AIUtils.PredictPosition(targetVessel, weaponManager.bombAirTime) - vessel.CoM).ProjectOnPlanePreNormalized(upDir); + aimingMode = AimingMode.Yaw; + } + break; + case WeaponClasses.SLW: + { + MissileBase torpedo = weaponManager.CurrentMissile; + if (torpedo != null) + { + targetAltitude = CombatAltitude < 100 ? CombatAltitude : 100; //100 vs 200 since helis are going to be going much slower, don't want torps bellyflopping; if we ever do sonobuoys or similar this should probably really be something like 15m, not 100 + if (distance < torpedo.engageRangeMax + (float)(vessel.srf_velocity - targetVessel.srf_velocity).magnitude) + { + if (weaponManager.firedMissiles < weaponManager.maxMissilesOnTarget) + targetVelocity = CombatSpeed; //slow to drop speed + + aimingMode = AimingMode.Yaw; + targetDirection = (MissileGuidance.GetAirToAirFireSolution(torpedo, targetVessel) - vessel.CoM).ProjectOnPlanePreNormalized(upDir); + } + if (weaponManager.firedMissiles >= weaponManager.maxMissilesOnTarget) + { + targetAltitude = bombingAltitude; + targetVelocity = MaxSpeed; //torps away, get out of there + orderedToExtend = true; + } + } + } + break; + case WeaponClasses.Gun: + case WeaponClasses.Rocket: + case WeaponClasses.DefenseLaser: + var gun = (ModuleWeapon)weaponManager.selectedWeapon; + orderedToExtend = false; + if (gun != null && (gun.yawRange == 0 || gun.maxPitch == gun.minPitch) && gun.FiringSolutionVector != null) + { + if (gun.yawRange == 0) aimingMode |= AimingMode.Yaw; + if (gun.maxPitch == gun.minPitch) aimingMode |= AimingMode.Pitch; + if (VectorUtils.Angle(vesselTransform.up, ((Vector3)gun.FiringSolutionVector).ProjectOnPlanePreNormalized(vesselTransform.right)) < MaxPitchAngle) + targetDirection = (Vector3)gun.FiringSolutionVector; + } + break; + } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Target combat alt: {targetAltitude}"); + if ((aimingMode & AimingMode.Yaw) > 0) + { + targetLatVelocity = Vector3.Dot(vessel.Velocity(), vesselTransform.right.ProjectOnPlanePreNormalized(upDir).normalized) * 4; //Zero out sideslip if aiming so torps/bombs don't go sideways + //Investigate using sidestrafe capability to adjust aim if target moving perpendicularly? + } + } + } + targetVelocity = Mathf.Clamp(targetVelocity, PoweredSteering ? CombatSpeed / 5 : (doReverse ? -MaxSpeed : 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 + && targetDistance.ProjectOnPlanePreNormalized(upDir).sqrMagnitude < 250f * 250f + && VectorUtils.Angle(vesselTransform.up, commandLeader.vessel.srf_velocity) < 0.8f) + { + targetDirection = Vector3.RotateTowards(commandLeader.vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), targetDistance, 0.2f, 0); + } + else + { + 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 (command == PilotCommands.Waypoints) + { + Pathfind(VectorUtils.WorldPositionToGeoCoords(waypointPosition, vessel.mainBody)); + } + else if (leftPath) + { + Pathfind(finalPositionGeo); //is surface navigation pathing nodes really necessary for an aircraft? + leftPath = false; + } + + const float targetRadius = 250f; + targetDirection = (assignedPositionWorld - vesselTransform.position).ProjectOnPlanePreNormalized(upDir); + + if (targetDirection.sqrMagnitude > targetRadius * targetRadius) + { + targetVelocity = MaxSpeed; + + if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; + SetStatus("Moving"); + if (IsRunningWaypoints) + { + if (BDArmorySettings.WAYPOINT_LOOP_INDEX > 1) + SetStatus($"Lap {activeWaypointLap}, Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + else + SetStatus($"Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + } + return; + } + + cycleWaypoint(); + + SetStatus($"Not doing anything in particular"); + targetDirection = vesselTransform.up; + } + + void Tactical() + { + var weaponManager = WeaponManager; + // enable RCS if we're in combat + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, weaponManager && targetVessel && !BDArmorySettings.PEACE_MODE + && (weaponManager.selectedWeapon != null || (vessel.CoM - targetVessel.CoM).sqrMagnitude < MaxEngagementRange * MaxEngagementRange) + || weaponManager.underFire || weaponManager.missileIsIncoming); + + // if weaponManager thinks we're under fire, do the evasive dance + if (weaponManager.underFire || weaponManager.missileIsIncoming) + { + targetVelocity = MaxSpeed; + if (weaponManager.underFire || weaponManager.incomingMissileDistance < 2500) + { + if (Mathf.Abs(weaveAdjustment) + Time.deltaTime * WeaveFactor > weaveLimit * WeaveFactor) weaveDirection *= -1; + weaveAdjustment += WeaveFactor * weaveDirection * Time.deltaTime; + targetLatVelocity = weaveAdjustment * MaxBankAngle; + } + else + { + weaveAdjustment = 0; + } + } + else + { + weaveAdjustment = 0; + } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"underFire {weaponManager.underFire}, aimingMode {aimingMode}, weaveAdjustment {weaveAdjustment}"); + } + + void AdjustThrottle(float targetSpeed) + { + altitudeControl.targetAltitude = targetAltitude; + + targetVelocity = Mathf.Clamp(targetVelocity, doReverse ? -MaxSpeed : 0, MaxSpeed); + targetSpeed = Mathf.Clamp(targetSpeed, doReverse ? -MaxSpeed : 0, MaxSpeed); + float velocitySignedSrfSpeed = VectorUtils.Angle(vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), vesselTransform.up) < 110 ? (float)vessel.srfSpeed : -(float)vessel.srfSpeed; + + if (float.IsNaN(targetSpeed)) //because yeah, I might have left division by zero in there somewhere + { + targetSpeed = CombatSpeed; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine("Target velocity NaN, set to CruiseSpeed."); + } + else + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Target velocity: {targetSpeed}; signed Velocity: {velocitySignedSrfSpeed}; brakeVel: {targetSpeed * velocitySignedSrfSpeed}; use brakes: {(targetSpeed * velocitySignedSrfSpeed < -5)}"); + } + + altitudeControl.targetSpeed = targetSpeed; + } + + //Controller Integral + Vector3 directionIntegral; + float pitchIntegral; + float yawIntegral; + float rollIntegral; + + void AttitudeControl(FlightCtrlState s) + { + Vector3 yawTarget = targetDirection.ProjectOnPlanePreNormalized(vesselTransform.forward); + + float yawError = VectorUtils.GetAngleOnPlane(yawTarget, vesselTransform.up, vesselTransform.right) + ((aimingMode & AimingMode.Yaw) > 0 ? 0 : weaveAdjustment); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"yaw target: {yawTarget}, yaw error: {yawError}"); + + 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 & AimingMode.Pitch) > 0) + pitchAngle = -VectorUtils.GetAngleOnPlane(targetDirection, vesselTransform.up, vesselTransform.forward); + else if (belowMinAltitude || targetVelocity == 0f) + pitchAngle = 0f; + else if (avoidingTerrain) + pitchAngle = 90 - VectorUtils.Angle(targetDirection.ProjectOnPlanePreNormalized(vesselTransform.right), upDir); + + float pitch = 90 - VectorUtils.Angle(vesselTransform.up, upDir); + + float pitchError = pitchAngle - pitch; + 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.GetAngleOnPlane(upDir, -vesselTransform.forward, vesselTransform.right); + 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.33f * -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.GetAngleOnPlane(rollTarget, upDir, vesselTransform.right); + } + else + rollTarget = Vector3.RotateTowards(upDir, -vesselTransform.right, targetRoll * Mathf.PI / 180f, 0f); + + float rollError = targetRoll - bank; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"target lat vel: {targetLatVelocity}, lat vel accel: {latAccel}; lateral vel: {latVel}, lat vel error: {latError}, target roll: {targetRoll}, bank: {bank}, roll error: {rollError}"); + + Vector3 localAngVel = vessel.angularVelocity; + #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 + float pitchProportional = 0.015f * steerMult * pitchError; + float yawProportional = 0.005f * steerMult * yawError; + float rollProportional = 0.015f * steerMult * rollError; + + float pitchDamping = steerDamping * -localAngVel.x; + float yawDamping = 0.33f * steerDamping * -localAngVel.z; + float rollDamping = 0.66f * 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 = (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.66f * steerKiAdjust * Mathf.Clamp(rollIntegral + rollError * Time.deltaTime, -1f, 1f); + + 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)) + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + } + + #endregion Actual AI Pilot + + #region Autopilot helper functions + + public override bool CanEngage() + { + return !vessel.LandedOrSplashed; + } + + GameObject vobj; + + Transform velocityTransform + { + get + { + if (!vobj) + { + vobj = new GameObject("velObject"); + vobj.transform.position = vessel.ReferenceTransform.position; + vobj.transform.parent = vessel.ReferenceTransform; + } + + return vobj.transform; + } + } + + void CheckLandingGear() + { + if (!vessel.LandedOrSplashed) + { + if (vessel.radarAltitude > Mathf.Min(50f, minAltitude / 2f)) + vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, false); + else + vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, true); + } + } + + void Takeoff() + { + belowMinAltitude = (float)vessel.radarAltitude < minAltitude; + if (vessel.Landed && (float)vessel.radarAltitude > 1) + { + vessel.Landed = false; // KSP sometimes isn't updating this correctly after spawning. + vessel.Splashed = vessel.altitude < 0; // Radar altitude could be > 1, while the craft is still underwater due to the way radarAlt works... + } + if (!belowMinAltitude) + { + initialTakeOff = false; + TakingOff = false; + } + } + + public override bool IsValidFixedWeaponTarget(Vessel target) + { + if (!vessel) return false; + + return true; + } + + Vector3? PredictCollisionWithVessel(Vessel v, float maxTime, float interval) + { + var weaponManager = WeaponManager; + //evasive will handle avoiding missiles + if (weaponManager && v == weaponManager.incomingMissileVessel + || v.rootPart.FindModuleImplementing() != null) + return null; + + float time = Mathf.Min(0.5f, maxTime); + while (time < maxTime) + { + Vector3 tPos = v.PredictPosition(time); + Vector3 myPos = vessel.PredictPosition(time); + if (Vector3.SqrMagnitude(tPos - myPos) < 2500f) + { + return Vector3.Dot(tPos - myPos, vesselTransform.right) > 0 ? -vesselTransform.right : vesselTransform.right; + } + + time = Mathf.MoveTowards(time, maxTime, interval); + } + + return null; + } + + void checkBypass(Vessel target) + { + if (!pathingMatrix.TraversableStraightLine( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), + vessel.mainBody, SurfaceType, MaxPitchAngle, AvoidMass)) + { + bypassTarget = target; + bypassTargetPos = VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody); + pathingWaypoints = pathingMatrix.Pathfind( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), + vessel.mainBody, SurfaceType, MaxPitchAngle, AvoidMass); + 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; + } + } + + private void Pathfind(Vector3 destination) + { + pathingWaypoints = pathingMatrix.Pathfind( + VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), + destination, vessel.mainBody, SurfaceType, MaxPitchAngle, AvoidMass); + intermediatePositionGeo = pathingWaypoints[0]; + } + + void cycleWaypoint() + { + if (pathingWaypoints.Count > 1) + { + pathingWaypoints.RemoveAt(0); + intermediatePositionGeo = pathingWaypoints[0]; + } + else if (bypassTarget != null) + { + pathingWaypoints.Clear(); + bypassTarget = null; + leftPath = true; + } + } + + #endregion Autopilot helper functions + + #region WingCommander + + Vector3 GetFormationPosition() + { + return commandLeader.vessel.CoM + Quaternion.LookRotation(commandLeader.vessel.up, upDir) * this.GetLocalFormationPosition(commandFollowIndex); + } + + #endregion WingCommander + } +} diff --git a/BDArmory/Control/IBDAIControl.cs b/BDArmory/Control/IBDAIControl.cs index 3701c0af8..c4e07ea00 100644 --- a/BDArmory/Control/IBDAIControl.cs +++ b/BDArmory/Control/IBDAIControl.cs @@ -1,23 +1,26 @@ -using BDArmory.Modules; -using UnityEngine; +using UnityEngine; namespace BDArmory.Control { + public enum AIType { None, GenericAI, PilotAI, SurfaceAI, VTOLAI, OrbitalAI }; public interface IBDAIControl { #region PartModule Vessel vessel { get; } Transform transform { get; } + Part part { get; } #endregion PartModule + AIType aiType { get; } + /// /// The weapon manager the AI connects to. /// - MissileFire weaponManager { get; } + MissileFire WeaponManager { get; } - void ActivatePilot(); + bool ActivatePilot(); void DeactivatePilot(); @@ -46,7 +49,7 @@ public interface IBDAIControl string currentStatus { get; } - void ReleaseCommand(); + void ReleaseCommand(bool resetAssignedPosition = true, bool storeCommand = true); void CommandFollow(ModuleWingCommander leader, int followerIndex); @@ -58,12 +61,15 @@ public interface IBDAIControl void CommandTakeOff(); + void CommandFollowWaypoints(); + Vector3d commandGPS { get; } PilotCommands currentCommand { get; } ModuleWingCommander commandLeader { get; } + int commandFollowIndex { get; } #endregion WingCommander } - public enum PilotCommands { Free, Attack, Follow, FlyTo } + public enum PilotCommands { Free, Attack, Follow, FlyTo, Waypoints } } diff --git a/BDArmory/Control/MissileFire.cs b/BDArmory/Control/MissileFire.cs new file mode 100644 index 000000000..507de3f7e --- /dev/null +++ b/BDArmory/Control/MissileFire.cs @@ -0,0 +1,10671 @@ +using BDArmory.Bullets; +using BDArmory.Competition; +using BDArmory.CounterMeasure; +using BDArmory.Extensions; +using BDArmory.GameModes; +using BDArmory.Guidances; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using static BDArmory.Weapons.Missiles.MissileBase; +using static BDArmory.Weapons.ModuleWeapon; + +namespace BDArmory.Control +{ + public class MissileFire : PartModule + { + #region Declarations + + [KSPField(guiName = "#LOC_BDArmory_WM_IsPrimaryWM", guiActive = true), UI_Label(scene = UI_Scene.All)] + bool _isPrimaryWM = true; + public bool IsPrimaryWM // Is this the WM that's controlling this vessel? Gets set by ActiveControllerVesselModule. + { + get { return _isPrimaryWM; } + set + { + if (_isPrimaryWM != (_isPrimaryWM = value)) // If the primary WM changed, immediately update our lists. + { + RefreshModules(); // Refresh all our module lists. + UpdateList(); // Update the weapons lists. + RefreshCMPriorities(); // Refresh the CM priorities. + if (_isPrimaryWM) SetParentWM(null); + } + } + } + public MissileFire ParentWM + { + get { return _parentWM; } + set { StartCoroutine(SetParentWM(value)); } + } // Keep a link to the parent WM so detached vessels can copy their state when they detach. + MissileFire _parentWM; + IEnumerator SetParentWM(MissileFire wm) // Set the parent WM at the end of the frame so that the previous value remains valid until the end of the frame. + { + yield return new WaitForEndOfFrame(); _parentWM = wm == this ? null : wm; + } + public string SourceVesselURL { get; set; } = null; // The craft file that this WM originated from. Set by ActiveController and used for tracking fighters. + + //weapons + private List weaponTypes = []; + private Dictionary> weaponRanges = []; + private Dictionary> weaponBoresights = []; + public IBDWeapon[] weaponArray; + + private List pointDefenseWeapons = []; + public ModuleWeapon[] pointDefenseWeaponArray; + private List pointDefenseMissiles = []; + public MissileBase[] pointDefenseMissileArray; + string[] pointDefenseIRMissileSkipArr = []; + int pointDefenseIRMissileCount = -1; + float pointDefenseMissileMaxARH = -1f; + float pointDefenseMissileMaxRange = -1f; + bool pointDefenseMissileHasInertial = false; + bool pointDefenseMissileHasLaser = false; + bool pointDefenseMissileHasRadar = false; + float maxTargetingLaserRange = -1; + + // extension for feature_engagementenvelope: specific lists by weapon engagement type + private List weaponTypesAir = []; + private List weaponTypesMissile = []; + private List weaponTypesGround = []; + private List weaponTypesSLW = []; + + [KSPField(guiActiveEditor = false, isPersistant = true, guiActive = false)] public int weaponIndex; + + //ScreenMessage armedMessage; + ScreenMessage selectionMessage; + string selectionText = ""; + + float startTime; + public int firedMissiles; + public Dictionary missilesAway; + public Dictionary queuedLaunches; + float queuedLaunchesTimeSinceLastAddition; + bool queuedLaunchesRequireClearing = false; + + public float totalHP; + public float currentHP; + + public bool hasLoadedRippleData; + float rippleTimer; + + public TargetSignatureData heatTarget; + + //[KSPField(isPersistant = true)] + public float rippleRPM + { + get + { + if (rippleFire) + { + return rippleDictionary[selectedWeapon.GetShortName()].rpm; + } + else + { + return 0; + } + } + set + { + if (selectedWeapon != null && rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) + { + rippleDictionary[selectedWeapon.GetShortName()].rpm = value; + } + } + } + + float triggerTimer; + Dictionary rippleGunCount = []; + Dictionary gunRippleIndex = []; + public float gunRippleRpm; + + public void incrementRippleIndex(string weaponname) + { + if (!gunRippleIndex.ContainsKey(weaponname)) + { + UpdateList(); + if (!gunRippleIndex.ContainsKey(weaponname)) + { + 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 + string rippleData = string.Empty; + Dictionary rippleDictionary; //weapon name, ripple option + public bool canRipple; + + //public float triggerHoldTime = 0.3f; + + //[KSPField(isPersistant = true)] + + public bool rippleFire + { + get + { + if (selectedWeapon == null) return false; + if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) + { + return rippleDictionary[selectedWeapon.GetShortName()].rippleFire; + } + //rippleDictionary.Add(selectedWeapon.GetShortName(), new RippleOption(false, 650)); + return false; + } + } + + public void ToggleRippleFire() + { + if (selectedWeapon != null) + { + RippleOption ro; + if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) + { + ro = rippleDictionary[selectedWeapon.GetShortName()]; + } + else + { + ro = new RippleOption(false, 650); //default to true ripple fire for guns, otherwise, false + if (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + ro.rippleFire = currentGun.useRippleFire; + } + rippleDictionary.Add(selectedWeapon.GetShortName(), ro); + } + + ro.rippleFire = !ro.rippleFire; + + if (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + using (var w = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (w.MoveNext()) + { + if (w.Current == null) continue; + if (w.Current.GetWeaponChannel() > weaponChannel) continue; + if (w.Current.GetShortName() == selectedWeapon.GetShortName()) + w.Current.useRippleFire = ro.rippleFire; + } + } + } + } + + public void AGToggleRipple(KSPActionParam param) + { + ToggleRippleFire(); + } + + void ParseRippleOptions() + { + rippleDictionary = []; + //Debug.Log("[BDArmory.MissileFire]: Parsing ripple options"); + if (!string.IsNullOrEmpty(rippleData)) + { + // Debug.Log("[BDArmory.MissileFire]: Ripple data: " + rippleData); + try + { + using (IEnumerator weapon = rippleData.Split(new char[] { ';' }).AsEnumerable().GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == string.Empty) continue; + + string[] options = weapon.Current.Split(new char[] { ',' }); + string wpnName = options[0]; + bool rf = bool.Parse(options[1]); + float rpm = float.Parse(options[2]); + RippleOption ro = new RippleOption(rf, rpm); + rippleDictionary.Add(wpnName, ro); + } + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.MissileFire]: Ripple data was invalid: " + e.Message); + rippleData = string.Empty; + } + } + else + { + //Debug.Log("[BDArmory.MissileFire]: Ripple data is empty."); + } + hasLoadedRippleData = true; + } + + void SaveRippleOptions(ConfigNode node) + { + if (rippleDictionary != null) + { + rippleData = string.Empty; + using (Dictionary.KeyCollection.Enumerator wpnName = rippleDictionary.Keys.GetEnumerator()) + while (wpnName.MoveNext()) + { + if (wpnName.Current == null) continue; + 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() + { + engageAir = !engageAir; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; + if (engageableWeapon != null) + { + engageableWeapon.engageAir = engageAir; + } + } + UpdateList(); + } + public void ToggleEngageMissile() + { + engageMissile = !engageMissile; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; + if (engageableWeapon != null) + { + engageableWeapon.engageMissile = engageMissile; + } + } + UpdateList(); + } + public void ToggleEngageSrf() + { + engageSrf = !engageSrf; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; + if (engageableWeapon != null) + { + engageableWeapon.engageGround = engageSrf; + } + } + UpdateList(); + } + public void ToggleEngageSLW() + { + engageSLW = !engageSLW; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; + if (engageableWeapon != null) + { + engageableWeapon.engageSLW = engageSLW; + } + } + UpdateList(); + } + + //bomb aimer + bool unguidedWeapon = false; + + public Vector3 bombAimerPosition = Vector3.zero; // Used for the UI + Vector3 bombAimerCPA = Vector3.zero; // Used for the AI + //Vector3 bombAimerTerrainNormal = default; + + List bombAimerTrajectory = []; + Texture2D bombAimerTexture = GameDatabase.Instance.GetTexture("BDArmory/Textures/grayCircle", false); + bool showBombAimer; + List<(Vector3, Texture2D, float, float)> missileAimerUI = []; // GUIUtils.DrawTextureOnWorldPos arguments: (position, texture, size, wobble) + + static GameObject boreRing; + Renderer r_ring; + //GameObject boreRadarRing; + //Renderer r_rRing; + + //targeting + private List loadedVessels = []; + float targetListTimer; + + //sounds + AudioSource audioSource; + public AudioSource warningAudioSource; + AudioSource targetingAudioSource; + AudioClip clickSound; + AudioClip warningSound; + AudioClip armOnSound; + AudioClip armOffSound; + AudioClip heatGrowlSound; + bool warningSounding; + + //missile warning + public bool missileIsIncoming; + public float incomingMissileLastDetected = 0; + public float incomingMissileDistance = float.MaxValue; + public float incomingMissileTime = float.MaxValue; + public Vessel incomingMissileVessel + { + get + { + if (_incomingMissileVessel != null && !_incomingMissileVessel.gameObject.activeInHierarchy) _incomingMissileVessel = null; + return _incomingMissileVessel; + } + set { _incomingMissileVessel = value; } + } + Vessel _incomingMissileVessel; + + //guard mode vars + float targetScanTimer; + float PDScanTimer = 0; + Vessel guardTarget; + //Vessel missileTarget; + public TargetInfo currentTarget; + public int engagedTargets = 0; + public List targetsAssigned; //secondary targets list + public List missilesAssigned; //secondary missile targets list + public List PDMslTgts; //pointDefense/APS targets list + public List PDBulletTgts; //pointDefense/APS targets list + public List PDRktTgts; //pointDefense/APS targets list + public List MslTurrets; //list of turrets holding interceptor missiles + TargetInfo overrideTarget; //used for setting target next guard scan for stuff like assisting teammates + float overrideTimer; + + public bool TargetOverride + { + get { return overrideTimer > 0; } + } + + //AIPilot + public IBDAIControl AI + { + get + { + if (_AI == null || !_AI.pilotEnabled || _AI.vessel != vessel) _AI = vessel.ActiveController().AI; + return _AI; + } + } + IBDAIControl _AI; + // Use: "var pilotAI = PilotAI;" to get a local copy for repeated use. If multiple types are needed in the same block, use the switch pattern in ActiveController. + BDModulePilotAI PilotAI { get { var ai = AI; return ai != null && ai.pilotEnabled && ai.aiType == AIType.PilotAI ? ai as BDModulePilotAI : null; } } + BDModuleSurfaceAI SurfaceAI { get { var ai = AI; return ai != null && ai.pilotEnabled && ai.aiType == AIType.SurfaceAI ? ai as BDModuleSurfaceAI : null; } } + + public float timeBombReleased; + float bombFlightTime; + public float bombAirTime => bombFlightTime; + + //targeting pods + public ModuleTargetingCamera mainTGP = null; + public List targetingPods { get { if (modulesNeedRefreshing) RefreshModules(); return _targetingPods; } } + List _targetingPods = []; + + //radar + public List radars { get { if (modulesNeedRefreshing) RefreshModules(); return _radars; } } + public List _radars = []; + public int MaxRadarLocks = 0; + public VesselRadarData vesselRadarData; + public bool _radarsEnabled = false; + public float GpsUpdateMax = -1; + public List irsts { get { if (modulesNeedRefreshing) RefreshModules(); return _irsts; } } + public List _irsts = []; + public bool _irstsEnabled = false; + public bool _sonarsEnabled = false; + //jammers + public List jammers { get { if (modulesNeedRefreshing) RefreshModules(); return _jammers; } } + public List _jammers = []; + + //cloak generators + public List cloaks { get { if (modulesNeedRefreshing) RefreshModules(); return _cloaks; } } + public List _cloaks = []; + + //other modules + public List wmModules { get { if (modulesNeedRefreshing) RefreshModules(); return _wmModules; } } + List _wmModules = []; + + 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; + + //RWR + private RadarWarningReceiver radarWarn; + + public RadarWarningReceiver rwr + { + get + { + if (!radarWarn || radarWarn.vessel != vessel) + { + return null; + } + return radarWarn; + } + set { radarWarn = value; } + } + + //GPS + public GPSTargetInfo designatedGPSInfo; + + public Vector3d designatedGPSCoords => designatedGPSInfo.gpsCoordinates; + public int designatedGPSCoordsIndex = -1; + + Vector3d designatedINSCoords = Vector3d.zero; + 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; + public Vector3 slavedPosition = Vector3.zero; + public Vector3 slavedVelocity; + public Vector3 slavedAcceleration; + public TargetSignatureData slavedTarget; + + //current weapon ref + public MissileBase CurrentMissile; + public MissileBase PreviousMissile; + + public ModuleWeapon currentGun + { + get + { + if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) + { + return selectedWeapon.GetWeaponModule(); + } + else + { + return null; + } + } + } + public ModuleWeapon previousGun + { + get + { + if (previousSelectedWeapon != null && (previousSelectedWeapon.GetWeaponClass() == WeaponClasses.Gun || previousSelectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || previousSelectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) + { + return previousSelectedWeapon.GetWeaponModule(); + } + else + { + return null; + } + } + } + + public bool underAttack; + float underAttackLastNotified = 0f; + public bool underFire; + float underFireLastNotified = 0f; + HashSet recentlyFiringWeaponClasses = [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 (weapon.timeSinceFired < BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f) return true; // Fired a gun recently. + } + return false; + } + } + + public Vector3 incomingThreatPosition; + public Vessel incomingThreatVessel; + public float incomingMissDistance; + public float incomingMissTime; + public float incomingThreatDistanceSqr; + public Vessel priorGunThreatVessel = null; + private ViewScanResults results; + + public bool debilitated = false; + + public bool guardFiringMissile; + public bool hasAntiRadiationOrdnance; + public RadarWarningReceiver.RWRThreatTypes[] antiradTargets; + public bool antiRadTargetAcquired; + Vector3 antiRadiationTarget; + public bool laserPointDetected; + + ModuleTargetingCamera foundCam; + + #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)] + 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)] + 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.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)] + public float + guardAngle = 360; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_VisualRange"),//Visual Range + UI_FloatSemiLogRange(minValue = 100f, maxValue = 200000f, sigFig = 1, scene = UI_Scene.All)] + public float + guardRange = 200000f; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_GunsRange"),//Guns Range + UI_FloatPowerRange(minValue = 0f, maxValue = 10000f, power = 2f, sigFig = 2, scene = UI_Scene.All)] + public float gunRange = 2500f; + public float maxGunRange = 10f; + public float maxVisualGunRangeSqr; + + [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)] + 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_MissilesOnTarget"),//Missiles/Target + UI_FloatRange(minValue = 1f, maxValue = maxAllowableMissilesOnTarget, stepIncrement = 1f, scene = UI_Scene.All)] + public float maxMissilesOnTarget = 1; + + #region TargetSettings + [KSPField(isPersistant = true)] + public bool targetCoM = true; + + [KSPField(isPersistant = true)] + public bool targetCommand = false; + + [KSPField(isPersistant = true)] + public bool targetEngine = false; + + [KSPField(isPersistant = true)] + public bool targetWeapon = false; + + [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 = StringUtils.Localize("#LOC_BDArmory_TargetCOM"); + [KSPEvent(guiActive = true, guiActiveEditor = true, active = true, guiName = "#LOC_BDArmory_Selecttargeting")]//Select Targeting Option + public void SelectTargeting() + { + BDTargetSelector.Instance.Open(this, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); + } + #endregion + + #region Target Priority + // Target priority variables + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Priority Toggle + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] + public bool targetPriorityEnabled = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_CurrentTarget", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + public string TargetLabel = ""; + + [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 = 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)] + public float targetBias = 1.1f; + + 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)] + public float targetWeightRange = 1f; + + 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)] + public float targetWeightATA = 1f; + + 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)] + public float targetWeightAoD = 0f; + + 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)] + public float targetWeightAccel = 0; + + 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)] + public float targetWeightClosureTime = 0f; + + 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)] + public float targetWeightWeaponNumber = 0; + + 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)] + public float targetWeightMass = 0; + + 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)] + public float targetWeightFriendliesEngaging = 1f; + + 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)] + public float targetWeightThreat = 1f; + + 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)] + public float targetWeightProtectTeammate = 0f; + + 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)] + public float targetWeightProtectVIP = 0f; + + 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)] + public float targetWeightAttackVIP = 0f; + #endregion + + #region Countermeasure Settings + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EvadeThreshold", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Evade time threshold + UI_FloatRange(minValue = 1f, maxValue = 60f, stepIncrement = 0.5f, scene = UI_Scene.All)] + public float evadeThreshold = 5f; // Works well + + [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)] + 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)] + 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.01f, maxValue = 1f, stepIncrement = 0.01f, 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_FloatSemiLogRange(minValue = 0.01f, maxValue = 10f, reducedPrecisionAtMin = true, 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)] + 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.01f, maxValue = 1f, stepIncrement = 0.01f, 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_FloatSemiLogRange(minValue = 0.01f, maxValue = 10f, reducedPrecisionAtMin = true, scene = UI_Scene.All)] + public float chaffWaitTime = 0.6f; // Works well + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SmokeRepetition", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Chaff dispensing repetition + UI_FloatRange(minValue = 1f, maxValue = 10, stepIncrement = 1f, scene = UI_Scene.All)] + public float smokeRepetition = 1f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SmokeInterval", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Chaff dispensing interval + UI_FloatSemiLogRange(minValue = 0.01f, maxValue = 4f, reducedPrecisionAtMin = true, scene = UI_Scene.All)] + public float smokeInterval = 1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SmokeWaitTime", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Chaff dispensing wait time + UI_FloatSemiLogRange(minValue = 0.01f, maxValue = 10f, reducedPrecisionAtMin = true, scene = UI_Scene.All)] + public float smokeWaitTime = 1f; // Works well + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_NonGuardModeCMs", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true), // Non-guard mode CMs. + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool nonGuardModeCMs = false; // Allows for manually flying the craft while still auto-deploying CMs. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicRadar", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true), // Disable Radar vs ARMs + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool DynamicRadarOverride = false; // toggle AI toggling radar when incoming ARMs + #endregion + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_IsVIP", advancedTweakable = true),// Is VIP, throwback to TF Classic (Hunted Game Mode) + UI_Toggle(enabledText = "#LOC_BDArmory_IsVIP_enabledText", disabledText = "#LOC_BDArmory_IsVIP_disabledText", scene = UI_Scene.All),]//yes--no + public bool isVIP = false; + + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_weaponChannel"), + UI_FloatRange(minValue = 0, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float weaponChannel = 0; // weaponChannel telling a weaponManager which weapons it may use + + public void ToggleGuardMode() + { + guardMode = !guardMode; + + if (!guardMode) + { + //disable turret firing and guard mode + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + weapon.Current.visualTargetVessel = null; + weapon.Current.visualTargetPart = null; + weapon.Current.autoFire = false; + if (!weapon.Current.isAPS) weapon.Current.aiControlled = false; + if (weapon.Current.dualModeAPS) weapon.Current.isAPS = true; + } + if (vesselRadarData) vesselRadarData.UnslaveTurrets(); // Unslave the turrets so that manual firing works. + weaponIndex = 0; + selectedWeapon = null; + if (IsPrimaryWM) // Disabling guard mode on the primary disables guard mode on any non-primary WMs on the craft. + foreach (var wm in VesselModuleRegistry.GetMissileFires(vessel).Where(wm => !wm.IsPrimaryWM && wm.guardMode)) + wm.ToggleGuardMode(); + } + else + { + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + weapon.Current.aiControlled = true; + if (weapon.Current.isAPS) weapon.Current.EnableWeapon(); + } + if (radars.Count > 0) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null || rd.Current.canLock) + { + if (rd.Current.sonarMode == ModuleRadar.SonarModes.None) + { + rd.Current.EnableRadar(); + float scanSpeed = rd.Current.radarAzFOV / rd.Current.scanRotationSpeed * 2; + if (GpsUpdateMax > 0 && scanSpeed < GpsUpdateMax) GpsUpdateMax = scanSpeed; + _radarsEnabled = true; + } + else if (rd.Current.sonarMode == ModuleRadar.SonarModes.passive) + // Only enable passive sonar, wouldn't want to ping the enemy + { + rd.Current.EnableRadar(); + float scanSpeed = rd.Current.radarAzFOV / rd.Current.scanRotationSpeed * 2; + if (GpsUpdateMax > 0 && scanSpeed < GpsUpdateMax) GpsUpdateMax = scanSpeed; + //_sonarsEnabled = true; + } + } + } + } + if (irsts.Count > 0) + { + using (List.Enumerator rd = irsts.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null) + { + rd.Current.EnableIRST(); + float scanSpeed = rd.Current.directionalFieldOfView / rd.Current.scanRotationSpeed * 2; + if (GpsUpdateMax > 0 && scanSpeed < GpsUpdateMax) GpsUpdateMax = scanSpeed; + _irstsEnabled = true; + } + } + } + if (hasAntiRadiationOrdnance) + { + if (rwr && !rwr.rwrEnabled) rwr.EnableRWR(); + if (rwr && rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; + } + if (_radarsEnabled || _irstsEnabled) + StartCoroutine(SetMaxRadarRangeRoutine()); + } + } + + private IEnumerator SetMaxRadarRangeRoutine() + { + // Wait for vesselRadarData to be available and any radars to be turned on + // and active before setting the radar range to the max + if (!vesselRadarData) + { + vesselRadarData = vessel.gameObject.GetComponent(); + if (vesselRadarData == null) + vesselRadarData = vessel.gameObject.AddComponent(); + } + WaitForFixedUpdate wait = new WaitForFixedUpdate(); + yield return wait; + yield return wait; + vesselRadarData.SetMaxRange(); + } + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_UnderAttackAG", advancedTweakable = true), + UI_ActionGroup(scene = UI_Scene.All)] + public KSPActionGroup underAttackAG = KSPActionGroup.None; + + [KSPAction("Toggle Guard Mode")] + public void AGToggleGuardMode(KSPActionParam param) + { + ToggleGuardMode(); + } + + [KSPAction("Enable Guard Mode")] + public void AGEnableGuardMode(KSPActionParam param) + { + if (!guardMode) ToggleGuardMode(); + } + + [KSPAction("Disable Guard Mode")] + public void AGDisableGuardMode(KSPActionParam param) + { + if (guardMode) ToggleGuardMode(); + } + + //[KSPField(isPersistant = true)] public bool guardMode; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_GuardMode"),//Guard Mode: + UI_Toggle(disabledText = "OFF", enabledText = "ON")] + public bool guardMode; + + public bool targetMissiles = false; + + [KSPAction("Jettison Weapon")] + public void AGJettisonWeapon(KSPActionParam param) + { + if (CurrentMissile) + { + using (var missile = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (missile.MoveNext()) + { + if (missile.Current == null) continue; + if (missile.Current.GetWeaponChannel() > weaponChannel) continue; + if (missile.Current.GetShortName() == CurrentMissile.GetShortName()) + { + missile.Current.Jettison(); + } + } + } + else if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket) + { + using (var rocket = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (rocket.MoveNext()) + { + if (rocket.Current == null) continue; + if (rocket.Current.GetWeaponChannel() > weaponChannel) continue; + if (rocket.Current.GetWeaponClass() != WeaponClasses.Rocket) continue; + rocket.Current.Jettison(); + } + } + } + + [KSPAction("Deploy Kerbals' Parachutes")] // If there's an EVAing kerbal. + public void AGDeployKerbalsParachute(KSPActionParam param) + { + foreach (var chute in VesselModuleRegistry.GetModules(vessel)) + { + if (chute == null) continue; + chute.deployAltitude = (float)vessel.radarAltitude + 100f; // Current height + 100 so that it deploys immediately. + chute.deploymentState = ModuleParachute.deploymentStates.STOWED; + chute.Deploy(); + } + } + + [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) + { + foreach (var part in vessel.parts) + { + if (part.protoModuleCrew.Count > 0) + { + PartExploderSystem.AddPartToExplode(part); + } + } + foreach (var tnt in VesselModuleRegistry.GetModules(vessel)) + { + if (tnt == null) continue; + tnt.ArmAG(null); + tnt.DetonateIfPossible(); + } + } + + public BDTeam Team + { + get + { + return BDTeam.Get(teamString); + } + set + { + if (!team_loaded) return; + if (!BDArmorySetup.Instance.Teams.ContainsKey(value.Name)) + BDArmorySetup.Instance.Teams.Add(value.Name, value); + teamString = value.Name; + alliesString = string.Join("; ", value.Allies); + team = value.Serialize(); + } + } + + // Team name + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Team")]//Team + public string teamString = "Neutral"; + + // Team name + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Allies")]//Team + public string alliesString = "None"; + + // Serialized team + [KSPField(isPersistant = true)] + public string team; + private bool team_loaded = false; + + [KSPAction("Next Team")] + public void AGNextTeam(KSPActionParam param) + { + NextTeam(); + } + + public delegate void ChangeTeamDelegate(MissileFire wm, BDTeam team); + + public static event ChangeTeamDelegate OnChangeTeam; + + public void SetTeam(BDTeam team) + { + if (HighLogic.LoadedSceneIsFlight) + { + SetTarget(null); // Without this, friendliesEngaging never gets updated + using (var wpnMgr = VesselModuleRegistry.GetMissileFires(vessel).GetEnumerator()) + while (wpnMgr.MoveNext()) + { + if (wpnMgr.Current == null) continue; + wpnMgr.Current.Team = team; + } + + if (vessel.gameObject.GetComponent()) + { + BDATargetManager.RemoveTarget(vessel.gameObject.GetComponent()); + Destroy(vessel.gameObject.GetComponent()); + } + OnChangeTeam?.Invoke(this, Team); + ResetGuardInterval(); + } + else if (HighLogic.LoadedSceneIsEditor) + { + using (var editorPart = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (editorPart.MoveNext()) + using (var wpnMgr = editorPart.Current.FindModulesImplementing().GetEnumerator()) + while (wpnMgr.MoveNext()) + { + if (wpnMgr.Current == null) continue; + wpnMgr.Current.Team = team; + } + } + } + + public void SetTeamByName(string teamName) + { + + } + + [KSPEvent(active = true, guiActiveEditor = true, guiActive = false)] + public void NextTeam(bool switchneutral = false) + { + if (!switchneutral) //standard switch behavior; don't switch to a neutral team + { + var teamList = new List { "A", "B" }; + using (var teams = BDArmorySetup.Instance.Teams.GetEnumerator()) + while (teams.MoveNext()) + if (!teamList.Contains(teams.Current.Key) && !teams.Current.Value.Neutral) + teamList.Add(teams.Current.Key); + teamList.Sort(); + SetTeam(BDTeam.Get(teamList[(teamList.IndexOf(Team.Name) + 1) % teamList.Count])); + } + else// alt-click; switch to first available neutral team + { + var neutralList = new List { "Neutral" }; + using (var teams = BDArmorySetup.Instance.Teams.GetEnumerator()) + while (teams.MoveNext()) + if (!neutralList.Contains(teams.Current.Key) && teams.Current.Value.Neutral) + neutralList.Add(teams.Current.Key); + neutralList.Sort(); + SetTeam(BDTeam.Get(neutralList[(neutralList.IndexOf(Team.Name) + 1) % neutralList.Count])); + } + } + + + [KSPEvent(guiActive = false, guiActiveEditor = true, active = true, guiName = "#LOC_BDArmory_SelectTeam")]//Select Team + public void SelectTeam() + { + BDTeamSelector.Instance.Open(this, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); + } + + [KSPField(isPersistant = true)] + public bool isArmed = false; + + [KSPAction("Arm/Disarm")] + public void AGToggleArm(KSPActionParam param) + { + ToggleArm(); + } + + public void ToggleArm() + { + isArmed = !isArmed; + if (isArmed) audioSource.PlayOneShot(armOnSound); + else audioSource.PlayOneShot(armOffSound); + } + [KSPField(isPersistant = false, guiActive = true, guiName = "#LOC_BDArmory_Weapon")]//Weapon + public string selectedWeaponString = "None"; + + /* //global toggle moved to BDASetup; decide if we need a per-WM toggle instead later for selective usage of DLZ + [KSPEvent(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissilesRange")]//Toggle DLZ + public void ToggleDLZ() + { + BDArmorySettings.USE_DLZ_LAUNCH_RANGE = !BDArmorySettings.USE_DLZ_LAUNCH_RANGE; + Events["ToggleDLZ"].guiName = $" {StringUtils.Localize("#LOC_BDArmory_MissilesRange")}: {(BDArmorySettings.USE_DLZ_LAUNCH_RANGE ? StringUtils.Localize("#LOC_BDArmory_true") : StringUtils.Localize("#LOC_BDArmory_false"))}";//"Use Dynamic Launch Range: True/False + GUIUtils.RefreshAssociatedWindows(part); + } + */ + IBDWeapon sw; + + public IBDWeapon selectedWeapon + { + get + { + if (sw != null) //we have a weapon set by bool SmartPick, or set by the last time this was called + { + var gun = sw.GetWeaponModule(); + if (gun == null || ((gun.useThisWeaponForAim || !(gun.isReloading || gun.isOverheated || gun.pointingAtSelf)) && (gun.ammoCount > 0 || BDArmorySettings.INFINITE_AMMO))) //it's a missile, or an aim-override enabled weapon with ammo + if (sw.GetPart().vessel == vessel) return sw; + } + sw = null; //weapon no longer on craft. Null in case below while loop doesn't find other weapons in the same group on craft. + //missile no longer on craft, or a gun that isn't aim overridden, or sw hasn't been set yet + if (weaponIndex <= 0) return sw; //no weapon selected + //if ((sw != null && sw.GetPart().vessel == vessel) || weaponIndex <= 0) return sw; //this is going to return the first gun of a weaponGroup, regardless of overheat/reload state, as long as gun was valid when first selected + // should only apply if selected weapon is missile/bomb/slw + + IBDWeapon candidateGun = null; + float candidateMuzVel = 0; + + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + if (weapon.Current.GetShortName() != selectedWeaponString) continue; + if (weapon.Current.GetWeaponClass() == WeaponClasses.Gun || weapon.Current.GetWeaponClass() == WeaponClasses.Rocket || weapon.Current.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + if (candidateGun == null) candidateGun = weapon.Current; //set this here to ensure a weapon gets elected, in event *all* guns are currently reloading/overheated/etc so Ai continues targeting + var gun = weapon.Current.GetWeaponModule(); + if (gun.useThisWeaponForAim) + { + if (gun.ammoCount > 0 || BDArmorySettings.INFINITE_AMMO) //don't force aim override if this weapon is no longer viable. + { + candidateGun = weapon.Current; + break; + } + } + 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 + //use longest range gun for aiming. Guns with vastly differing aim lead (rockets + lasers, railguns + grenade launchers, etc.) really should not be grouped in the same weapongroup, or at least have range brakcets defined. + float muzVel = gun.eWeaponType == WeaponTypes.Rocket ? gun.thrust / gun.rocketMass : gun.eWeaponType == WeaponTypes.Ballistic ? gun.bulletVelocity : 300000000; + if (gun.GetEngageRange() >= candidateGun.GetEngageRange()) + { + if (muzVel / gun.GetEngageRange() > candidateMuzVel) //if both weapons have the same range, which has a lower lead time? + { + candidateGun = weapon.Current; + candidateMuzVel = muzVel; + } + } + } + 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; + + if (msl.launched || msl.HasFired) continue; //return first missile that is ready to fire + if (msl.GetEngageRange() != selectedWeaponsEngageRangeMax) continue; + if (msl.GetEngageFOV() != selectedWeaponsMissileFOV) continue; + sw = weapon.Current; + UpdateSelectedWeaponState(); // Update things like CurrentMissile. + break; + } + } + if (candidateGun != null) sw = candidateGun; + return sw; + } + set + { + if (sw == value) return; + previousSelectedWeapon = sw; + sw = value; + selectedWeaponString = GetWeaponName(value); + selectedWeaponsEngageRangeMax = GetWeaponRange(value); + selectedWeaponsMissileFOV = GetMissileFOV(value); + UpdateSelectedWeaponState(); + } + } + + IBDWeapon previousSelectedWeapon { get; set; } + + public float selectedWeaponsEngageRangeMax { get; private set; } = 0; + public float selectedWeaponsMissileFOV { get; private set; } = -1; + + [KSPAction("Fire Missile")] + public void AGFire(KSPActionParam param) + { + FireMissileManually(false); + } + + [KSPAction("Fire Guns (Hold)")] + public void AGFireGunsHold(KSPActionParam param) + { + if (weaponIndex <= 0 || (selectedWeapon.GetWeaponClass() != WeaponClasses.Gun && + selectedWeapon.GetWeaponClass() != WeaponClasses.Rocket && + selectedWeapon.GetWeaponClass() != WeaponClasses.DefenseLaser)) return; + using (var weap = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weap.MoveNext()) + { + if (weap.Current == null) continue; + if (weap.Current.GetWeaponChannel() > weaponChannel) continue; + if (weap.Current.weaponState != ModuleWeapon.WeaponStates.Enabled || + weap.Current.GetShortName() != selectedWeapon.GetShortName()) continue; + weap.Current.AGFireHold(param); + } + } + + [KSPAction("Fire Guns (Toggle)")] + public void AGFireGunsToggle(KSPActionParam param) + { + if (weaponIndex <= 0 || (selectedWeapon.GetWeaponClass() != WeaponClasses.Gun && + selectedWeapon.GetWeaponClass() != WeaponClasses.Rocket && + selectedWeapon.GetWeaponClass() != WeaponClasses.DefenseLaser)) return; + using (var weap = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weap.MoveNext()) + { + if (weap.Current == null) continue; + if (weap.Current.GetWeaponChannel() > weaponChannel) continue; + if (weap.Current.weaponState != ModuleWeapon.WeaponStates.Enabled || + weap.Current.GetShortName() != selectedWeapon.GetShortName()) continue; + weap.Current.AGFireToggle(param); + } + } + + [KSPAction("Next Weapon")] + public void AGCycle(KSPActionParam param) + { + CycleWeapon(true); + } + + [KSPAction("Previous Weapon")] + public void AGCycleBack(KSPActionParam param) + { + CycleWeapon(false); + } + + [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_OpenGUI", active = true)]//Open GUI + public void ToggleToolbarGUI() + { + BDArmorySetup.windowBDAToolBarEnabled = !BDArmorySetup.windowBDAToolBarEnabled; + } + + public void SetAFCAA() + { + UI_FloatRange field = (UI_FloatRange)Fields["AutoFireCosAngleAdjustment"].uiControlEditor; + field.onFieldChanged = OnAFCAAUpdated; + // field = (UI_FloatRange)Fields["AutoFireCosAngleAdjustment"].uiControlFlight; // Not visible in flight mode, use the guard menu instead. + // field.onFieldChanged = OnAFCAAUpdated; + OnAFCAAUpdated(null, null); + } + + public void OnAFCAAUpdated(BaseField field, object obj) + { + adjustedAutoFireCosAngle = Mathf.Cos((AutoFireCosAngleAdjustment * Mathf.Deg2Rad)); + //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(); + private string dynRangeDebug = string.Empty; + #endregion Declarations + + #region KSP Events + + public override void OnSave(ConfigNode node) + { + base.OnSave(node); + + if (HighLogic.LoadedSceneIsFlight) + { + SaveRippleOptions(node); + } + } + + public override void OnLoad(ConfigNode node) + { + base.OnLoad(node); + if (HighLogic.LoadedSceneIsFlight) + { + rippleData = string.Empty; + if (node.HasValue("RippleData")) + { + rippleData = node.GetValue("RippleData"); + } + ParseRippleOptions(); + } + } + + public override void OnAwake() + { + 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; + } + + public void Start() + { + team_loaded = true; + Team = BDTeam.Deserialize(team); + UpdateMaxGuardRange(); + SetAFCAA(); + startTime = Time.time; + if (HighLogic.LoadedSceneIsFlight) + { + part.force_activate(); + UpdateList(); + if (weaponArray.Length > 0) selectedWeapon = weaponArray[weaponIndex]; + selectionMessage = new ScreenMessage("", 2.0f, ScreenMessageStyle.LOWER_CENTER); + if (guardMode) + { + ToggleGuardMode(); // Disable guard mode + if (!BDArmorySettings.DISABLE_GUARDMODE_ON_SPAWN) + StartCoroutine(ReenableGuardModeWhenReady()); + } + + rippleTimer = Time.time; + targetListTimer = Time.time; + + wingCommander = part.FindModuleImplementing(); + + audioSource = gameObject.AddComponent(); + audioSource.minDistance = 1; + audioSource.maxDistance = 500; + audioSource.dopplerLevel = 0; + audioSource.spatialBlend = 1; + + warningAudioSource = gameObject.AddComponent(); + warningAudioSource.minDistance = 1; + warningAudioSource.maxDistance = 500; + warningAudioSource.dopplerLevel = 0; + warningAudioSource.spatialBlend = 1; + + targetingAudioSource = gameObject.AddComponent(); + targetingAudioSource.minDistance = 1; + targetingAudioSource.maxDistance = 250; + targetingAudioSource.dopplerLevel = 0; + targetingAudioSource.loop = true; + targetingAudioSource.spatialBlend = 1; + + if (SurfaceVisionOffset == null) + { + SurfaceVisionOffset = new FloatCurve(); + SurfaceVisionOffset.Add(1500, 1.88f); + SurfaceVisionOffset.Add(2000, 3.35f); + SurfaceVisionOffset.Add(3000, 7.5f); + SurfaceVisionOffset.Add(4000, 13.35f); + SurfaceVisionOffset.Add(5000, 20.85f); + SurfaceVisionOffset.Add(6000, 30f); + SurfaceVisionOffset.Add(8000, 53.4f); + SurfaceVisionOffset.Add(10000, 83.4f); + } + + StartCoroutine(MissileWarningResetRoutine()); + + UpdateVolume(); + BDArmorySetup.OnVolumeChange += UpdateVolume; + BDArmorySetup.OnSavedSettings += ClampVisualRange; + + StartCoroutine(StartupListUpdater()); + firedMissiles = 0; + missilesAway = []; + rippleGunCount = []; + queuedLaunches = []; + + GameEvents.onPartJointBreak.Add(OnPartJointBreak); + GameEvents.onPartDie.Add(OnPartDie); + GameEvents.onVesselPartCountChanged.Add(UpdateMaxGunRange); + GameEvents.onVesselPartCountChanged.Add(UpdateCurrentHP); + GameEvents.onVesselPartCountChanged.Add(OnVesselPartCountChanged); + + totalHP = GetTotalHP(); + currentHP = totalHP; + UpdateMaxGunRange(vessel); + + // Update the max visual gun range (sqr) whenever the gun range or guard range changes. + { + ((UI_FloatSemiLogRange)Fields["guardRange"].uiControlFlight).onFieldChanged = UpdateVisualGunRangeSqr; + ((UI_FloatPowerRange)Fields["gunRange"].uiControlFlight).onFieldChanged = UpdateVisualGunRangeSqr; + UpdateVisualGunRangeSqr(null, null); + } + + modulesNeedRefreshing = true; + cmPrioritiesNeedRefreshing = true; + var SF = vessel.rootPart.FindModuleImplementing(); + if (SF == null) + { + SF = (ModuleSpaceFriction)vessel.rootPart.AddModule("ModuleSpaceFriction"); + } + //either have this added on spawn to allow vessels to respond to space hack settings getting toggled, or have the Spacefriction module it's own separate part + if (boreRing == null) + { + boreRing = GameDatabase.Instance.GetModel("BDArmory/Models/boresight/boresight"); + if (boreRing == null) + { + Debug.LogError("[BDArmory.MissileFire]: model BDArmory/Models/boresight/boresight not found."); + boreRing = GameObject.CreatePrimitive(PrimitiveType.Sphere); + var dc = boreRing.GetComponent(); + if (dc) + { + dc.enabled = false; + Destroy(dc); + } + } + } + //Renderer d = ring.GetComponentInChildren(); + //if (d != null) + //{ + //d.material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + //d.material.SetColor("_TintColor", Color.green); + //} + /* + var radarRing = GameDatabase.Instance.GetModel("BDArmory/Models/boresight/radarBoresight"); + if (radarRing == null) + { + Debug.LogError("[BDArmory.MissileFire]: model BDArmory/Models/boresight/radarBoresight not found."); + radarRing = GameObject.CreatePrimitive(PrimitiveType.Sphere); + var dc = radarRing.GetComponent(); + if (dc) + { + dc.enabled = false; + Destroy(dc); + } + } + Renderer rr = radarRing.GetComponentInChildren(); + if (rr != null) + { + rr.material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + rr.material.SetColor("_TintColor", Color.green); + radarRing.SetActive(false); + boresights[1] = ObjectPool.CreateObjectPool(radarRing, 1, true, true); + } + */ + if (ShowBoreRing(false)) + { + boreRing.transform.SetPositionAndRotation(vessel.CoM, transform.rotation); + boreRing.transform.localScale = Vector3.zero; + r_ring = boreRing.GetComponent(); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.MissileFire]: boresight set up."); + } + /* + if (boresights[1] != null) + { + boreRadarRing = boresights[1].GetPooledObject(); + boreRadarRing.transform.SetPositionAndRotation(transform.position, transform.rotation); + boreRadarRing.transform.localScale = Vector3.zero; + r_rRing = boreRadarRing.GetComponentInChildren(); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.MissileFire]: radar boresight set up."); + } + */ + } + else if (HighLogic.LoadedSceneIsEditor) + { + GameEvents.onEditorPartPlaced.Add(UpdateMaxGunRange); + GameEvents.onEditorPartDeleted.Add(UpdateMaxGunRange); + UpdateMaxGunRange(part); + } + 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") + "; " : ""); + + // Override inCargoBay for missiles in cargo bays in case they weren't set by the user (was being done in SetCargoBays each time before! Also, it doesn't seem to work for bombs in cargo bays?). + foreach (var ml in VesselModuleRegistry.GetModules(vessel)) + { + if (ml == null) continue; + if (ml.part.ShieldedFromAirstream) ml.inCargoBay = true; // Override inCargoBay if we see that the missile is shielded from the airstream. + } + + if (HighLogic.LoadedSceneIsFlight) TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Earlyish, PointDefence); // Perform point defence checks before bullets get moved to avoid order of operation issues. + } + + void OnPartDie() + { + OnPartDie(part); + } + + void OnPartDie(Part p) + { + if (p == part) + { + try + { + Destroy(this); // Force this module to be removed from the gameObject as something is holding onto part references and causing a memory leak. + GameEvents.onPartDie.Remove(OnPartDie); + GameEvents.onPartJointBreak.Remove(OnPartJointBreak); + } + catch (Exception e) + { + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Error OnPartDie: " + e.Message); + Debug.Log("[BDArmory.MissileFire]: Error OnPartDie: " + e.Message); + } + } + modulesNeedRefreshing = true; + weaponsListNeedsUpdating = true; + cmPrioritiesNeedRefreshing = true; + if (vessel != null) + { + var TI = vessel.gameObject.GetComponent(); + if (TI != null) + { + TI.targetPartListNeedsUpdating = true; + } + } + } + + void OnVesselPartCountChanged(Vessel v) + { + if (v == null || vessel != v) return; + modulesNeedRefreshing = true; + weaponsListNeedsUpdating = true; + cmPrioritiesNeedRefreshing = true; + } + + void OnPartJointBreak(PartJoint j, float breakForce) + { + if (!part) + { + GameEvents.onPartJointBreak.Remove(OnPartJointBreak); + } + if (vessel == null) + { + Destroy(this); + return; + } + + if (HighLogic.LoadedSceneIsFlight && ((j.Parent && j.Parent.vessel == vessel) || (j.Child && j.Child.vessel == vessel))) + { + modulesNeedRefreshing = true; + weaponsListNeedsUpdating = true; + cmPrioritiesNeedRefreshing = true; + } + } + + public int GetTotalHP() // get total craft HP + { + int HP = 0; + using (List.Enumerator p = vessel.parts.GetEnumerator()) + while (p.MoveNext()) + { + if (p.Current == null) continue; + if (p.Current.Modules.GetModule()) continue; // don't grab missiles + if (p.Current.Modules.GetModule()) continue; // don't grab bits that are going to fall off + if (p.Current.FindParentModuleImplementing()) continue; // should grab ModularMissiles too + /* + if (p.Current.Modules.GetModule() != null) + { + var hp = p.Current.Modules.GetModule(); + totalHP += hp.Hitpoints; + } + */ + ++HP; + // ++totalHP; + //Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " part count: " + totalHP); + } + return HP; + } + + void UpdateCurrentHP(Vessel v) + { + if (v == vessel) + { currentHP = GetTotalHP(); } + } + + public override void OnUpdate() + { + if (!HighLogic.LoadedSceneIsFlight) + { + return; + } + + base.OnUpdate(); + + if (!IsPrimaryWM) return; // Don't do anything if we're not in control. + + UpdateTargetingAudio(); + + if (vessel.isActiveVessel) + { + if (!guardMode) // Manual firing.) + { + bool missileTriggerHeld = false; + if (!CheckMouseIsOnGui() && isArmed && BDInputUtils.GetKey(BDInputSettingsFields.WEAP_FIRE_KEY) && !ModuleTargetingCamera.IsSlewing) + { + 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 (selectedWeapon != null && + (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile + || selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb + || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW + )) + { + canRipple = true; + FireMissileManually(true); + } + 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 + { + canRipple = true; + } + else + { + 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 its authorisation check for guns! + } + + //BOMB/MISSILE HUD - calculate GUI element positions here instead of OnGUI for smoother display + missileAimerUI.Clear(); + MissileBase ml = CurrentMissile; + dynRangeDebug = string.Empty; + if (ml) + { + if (showBombAimer) + { + MissileLauncher msl = CurrentMissile as MissileLauncher; + if (vessel.altitude > msl.GetBlastRadius()) + { + if (ShowBoreRing(true)) + { + Quaternion rotation = Quaternion.LookRotation(FlightCamera.fetch.mainCamera.transform.forward, boreRing.transform.forward); + boreRing.transform.SetPositionAndRotation(bombAimerPosition, rotation); + if (guardTarget && (msl.guidanceActive && foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude <= 100)) + missileAimerUI.Add((bombAimerPosition, BDArmorySetup.Instance.largeGreenCircleTexture, 256, 3)); + boreRing.transform.localScale = Mathf.Min(150, msl.GetBlastRadius() * 0.68f) / 10 * Vector3.one; //ring model has 10m radius. GBR uses a min of 0.68x radius for single bombs + missileAimerUI.Add((bombAimerPosition, BDArmorySetup.Instance.greenCross, 48, 0)); + if (guardTarget) missileAimerUI.Add((AIUtils.PredictPosition(guardTarget, bombFlightTime, immediate: false), BDArmorySetup.Instance.greenDotTexture, 6, 3)); + } + } + else + { + missileAimerUI.Add((bombAimerPosition, BDArmorySetup.Instance.greenSpikedPointCircleTexture, 128, 0)); + } + } + else + { + ShowBoreRing(false); + //float dynamicBoresight = ml.maxOffBoresight * ((vessel.LandedOrSplashed || (guardTarget && guardTarget.LandedOrSplashed) || ml.uncagedLock) ? 0.75f : 0.35f); // for larger boresights (> ~60 or so) may want thinner ring model so ring isn't stupidly thick at larger scale. + // boresights > 90 or so may want to simply be capped, else they'll fill the whole screen for something that has a 120deg bore, or a 180deg, or a 240deg, or whatever + //dynamicBoresight = Mathf.Clamp(dynamicBoresight, 1, 90); + Vector3 missileReferencePosition = ml.MissileReferenceTransform.position; + //float AoA = Mathf.Min(VectorUtils.Angle(vessel.vesselTransform.up, -VectorUtils.GetUpDirection(vessel.CoM)), 90); + //float unlockedAimerDist = vessel.altitude < Mathf.Cos(AoA) * 2000 ? (Mathf.Cos(AoA) * 2000) - 15 : 2000; //account for distance to water, since raycasts ignore it. + //Quaternion rotation = unlockedAimerDist < 1995 ? Quaternion.LookRotation(VectorUtils.GetUpDirection(vessel.CoM), boreRing.transform.forward) : ml.MissileReferenceTransform.rotation; + + if (ml.GetWeaponClass() == WeaponClasses.SLW && !vessel.LandedOrSplashed) //if flying with air-drop torps, adjust aimer pos based on predicted water impact point. torps aren't AAMs + { + Vector3 torpImpactPos = ml.MissileReferenceTransform.position + vessel.srf_vel_direction * (vessel.horizontalSrfSpeed * bombFlightTime); //might need a projectonPlane, check what srf_vel_dir actually outputs - parallel to surface, or vel direction when !orbit? + missileReferencePosition = torpImpactPos - ((float)FlightGlobals.getAltitudeAtPos(torpImpactPos) * VectorUtils.GetUpDirection(torpImpactPos)); + //rotation = Quaternion.LookRotation(VectorUtils.GetUpDirection(vessel.CoM), boreRing.transform.forward); + } + /* + Ray terrainDist = new Ray(ml.MissileReferenceTransform.position, ml.GetForwardTransform()); + if (Physics.Raycast(terrainDist, out RaycastHit hit, 2000, (int)LayerMasks.Scenery)) + { + unlockedAimerDist = hit.distance - 5; + bombAimerPosition = hit.point; + rotation = Quaternion.LookRotation(hit.normal, boreRing.transform.forward); + } + */ + switch (ml.TargetingMode) + { + case MissileBase.TargetingModes.Laser: + { + if (laserPointDetected && foundCam) + { + missileAimerUI.Add((foundCam.groundTargetPosition, BDArmorySetup.Instance.greenCircleTexture, 48, 1)); + } + else + { + missileAimerUI.Add((missileReferencePosition + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 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) + { + missileAimerUI.Add((cam.Current.groundTargetPosition, BDArmorySetup.Instance.greenDiamondTexture, 18, 0)); + } + } + break; + } + case MissileBase.TargetingModes.Heat: + { + // MissileBase ml = CurrentMissile; redundant? + //boreRadarRing.SetActive(false); + //boreRing.SetActive(true); + //boreRing.transform.rotation = ml.MissileReferenceTransform.rotation; + if (heatTarget.exists) + { + missileAimerUI.Add((heatTarget.position, BDArmorySetup.Instance.greenCircleTexture, 36, 3)); + float distanceToTarget = Vector3.Distance(heatTarget.position, missileReferencePosition); + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 128, 0)); + //boreRing.transform.position = missileReferencePosition + (distanceToTarget * ml.GetForwardTransform()); + //boreRing.transform.localScale = Vector3.one * ((Mathf.Sin(Mathf.Deg2Rad * dynamicBoresight) * distanceToTarget) / 10); + + Vector3 fireSolution = MissileGuidance.GetAirToAirFireSolution(ml, heatTarget.position, heatTarget.velocity); + Vector3 fsDirection = (fireSolution - missileReferencePosition).normalized; + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, 6, 0)); + } + else + { + missileAimerUI.Add((missileReferencePosition + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.greenCircleTexture, 36, 3)); + missileAimerUI.Add((missileReferencePosition + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 156, 0)); + //boreRing.transform.SetPositionAndRotation(missileReferencePosition + (unlockedAimerDist * ml.GetForwardTransform()), rotation); + //boreRing.transform.localScale = Vector3.one * (Mathf.Sin(Mathf.Deg2Rad * dynamicBoresight) * unlockedAimerDist) / 10; //ring model has 10m radius. + } + break; + } + case MissileBase.TargetingModes.Radar: + { + //MissileBase ml = CurrentMissile; //... and inconsistant? + //if(radar && radar.locked) + if (vesselRadarData && vesselRadarData.locked) + { + //boreRadarRing.SetActive(true); + //boreRing.SetActive(false); + float distanceToTarget = Vector3.Distance(vesselRadarData.lockedTargetData.targetData.predictedPosition, missileReferencePosition); + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.dottedLargeGreenCircle, 128, 0)); + //boreRadarRing.transform.position = missileReferencePosition + (distanceToTarget * ml.GetForwardTransform()); + //boreRadarRing.transform.rotation = ml.MissileReferenceTransform.rotation; + //boreRadarRing.transform.localScale = Vector3.one * ((Mathf.Sin(Mathf.Deg2Rad * dynamicBoresight) * distanceToTarget) / 10); + + //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 - missileReferencePosition).normalized; + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, 6, 0)); + + //if (BDArmorySettings.DEBUG_MISSILES) + if (BDArmorySettings.DEBUG_TELEMETRY) + { + MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(ml, vesselRadarData.lockedTargetData.targetData.velocity, vesselRadarData.lockedTargetData.targetData.predictedPosition); + dynRangeDebug += "MaxDLZ: " + dlz.maxLaunchRange; + dynRangeDebug += "\nMinDLZ: " + dlz.minLaunchRange; + } + } + else + { + //boreRadarRing.SetActive(false); + //boreRing.SetActive(true); + missileAimerUI.Add((missileReferencePosition + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 48, 0)); + //boreRing.transform.SetPositionAndRotation(missileReferencePosition + (unlockedAimerDist * ml.GetForwardTransform()), rotation); + //boreRing.transform.localScale = Vector3.one * (Mathf.Sin(Mathf.Deg2Rad * 1) * unlockedAimerDist) / 10; + } + break; + } + case MissileBase.TargetingModes.AntiRad: + { + if (rwr && rwr.rwrEnabled && rwr.displayRWR) + { + MissileLauncher msl = CurrentMissile as MissileLauncher; + for (int i = 0; i < rwr.pingsData.Length; i++) + { + Vector3 position; + if (rwr.pingsData[i].exists && msl.antiradTargets.Contains(rwr.pingsData[i].signalType) && Vector3.Dot((position = rwr.pingsData[i].position) - ml.transform.position, ml.GetForwardTransform()) > 0) + { + missileAimerUI.Add((position, BDArmorySetup.Instance.greenDiamondTexture, 22, 0)); + } + } + } + + if (antiRadTargetAcquired) + { + missileAimerUI.Add((antiRadiationTarget, BDArmorySetup.Instance.openGreenSquare, 22, 0)); + } + break; + } + case MissileBase.TargetingModes.Inertial: + { + //MissileBase ml = CurrentMissile; + float distanceToTarget = 0; + + if (vesselRadarData) + { + TargetSignatureData targetData = TargetSignatureData.noTarget; + if (_radarsEnabled || ml.GetWeaponClass() == WeaponClasses.SLW && _sonarsEnabled) + { + if (vesselRadarData.locked) + targetData = vesselRadarData.lockedTargetData.targetData; + else + targetData = vesselRadarData.detectedRadarTarget(guardTarget != null ? guardTarget : null, this); + } + else if (_irstsEnabled) + targetData = vesselRadarData.activeIRTarget(null, this); + + if (targetData.exists) + { + //boreRadarRing.SetActive(true); + //boreRing.SetActive(false); + + distanceToTarget = Vector3.Distance(targetData.predictedPosition, missileReferencePosition); + Vector3 fireSolution = MissileGuidance.GetAirToAirFireSolution(ml, targetData.predictedPosition, targetData.velocity); + Vector3 fsDirection = (fireSolution - missileReferencePosition).normalized; + if (vesselRadarData.locked) + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, 6, 0)); + else + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenCircleTexture, 36, 5)); + missileAimerUI.Add((missileReferencePosition + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.dottedLargeGreenCircle, 128, 0)); + //boreRadarRing.transform.position = missileReferencePosition + (distanceToTarget * ml.GetForwardTransform()); + //boreRadarRing.transform.rotation = ml.MissileReferenceTransform.rotation; + //boreRadarRing.transform.localScale = Vector3.one * ((Mathf.Sin(Mathf.Deg2Rad * dynamicBoresight) * distanceToTarget) / 10); + } + else + { + //boreRadarRing.SetActive(false); + //boreRing.SetActive(true); + missileAimerUI.Add((missileReferencePosition + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 48, 0)); + //boreRing.transform.SetPositionAndRotation(missileReferencePosition + (unlockedAimerDist * ml.GetForwardTransform()), rotation); + //boreRing.transform.localScale = Vector3.one * (Mathf.Sin(Mathf.Deg2Rad * 1) * unlockedAimerDist) / 10; + break; + } + } + else + { + //boreRadarRing.SetActive(false); + //.SetActive(true); + missileAimerUI.Add((missileReferencePosition + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 48, 0)); + //boreRing.transform.SetPositionAndRotation(missileReferencePosition + (unlockedAimerDist * ml.GetForwardTransform()), rotation); + //boreRing.transform.localScale = Vector3.one * (Mathf.Sin(Mathf.Deg2Rad * 1) * unlockedAimerDist) / 10; + } + break; + } + case MissileBase.TargetingModes.None: + { + if (selectedWeapon.GetWeaponClass() != WeaponClasses.Bomb) + { + //boreRadarRing.SetActive(false); + //boreRing.SetActive(true); + missileAimerUI.Add((missileReferencePosition + (1250 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, 48, 0)); + //boreRing.transform.SetPositionAndRotation(missileReferencePosition + (unlockedAimerDist * ml.GetForwardTransform()), rotation); + //boreRing.transform.localScale = Vector3.one * (Mathf.Sin(Mathf.Deg2Rad * 1) * unlockedAimerDist) / 10; + } + break; + } + } + if (ml.TargetingMode == MissileBase.TargetingModes.Gps || BDArmorySetup.Instance.showingWindowGPS) + { + if (designatedGPSCoords != Vector3d.zero) + { + missileAimerUI.Add((VectorUtils.GetWorldSurfacePostion(designatedGPSCoords, vessel.mainBody), BDArmorySetup.Instance.greenSpikedPointCircleTexture, 22, 0)); + } + } + } + } + else + { + ShowBoreRing(false); + //boreRadarRing.SetActive(false); + } + } + } + + 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(CurrentMissile); + SearchForRadarSource(); + } + CalculateMissilesAway(); + ClearQueuedLaunches(); + } + + public void UpdateQueuedLaunches(TargetInfo target, MissileBase missile, bool addition, bool sourceVessel = true) + { + if (!guardMode) return; + if (target) + { + bool activeSARH = (missile.TargetingMode == MissileBase.TargetingModes.Radar || missile.TargetingMode == MissileBase.TargetingModes.Gps); + if (queuedLaunches.TryGetValue(target, out int[] tempArr)) + { + if (addition) + { + queuedLaunchesTimeSinceLastAddition = Time.time; + queuedLaunchesRequireClearing = true; + if (sourceVessel) + tempArr[0]++; + if (activeSARH) + tempArr[1]++; + } + else + { + if (sourceVessel) + tempArr[0]--; + if (activeSARH) + tempArr[1]--; + } + } + else + { + if (addition) + { + queuedLaunchesTimeSinceLastAddition = Time.time; + queuedLaunchesRequireClearing = true; + queuedLaunches.Add(target, [sourceVessel ? 1 : 0, activeSARH ? 1 : 0]); + } + else + Debug.LogWarning($"[BDArmory.MissileFire]: {vessel.GetName()} attempted to remove missile: {missile.shortName} from queuedLaunches for: {target.Vessel.GetName()} but no entry was found! queuedLaunches: {string.Join(", ", queuedLaunches.Select(ql => $"{ql.Key.Vessel.GetName()}:{string.Join(",", ql.Value)}"))}"); + } + if (sourceVessel && target == currentTarget) + { + if (addition) + firedMissiles++; + else + firedMissiles--; + } + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.GetName()} updating queuedLaunches for {((target != null && target.Vessel != null) ? target.Vessel.GetName() : "null")}, activeSARH: {activeSARH}, addition: {addition}."); + } + //else + // Debug.LogWarning($"[BDArmory.MissileFire] Attempted to update queuedLaunches with missile: {missile.shortName} but target was null!"); + } + + private void ClearQueuedLaunches() + { + if (queuedLaunchesRequireClearing && queuedLaunchesTimeSinceLastAddition - Time.time > 20f) + { + queuedLaunches.Clear(); + queuedLaunchesRequireClearing = false; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire] Clearing queuedLaunches."); + } + } + + public void UpdateMissilesAway(TargetInfo target, MissileBase missile, bool sourceVessel = true) + { + if (!guardMode) return; + if (target) + { + bool activeSARH = (missile.TargetingMode == MissileBase.TargetingModes.Radar || missile.TargetingMode == MissileBase.TargetingModes.Gps); + if (missilesAway.TryGetValue(target, out int[] tempArr)) + { + if (sourceVessel) + tempArr[0]++; + if (activeSARH) + tempArr[1]++; + } + else + { + missilesAway.Add(target, [sourceVessel ? 1 : 0, activeSARH ? 1 : 0]); + engagedTargets++; + } + + if (sourceVessel && currentTarget != null && currentTarget == target) //change to previous target? + firedMissiles++; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire] Updating missilesAway for {((target != null && target.Vessel != null) ? target.Vessel.GetName() : "null")}, activeSARH: {activeSARH}."); + } + //else + // Debug.LogWarning($"[BDArmory.MissileFire] Attempted to update missilesAway with missile: {missile.shortName} but target was null!"); + } + + public int[] GetMissilesAway(TargetInfo target) + { + if (!guardMode) return [0, 0]; + if (target) + { + int[] results = [0, 0]; + if (missilesAway.TryGetValue(target, out int[] missiles)) //change to previous target?) + { + results[0] += missiles[0]; + results[1] += missiles[1]; + } + if (queuedLaunches.TryGetValue(target, out int[] launching)) + { + results[0] += launching[0]; + results[1] += launching[1]; + } + return results; + } + return [0, 0]; + } + + private void CalculateMissilesAway() //FIXME - add check for identically named vessels + { + + missilesAway.Clear(); + // int tempMissilesAway = 0; + //firedMissiles = 0; + if (!guardMode) return; + bool sourceVessel; + MissileBase missileBase; + + using (List.Enumerator Missiles = BDATargetManager.FiredMissiles.GetEnumerator()) + while (Missiles.MoveNext()) + { + if (Missiles.Current == null) continue; + + missileBase = Missiles.Current as MissileBase; + + if (missileBase.targetVessel == null) continue; + sourceVessel = missileBase.SourceVessel == this.vessel; + if (!sourceVessel) + { + if (!missileBase.ActiveRadar && missileBase.TargetingMode == MissileBase.TargetingModes.Radar && missileBase.radarTarget.exists && missileBase.radarTarget.lockedByRadar) + { + if (missileBase.radarTarget.lockedByRadar.vessel != this.vessel) + continue; + } + else + continue; + } + //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 + { + bool activeSARH = (missileBase.TargetingMode == MissileBase.TargetingModes.Radar && !missileBase.ActiveRadar) || (missileBase.TargetingMode == MissileBase.TargetingModes.Gps && missileBase.gpsUpdates >= 0); + if (missilesAway.TryGetValue(missileBase.targetVessel, out int[] tempArr)) + { + if (sourceVessel) + tempArr[0]++; //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 + if (activeSARH) + tempArr[1]++; + } + else + { + missilesAway.Add(missileBase.targetVessel, [sourceVessel ? 1 : 0, activeSARH ? 1 : 0]); + } + } + } + firedMissiles = 0; + if (currentTarget != null) //change to previous target? + { + if (missilesAway.TryGetValue(currentTarget, out int[] missiles)) + firedMissiles += missiles[0]; + if (queuedLaunches.TryGetValue(currentTarget, out int[] launching)) + firedMissiles += launching[0]; + } + if (!BDATargetManager.FiredMissiles.Contains(PreviousMissile)) PreviousMissile = null; + engagedTargets = missilesAway.Count; + //this.missilesAway = tempMissilesAway; + } + + public void CheckMissiles() + { + // Only do this check if we're in guard mode, assume that if a human player is control they're properly managing their radars + if (!guardMode) return; + + MissileBase missileBase; + + using (List.Enumerator Missiles = BDATargetManager.FiredMissiles.GetEnumerator()) + while (Missiles.MoveNext()) + { + if (Missiles.Current == null) continue; + + missileBase = Missiles.Current as MissileBase; + + if (missileBase.targetVessel == null) continue; + // We assume that if the FiredByWM is the ParentWM that both vessels are on the same team, maybe not always a correct assumption? + if (missileBase.FiredByWM == ParentWM) + { + // Ensure the missile is actually one that has launched + 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 + { + // If the missile is using a radar for targeting + if ((missileBase.TargetingMode == MissileBase.TargetingModes.Radar && !missileBase.ActiveRadar) || (missileBase.TargetingMode == MissileBase.TargetingModes.Gps && missileBase.gpsUpdates >= 0)) + { + // Determine if the missile is using a radar that is onboard this vessel + if (missileBase.radarTarget.exists && missileBase.radarTarget.lockedByRadar && missileBase.radarTarget.lockedByRadar.vessel == vessel) + { + // If it is, then swap command of the missile over to this vessel + missileBase.FiredByWM = this; + missileBase.SourceVessel = vessel; + + // Check if we have VRD and that it's our VRD + if (!vesselRadarData || vesselRadarData.vessel != vessel) + { + vesselRadarData = vessel.gameObject.GetComponent(); + if (vesselRadarData == null) + vesselRadarData = vessel.gameObject.AddComponent(); + + vesselRadarData.weaponManager = this; + } + + // Set VRD + missileBase.vrd = vesselRadarData; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire] Missile source swap! {vessel}, launched from {ParentWM.vessel} has taken command of missile {missileBase.shortName}!"); + } + } + } + } + } + } + public override void OnFixedUpdate() + { + base.OnFixedUpdate(); + if (vessel == null || !vessel.gameObject.activeInHierarchy) return; + if (!IsPrimaryWM) + { + if (modulesNeedRefreshing) RefreshModules(); // Refresh the modules in case we've become the primary WM. + return; // Don't do anything else if we're not in control. + } + if (weaponsListNeedsUpdating) UpdateList(); + + if (!vessel.packed) + { + UpdateWeaponIndex(); + UpdateGuidanceTargets(); + } + + if (guardMode && vessel.IsControllable) //isControllable returns false if Commsnet is enabled and probecore craft has no antenna + { + GuardMode(); + } + else + { + if (nonGuardModeCMs && vessel.IsControllable) UpdateGuardViewScan(); // Scan for missiles and automatically deploy CMs / enable RWR. + targetScanTimer = -100; + } + bombFlightTime = BombAimer(); + } + + void PointDefence() + { + if (vessel.IsControllable) + { + if (Time.time - PDScanTimer > 0.1f) + { + PointDefenseTurretFiring(); + PDScanTimer = Time.time; + } + } + else PDScanTimer = -100; + } + + void OnDestroy() + { + BDArmorySetup.OnVolumeChange -= UpdateVolume; + BDArmorySetup.OnSavedSettings -= ClampVisualRange; + GameEvents.onPartJointBreak.Remove(OnPartJointBreak); + GameEvents.onPartDie.Remove(OnPartDie); + GameEvents.onVesselPartCountChanged.Remove(UpdateMaxGunRange); + GameEvents.onVesselPartCountChanged.Remove(UpdateCurrentHP); + GameEvents.onVesselPartCountChanged.Remove(OnVesselPartCountChanged); + GameEvents.onEditorPartPlaced.Remove(UpdateMaxGunRange); + GameEvents.onEditorPartDeleted.Remove(UpdateMaxGunRange); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Earlyish, PointDefence); + if (boreRing != null) ShowBoreRing(false); + //if (boreRadarRing != null) boreRadarRing.SetActive(false); + } + + void ClampVisualRange() + { + guardRange = Mathf.Clamp(guardRange, BDArmorySettings.RUNWAY_PROJECT ? 20000 : 0, BDArmorySettings.MAX_GUARD_VISUAL_RANGE); + } + + void OnGUI() + { + if (!IsPrimaryWM) return; // Don't do anything if we're not in control. + if (!BDArmorySettings.DEBUG_LINES && lr != null) { lr.enabled = false; } + if (HighLogic.LoadedSceneIsFlight && vessel == FlightGlobals.ActiveVessel && + BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled) + { + if (BDArmorySettings.DEBUG_LINES) + { + if (incomingMissileVessel) + { + GUIUtils.DrawLineBetweenWorldPositions(part.transform.position, + incomingMissileVessel.transform.position, 5, Color.cyan); + } + if (guardTarget != null) + GUIUtils.DrawLineBetweenWorldPositions(guardTarget.LandedOrSplashed ? guardTarget.CoM + ((guardTarget.vesselSize.y / 2) * vessel.up) : guardTarget.CoM, + ((vessel.LandedOrSplashed && (guardTarget.CoM - transform.position).sqrMagnitude > 2250000f) ? + transform.position + (SurfaceVisionOffset.Evaluate((guardTarget.CoM - transform.position).magnitude) * vessel.up) : transform.position), 3, Color.yellow); + } + + /* + if (showBombAimer) + { + //MissileBase ml = CurrentMissile; + if (ml) + { + float size = 128; + Texture2D texture = BDArmorySetup.Instance.greenCircleTexture; + + if ((ml is MissileLauncher && ((MissileLauncher)ml).guidanceActive) || ml is BDModularGuidance) + { + texture = BDArmorySetup.Instance.largeGreenCircleTexture; + size = 256; + } + GUIUtils.DrawTextureOnWorldPos(bombAimerPosition, texture, new Vector2(size, size), 0); + } + //also show this for airdropped torpedoes? Have torpedo aimer circle start from surface instead of torpedo prograde for airdropped torps? If yes, then this should also be in OnUpdate instead of here and add an entry to missileAimerUI. + } + */ + //MISSILE LOCK HUD + foreach (var entry in missileAimerUI) + { + var (position, texture, size, wobble) = entry; + GUIUtils.DrawTextureOnWorldPos(position, texture, new Vector2(size, size), wobble); + } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES || BDArmorySettings.DEBUG_WEAPONS) + debugString.Length = 0; + int lineCount = 0; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugString.AppendLine($"Missiles away: {firedMissiles}; Current Target: {currentTarget}; targeted vessels: {engagedTargets}"); + + if (missileIsIncoming) + { + foreach (var incomingMissile in results.incomingMissiles) + 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 (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 = []; + List weaponAimDebugStrings = []; + HashSet validClasses = [WeaponClasses.Gun, WeaponClasses.Rocket, WeaponClasses.DefenseLaser]; + foreach (var weaponCandidate in VesselModuleRegistry.GetModules(vessel)) // Show each weapon, not each weapon group (which might contain multiple weapon types). + { + 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)); + + weaponAimDebugStrings.Add($" - {weapon.shortName} - 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 {Mathf.Min(1, weapon.targetCosAngle)}, wpn CosAngle {weapon.targetAdjustedMaxCosAngle}, Wpn Autofire {weapon.autoFire}{(weapon.autoFire ? "" : $" ({weapon.autoFireFailReason})")}, 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 != 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}, stale target: {staleTarget}{(staleTarget ? $" ({weapon.staleGoodTargetTime:0.0}s/{detectedTargetTimeout:0.0}s)" : "")}"); + 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}°)"); + if (weapon.targetInVisualRange && BDArmorySettings.AIMING_VISUAL_MALUS > 0) weaponAimDebugStrings.Add($" - Malus: {BDArmorySettings.AIMING_VISUAL_MALUS * weapon.kinematicAimMalus.magnitude:F2}m, shots: {weapon.shotsFiredSinceAcquiringTarget}, reduction: {weapon.malusReduction:G4}x"); + +#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) + { + if (weaponHeatDebugStrings.Count > 0) debugString.AppendLine("Weapon Heat:\n" + string.Join("\n", weaponHeatDebugStrings)); + if (weaponAimDebugStrings.Count > 0) debugString.AppendLine("Aim debugging:\n" + string.Join("\n", weaponAimDebugStrings)); + lineCount += weaponHeatDebugStrings.Count + weaponAimDebugStrings.Count; + } + if (!string.IsNullOrEmpty(bombAimerDebugString)) debugString.AppendLine($"Bomb aimer: {bombAimerDebugString}"); + } + } + lineCount += debugString.Length; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES || BDArmorySettings.DEBUG_WEAPONS) + GUI.Label(new Rect(200, Screen.height - 700, Screen.width / 2 - 200, 16 * lineCount), debugString.ToString()); + if (BDArmorySettings.DEBUG_TELEMETRY && !string.IsNullOrEmpty(dynRangeDebug)) + GUI.Label(new Rect(800, 600, 200, 200), dynRangeDebug); + } + //else + //{ + // if (boreRing != null) boreRing.SetActive(false); + // //if (boreRadarRing != null) boreRadarRing.SetActive(false); + //} + } + + bool CheckMouseIsOnGui() + { + return GUIUtils.CheckMouseIsOnGui(); + } + + #endregion KSP Events + + #region Enumerators + + IEnumerator StartupListUpdater() + { + yield return new WaitWhileFixed(() => !FlightGlobals.ready || vessel != null || vessel.packed || !vessel.loaded); + UpdateList(); + } + + IEnumerator ReenableGuardModeWhenReady() + { + yield return new WaitWhileFixed(() => !FlightGlobals.ready || (vessel is not null && (vessel.packed || !vessel.loaded))); // Wait at least one frame so other modules have started. + ToggleGuardMode(); // Then re-enable it so that other effects from enabling it occur. + } + + IEnumerator MissileWarningResetRoutine() + { + while (enabled) + { + 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; + } + } + + 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.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; + } + + 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.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Triggering under attack warning on {vessel.vesselName} by {incomingThreatVessel.vesselName}"); } + if (underAttackAG != KSPActionGroup.None) vessel.ActionGroups.SetGroup(underAttackAG, true); + 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}"); } + if (underAttackAG != KSPActionGroup.None) vessel.ActionGroups.SetGroup(underAttackAG, false); + underAttack = false; + } + + IEnumerator GuardTurretRoutine() + { + if (SetDeployableWeapons()) + { + yield return new WaitForSecondsFixed(2f); + } + + if (gameObject.activeInHierarchy) + //target is out of visual range, try using sensors + { + if (guardTarget.LandedOrSplashed) + { + if (targetingPods.Count > 0) + { + float scaledDistance = Mathf.Max(400f, 0.004f * (float)guardTarget.srfSpeed * (float)guardTarget.srfSpeed); + using (List.Enumerator tgp = targetingPods.GetEnumerator()) + while (tgp.MoveNext()) + { + if (tgp.Current == null) continue; + if (!tgp.Current.enabled || (tgp.Current.cameraEnabled && tgp.Current.groundStabilized && + !((tgp.Current.groundTargetPosition - guardTarget.CoM).sqrMagnitude > scaledDistance))) continue; + tgp.Current.EnableCamera(); + yield return StartCoroutine(tgp.Current.PointToPositionRoutine(guardTarget.CoM, guardTarget)); + //yield return StartCoroutine(tgp.Current.PointToPositionRoutine(TargetInfo.TargetCOMDispersion(guardTarget))); + if (!tgp.Current) continue; + if (tgp.Current.groundStabilized && guardTarget && + (tgp.Current.groundTargetPosition - guardTarget.CoM).sqrMagnitude < scaledDistance) + { + tgp.Current.slaveTurrets = true; + StartGuardTurretFiring(); + yield break; + } + tgp.Current.DisableCamera(); + } + } + + if (!guardTarget || (guardTarget.CoM - vessel.CoM).sqrMagnitude > guardRange * guardRange) + { + SetTarget(null); //disengage, sensors unavailable. + yield break; + } + } + else + { + // Turn on radars if off + if (!results.foundAntiRadiationMissile) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if ((rd.Current != null || rd.Current.canLock) && rd.Current.sonarMode == ModuleRadar.SonarModes.None) + { + rd.Current.EnableRadar(); + _radarsEnabled = true; + } + } + } + + // Try to lock target, or if already locked, fire on it + if (vesselRadarData && + (!vesselRadarData.locked || + (vesselRadarData.lockedTargetData.targetData.predictedPosition - guardTarget.CoM) + .sqrMagnitude > 40 * 40)) + { + //vesselRadarData.TryLockTarget(guardTarget.transform.position); + //vesselRadarData.TryLockTarget(guardTarget); + + if (vesselRadarData.locked) + { + if (!vesselRadarData.SwitchActiveLockedTarget(guardTarget)) + vesselRadarData.TryLockTarget(guardTarget); + } + else + vesselRadarData.TryLockTarget(guardTarget); + + yield return new WaitForSecondsFixed(0.5f); + if (guardTarget && vesselRadarData && vesselRadarData.locked && + vesselRadarData.lockedTargetData.vessel == guardTarget) + { + vesselRadarData.SlaveTurrets(); + StartGuardTurretFiring(); + yield break; + } + } + else if (guardTarget && vesselRadarData && vesselRadarData.locked && + vesselRadarData.lockedTargetData.vessel == guardTarget) + { + vesselRadarData.SlaveTurrets(); + StartGuardTurretFiring(); + yield break; + } + + if (!guardTarget || (guardTarget.CoM - vessel.CoM).sqrMagnitude > guardRange * guardRange) + { + SetTarget(null); //disengage, sensors unavailable. + yield break; + } + } + } + + StartGuardTurretFiring(); + yield break; + } + + IEnumerator ResetMissileThreatDistanceRoutine() + { + yield return new WaitForSecondsFixed(8); + incomingMissileDistance = float.MaxValue; + incomingMissileTime = float.MaxValue; + } + + IEnumerator GuardMissileRoutine(Vessel targetVessel, MissileBase ml) + { + if (ml && !guardFiringMissile) + { + bool dumbfiring = false; + guardFiringMissile = true; + var wait = new WaitForFixedUpdate(); + float tryLockTime = targetVessel.IsMissile() ? 0.05f : 0.25f; // More urgency for incoming missiles + switch (ml.TargetingMode) + { + case MissileBase.TargetingModes.Radar: + { + if (vesselRadarData) //no check for radar present, but off/out of juice + { + float BayTriggerTime = -1; + if (SetCargoBays()) + { + BayTriggerTime = Time.time; + //yield return new WaitForSecondsFixed(2f); //so this doesn't delay radar targeting stuff below + } + float attemptLockTime = Time.time; + while (ml && (!vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != targetVessel)) && Time.time - attemptLockTime < 2) + { + bool lockSuccess = false; + if (vesselRadarData.locked) + { + if (vesselRadarData.SwitchActiveLockedTarget(targetVessel)) + lockSuccess = true; + else + { + // if a low lock capacity radar, and it already has a lock on another target, TLT will return false, because the radar already at lock cap + // end result: radar lock stuck on wrong target; need unlock, then lock if lock num = max locks + //if availableLocks, tryLocktarget, else, unlock target -> try locktarget + /*if (MaxRadarLocks <= possibleTargets.Count) //not currently checking if available radar locks are viable, e.g. a rear-facing radar w/ lock capability + { + if (PreviousMissile == null || (PreviousMissile.TargetingMode != MissileBase.TargetingModes.Radar && PreviousMissile.TargetingMode != MissileBase.TargetingModes.Inertial && PreviousMissile.TargetingMode != MissileBase.TargetingModes.Gps)) + vesselRadarData.UnlockAllTargets(); + else if (PreviousMissile.TargetingMode == MissileBase.TargetingModes.Radar) + { + if (PreviousMissile.ActiveRadar && PreviousMissile.targetVessel != null) //previous missile has gone active + vesselRadarData.UnlockSelectedTarget(PreviousMissile.targetVessel.Vessel); //no longer need that lock for guidance, remove + else + { + if (MaxRadarLocks > 1) //guiding a SARH, but we have a spare lock... + { + vesselRadarData.UnlockAllTargets(); //clear everything... + vesselRadarData.TryLockTarget(PreviousMissile.targetVessel.Vessel); //and immediately relock the SARH target vessel as a work around for only having unlock everything, and unlockselected + } + else + { + if (!CurrentMissile.radarLOAL) //LOAL missiles at least can be dumbfired... + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} cannot fire radar missile, all locks in use! Aborting loaunch!"); + break; //we need single lock for our previous SARH against the previous target, break; current radar missile will have to wait. + } + } + } + vesselRadarData.TryLockTarget(targetVessel); + }*/ + lockSuccess = vesselRadarData.TryLockTarget(targetVessel); + + /*if (!vesselRadarData.TryLockTarget(targetVessel)) + { + if (!CurrentMissile.radarLOAL) //LOAL missiles at least can be dumbfired... + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} cannot fire radar missile, all locks in use! Aborting launch!"); + break; //we need single lock for our previous SARH against the previous target, break; current radar missile will have to wait. + }*/ + } + } + else + lockSuccess = vesselRadarData.TryLockTarget(targetVessel); + + // If not successful, wait for `tryLockTime` before making another lock attempt + if (!lockSuccess) + yield return new WaitForSecondsFixed(tryLockTime); + else + { + // If successful, wait for a FixedUpdate while `UpdateLockedTargets` runs + yield return wait; + break; + } + } + // 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 + float attemptStartTime = Time.time; + MissileLauncher mlauncher = ml as MissileLauncher; + if (targetVessel) + { + float angle = 999; + float turretStartTime = attemptStartTime; + if (ml.customTurret.Count > 0) + { + vesselRadarData.SlaveTurrets(); + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > 5)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel.CoM, targetVessel.Velocity(), + (ml.GuidanceMode == GuidanceModes.AAMLoft || ml.GuidanceMode == GuidanceModes.Kappa)); + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing radarMsl custom turret to bear, angle {angle:F2}..."); + } + } + else + { + if (mlauncher) + { + if (mlauncher.missileTurret && vesselRadarData.locked) + { + // Technically this call can be removed, especially since it could interfere with simultaneous multi-turret + // operations, but it also provides a better track + vesselRadarData.SlaveTurrets(); + mlauncher.missileTurret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && angle > mlauncher.missileTurret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + //mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, targetVessel.CoM, targetVessel.Velocity(), mlauncher.missileTurret.turretLoft, mlauncher.missileTurret.turretLoftFac); + //mlauncher.missileTurret.SlavedAim(); + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing radarMsl turret to bear..."); + yield return wait; + } + mlauncher.missileTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && mlauncher.multiLauncher.turret && vesselRadarData.locked) + { + // Technically this call can be removed, especially since it could interfere with simultaneous multi-turret + // operations, but it also provides a better track + vesselRadarData.SlaveTurrets(); + mlauncher.multiLauncher.turret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && angle > mlauncher.multiLauncher.turret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.multiLauncher.turret.finalTransform.forward, mlauncher.multiLauncher.turret.slavedTargetPosition - mlauncher.multiLauncher.turret.finalTransform.position); + //mlauncher.multiLauncher.turret.slaved = true; + mlauncher.multiLauncher.turret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, targetVessel.CoM, targetVessel.Velocity(), mlauncher.multiLauncher.turret.turretLoft, mlauncher.multiLauncher.turret.turretLoftFac); + //mlauncher.multiLauncher.turret.SlavedAim(); + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing radarMsl turret to bear..."); + yield return wait; + } + mlauncher.multiLauncher.turret.slavedGuard = false; + } + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + } + } + yield return wait; + + // if (ml && guardTarget && vesselRadarData.locked && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) + //no check if only non-locking scanning radars on craft + //if (ml && guardTarget && ((vesselRadarData.locked && vesselRadarData.lockedTargetData.vessel == guardTarget) || ml.radarLOAL) && GetLaunchAuthorization(guardTarget, this)) //allow lock on after launch missiles to fire of target scanned by not locked? + if (ml && targetVessel) + { + if (vesselRadarData && vesselRadarData.locked && vesselRadarData.lockedTargetData.vessel == targetVessel) + { + if (GetLaunchAuthorization(targetVessel, this, ml)) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing on target {targetVessel.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(ml, true, targetVessel); + //StartCoroutine(MissileAwayRoutine(mlauncher)); + } + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName}'s {(CurrentMissile ? CurrentMissile.name : "null missile")} could not lock, attempting unguided fire."); + dumbfiring = true; //so let them be used as unguided ordnance + } + } + } + else //no radar, missiles now expensive unguided ordnance + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName}'s {(CurrentMissile ? CurrentMissile.name : "null missile")} has no radar, attempting unguided fire."); + dumbfiring = true; //so let them be used as unguided ordnance + } + break; + } + case 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() //maybe use vrd.UnlockCurrentTarget() instead? //Or check if previousMissile isn't a SARH that needs that lock... + vesselRadarData.UnslaveTurrets(); + } + + if (SetCargoBays()) + { + yield return new WaitForSecondsFixed(2f); + } + + float attemptStartTime = Time.time; + float attemptDuration = Mathf.Max(targetScanInterval * 0.75f, 5f); + MissileLauncher mlauncher = ml as MissileLauncher; + // Have to get both due to the potential for the missile to be a cluster missile on a standard missileTurret + // Also good to have them so we can ensure slavedGuard gets set to false at the end even if ml/mLauncher is destroyed + MissileTurret mLauncherTurret = mlauncher.missileTurret; + MissileTurret multiLauncherTurret = mlauncher.multiLauncher ? mlauncher.multiLauncher.turret : null; + if (mLauncherTurret) mLauncherTurret.slavedGuard = true; + if (multiLauncherTurret) multiLauncherTurret.slavedGuard = true; + while (ml && targetVessel && Time.time - attemptStartTime < attemptDuration && (!heatTarget.exists || (heatTarget.predictedPosition - targetVessel.CoM).sqrMagnitude > 40 * 40)) + { + if (ml.customTurret.Count > 0) + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + ml.customTurret[i].slavedTargetPosition = targetVessel.CoM; + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + } + else + { + // We assume that we know where the target is... + if (mLauncherTurret) + { + // Point turret towards it if it exists... + mLauncherTurret.slavedTargetPosition = targetVessel.CoM; + } + if (multiLauncherTurret) + { + // Point turret towards it if it exists... + multiLauncherTurret.slavedTargetPosition = targetVessel.CoM; + } + } + yield return wait; + } + if (mLauncherTurret) mLauncherTurret.slavedGuard = false; + if (multiLauncherTurret) multiLauncherTurret.slavedGuard = false; + + if (BDArmorySettings.DEBUG_MISSILES && CurrentMissile) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName}'s {CurrentMissile.GetShortName()} has heatTarget: {heatTarget.exists}"); + //try uncaged IR lock with radar + if (ml && ml.activeRadarRange > 0) //defaults to 6k for non-radar missiles, using negative value for differentiating passive acoustic vs heater + { + if (targetVessel && !heatTarget.exists && vesselRadarData) + { + if (_radarsEnabled) + { + if (vesselRadarData.locked) + { + if ((vesselRadarData.lockedTargetData.targetData.predictedPosition - targetVessel.CoM).sqrMagnitude > 40 * 40) + { + if (!vesselRadarData.SwitchActiveLockedTarget(targetVessel)) + vesselRadarData.TryLockTarget(targetVessel); + yield return new WaitForSecondsFixed(Mathf.Min(1, (targetScanInterval * tryLockTime))); + } + } + else + vesselRadarData.TryLockTarget(targetVessel); + } + else if (_irstsEnabled) + { + heatTarget = vesselRadarData.activeIRTarget(targetVessel, this); + yield return new WaitForSecondsFixed(Mathf.Min(1, (targetScanInterval * tryLockTime))); + } + } + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName}'s heatTarget locked"); + // if (AIMightDirectFire() && ml && heatTarget.exists) + // { + // float LAstartTime = Time.time; + // while (Time.time - LAstartTime < 3 && AIMightDirectFire() && GetLaunchAuthorization(guardTarget, this)) + // { + // yield return new WaitForFixedUpdate(); + // } + // yield return new WaitForSecondsFixed(0.5f); + // } + + //wait for missile turret to point at target + attemptStartTime = Time.time; + //mlauncher = ml as MissileLauncher; + if (targetVessel) + { + float angle = 999; + float turretStartTime = attemptStartTime; + if (ml.customTurret.Count > 0 && heatTarget.exists) + { + vesselRadarData.SlaveTurrets(); + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > 5)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, heatTarget.predictedPosition, heatTarget.velocity, + (ml.GuidanceMode == GuidanceModes.AAMLoft || ml.GuidanceMode == GuidanceModes.Kappa)); + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing IRMsl custom turret to bear..."); + } + } + else + { + if (mlauncher) + { + if (mLauncherTurret && heatTarget.exists) + { + mLauncherTurret.slavedGuard = true; + while (heatTarget.exists && Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && angle > mLauncherTurret.fireFOV) + { + //mlauncher.missileTurret.slaved = true; + mLauncherTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, heatTarget.predictedPosition, heatTarget.velocity, mLauncherTurret.turretLoft, mLauncherTurret.turretLoftFac); + //mlauncher.missileTurret.SlavedAim(); + yield return wait; + angle = VectorUtils.Angle(mLauncherTurret.finalTransform.forward, mLauncherTurret.slavedTargetPosition - mLauncherTurret.finalTransform.position); + } + mLauncherTurret.slavedGuard = false; + } + if (multiLauncherTurret && heatTarget.exists) + { + multiLauncherTurret.slavedGuard = true; + while (heatTarget.exists && Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && angle > multiLauncherTurret.fireFOV) + { + //mlauncher.multiLauncher.turret.slaved = true; + multiLauncherTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, heatTarget.predictedPosition, heatTarget.velocity, multiLauncherTurret.turretLoft, multiLauncherTurret.turretLoftFac); + //mlauncher.multiLauncher.turret.SlavedAim(); + yield return wait; + angle = VectorUtils.Angle(multiLauncherTurret.finalTransform.forward, multiLauncherTurret.slavedTargetPosition - multiLauncherTurret.finalTransform.position); + } + multiLauncherTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + } + } + yield return wait; + + if (ml && heatTarget.exists && heatTarget.signalStrength * ((BDArmorySettings.ASPECTED_IR_SEEKERS && Vector3.Dot(targetVessel.vesselTransform.up, ml.transform.forward) > 0.25f) ? ml.frontAspectHeatModifier : 1) < ml.heatThreshold) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: Heatseeker heat threashold not met, aborting launch attempt."); + break; //in case rearAspect missile doesn't have a heatTarget, then GMR creates a heatTarget via radar lock in the 'if (targetVessel && !heatTarget.exists && vesselRadarData)' codeblock above + } + // if (guardTarget && ml && heatTarget.exists && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) + if (targetVessel && ml && heatTarget.exists && heatTarget.vessel == targetVessel && GetLaunchAuthorization(targetVessel, this, ml)) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing on target {targetVessel.GetName()}"); + + FireCurrentMissile(ml, true, targetVessel); + //StartCoroutine(MissileAwayRoutine(mlauncher)); + } + //else //event that heatTarget.exists && heatTarget != guardtarget? + break; + } + case MissileBase.TargetingModes.Gps: + { + float targetAccuracyThreshold = Mathf.Max(400, 0.013f * (float)targetVessel.srfSpeed * (float)targetVessel.srfSpeed); + if (SetCargoBays()) + { + yield return new WaitForSecondsFixed(2f); + if (vessel == null || targetVessel == null) break; + } + //have GPS missiles require a targeting cam for coords? GPS bombs require one. + float attemptStartTime; + bool foundTargetInDatabase = false; + using (List.Enumerator gps = BDATargetManager.GPSTargetList(Team).GetEnumerator()) + while (gps.MoveNext()) + { + if ((gps.Current.worldPos - targetVessel.CoM).sqrMagnitude > 100) continue; + designatedGPSInfo = gps.Current; + foundTargetInDatabase = true; + break; + } + if (!foundTargetInDatabase) + { + bool assignedCamera = false; //add sanity check condition for targetingPods > 0, but none of them are valid + 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; + if (tgp.Current.maxRayDistance * tgp.Current.maxRayDistance < (tgp.Current.cameraParentTransform.position - targetVessel.CoM).sqrMagnitude) continue; //target further than max camera range (def ~15.5km) + tgp.Current.EnableCamera(); + tgp.Current.CoMLock = true; + assignedCamera = true; + yield return StartCoroutine(tgp.Current.PointToPositionRoutine(targetVessel.CoM, targetVessel)); + } + } + else //no cam, do friendlies have one? + { + if (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude < targetAccuracyThreshold) //ally target acquisition + { + assignedCamera = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No targetCam, using allied {foundCam.vessel.vesselName}'s target camera!"); + } + } + //search for a laser point that corresponds with target vessel + if (assignedCamera) //not using laserPointdetected/foundCam because that's true as long as *somewhere* there is an active cam (which may be out of range of our particular target here) + { + attemptStartTime = Time.time; + float attemptDuration = targetScanInterval * 0.75f; + while (Time.time - attemptStartTime < attemptDuration && (!laserPointDetected || (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude > targetAccuracyThreshold))) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: lasDot: {laserPointDetected}; foundCam: {foundCam}; Attempting camera lock... {(foundCam ? (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude : "")}"); + yield return wait; + } + //if (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude > Mathf.Max(400, 0.013f * (float)guardTarget.srfSpeed * (float)guardTarget.srfSpeed)) + if (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude < targetAccuracyThreshold) + designatedGPSInfo = new GPSTargetInfo(VectorUtils.WorldPositionToGeoCoords(foundCam.groundTargetPosition, vessel.mainBody), targetVessel.vesselName.Substring(0, Mathf.Min(12, targetVessel.vesselName.Length))); + else //cam gimbal locked/target behind a hill or something + { + dumbfiring = true; //so let them be used as unguided ordnance + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No Camera lock! Available cams: {targetingPods.Count}; assignedCam: {assignedCamera}; Tgt Error Sqr {(foundCam ? (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude : -999)}); attempting radar lock..."); + assignedCamera = false; + } + } + if (!assignedCamera) //no cam, get ranging from radar lock? Limit to aerial targets only to not obsolete the tgtCam? Or see if the speed improvements to camera tracking speed permit cams to now be able to track planes + { + // unguidedWeapon = true; //so let them be used as unguided ordnance + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No targeting cam! Available cams: {targetingPods.Count}; switching to unguided firing"); + //break; + if (vesselRadarData) + { + //comment out section below, and uncomment above, if we don't want radar locks to provide GPS ranging/coord data + float attemptLockTime = Time.time; + while (ml && (!vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != targetVessel)) && Time.time - attemptLockTime < 2) + { + bool lockSuccess = false; + if (vesselRadarData.locked) + { + if (vesselRadarData.SwitchActiveLockedTarget(targetVessel)) + lockSuccess = true; + else + { + lockSuccess = vesselRadarData.TryLockTarget(targetVessel); + /*if (!vesselRadarData.TryLockTarget(targetVessel)) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} cannot fire GPS missile via radar guidance, all locks in use!"); + break; //we need single lock for our previous SARH against the previous target, break; current radar missile will have to wait. + }*/ + } + } + else + lockSuccess = vesselRadarData.TryLockTarget(targetVessel); + + // If not successful, wait for `tryLockTime` before making another lock attempt + if (!lockSuccess) + yield return new WaitForSecondsFixed(tryLockTime); + else + { + // If successful, wait for a FixedUpdate while `UpdateLockedTargets` runs + yield return wait; + break; + } + } + if (vesselRadarData && vesselRadarData.locked && vesselRadarData.lockedTargetData.vessel == targetVessel) //no GPS coords, missile is now expensive rocket + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: Radar lock! Firing GPS via radar contact."); + dumbfiring = false; //reset if coming from aborted tgtCam lock + designatedGPSInfo = new GPSTargetInfo(VectorUtils.WorldPositionToGeoCoords(targetVessel.CoM, vessel.mainBody), targetVessel.vesselName.Substring(0, Mathf.Min(12, targetVessel.vesselName.Length))); + } + else + { + dumbfiring = true; //so let them be used as unguided ordnance + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No Radar locks! Available cams: {targetingPods.Count}; switching to unguided firing"); + break; + } + } + else + { + dumbfiring = true; //so let them be used as unguided ordnance + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No Radar! Available cams: {targetingPods.Count}; switching to unguided firing"); + break; + } + } + } + attemptStartTime = Time.time; + MissileLauncher mlauncher; + mlauncher = ml as MissileLauncher; + if (targetVessel) + { + float angle = 999; + float turretStartTime = attemptStartTime; + if (ml.customTurret.Count > 0) + { + vesselRadarData.SlaveTurrets(); + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > 5)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel.CoM, targetVessel.Velocity(), + (ml.GuidanceMode == GuidanceModes.AAMLoft || ml.GuidanceMode == GuidanceModes.Kappa)); + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing GPSMsl custom turret to bear..."); + } + } + else + { + if (mlauncher) + { + if (mlauncher.missileTurret) + { + mlauncher.missileTurret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && angle > mlauncher.missileTurret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + //mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, targetVessel.CoM, targetVessel.Velocity(), mlauncher.missileTurret.turretLoft, mlauncher.missileTurret.turretLoftFac); + //mlauncher.missileTurret.SlavedAim(); + yield return wait; + } + mlauncher.missileTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && mlauncher.multiLauncher.turret) + { + mlauncher.multiLauncher.turret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && angle > mlauncher.multiLauncher.turret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.multiLauncher.turret.finalTransform.forward, mlauncher.multiLauncher.turret.slavedTargetPosition - mlauncher.multiLauncher.turret.finalTransform.position); + //mlauncher.multiLauncher.turret.slaved = true; + mlauncher.multiLauncher.turret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, targetVessel.CoM, targetVessel.Velocity(), mlauncher.multiLauncher.turret.turretLoft, mlauncher.multiLauncher.turret.turretLoftFac); + //mlauncher.multiLauncher.turret.SlavedAim(); + yield return wait; + } + mlauncher.multiLauncher.turret.slavedGuard = false; + } + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + } + } + yield return wait; + if (!dumbfiring && vessel && targetVessel) + designatedGPSInfo = new GPSTargetInfo(VectorUtils.WorldPositionToGeoCoords(targetVessel.CoM, vessel.mainBody), targetVessel.vesselName.Substring(0, Mathf.Min(12, targetVessel.vesselName.Length))); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing GPS missile at {designatedGPSInfo.worldPos}"); + + TargetData targetData = null; + + if (!dumbfiring) + { + targetData = new TargetData(designatedGPSCoords); + FireCurrentMissile(ml, true, targetVessel, targetData); + } + //if (FireCurrentMissile(true)) + // StartCoroutine(MissileAwayRoutine(ml)); //NEW: try to prevent launching all missile complements at once... + break; + } + case 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 = ml as MissileLauncher; + MissileTurret mLauncherTurret = mlauncher.missileTurret; + MissileTurret multiLauncherTurret = mlauncher.multiLauncher ? mlauncher.multiLauncher.turret : null; + if (mLauncherTurret) mLauncherTurret.slavedGuard = true; + if (multiLauncherTurret) multiLauncherTurret.slavedGuard = true; + while (Time.time - attemptStartTime < attemptDuration && (!antiRadTargetAcquired || !AntiRadDistanceCheck())) + { + if (ml.customTurret.Count > 0) + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + ml.customTurret[i].slavedTargetPosition = targetVessel.CoM; + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + } + else + { + // We assume that we know where the target is... + if (mLauncherTurret) + { + // Point turret towards it if it exists... + mLauncherTurret.slavedTargetPosition = targetVessel.CoM; + } + if (multiLauncherTurret) + { + // Point turret towards it if it exists... + multiLauncherTurret.slavedTargetPosition = targetVessel.CoM; + } + } + yield return wait; + } + if (mLauncherTurret) mLauncherTurret.slavedGuard = false; + if (multiLauncherTurret) multiLauncherTurret.slavedGuard = false; + + attemptStartTime = Time.time; + //MissileLauncher mlauncher = ml as MissileLauncher; + if (targetVessel && antiRadTargetAcquired) + { + float angle = 999; + float turretStartTime = attemptStartTime; + if (ml.customTurret.Count > 0) + { + vesselRadarData.SlaveTurrets(); + while (antiRadTargetAcquired && Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > 5)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, antiRadiationTarget, targetVessel.Velocity(), + (ml.GuidanceMode == GuidanceModes.AAMLoft || ml.GuidanceMode == GuidanceModes.Kappa)); + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing ARADMsl custom turret to bear..."); + } + } + else + { + if (mlauncher) + { + if (mlauncher.missileTurret) + { + mlauncher.missileTurret.slavedGuard = true; + while (antiRadTargetAcquired && Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && antiRadTargetAcquired && angle > mlauncher.missileTurret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + //mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, antiRadiationTarget, targetVessel.Velocity(), mlauncher.missileTurret.turretLoft, mlauncher.missileTurret.turretLoftFac); + //mlauncher.missileTurret.SlavedAim(); + yield return wait; + } + mlauncher.missileTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && mlauncher.multiLauncher.turret) + { + mlauncher.multiLauncher.turret.slavedGuard = true; + while (antiRadTargetAcquired && Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && mlauncher && antiRadTargetAcquired && angle > mlauncher.multiLauncher.turret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.multiLauncher.turret.finalTransform.forward, mlauncher.multiLauncher.turret.slavedTargetPosition - mlauncher.multiLauncher.turret.finalTransform.position); + //mlauncher.multiLauncher.turret.slaved = true; + mlauncher.multiLauncher.turret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, antiRadiationTarget, targetVessel.Velocity(), mlauncher.multiLauncher.turret.turretLoft, mlauncher.multiLauncher.turret.turretLoftFac); + //mlauncher.multiLauncher.turret.SlavedAim(); + yield return wait; + } + mlauncher.multiLauncher.turret.slavedGuard = false; + } + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + } + } + yield return wait; + if (!antiRadTargetAcquired) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} aborted firing antirad Missile, no antirad target"); + break; + } + if (ml && antiRadTargetAcquired && AntiRadDistanceCheck()) + { + FireCurrentMissile(ml, true, guardTarget); + //StartCoroutine(MissileAwayRoutine(ml)); + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing antiRad missile at {antiRadiationTarget}"); + } + } + break; + } + case MissileBase.TargetingModes.Laser: + { + if (SetCargoBays()) + { + yield return new WaitForSecondsFixed(2f); + } + float targetpaintAccuracyThreshold = Mathf.Max(100f, 0.013f * (float)targetVessel.srfSpeed * (float)targetVessel.srfSpeed); + 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; + if (tgp.Current.maxRayDistance * tgp.Current.maxRayDistance < (tgp.Current.cameraParentTransform.position - targetVessel.CoM).sqrMagnitude) continue; //target further than max camera range (def ~15.5km) + tgp.Current.CoMLock = true; + yield return StartCoroutine(tgp.Current.PointToPositionRoutine(targetVessel.CoM, targetVessel)); + //if (tgp.Current.groundStabilized && (tgp.Current.GroundtargetPosition - guardTarget.transform.position).sqrMagnitude < 20 * 20) + //if ((tgp.Current.groundTargetPosition - guardTarget.transform.position).sqrMagnitude < 10 * 10) + //{ + // tgp.Current.CoMLock = true; // make the designator continue to paint target + // break; + //} + } + } + else //no cam, laser missiles now expensive unguided ordnance + { + if (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude < targetpaintAccuracyThreshold) //ally target acquisition + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No targetCam, using allied {foundCam.vessel.vesselName}'s Laser target!"); + } + else //allied laser dot isn't present + { + dumbfiring = true; //so let them be used as unguided ordnance + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No Laser target! Available cams: {targetingPods.Count}; switching to unguided firing"); + break; + } + } + //search for a laser point that corresponds with target vessel + float attemptStartTime = Time.time; + float attemptDuration = targetScanInterval * 0.75f; + MissileLauncher mlauncher = ml as MissileLauncher; + // Have to get both due to the potential for the missile to be a cluster missile on a standard missileTurret + // Also good to have them so we can ensure slavedGuard gets set to false at the end even if ml/mLauncher is destroyed + MissileTurret mLauncherTurret = mlauncher.missileTurret; + MissileTurret multiLauncherTurret = mlauncher.multiLauncher ? mlauncher.multiLauncher.turret : null; + if (mLauncherTurret) mLauncherTurret.slavedGuard = true; + if (multiLauncherTurret) multiLauncherTurret.slavedGuard = true; + while (Time.time - attemptStartTime < attemptDuration && (!laserPointDetected || (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude > targetpaintAccuracyThreshold))) + { + if (ml.customTurret.Count > 0) + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + ml.customTurret[i].slavedTargetPosition = targetVessel.CoM; + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + } + else + { + // We assume that we know where the target is... + if (mLauncherTurret) + { + // Point turret towards it if it exists... + mLauncherTurret.slavedTargetPosition = targetVessel.CoM; + } + if (multiLauncherTurret) + { + // Point turret towards it if it exists... + multiLauncherTurret.slavedTargetPosition = targetVessel.CoM; + } + } + yield return wait; + } + if (mLauncherTurret) mLauncherTurret.slavedGuard = false; + if (multiLauncherTurret) multiLauncherTurret.slavedGuard = false; + + if (targetVessel && foundCam) + { + float angle = 999; + float turretStartTime = attemptStartTime; + if (ml.customTurret.Count > 0) + { + vesselRadarData.SlaveTurrets(); + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > 5)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = foundCam.groundTargetPosition; + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing LSRMsl custom turret to bear..."); + } + } + else + { + if (mlauncher) + { + if (mlauncher.missileTurret) + { + mlauncher.missileTurret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && mlauncher && mlauncher.isActiveAndEnabled && foundCam && angle > mlauncher.missileTurret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + //mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = foundCam.targetPointPosition; + //mlauncher.missileTurret.SlavedAim(); + yield return wait; + } + mlauncher.missileTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && mlauncher.multiLauncher.turret) + { + mlauncher.multiLauncher.turret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && mlauncher && mlauncher.isActiveAndEnabled && foundCam && angle > mlauncher.multiLauncher.turret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.multiLauncher.turret.finalTransform.forward, mlauncher.multiLauncher.turret.slavedTargetPosition - mlauncher.multiLauncher.turret.finalTransform.position); + //mlauncher.multiLauncher.turret.slaved = true; + mlauncher.multiLauncher.turret.slavedTargetPosition = foundCam.targetPointPosition; + //mlauncher.multiLauncher.turret.SlavedAim(); + yield return wait; + } + mlauncher.multiLauncher.turret.slavedGuard = false; + } + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + } + } + yield return wait; + //Debug.Log($"[GMR_Debug] waiting... laspoint: {laserPointDetected}; foundCam: {foundCam != null}; targetVessel: {targetVessel != null}"); + if (ml && laserPointDetected && foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude < targetpaintAccuracyThreshold) + { + FireCurrentMissile(ml, true, targetVessel); + //StartCoroutine(MissileAwayRoutine(ml)); + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: Laser Target Error: laserdot:{laserPointDetected}, cam:{(foundCam != null ? foundCam : "null")}, pointingatTgt:{(foundCam != null ? (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude < Mathf.Max(100, 0.013f * (float)targetVessel.srfSpeed * (float)targetVessel.srfSpeed) : "null")}"); + //remember to check if the values you're debugging actually exist before referencing them... + } + break; + } + case MissileBase.TargetingModes.Inertial: + { + TargetSignatureData INSTarget = TargetSignatureData.noTarget; + TargetData targetData = new TargetData(); + if (vesselRadarData) + { + float BayTriggerTime = -1; + if (SetCargoBays()) + { + BayTriggerTime = Time.time; + //yield return new WaitForSecondsFixed(2f); + if (vessel == null || targetVessel == null) break; + } + + bool locked = false; + + if (ml.GetWeaponClass() == WeaponClasses.SLW) + { + if (_sonarsEnabled) + INSTarget = vesselRadarData.detectedRadarTarget(targetVessel, this); //detected by radar scan? + } + else + { + if (_radarsEnabled) + (INSTarget, locked) = vesselRadarData.detectedRadarTargetLock(targetVessel, this); //detected by radar scan? + if (!INSTarget.exists && _irstsEnabled) + INSTarget = vesselRadarData.activeIRTarget(null, this); //how about IRST? + } + + float attemptStartTime = Time.time; + if (!locked) + { + float attemptLockTime = Time.time; + while (ml && vesselRadarData && (!vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != targetVessel)) && Time.time - attemptLockTime < 2f) + { + bool lockSuccess = false; + if (!vesselRadarData.locked) //we got radar, can we get a lock for better datalink update rate? + { + if (vesselRadarData.SwitchActiveLockedTarget(targetVessel)) + lockSuccess = true; + else + lockSuccess = vesselRadarData.TryLockTarget(targetVessel); + } + else + lockSuccess = vesselRadarData.TryLockTarget(targetVessel); + + // If not successful, wait for `tryLockTime` before making another lock attempt + if (!lockSuccess) + yield return new WaitForSecondsFixed(tryLockTime); + else + { + // If successful, wait for a FixedUpdate while `UpdateLockedTargets` runs + yield return wait; + break; + } + } + } + else + { + vesselRadarData.SwitchActiveLockedTarget(targetVessel); + yield return wait; + } + + if (vessel && INSTarget.exists && INSTarget.vessel == targetVessel) + { + //targetData.targetGEOPos = VectorUtils.WorldPositionToGeoCoords(MissileGuidance.GetAirToAirFireSolution(ml, targetVessel, out float INStimetogo), targetVessel.mainBody); + //targetData.INStimetogo = INStimetogo; + //targetData.TimeOfLastINS = Time.time; + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No radar or IRST target! Switching to unguided firing."); + dumbfiring = true; //so let them be used as unguided ordnance + break; + } + MissileLauncher mlauncher = ml as MissileLauncher; + + if (targetVessel) + { + float angle = 999; + float turretStartTime = attemptStartTime; + if (ml.customTurret.Count > 0) + { + vesselRadarData.SlaveTurrets(); + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > 5)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel.CoM, targetVessel.Velocity(), + (ml.GuidanceMode == GuidanceModes.AAMLoft || ml.GuidanceMode == GuidanceModes.Kappa)); + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing LSRMsl custom turret to bear..."); + } + } + else + { + if (mlauncher) + { + if (mlauncher.missileTurret) + { + mlauncher.missileTurret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && mlauncher && targetVessel && angle > mlauncher.missileTurret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + //mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, targetVessel.CoM, targetVessel.Velocity(), mlauncher.missileTurret.turretLoft, mlauncher.missileTurret.turretLoftFac); + //mlauncher.missileTurret.SlavedAim(); + yield return wait; + } + mlauncher.missileTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && mlauncher.multiLauncher.turret) + { + mlauncher.multiLauncher.turret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && mlauncher && targetVessel && angle > mlauncher.multiLauncher.turret.fireFOV) + { + angle = VectorUtils.Angle(mlauncher.multiLauncher.turret.finalTransform.forward, mlauncher.multiLauncher.turret.slavedTargetPosition - mlauncher.multiLauncher.turret.finalTransform.position); + //mlauncher.multiLauncher.turret.slaved = true; + mlauncher.multiLauncher.turret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, targetVessel.CoM, targetVessel.Velocity(), mlauncher.multiLauncher.turret.turretLoft, mlauncher.multiLauncher.turret.turretLoftFac); + //mlauncher.multiLauncher.turret.SlavedAim(); + yield return wait; + } + mlauncher.multiLauncher.turret.slavedGuard = false; + } + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + { + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + } + } + } + yield return wait; + + if (vessel && ml && targetVessel) + { + 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)); + } + if (!dumbfiring && INSTarget.exists && GetLaunchAuthorization(targetVessel, this, ml)) + { + targetData.targetGEOPos = VectorUtils.WorldPositionToGeoCoords(MissileGuidance.GetAirToAirFireSolution(ml, targetVessel, out float INStimetogo), targetVessel.mainBody); + targetData.INStimetogo = INStimetogo; + targetData.TimeOfLastINS = Time.time; + + FireCurrentMissile(ml, true, targetVessel, targetData); + //StartCoroutine(MissileAwayRoutine(ml)); + break; + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No radar or IRST target! Switching to unguided firing."); + dumbfiring = true; //so let them be used as unguided ordnance + } + } + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No radar or IRST target! Switching to unguided firing."); + dumbfiring = true; //so let them be used as unguided ordnance + } + break; + } + case MissileBase.TargetingModes.None: + { + dumbfiring = true; + break; + } + } + if (dumbfiring) //unguidedWeapon + { + MissileLauncher mlauncher = ml as MissileLauncher; + if (mlauncher && mlauncher.multiLauncher && mlauncher.multiLauncher.overrideReferenceTransform) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName}'s {(CurrentMissile ? CurrentMissile.name : "null missile")} launched from MML, aborting unguided launch."); + } + else + { + if ((ml.TargetingMode == MissileBase.TargetingModes.None) || ((targetVessel.CoM - ml.vessel.CoM).sqrMagnitude < (ml.GetWeaponClass() == WeaponClasses.SLW && !vessel.Splashed ? (ml.maxStaticLaunchRange * ml.maxStaticLaunchRange) / 4 + vessel.horizontalSrfSpeed * bombFlightTime : (0.01f * ml.maxStaticLaunchRange * ml.maxStaticLaunchRange)))) //Extend range threshold for airdropped torps to account dist covered while dropping + { + if (SetCargoBays()) + { + yield return new WaitForSecondsFixed(2f); + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} attempting to fire unguided missile on target {targetVessel.GetName()} at range {(targetVessel.CoM - vessel.CoM).magnitude}"); + + float attemptStartTime = Time.time; + if (targetVessel) + { + float angle = 999; + float turretStartTime = attemptStartTime; + float dumbfireFOV = 1f; // Match firing conditions for dumbfired weapons in GetLaunchAuthorization + if (ml.customTurret.Count > 0) + { + vesselRadarData.SlaveTurrets(); + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && targetVessel && ml && angle > dumbfireFOV)//ml.customTurret[0].fireFOV + { + for (int i = 0; i < ml.customTurret.Count; i++) + { + if (ml.customTurret[i] == null) continue; + if (ml.customTurret[i].vessel != vessel) continue; + angle = VectorUtils.Angle(ml.MissileReferenceTransform.forward, ml.customTurret[i].slavedTargetPosition - ml.MissileReferenceTransform.position); + ml.customTurret[i].slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel); + ml.customTurret[i].AimToTarget(ml.customTurret[i].slavedTargetPosition); + } + yield return wait; + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}] bringing LSRMsl custom turret to bear..."); + } + } + else + { + if (mlauncher) + { + if (mlauncher.missileTurret) + { + mlauncher.missileTurret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && mlauncher && targetVessel && angle > dumbfireFOV) + { + angle = VectorUtils.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + //mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel); + //mlauncher.missileTurret.SlavedAim(); + yield return wait; + } + mlauncher.missileTurret.slavedGuard = false; + } + if (mlauncher.multiLauncher && mlauncher.multiLauncher.turret) + { + mlauncher.multiLauncher.turret.slavedGuard = true; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2) && mlauncher && targetVessel && angle > dumbfireFOV) + { + angle = VectorUtils.Angle(mlauncher.multiLauncher.turret.finalTransform.forward, mlauncher.multiLauncher.turret.slavedTargetPosition - mlauncher.multiLauncher.turret.finalTransform.position); + //mlauncher.multiLauncher.turret.slaved = true; + mlauncher.multiLauncher.turret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel); + //mlauncher.multiLauncher.turret.SlavedAim(); + yield return wait; + } + mlauncher.multiLauncher.turret.slavedGuard = false; + } + } + } + } + yield return wait; + if (ml && targetVessel && GetLaunchAuthorization(targetVessel, this, ml)) + { + FireCurrentMissile(ml, true); + } + } + } + dumbfiring = false; + } + guardFiringMissile = false; + } + } + IEnumerator GuardBombRoutine() + { + guardFiringMissile = true; + float bombStartTime = Time.time; + float bombAttemptDuration = Mathf.Max(targetScanInterval, 12f); + float radius = CurrentMissile.GetBlastRadius() * Mathf.Max(0.68f * CurrentMissile.clusterbomb, 1f) * Mathf.Min(0.68f + 1.4f * (maxMissilesOnTarget - 1f), 1.5f); + radius = Mathf.Min(radius, 150f); + float targetToleranceSqr = Mathf.Max(100, 0.013f * (float)guardTarget.srfSpeed * (float)guardTarget.srfSpeed); + + bool doProxyCheck = true; + + float radiusSqr = radius * radius; + float prevCPADistSqr = float.MaxValue; + float prevHitDistSqr = float.MaxValue; + var wait = new WaitForFixedUpdate(); + + while (guardTarget && Time.time - bombStartTime < bombAttemptDuration && weaponIndex > 0 && + weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb && firedMissiles < maxMissilesOnTarget) + { + BDModulePilotAI pilotAI = null; + BDModuleVTOLAI vtolAI = null; + var ai = AI; + if (ai != null && ai.pilotEnabled) switch (ai.aiType) + { + case AIType.PilotAI: pilotAI = ai as BDModulePilotAI; break; + case AIType.VTOLAI: vtolAI = ai as BDModuleVTOLAI; break; + } + Vector3 leadTarget = guardTarget.CoM; + if (bombFlightTime > 0) + { + leadTarget = AIUtils.PredictPosition(guardTarget, bombFlightTime, immediate: false);//lead moving ground target to properly line up bombing run; bombs fire solution already plotted in missileFire, torps more or less hit top speed instantly, so simplified fire solution can be used. Use smoothed acceleration for ground targets. + } + float CPADistSqr = (bombAimerCPA - leadTarget).sqrMagnitude; + if (CPADistSqr < radiusSqr * 400f) + { + if (SetCargoBays()) + yield return new WaitForSecondsFixed(2f); + MissileLauncher mlauncher = CurrentMissile as MissileLauncher; + if (mlauncher.multiLauncher && !mlauncher.multiLauncher.turret) + yield return new WaitForSecondsFixed(mlauncher.multiLauncher.deploySpeed); + } + if ((CurrentMissile.TargetingMode == MissileBase.TargetingModes.Gps && (designatedGPSInfo.worldPos - guardTarget.CoM).sqrMagnitude > targetToleranceSqr) //Was blastRadius, but these are precision guided munitions. Let's use a little precision here + || (CurrentMissile.TargetingMode == MissileBase.TargetingModes.Laser && (!laserPointDetected || (foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude > targetToleranceSqr)))) + { + //check database for target first + float twoxsqrRad = 4f * radiusSqr; + bool foundTargetInDatabase = false; + using (List.Enumerator gps = BDATargetManager.GPSTargetList(Team).GetEnumerator()) + while (gps.MoveNext()) + { + if (!((gps.Current.worldPos - guardTarget.CoM).sqrMagnitude < twoxsqrRad)) continue; + designatedGPSInfo = gps.Current; + foundTargetInDatabase = true; + break; + } + + //no target in gps database, acquire via targeting pod + if (!foundTargetInDatabase) + { + if (targetingPods.Count > 0) + { + using (List.Enumerator tgp = targetingPods.GetEnumerator()) + while (tgp.MoveNext()) + { + if (tgp.Current == null) continue; + tgp.Current.EnableCamera(); + tgp.Current.CoMLock = true; + yield return StartCoroutine(tgp.Current.PointToPositionRoutine(guardTarget.CoM, guardTarget)); + } + } + float attemptStartTime = Time.time; + float attemptDuration = targetScanInterval * 0.75f; + while (Time.time - attemptStartTime < attemptDuration && (!laserPointDetected || (foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude > targetToleranceSqr))) + { + yield return wait; + } + + if (guardTarget && (foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude <= targetToleranceSqr)) + { + radius = 500; + radiusSqr = radius * radius; + } + else //no coords, treat as standard unguided bomb + { + if (foundCam) foundCam.DisableCamera(); + //designatedGPSInfo = new GPSTargetInfo(); + } + } + } + if (CPADistSqr > radiusSqr + || Vector3.Dot(vessel.up, vessel.transform.forward) > 0) // roll check + { + if ((leadTarget - vessel.CoM).sqrMagnitude < (pilotAI ? pilotAI.extendDistanceAirToGround * pilotAI.extendDistanceAirToGround : 4000000) && //Check the target is within bombing run dist or 2km if non-pilotAI + ((CPADistSqr > Mathf.Max(4f * radiusSqr, (pilotAI && pilotAI.divebombing ? 1000000 : 40000f)) && Vector3.Dot((leadTarget - bombAimerCPA).ProjectOnPlanePreNormalized(vessel.up), (leadTarget - vessel.CoM).ProjectOnPlanePreNormalized(vessel.up)) < 0) // not overshooting the target by more than twice the blast radius or 200m if levelbombing, 1km if divebombing, + || (CPADistSqr < 4 * radiusSqr && Vector3.Dot(vessel.up, vessel.transform.forward) > 0))) //or on final approach and upside down + { + if (pilotAI) + { + if (pilotAI.extendingReason != "too close to bomb") //don't spam this every frame + pilotAI.RequestExtend("too close to bomb", guardTarget, minDistance: pilotAI.extendDistanceAirToGround, ignoreCooldown: true); // Extend from target vessel by A2G extendDist + distance bomb would cover while falling + } + else if (vtolAI) + { + vtolAI.orderedToExtend = true; + } + //TODO - VTOL AI extend support. Pretty likely to not be doing bombing with surface AI(...*maybe* depthcharges...?) or Orbital AI + break; + } + yield return wait; + } + else + { + if (doProxyCheck) + { + float hitDistSqr = (bombAimerPosition - leadTarget).sqrMagnitude; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.MissileFire]: proxy check: {BDAMath.Sqrt(prevCPADistSqr)} -> {BDAMath.Sqrt(CPADistSqr)} ({BDAMath.Sqrt(prevHitDistSqr)} -> {BDAMath.Sqrt(hitDistSqr)}). target alt: {BodyUtils.GetRadarAltitudeAtPos(leadTarget, false)}, aimer alt: {BodyUtils.GetRadarAltitudeAtPos(bombAimerCPA, true)}, {bombAimerDebugString}"); + if (CPADistSqr > prevCPADistSqr && hitDistSqr > prevHitDistSqr) // || (radiusSqr / targetDistSqr) > 4) //Waiting until closest approach or within 1/2 blastRadius. + { // CPA distance gives the closest approach and hit distance mostly avoids wobbles in the closing distance. Both should be increasing once past the target. FIXME Seems to work well for landed/splashed targets, but needs checking for bombing airborne targets. + doProxyCheck = false; + } + else + { + prevCPADistSqr = CPADistSqr; + prevHitDistSqr = hitDistSqr; + } + } + + if (!doProxyCheck) + { + if (guardTarget && (foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude <= targetToleranceSqr)) //was tgp.groundtargetposition + { + designatedGPSInfo = new GPSTargetInfo(foundCam.bodyRelativeGTP, "Guard Target"); + } + FireCurrentMissile(CurrentMissile, true); + timeBombReleased = Time.time; + yield return new WaitForSecondsFixed(rippleFire ? 60f / rippleRPM : 0.06f); + if (firedMissiles >= maxMissilesOnTarget) // If not, continue bombing until overshooting. + { + yield return new WaitForSecondsFixed(1f); // Wait briefly to avoid hitting the bomb with the wings. + if (pilotAI) + { + pilotAI.RequestExtend("bombs away!", null, 1.5f * radius, guardTarget.CoM, ignoreCooldown: true); // Extend from the place the bomb is expected to fall. (1.5*radius as per the comment in BDModulePilot.) + } //maybe something similar should be adapted for any missiles with nuke warheads...? + //if (vtolAI) vtolAI.orderedToExtend = true; + } + } + else + { + yield return wait; + } + } + } + + designatedGPSInfo = new GPSTargetInfo(); + guardFiringMissile = false; + } + + // DEPRECATED -> SwitchActiveLockedTarget now does this, but better + private bool GuardCheckLock(Vessel targetVessel) + { + List possibleTargets = vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == targetVessel) + { + return true; + } + } + + return false; + } + + void ClearUnneededLocks(bool unlockAll = false) + { + List locks = vesselRadarData.GetLockedTargets(); + for (int i = 0; i < locks.Count; i++) + { + if (GetMissilesAway(locks[i].targetInfo)[1] == 0) + { + vesselRadarData.UnlockSelectedTarget(locks[i].vessel); + if (!unlockAll) + return; + } + } + } + + //IEnumerator MissileAwayRoutine(MissileBase ml) + //{ + // missilesAway++; + + // MissileLauncher launcher = ml as MissileLauncher; + // if (launcher != null) + // { + // float timeStart = Time.time; + // float timeLimit = Mathf.Max(launcher.dropTime + launcher.cruiseTime + launcher.boostTime + 4, 10); + // while (ml) + // { + // if (ml.guidanceActive && Time.time - timeStart < timeLimit) + // { + // yield return null; + // } + // else + // { + // break; + // } + + // } + // } + // else + // { + // while (ml) + // { + // if (ml.MissileState != MissileBase.MissileStates.PostThrust) + // { + // yield return null; + + // } + // else + // { + // break; + // } + // } + // } + + // missilesAway--; + //} + + //IEnumerator BombsAwayRoutine(MissileBase ml) + //{ + // missilesAway++; + // float timeStart = Time.time; + // float timeLimit = 3; + // while (ml) + // { + // if (Time.time - timeStart < timeLimit) + // { + // yield return null; + // } + // else + // { + // break; + // } + // } + // missilesAway--; + //} + #endregion Enumerators + + #region Audio + + void UpdateVolume() + { + if (audioSource) + { + audioSource.volume = BDArmorySettings.BDARMORY_UI_VOLUME; + } + if (warningAudioSource) + { + warningAudioSource.volume = BDArmorySettings.BDARMORY_UI_VOLUME; + } + if (targetingAudioSource) + { + targetingAudioSource.volume = BDArmorySettings.BDARMORY_UI_VOLUME; + } + } + + void UpdateTargetingAudio() + { + if (BDArmorySetup.GameIsPaused) + { + if (targetingAudioSource.isPlaying) + { + targetingAudioSource.Stop(); + } + return; + } + 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) + { + targetingAudioSource.clip = heatGrowlSound; + } + + if (heatTarget.exists && CurrentMissile && CurrentMissile.heatThreshold < heatTarget.signalStrength) + { + targetingAudioSource.pitch = Mathf.MoveTowards(targetingAudioSource.pitch, 2, 8 * Time.deltaTime); + } + else + { + targetingAudioSource.pitch = Mathf.MoveTowards(targetingAudioSource.pitch, 1, 8 * Time.deltaTime); + } + + if (!targetingAudioSource.isPlaying) + { + targetingAudioSource.Play(); + } + } + else + { + if (targetingAudioSource.isPlaying) + { + targetingAudioSource.Stop(); + } + } + } + else + { + targetingAudioSource.pitch = 1; + if (targetingAudioSource.isPlaying) + { + targetingAudioSource.Stop(); + } + } + } + + IEnumerator WarningSoundRoutine(float distance, MissileBase ml)//give distance parameter + { + bool detectedLaunch = false; + if (rwr && (rwr.omniDetection || (!rwr.omniDetection && ml.TargetingMode == MissileBase.TargetingModes.Radar && ml.ActiveRadar) || _irstsEnabled)) //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; + BDArmorySetup.Instance.missileWarning = true; + warningAudioSource.pitch = distance < 800 ? 1.45f : 1f; + warningAudioSource.PlayOneShot(warningSound); + + float waitTime = distance < 800 ? .25f : 1.5f; + + yield return new WaitForSecondsFixed(waitTime); + + if (ml && ml.vessel && CanSeeTarget(ml)) + { + BDATargetManager.ReportVessel(ml.vessel, this); + } + } + warningSounding = false; + } + + #endregion Audio + + #region CounterMeasure + + public bool isChaffing; + public bool isFlaring; + public bool isSmoking; + public bool isDecoying; + public bool isBubbling; + public bool isECMJamming; + public bool isCloaking; + + bool isLegacyCMing; + + int cmCounter; + int cmAmount = 5; + + public void FireAllCountermeasures(int count) + { + if (!isChaffing && !isFlaring && !isSmoking && ThreatClosingTime(incomingMissileVessel) > cmThreshold) + { + StartCoroutine(AllCMRoutine(count)); + } + } + + public void FireECM(float duration) + { + if (!isECMJamming) + { + StartCoroutine(ECMRoutine(duration)); + } + } + + public void FireOCM(bool thermalthreat) + { + if (!isCloaking) + { + StartCoroutine(CloakRoutine(thermalthreat)); + } + } + + public void FireChaff() + { + if (!isChaffing && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) + { + StartCoroutine(ChaffRoutine((int)chaffRepetition, chaffInterval)); + } + } + + public void FireFlares() + { + if (!isFlaring && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) + { + StartCoroutine(FlareRoutine((int)cmRepetition, cmInterval)); + StartCoroutine(ResetMissileThreatDistanceRoutine()); + } + } + + public void FireSmoke() + { + if (!isSmoking && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) + { + StartCoroutine(SmokeRoutine((int)smokeRepetition, smokeInterval)); + } + } + + public void FireDecoys() + { + if (!isDecoying && ThreatClosingTime(incomingMissileVessel) <= cmThreshold * 5) //because torpedoes substantially slower than missiles; 5s before impact for something moving 50m/s is 250m away + { + StartCoroutine(DecoyRoutine((int)cmRepetition, cmInterval)); + } + } + + public void FireBubbles() + { + if (!isBubbling && ThreatClosingTime(incomingMissileVessel) <= cmThreshold * 5) + { + StartCoroutine(BubbleRoutine((int)chaffRepetition, chaffInterval)); + } + } + + IEnumerator ECMRoutine(float duration) + { + isECMJamming = true; + //yield return new WaitForSecondsFixed(UnityEngine.Random.Range(0.2f, 1f)); + if (duration > 0) + { + using (var ecm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ecm.MoveNext()) + { + if (ecm.Current == null) continue; + if (ecm.Current.isMissileECM) continue; + if (ecm.Current.manuallyEnabled) continue; + if (ecm.Current.jammerEnabled) + { + ecm.Current.manuallyEnabled = true; + continue; + } + ecm.Current.EnableJammer(); + } + yield return new WaitForSecondsFixed(duration); + } + isECMJamming = false; + + using (var ecm1 = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ecm1.MoveNext()) + { + if (ecm1.Current == null) continue; + 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.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) + { + DropCM(CMDropper.CountermeasureTypes.Chaff); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); + } + yield return new WaitForSecondsFixed(chaffWaitTime); + isChaffing = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending chaff routine"); + } + + IEnumerator FlareRoutine(int repetition, float interval) + { + isFlaring = true; + 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) + { + DropCM(CMDropper.CountermeasureTypes.Flare); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); + } + yield return new WaitForSecondsFixed(cmWaitTime); + isFlaring = false; + 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(smokeWaitTime); + isSmoking = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending smoke routine"); + } + IEnumerator DecoyRoutine(int repetition, float interval) + { + isDecoying = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting decoy routine"); + // yield return new WaitForSecondsFixed(0.2f); // Reaction time delay + for (int i = 0; i < repetition; ++i) + { + DropCM(CMDropper.CountermeasureTypes.Decoy); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); + } + yield return new WaitForSecondsFixed(cmWaitTime); + isDecoying = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending decoy routine"); + } + IEnumerator BubbleRoutine(int repetition, float interval) + { + isBubbling = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting bubblescreen routine"); + // yield return new WaitForSecondsFixed(0.2f); // Reaction time delay + for (int i = 0; i < repetition; ++i) + { + DropCM(CMDropper.CountermeasureTypes.Bubbles); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); + } + yield return new WaitForSecondsFixed(cmWaitTime); + isBubbling = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending bubble routine"); + } + IEnumerator AllCMRoutine(int count) + { + // Use this routine for missile threats that are outside of the cmThreshold + isFlaring = true; + isChaffing = true; + isSmoking = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting All CM routine"); + for (int i = 0; i < count; ++i) + { + 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; + isSmoking = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending All CM routine"); + } + + IEnumerator LegacyCMRoutine() + { + isLegacyCMing = true; + yield return new WaitForSecondsFixed(UnityEngine.Random.Range(.2f, 1f)); + if (incomingMissileDistance < 2500) + { + cmAmount = Mathf.RoundToInt((2500 - incomingMissileDistance) / 400); + using (var cm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (cm.MoveNext()) + { + if (cm.Current == null) continue; + cm.Current.DropCM(); + } + cmCounter++; + if (cmCounter < cmAmount) + { + yield return new WaitForSecondsFixed(0.15f); + } + else + { + cmCounter = 0; + yield return new WaitForSecondsFixed(UnityEngine.Random.Range(.5f, 1f)); + } + } + isLegacyCMing = false; + } + + Dictionary cmCurrentPriorities = []; + void RefreshCMPriorities() + { + cmCurrentPriorities.Clear(); + foreach (var cm in VesselModuleRegistry.GetModules(vessel)) + { + if (cm == null) continue; + if (cm.isMissileCM) 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 (cm.isMissileCM) 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) + { + StartCoroutine(WarningSoundRoutine(distance, ml)); + } + + //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 + + #region Fire + + bool FireCurrentMissile(MissileBase missile, bool checkClearance, Vessel targetVessel = null, TargetData targetData = null) + { + 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) || ml.launched)) + { + using (var otherMissile = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (otherMissile.MoveNext()) + { + if (otherMissile.Current == null) continue; + if (otherMissile.Current.GetWeaponChannel() > weaponChannel) continue; + if (otherMissile.Current == ml || otherMissile.Current.GetShortName() != ml.GetShortName() || + !CheckBombClearance(otherMissile.Current)) continue; + if (otherMissile.Current.GetEngagementRangeMax() != selectedWeaponsEngageRangeMax) continue; + if (otherMissile.Current.GetEngageFOV() != selectedWeaponsMissileFOV) continue; + if (otherMissile.Current.launched) continue; + CurrentMissile = otherMissile.Current; + selectedWeapon = otherMissile.Current; + return FireCurrentMissile(otherMissile.Current, false, targetVessel, targetData); + } + 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), targetVessel, targetData); + } + else if (ml is MissileLauncher && ((MissileLauncher)ml).rotaryRail) + { + ((MissileLauncher)ml).rotaryRail.FireMissile(((MissileLauncher)ml), targetVessel, targetData); + } + else if (ml is MissileLauncher && ((MissileLauncher)ml).deployableRail) + { + ((MissileLauncher)ml).deployableRail.FireMissile(((MissileLauncher)ml), targetVessel, targetData); + } + else + { + SendTargetDataToMissile(ml, targetVessel, true, targetData, targetVessel == null); + ml.FireMissile(); + PreviousMissile = ml; + } + + if (guardMode) + { + if (ml.GetWeaponClass() == WeaponClasses.Bomb) + { + //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 && (vessel.CoM + (timeToImpact * (Vector3)vessel.Velocity())).CloserToThan(currentTarget.position + (timeToImpact * 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 + { + if (vesselRadarData && vesselRadarData.autoCycleLockOnFire) + { + vesselRadarData.CycleActiveLock(); + } + } + } + else + { + SendTargetDataToMissile(missile, targetVessel, true, targetData, false); + missile.FireMissile(); + PreviousMissile = missile; + } + + //PreviousMissile = CurrentMissile; + UpdateList(); + if (DisengageAfterFiring) + { + var pilotAI = PilotAI; + 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; + } + + void FireMissile() + { + if (weaponIndex == 0) + { + return; + } + + if (selectedWeapon == null) + { + return; + } + if (guardMode && (firedMissiles >= maxMissilesOnTarget)) + { + return; + } + if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || + selectedWeapon.GetWeaponClass() == WeaponClasses.SLW || + selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) + { + FireCurrentMissile(CurrentMissile, 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 + + void DisplaySelectedWeaponMessage() + { + if (BDArmorySetup.GAME_UI_ENABLED && vessel == FlightGlobals.ActiveVessel) + { + ScreenMessages.RemoveMessage(selectionMessage); + selectionMessage.textInstance = null; + selectionText = $"Selected Weapon: {(GetWeaponName(weaponArray[weaponIndex])).ToString()}"; + selectionMessage.message = selectionText; + selectionMessage.style = ScreenMessageStyle.UPPER_CENTER; + + ScreenMessages.PostScreenMessage(selectionMessage); + } + } + + string GetWeaponName(IBDWeapon weapon) + { + if (weapon == null) + { + return StringUtils.Localize("#LOC_BDArmory_WMWindow_NoneWeapon"); + } + else + { + return weapon.GetShortName(); + } + } + float GetWeaponRange(IBDWeapon weapon) + { + if (weapon == null) + { + return -1; + } + else + { + return weapon.GetEngageRange(); + } + } + float GetMissileFOV(IBDWeapon weapon) + { + if (weapon == null) + { + return -1; + } + else + { + return weapon.GetEngageFOV(); + } + } + public void UpdateList() + { + weaponsListNeedsUpdating = false; + weaponTypes.Clear(); + weaponRanges.Clear(); + weaponBoresights.Clear(); + // extension for feature_engagementenvelope: also clear engagement specific weapon lists + weaponTypesAir.Clear(); + weaponTypesMissile.Clear(); + targetMissiles = false; + weaponTypesGround.Clear(); + weaponTypesSLW.Clear(); + pointDefenseWeapons.Clear(); + pointDefenseMissiles.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 + hasAntiRadiationOrdnance = false; + if (vessel == null || !vessel.loaded) return; + + pointDefenseIRMissileCount = 0; + pointDefenseMissileMaxARH = -1; + pointDefenseMissileHasLaser = false; + pointDefenseMissileHasInertial = false; + pointDefenseMissileHasRadar = false; + pointDefenseMissileMaxRange = -1; + + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + string weaponName = weapon.Current.GetShortName(); + bool alreadyAdded = false; + + 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); + var gun = weapon.Current.GetWeaponModule(); + //dont add empty rocket pods + if ((gun.rocketPod && !gun.externalAmmo) && gun.GetRocketResource().amount < 1 && !BDArmorySettings.INFINITE_AMMO) + { + continue; + } + //dont add APS + if (gun.isAPS) + { + pointDefenseWeapons.Add(gun); + if (!gun.dualModeAPS) continue; + } + } + + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + + bool currIsMissile = false; + MissileBase mb = null; + if (weapon.Current.GetWeaponClass() == WeaponClasses.Missile || weapon.Current.GetWeaponClass() == WeaponClasses.Bomb || weapon.Current.GetWeaponClass() == WeaponClasses.SLW) + { + currIsMissile = true; + mb = weapon.Current.GetPart().FindModuleImplementing(); + } + + using (List.Enumerator weap = weaponTypes.GetEnumerator()) + while (weap.MoveNext()) + { + if (weap.Current == null) continue; + if (weap.Current.GetShortName() == weaponName) + { + if (currIsMissile) + { + bool rangeAdd = false; + bool fovAdd = false; + float range = mb.engageRangeMax; + + if (weaponRanges.TryGetValue(weaponName, out var registeredRanges)) + { + if (registeredRanges.Contains(range)) + rangeAdd = true; + } + float boresight = mb.missileFireAngle; + + if (weaponBoresights.TryGetValue(weaponName, out var registeredFOVs)) + { + if (registeredFOVs.Contains(boresight)) + fovAdd = true; + } + if (rangeAdd && fovAdd) alreadyAdded = true; + } + else + alreadyAdded = true; + //break; + } + } + + if (!alreadyAdded) + { + weaponTypes.Add(weapon.Current); + if (currIsMissile) + { + float range = mb.engageRangeMax; + + if (weaponRanges.TryGetValue(weaponName, out var registeredRanges)) + { + registeredRanges.Add(range); + } + else + weaponRanges.Add(weaponName, [range]); + + float boresight = mb.missileFireAngle; + + if (weaponBoresights.TryGetValue(weaponName, out var registeredFoVs)) + { + registeredFoVs.Add(boresight); + } + else + weaponBoresights.Add(weaponName, [boresight]); + + if (mb.engageMissile) + { + switch (mb.TargetingMode) + { + case MissileBase.TargetingModes.Radar: + { + pointDefenseMissileHasRadar = true; + if (mb.activeRadarRange > pointDefenseMissileMaxARH) + pointDefenseMissileMaxARH = mb.activeRadarRange; + } + break; + case MissileBase.TargetingModes.Inertial: + { + pointDefenseMissileHasInertial = true; + } + break; + case MissileBase.TargetingModes.Laser: + { + pointDefenseMissileHasLaser = true; + } + break; + case MissileBase.TargetingModes.Heat: + { + pointDefenseIRMissileCount++; + } + break; + } + } + } + } + + EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; + bool tempIsAntiMissile = false; + + if (engageableWeapon != null) + { + if (engageableWeapon.GetEngageAirTargets()) weaponTypesAir.Add(weapon.Current); + if (engageableWeapon.GetEngageMissileTargets()) + { + weaponTypesMissile.Add(weapon.Current); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire] Adding {weapon.Current.GetShortName()}; {weapon.Current.GetPart().persistentId} to engageMissiles list..."); + targetMissiles = true; + tempIsAntiMissile = true; + } + if (engageableWeapon.GetEngageGroundTargets()) weaponTypesGround.Add(weapon.Current); + if (engageableWeapon.GetEngageSLWTargets()) weaponTypesSLW.Add(weapon.Current); + } + else + { + weaponTypesAir.Add(weapon.Current); + weaponTypesMissile.Add(weapon.Current); + weaponTypesGround.Add(weapon.Current); + weaponTypesSLW.Add(weapon.Current); + tempIsAntiMissile = true; + } + + if (currIsMissile) + { + MissileLauncher ml = weapon.Current.GetPart().FindModuleImplementing(); + BDModularGuidance mmg = weapon.Current.GetPart().FindModuleImplementing(); + mb.GetMissileCount(); // #191, Do it this way so the GetMissileCount only updates when missile fired + + if (tempIsAntiMissile) + pointDefenseMissiles.Add(mb); + + if ((ml is not null && ml.TargetingMode == MissileBase.TargetingModes.AntiRad) || (mmg is not null && mmg.TargetingMode == MissileBase.TargetingModes.AntiRad)) + { + hasAntiRadiationOrdnance = true; + //antiradTargets = OtherUtils.ParseToFloatArray(ml != null ? ml.antiradTargetTypes : "0,5"); //limited Antirad options for MMG + //FIXME shouldn't this be set as part of currentMissile? Else having multiple ARH with different target types would overwrite this with potentially the wrong set of target types + //or otherwise have this array contain the target types for *all* ARH ordnance on the vessel. + antiradTargets.Union(OtherUtils.ParseEnumArray(ml != null ? ml.antiradTargetTypes : "0,5")); + } + } + } + + //weaponTypes.Sort(); + weaponTypes = weaponTypes.OrderBy(w => w.GetShortName()).ToList(); + + List tempList = [null, .. weaponTypes]; + + weaponArray = tempList.ToArray(); + pointDefenseWeaponArray = pointDefenseWeapons.ToArray(); + if (pointDefenseMissiles.Count > 0) + { + // Least capable to most capable missile + pointDefenseMissiles = pointDefenseMissiles.OrderBy(m => m.GetEngagementRangeMax()).ToList(); + pointDefenseMissileMaxRange = pointDefenseMissiles[pointDefenseMissiles.Count - 1].GetEngagementRangeMax(); + } + pointDefenseMissileArray = pointDefenseMissiles.ToArray(); + + if (pointDefenseIRMissileCount > pointDefenseIRMissileSkipArr.Length) + pointDefenseIRMissileSkipArr = new string[pointDefenseIRMissileCount]; + + if (weaponIndex >= weaponArray.Length) + { + hasSingleFired = true; + triggerTimer = 0; + } + PrepareWeapons(); + } + + 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; + } + + if (vessel.isActiveVessel && weaponIndex != 0) + { + SetDeployableWeapons(); + DisplaySelectedWeaponMessage(); + } + } + + if (weaponIndex == 0) + { + selectedWeapon = null; + hasSingleFired = true; + } + + MissileBase aMl = GetAsymMissile(); + if (aMl) + { + selectedWeapon = aMl; + } + MissileBase rMl = GetRotaryReadyMissile(); + if (rMl) + { + selectedWeapon = rMl; + } + UpdateSelectedWeaponState(); + } + + private void UpdateSelectedWeaponState() + { + if (vessel == null) return; + + MissileBase aMl = GetAsymMissile(); + if (aMl) + { + CurrentMissile = aMl; + } + + MissileBase rMl = GetRotaryReadyMissile(); + if (rMl) + { + CurrentMissile = rMl; + } + + 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.GetPartName() != selectedWeapon.GetPartName() || CurrentMissile.engageRangeMax != selectedWeaponsEngageRangeMax || CurrentMissile.missileFireAngle != selectedWeaponsMissileFOV || CurrentMissile.vessel != vessel) + { + using var Missile = VesselModuleRegistry.GetModules(vessel).GetEnumerator(); + while (Missile.MoveNext()) + { + if (Missile.Current == null) continue; + if (Missile.Current.GetWeaponChannel() > weaponChannel) continue; + if (Missile.Current.GetPartName() != selectedWeapon.GetPartName()) continue; + if (Missile.Current.launched) continue; + if (Missile.Current.engageRangeMax != selectedWeaponsEngageRangeMax) continue; + if (Missile.Current.missileFireAngle != selectedWeaponsMissileFOV) continue; + CurrentMissile = Missile.Current; + break; + } + //CurrentMissile = selectedWeapon.GetPart().FindModuleImplementing(); + } + } + else + { + CurrentMissile = null; + } + if (CurrentMissile != null) + selectedWeapon = CurrentMissile; + //selectedWeapon = weaponArray[weaponIndex]; + if (CurrentMissile != null) selectedWeapon = CurrentMissile; // Make sure selectedWeapon matches the actually selected missile. + + //gun ripple stuff + 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.Clear(); + // This value will be incremented as we set the ripple weapons + rippleGunCount.Clear(); + float weaponRpm = 0; // used to set the rippleGunRPM + + // JDK: this looks like it can be greatly simplified... + + #region Old Code (for reference. remove when satisfied new code works as expected. + + //List tempListModuleWeapon = vessel.FindPartModulesImplementing(); + //foreach (ModuleWeapon weapon in tempListModuleWeapon) + //{ + // if (selectedWeapon.GetShortName() == weapon.GetShortName()) + // { + // weapon.rippleIndex = Mathf.RoundToInt(counter); + // weaponRPM = weapon.roundsPerMinute; + // ++counter; + // rippleGunCount++; + // } + //} + //gunRippleRpm = weaponRPM * counter; + //float timeDelayPerGun = 60f / (weaponRPM * counter); + ////number of seconds between each gun firing; will reduce with increasing RPM or number of guns + //foreach (ModuleWeapon weapon in tempListModuleWeapon) + //{ + // if (selectedWeapon.GetShortName() == weapon.GetShortName()) + // { + // weapon.initialFireDelay = timeDelayPerGun; //set the time delay for moving to next index + // } + //} + + //RippleOption ro; //ripplesetup and stuff + //if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) + //{ + // ro = rippleDictionary[selectedWeapon.GetShortName()]; + //} + //else + //{ + // ro = new RippleOption(currentGun.useRippleFire, 650); //take from gun's persistant value + // rippleDictionary.Add(selectedWeapon.GetShortName(), ro); + //} + + //foreach (ModuleWeapon w in vessel.FindPartModulesImplementing()) + //{ + // if (w.GetShortName() == selectedWeapon.GetShortName()) + // w.useRippleFire = ro.rippleFire; + //} + + #endregion Old Code (for reference. remove when satisfied new code works as expected. + + // TODO: JDK verify new code works as expected. + // New code, simplified. + + //First lest set the Ripple Option. Doing it first eliminates a loop. + RippleOption ro; //ripplesetup and stuff + if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) + { + ro = rippleDictionary[selectedWeapon.GetShortName()]; + } + else + { + ro = new RippleOption(currentGun.useRippleFire, 650); //take from gun's persistant value + rippleDictionary.Add(selectedWeapon.GetShortName(), ro); + } + + //Get ripple weapon count, so we don't have to enumerate the whole list again. + List rippleWeapons = []; + using (var weapCnt = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapCnt.MoveNext()) + { + if (weapCnt.Current == null) continue; + if (weapCnt.Current.GetWeaponChannel() > weaponChannel) continue; + if (selectedWeapon.GetShortName() != weapCnt.Current.GetShortName()) continue; + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + weaponRpm = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + else + { + if (!weapCnt.Current.BurstFire) + { + weaponRpm = weapCnt.Current.roundsPerMinute; + } + else + { + weaponRpm = 60 / weapCnt.Current.ReloadTime; + } + } + rippleWeapons.Add(weapCnt.Current); + counter += weaponRpm; // grab sum of weapons rpm + } + gunRippleRpm = counter; + + //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; + weapon.Current.useRippleFire = ro.rippleFire; + 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); + } + } + + ToggleTurret(); + SetMissileTurrets(); + SetDeployableRails(); + SetRotaryRails(); + } + + readonly HashSet baysOpened = []; + bool SetCargoBays() + { + bool openingBays = false; + bool openBays = weaponIndex > 0 && CurrentMissile && (guardTarget || !guardMode); + + // Custom bays + uint customBayGroup = 0; + if (openBays && uint.TryParse(CurrentMissile.customBayGroup, out customBayGroup)) + { + if (customBayGroup > 0 && !baysOpened.Contains(customBayGroup)) // We haven't opened this bay yet + { + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[(int)customBayGroup]); + openingBays = true; + baysOpened.Add(customBayGroup); + } + } + foreach (var bay in baysOpened.Where(e => e <= 16).ToList()) // Close other custom bays that might be open + { + if (bay == customBayGroup) continue; + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[(int)bay]); + baysOpened.Remove(bay); // Bay is no longer open + } + + // Regular bays + // - Iterate through all bays, opening those containing the current missile and closing others. + // - This is independent of custom bays and overrides them if both are set. + // - What about symmetric counterparts? Do we need to open those? + foreach (var bay in VesselModuleRegistry.GetModules(vessel)) + { + if (bay == null) continue; + if (openBays && CurrentMissile.inCargoBay && CurrentMissile.part.airstreamShields.Contains(bay)) + { + ModuleAnimateGeneric anim = bay.part.Modules.GetModule(bay.DeployModuleIndex) as ModuleAnimateGeneric; + if (anim == null) continue; + + string toggleOption = anim.Events["Toggle"].guiName; + if (toggleOption == "Open") + { + anim.Toggle(); + openingBays = true; + baysOpened.Add(bay.GetPersistentId()); + } + } + else + { + if (!baysOpened.Contains(bay.GetPersistentId())) continue; // Only close bays we've opened. + ModuleAnimateGeneric anim = bay.part.Modules.GetModule(bay.DeployModuleIndex) as ModuleAnimateGeneric; + if (anim == null) continue; + + string toggleOption = anim.Events["Toggle"].guiName; + if (toggleOption == "Close") + { + anim.Toggle(); + } + } + } + + return openingBays; + } + + private HashSet wepsDeployed = []; + private bool SetDeployableWeapons() + { + bool deployingWeapon = false; + + if (weaponIndex > 0 && currentGun) + { + if (uint.Parse(currentGun.deployWepGroup) > 0) // Weapon uses a deploy action group, activate it to fire + { + uint deployWepGroup = uint.Parse(currentGun.deployWepGroup); + if (!wepsDeployed.Contains(deployWepGroup)) // We haven't deployed this weapon yet + { + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[(int)deployWepGroup]); + deployingWeapon = true; + wepsDeployed.Add(deployWepGroup); + } + else + { + foreach (var wep in wepsDeployed.Where(e => e <= 16).ToList()) // Store other Weapons that might be deployed + { + if (wep != deployWepGroup) + { + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[(int)wep]); + wepsDeployed.Remove(wep); // Weapon is no longer deployed + } + } + } + } + else + { + foreach (var wep in wepsDeployed.Where(e => e <= 16).ToList()) // Store weapons + { + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[(int)wep]); + wepsDeployed.Remove(wep); // Weapon is no longer deployed + } + } + } + else + { + foreach (var wep in wepsDeployed.Where(e => e <= 16).ToList()) // Store weapons + { + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[(int)wep]); + wepsDeployed.Remove(wep); // Weapon is no longer deployed + } + } + + return deployingWeapon; + } + + void SetRotaryRails() + { + if (weaponIndex == 0) return; + + if (selectedWeapon == null) return; + + if ( + !(selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || + selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb || + selectedWeapon.GetWeaponClass() == WeaponClasses.SLW)) return; + + if (!CurrentMissile) return; + + //TODO BDModularGuidance: Rotatory Rail? + MissileLauncher cm = CurrentMissile as MissileLauncher; + if (cm == null) return; + using (var rotRail = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (rotRail.MoveNext()) + { + if (rotRail.Current == null) continue; + if (rotRail.Current.missileCount == 0) + { + //Debug.Log("[BDArmory.MissileFire]: SetRotaryRails(): rail has no missiles"); + continue; + } + + //Debug.Log("[BDArmory.MissileFire]: SetRotaryRails(): rotRail.Current.readyToFire: " + rotRail.Current.readyToFire + ", rotRail.Current.readyMissile: " + ((rotRail.Current.readyMissile != null) ? rotRail.Current.readyMissile.part.name : "null") + ", rotRail.Current.nextMissile: " + ((rotRail.Current.nextMissile != null) ? rotRail.Current.nextMissile.part.name : "null")); + + //Debug.Log("[BDArmory.MissileFire]: current missile: " + cm.part.name); + + if (rotRail.Current.readyToFire) + { + if (!rotRail.Current.readyMissile) + { + rotRail.Current.RotateToMissile(cm); + return; + } + + if (rotRail.Current.readyMissile.GetPartName() != cm.GetPartName()) + { + rotRail.Current.RotateToMissile(cm); + } + } + else + { + if (!rotRail.Current.nextMissile) + { + rotRail.Current.RotateToMissile(cm); + } + else if (rotRail.Current.nextMissile.GetPartName() != cm.GetPartName()) + { + rotRail.Current.RotateToMissile(cm); + } + } + } + } + + void SetMissileTurrets() + { + MissileLauncher cm = CurrentMissile as MissileLauncher; + using (var mt = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mt.MoveNext()) + { + if (mt.Current == null) continue; + if (!mt.Current.isActiveAndEnabled) continue; + if (weaponIndex > 0 && cm) + { + if (mt.Current.ContainsMissileOfType(cm) && (!mt.Current.activeMissileOnly || cm.missileTurret == mt.Current)) + { + mt.Current.EnableTurret(CurrentMissile, true); + } + } + else + { + if (MslTurrets.Contains(mt.Current)) continue; + mt.Current.DisableTurret(); + } + } + using (var ct = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ct.MoveNext()) + { + if (ct.Current == null) continue; + if (!ct.Current.isActiveAndEnabled) continue; + if (weaponIndex > 0 && CurrentMissile) + { + if (CurrentMissile.customTurret.Contains(ct.Current)) + { + ct.Current.manuallyControlled = true; + } + } + else //does this need a condition for weaponIndex > 0 (weapon selected) but not the missile turret? + { + ct.Current.ReturnTurret(); + } + } + if (weaponIndex > 0 && cm && cm.multiLauncher && cm.multiLauncher.turret) + { + cm.multiLauncher.turret.EnableTurret(CurrentMissile, true); + } + } + void SetDeployableRails() + { + MissileLauncher cm = CurrentMissile as MissileLauncher; + using var mt = VesselModuleRegistry.GetModules(vessel).GetEnumerator(); + while (mt.MoveNext()) + { + if (mt.Current == null) continue; + if (!mt.Current.isActiveAndEnabled) continue; + if (weaponIndex > 0 && cm && mt.Current.ContainsMissileOfType(cm) && cm.deployableRail == mt.Current && !cm.launched) + { + mt.Current.EnableRail(); + } + else + { + mt.Current.DisableRail(); + } + } + } + + public void CycleWeapon(bool forward) + { + if (forward) weaponIndex++; + else weaponIndex--; + weaponIndex = (int)Mathf.Repeat(weaponIndex, weaponArray.Length); + + hasSingleFired = true; + triggerTimer = 0; + + UpdateList(); + SetDeployableWeapons(); + DisplaySelectedWeaponMessage(); + + if (vessel.isActiveVessel && !guardMode) + { + SetCargoBays(); + audioSource.PlayOneShot(clickSound); + } + } + + public void CycleWeapon(int index) + { + if (index >= weaponArray.Length) + { + index = 0; + } + weaponIndex = index; + UpdateList(); + + if (vessel.isActiveVessel && !guardMode) + { + audioSource.PlayOneShot(clickSound); + SetCargoBays(); + SetDeployableWeapons(); + DisplaySelectedWeaponMessage(); + } + } + + public Part FindSym(Part p) + { + using (List.Enumerator pSym = p.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + if (pSym.Current != p && pSym.Current.vessel == vessel) + { + return pSym.Current; + } + } + + return null; + } + + private MissileBase GetAsymMissile() + { + if (weaponIndex == 0) return null; + if (weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb || + weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Missile || + weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.SLW) + { + MissileBase firstMl = null; + using (var ml = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ml.MoveNext()) + { + if (ml.Current == null) continue; + if (ml.Current.GetWeaponChannel() > weaponChannel) continue; + MissileLauncher launcher = ml.Current as MissileLauncher; + if (launcher != null) + { + if (weaponArray[weaponIndex].GetPart() == null || launcher.GetPartName() != weaponArray[weaponIndex].GetPartName()) continue; + if (launcher.launched) continue; + if (launcher.engageRangeMax != selectedWeaponsEngageRangeMax) continue; + if (launcher.missileFireAngle != selectedWeaponsMissileFOV) continue; + } + else + { + BDModularGuidance guidance = ml.Current as BDModularGuidance; + if (guidance != null) + { //We have set of parts not only a part + if (guidance.GetShortName() != weaponArray[weaponIndex]?.GetShortName()) continue; + } + } + if (firstMl == null) firstMl = ml.Current; + + if (!FindSym(ml.Current.part)) + { + return ml.Current; + } + } + return firstMl; + } + return null; + } + + private MissileBase GetRotaryReadyMissile() + { + if (weaponIndex == 0) return null; + if (weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb || + weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Missile || + weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.SLW) + { + //TODO BDModularGuidance, ModuleDrone: Implemente rotaryRail support + MissileLauncher missile = CurrentMissile as MissileLauncher; + if (missile == null) return null; + if (weaponArray[weaponIndex].GetPart() != null && missile.GetPartName() == weaponArray[weaponIndex].GetPartName()) + { + if (!missile.rotaryRail) + { + return missile; + } + if (missile.rotaryRail.readyToFire && missile.rotaryRail.readyMissile == CurrentMissile && !missile.launched) + { + return missile; + } + } + using (var ml = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ml.MoveNext()) + { + if (ml.Current == null) 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.GetPartName() == weaponArray[weaponIndex].GetPartName() && ml.Current.rotaryRail.readyMissile.GetEngageFOV() == weaponArray[weaponIndex].GetEngageFOV()) + { + return ml.Current.rotaryRail.readyMissile; + } + } + return null; + } + return null; + } + + bool CheckBombClearance(MissileBase ml) + { + if (!BDArmorySettings.BOMB_CLEARANCE_CHECK) return true; + + if (ml.part.ShieldedFromAirstream) + { + return false; + } + + //TODO BDModularGuidance: Bombs and turrents + MissileLauncher launcher = ml as MissileLauncher; + if (launcher != null) + { + Transform referenceTransform = (launcher.multiLauncher != null && launcher.multiLauncher.overrideReferenceTransform) ? launcher.part.FindModelTransform(launcher.multiLauncher.launchTransformName).GetChild(0) : launcher.MissileReferenceTransform; + if (launcher.rotaryRail && launcher.rotaryRail.readyMissile != ml) + { + return false; + } + + if (launcher.missileTurret && !launcher.missileTurret.turretEnabled) + { + return false; + } + //TODO - custom missile turret clearance check? + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); + if (ml.dropTime >= 0.1f) + { + //debug lines + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + { + lr = GetComponent(); + if (!lr) { lr = gameObject.AddComponent(); } + lr.enabled = true; + lr.startWidth = .1f; + lr.endWidth = .1f; + } + + float radius = launcher.decoupleForward ? launcher.ClearanceRadius : launcher.ClearanceLength; + float time = Mathf.Min(ml.dropTime, 2f); + Vector3 direction = ((launcher.decoupleForward + ? referenceTransform.forward + : -referenceTransform.up) * launcher.decoupleSpeed * time) + + ((FlightGlobals.getGeeForceAtPosition(vessel.CoM) - vessel.acceleration) * + 0.5f * time * time); + Vector3 crossAxis = Vector3.Cross(direction, referenceTransform.transform.right).normalized; + + float rayDistance; + if (launcher.thrust == 0 || launcher.cruiseThrust == 0) + { + rayDistance = 8; + } + else + { + //distance till engine starts based on grav accel and vessel accel + rayDistance = direction.magnitude; + } + + Vector3 referencePos = referenceTransform.position; + Ray[] rays = + { + new Ray(referencePos - (radius*crossAxis), direction), + new Ray(referencePos + (radius*crossAxis), direction), + new Ray(referencePos, direction) + }; + + if (lr != null && lr.enabled) + { + lr.useWorldSpace = false; + lr.positionCount = 4; + lr.SetPosition(0, transform.InverseTransformPoint(rays[0].origin)); + lr.SetPosition(1, transform.InverseTransformPoint(rays[0].GetPoint(rayDistance))); + lr.SetPosition(2, transform.InverseTransformPoint(rays[1].GetPoint(rayDistance))); + lr.SetPosition(3, transform.InverseTransformPoint(rays[1].origin)); + } + + using (IEnumerator rt = rays.AsEnumerable().GetEnumerator()) + while (rt.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 = 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; + } + + { //forward check for no-drop missiles + var ray = new Ray(ml.MissileReferenceTransform.position, ml.MissileReferenceTransform.forward); + 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. + { + 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. + // Debug.Log($"DEBUG Refreshing modules on {vessel} with AI {AI} ({AI.part.persistentId}) and WM {this} ({part.persistentId}, primary: {IsPrimaryWM})"); + _radars = VesselModuleRegistry.GetModules(vessel); + if (_radars != null) + { + // 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; + } + } + using (List.Enumerator rd = _radars.GetEnumerator()) //now refresh lock array size with new maxradarLock value + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.canLock) + { + rd.Current.RefreshLockArray(); + } + } + } + _irsts = VesselModuleRegistry.GetModules(vessel); + _jammers = VesselModuleRegistry.GetModules(vessel); + _cloaks = VesselModuleRegistry.GetModules(vessel); + _targetingPods = VesselModuleRegistry.GetModules(vessel); + + maxTargetingLaserRange = -1f; + + if (_targetingPods != null) + { + foreach (ModuleTargetingCamera targetingPod in _targetingPods) + if (targetingPod.maxRayDistance > maxTargetingLaserRange) + maxTargetingLaserRange = targetingPod.maxRayDistance; + } + _wmModules = VesselModuleRegistry.GetModules(vessel); + } + + #endregion Weapon Info + + #region Targeting + + #region Smart Targeting + + void SmartFindTarget() + { + var lastTarget = currentTarget; + List targetsTried = []; + 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 ((vessel.CoM + (Vector3)vessel.Velocity()).CloserToThan(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 (GetMissilesAway(target.Current)[0] >= maxMissilesOnTarget) + { + targetsAssigned.Add(target.Current); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.GetName()} 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 + { + targetsTried.Add(overrideTarget); + SetTarget(overrideTarget); + if (SmartPickWeapon_EngagementEnvelope(overrideTarget)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging an override target with {selectedWeapon}"); + } + overrideTimer = 15f; + return; + } + else if (BDArmorySettings.DEBUG_AI) + { + 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 + + TargetInfo potentialTarget = null; + //=========HIGH PRIORITY MISSILES============= + //first engage any missiles targeting this vessel + if (targetMissiles) + { + potentialTarget = BDATargetManager.GetMissileTarget(this, true); + if (potentialTarget) + { + targetsTried.Add(potentialTarget); + SetTarget(potentialTarget); + if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging incoming missile ({potentialTarget.Vessel.GetName()}:{potentialTarget.Vessel.parts[0].persistentId}) with {selectedWeapon}"); + } + return; + } + } + + //then engage any missiles that are not engaged + potentialTarget = BDATargetManager.GetUnengagedMissileTarget(this); + if (potentialTarget) + { + targetsTried.Add(potentialTarget); + SetTarget(potentialTarget); + if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging unengaged missile ({potentialTarget.Vessel.GetName()}:{potentialTarget.Vessel.parts[0].persistentId}) with {selectedWeapon}"); + } + return; + } + } + } + //=========END HIGH PRIORITY MISSILES============= + + //============VESSEL THREATS============ + // select target based on competition style + if (BDArmorySettings.DEFAULT_FFA_TARGETING) + { + potentialTarget = BDATargetManager.GetClosestTargetWithBiasAndHysteresis(this); + 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 "; + } + else + { + if (!vessel.LandedOrSplashed) + { + var pilotAI = PilotAI; + 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 "; + } + } + else + { + potentialTarget = BDATargetManager.GetLeastEngagedTarget(this); + targetDebugText = " is engaging the least engaged target with "; + } + } + + if (potentialTarget) + { + targetsTried.Add(potentialTarget); + SetTarget(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.DEBUG_AI) + { + 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; + } + else if (!BDArmorySettings.DISABLE_RAMMING) + { + var pilotAI = PilotAI; + if (!HasWeaponsAndAmmo() && pilotAI != null && pilotAI.allowRamming && (pilotAI.allowRammingGroundTargets || !potentialTarget.Vessel.LandedOrSplashed)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName + targetDebugText} ramming."); + } + return; + } + } + } + + //then engage the closest enemy + potentialTarget = BDATargetManager.GetClosestTarget(this); + if (potentialTarget) + { + targetsTried.Add(potentialTarget); + SetTarget(potentialTarget); + /* + if (CrossCheckWithRWR(potentialTarget) && TryPickAntiRad(potentialTarget)) + { + if (BDArmorySettings.DEBUG_LABELS) + { + Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging the closest radar target with " + + selectedWeapon.GetShortName()); + } + return; + } + */ + if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging the closest target ({potentialTarget.Vessel.vesselName}) with {selectedWeapon.GetShortName()}"); + } + return; + } + } + //============END VESSEL THREATS============ + + //============LOW PRIORITY MISSILES========= + if (targetMissiles) + { + //try to engage least engaged hostile missiles first + potentialTarget = BDATargetManager.GetMissileTarget(this); + if (potentialTarget) + { + targetsTried.Add(potentialTarget); + SetTarget(potentialTarget); + if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging the least engaged missile ({potentialTarget.Vessel.vesselName}) with {selectedWeapon.GetShortName()}"); + } + return; + } + } + + //then try to engage closest hostile missile + potentialTarget = BDATargetManager.GetClosestMissileTarget(this); + if (potentialTarget) + { + targetsTried.Add(potentialTarget); + SetTarget(potentialTarget); + if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging the closest hostile missile ({potentialTarget.Vessel.vesselName}) with {selectedWeapon.GetShortName()}"); + } + return; + } + } + } + //==========END LOW PRIORITY MISSILES============= + + //if nothing works, get all remaining targets and try weapons against them + using (List.Enumerator finalTargets = BDATargetManager.GetAllTargetsExcluding(targetsTried, this).GetEnumerator()) + while (finalTargets.MoveNext()) + { + if (finalTargets.Current == null) continue; + SetTarget(finalTargets.Current); + if (!SmartPickWeapon_EngagementEnvelope(finalTargets.Current)) continue; + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging a final target with {selectedWeapon.GetShortName()}"); + } + return; + } + + //no valid targets found + if (potentialTarget == null || selectedWeapon == null) + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is disengaging - no valid weapons - no valid targets"); + } + CycleWeapon(0); + SetTarget(null); + + if (vesselRadarData && vesselRadarData.locked && missilesAway.Count == 0) // Don't unlock targets while we've got missiles in the air. + { + vesselRadarData.UnlockAllTargets(false); + } + return; + } + + Debug.Log("[BDArmory.MissileFire]: Unhandled target case"); + } + + void SmartFindSecondaryTargets() + { + //Debug.Log("[BDArmory.MTD]: Finding 2nd targets"); + using (List.Enumerator secTgt = targetsAssigned.GetEnumerator()) + while (secTgt.MoveNext()) + { + if (secTgt.Current == null) continue; + if (secTgt.Current == currentTarget) continue; + secTgt.Current.Disengage(this); + } + targetsAssigned.Clear(); + using (List.Enumerator mslTgt = missilesAssigned.GetEnumerator()) + while (mslTgt.MoveNext()) + { + if (mslTgt.Current == null) continue; + if (mslTgt.Current == currentTarget) continue; + mslTgt.Current.Disengage(this); + } + missilesAssigned.Clear(); + if (!currentTarget.isMissile) + { + targetsAssigned.Add(currentTarget); + } + else + { + missilesAssigned.Add(currentTarget); + } + List targetsTried = []; + + //Secondary targeting priorities + //1. incoming missile threats + //2. highest priority non-targeted target + //3. closest non-targeted target + if (targetMissiles) + { + for (int i = 0; i < Math.Max(multiTargetNum, multiMissileTgtNum) - 1; i++) + { + 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 < Math.Max(multiTargetNum, multiMissileTgtNum) - 1; i++) //primary target already added, so subtract 1 from nultitargetnum + { + TargetInfo potentialTarget = null; + //============VESSEL THREATS============ + + //then engage the closest enemy + potentialTarget = BDATargetManager.GetHighestPriorityTarget(this); + if (potentialTarget) + { + targetsAssigned.Add(potentialTarget); + targetsTried.Add(potentialTarget); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting priority target {potentialTarget.Vessel.GetName()} as secondary target {i}"); + } + else + { + potentialTarget = BDATargetManager.GetClosestTarget(this); + if (BDArmorySettings.DEFAULT_FFA_TARGETING) + { + potentialTarget = BDATargetManager.GetClosestTargetWithBiasAndHysteresis(this); + } + if (potentialTarget) + { + targetsAssigned.Add(potentialTarget); + targetsTried.Add(potentialTarget); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting bias target {potentialTarget.Vessel.GetName()} as secondary target {i}"); + } + else + { + using (List.Enumerator target = BDATargetManager.TargetList(Team).GetEnumerator()) + while (target.MoveNext()) + { + if (target.Current == null) continue; + if (target.Current.WeaponManager == null) continue; + if (target.Current && target.Current.Vessel && CanSeeTarget(target.Current) && !targetsTried.Contains(target.Current)) + { + targetsAssigned.Add(target.Current); + targetsTried.Add(target.Current); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting first remaining target {target.Current.Vessel.GetName()} as secondary target {i}"); + break; + } + } + } + } + } + if (targetsAssigned.Count + missilesAssigned.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) + { + TargetScoreLabel = ""; + TargetLabel = ""; + return; + } + + // 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"]; + var TargetProtectVIPFields = Fields["targetWeightProtectVIP"]; + var TargetAttackVIPFields = Fields["targetWeightAttackVIP"]; + + // Calculate score values + var targetWM = target.WeaponManager; + float targetBiasValue = targetBias; + float targetRangeValue = target.TargetPriRange(this); + float targetPreferencevalue = target.TargetPriEngagement(targetWM, this.vessel.radarAltitude); + float targetATAValue = target.TargetPriATA(this); + float targetAoDValue = target.TargetPriAoD(this); + float targetAccelValue = target.TargetPriAcceleration(); + float targetClosureTimeValue = target.TargetPriClosureTime(this); + float targetWeaponNumberValue = target.TargetPriWeapons(targetWM, this); + float targetMassValue = target.TargetPriMass(targetWM, this); + float targetDamageValue = target.TargetPriDmg(targetWM); + float targetFriendliesEngagingValue = target.TargetPriFriendliesEngaging(this); + float targetThreatValue = target.TargetPriThreat(targetWM, this); + float targetProtectTeammateValue = target.TargetPriProtectTeammate(targetWM, this); + float targetProtectVIPValue = target.TargetPriProtectVIP(targetWM, this); + float targetAttackVIPValue = target.TargetPriAttackVIP(targetWM); + + // 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 + + targetWeightProtectTeammate * targetProtectTeammateValue + + targetWeightProtectVIP * targetProtectVIPValue + + targetWeightAttackVIP * targetAttackVIPValue); + + // Update GUI + 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.GetName(); + } + + bool CheckLockStatus(Vessel targetVessel, bool radar, ref bool radarDetected, ref bool skipRadarDetectionCheck) + { + // If radars/sonars are not enabled, or VRD is null or we don't have a lock + if ((radar ? !_radarsEnabled : !_sonarsEnabled) || vesselRadarData == null || !vesselRadarData.locked) + { + radarDetected = false; + skipRadarDetectionCheck = true; + return false; + } + + if ((vesselRadarData.lockedTargetData.vessel == targetVessel) || vesselRadarData.SwitchActiveLockedTarget(targetVessel)) + { + radarDetected = true; + skipRadarDetectionCheck = true; + return true; + } + + return false; + } + + bool CheckDetectionStatus(Vessel targetVessel, bool radar, ref bool radarLocked, ref bool skipRadarLockCheck) + { + if (!vesselRadarData) + { + skipRadarLockCheck = true; + return false; + } + + if (radar ? !_radarsEnabled : !_sonarsEnabled) + { + (TargetSignatureData tempData, bool tempLocked) = vesselRadarData.detectedRadarTargetLock(targetVessel, this); + if (tempData.exists) + { + if (tempLocked) + { + radarLocked = true; + skipRadarLockCheck = true; + } + return true; + } + } + else + { + skipRadarLockCheck = true; + // Technically IRST detected, but this is the only use case for this bool + // For sonars we don't need to check IRSTs (I think?) + if (radar && _irstsEnabled && vesselRadarData.activeIRTarget(targetVessel, this).exists) + return true; + } + + return false; + } + + bool CheckAntiRadStatus(Vessel targetVessel, in bool[] RWRThreatTypes) + { + bool foundTarget = false; + for (int i = 0; i < rwr.pingsData.Length; i++) + { + if ((rwr.pingsData[i].position - targetVessel.CoM).sqrMagnitude < 20f * 20f) //is current target a hostile radar source? + { + RWRThreatTypes[(int)rwr.pingsData[i].signalType] = true; + foundTarget = true; + } + } + return foundTarget; + } + + // extension for feature_engagementenvelope: new smartpickweapon method + bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) + { + // Part 1: Guard conditions (when not to pick a weapon) + // ------ + if (!target) + return false; + + var ai = AI; + if (ai != null && ai.pilotEnabled && !ai.CanEngage()) // AI exists and is enabled, but can't engage. + return false; + + if (target.isMissile && (target.isSplashed || target.isUnderwater)) + return false; // Don't try to engage torpedos, it doesn't work + + // Part 2: check weapons against individual target types + // ------ + + (double tempDistance, Vector3 targetDir) = ((target.position + target.velocity) - (vessel.CoM + vessel.Velocity())).MagNorm(); + float distance = (float)tempDistance; + IBDWeapon targetWeapon = null; + float targetWeaponRPM = -1; + float targetWeaponTDPS = 0; + float targetWeaponImpact = -1; + // float targetLaserDamage = 0; + float targetYield = -1; + float targetBombYield = -1; + float targetRocketPower = -1; + float targetRocketAccel = -1; + float targetHeatSignature = -1; + int targetWeaponPriority = -1; + bool candidateAGM = false; + bool candidateAntiRad = false; + var surfaceAI = SurfaceAI; + + bool skipRadarLockCheck = false; + bool radarLocked = false; + bool skipRadarDetectionCheck = false; + bool radarDetected = false; + + bool skipRWRCheck = false; + bool[] RWRTypes = new bool[10]; + + Vessel targetVessel = target.Vessel; + if (target.isMissile) + { + // iterate over weaponTypesMissile and pick suitable one based on engagementRange (and dynamic launch zone for missiles) + // Prioritize by: + // 1. Lasers + // 2. Guns + // 3. AA missiles + using (List.Enumerator item = weaponTypesMissile.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + // candidate, check engagement envelope + if (!CheckEngagementEnvelope(item.Current, distance, targetVessel)) continue; + // weapon usable, if missile continue looking for lasers/guns, else take it + WeaponClasses candidateClass = item.Current.GetWeaponClass(); + switch (candidateClass) + { + case (WeaponClasses.DefenseLaser): + { + ModuleWeapon Laser = item.Current as ModuleWeapon; + float candidateYTraverse = Laser.yawRange; + float candidatePTraverse = Laser.maxPitch; + bool electrolaser = Laser.electroLaser; + Transform fireTransform = Laser.fireTransforms[0]; + + if (Laser.BurstFire && Laser.RoundsRemaining > 0 && Laser.RoundsRemaining < Laser.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponPriority = 99; + continue; + } + + if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) continue; + + if (targetWeapon != null && (candidateYTraverse > 0 || candidatePTraverse > 0)) //prioritize turreted lasers + { + ModuleTurret turret = Laser.turret; + if (!TargetInTurretRange(turret, 15, default, Laser) || !TargetInCustomTurretRange(Laser, 15, default)) continue; // weight selection towards turrets that can fire on missile + targetWeapon = item.Current; + break; + } + targetWeapon = item.Current; // then any laser + break; + } + + case (WeaponClasses.Gun): + { + // For point defense, favor turrets and RoF + ModuleWeapon Gun = item.Current as ModuleWeapon; + float candidateRPM = Gun.roundsPerMinute; + float candidateYTraverse = Gun.yawRange; + float candidatePTraverse = Gun.maxPitch; + float candidateMinrange = Gun.engageRangeMin; + float candidateMaxRange = Gun.engageRangeMax; + bool candidatePFuzed = Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Proximity || Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Flak; + bool candidateVTFuzed = Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Timed || Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Flak; + float Cannistershot = Gun.ProjectileCount; + + if (Gun.BurstFire && Gun.RoundsRemaining > 0 && Gun.RoundsRemaining < Gun.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + + Transform fireTransform = Gun.fireTransforms[0]; + + if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) continue; + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + if (candidateYTraverse > 0 || candidatePTraverse > 0) + { + ModuleTurret turret = Gun.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Gun) ? 2.0f : TargetInCustomTurretRange(Gun, 5, default) ? 2.0f : 0.01f; // weight selection towards turrets that can fire on missile + } + if (candidatePFuzed || candidateVTFuzed) + { + candidateRPM *= 1.5f; // weight selection towards flak ammo + } + if (Cannistershot > 1) + { + candidateRPM *= (1 + ((Cannistershot / 2) / 100)); // weight selection towards cluster ammo based on submunition count + } + 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 ((targetWeapon != null) && (targetWeaponRPM > candidateRPM)) + continue; //dont replace better guns (but do replace missiles) + + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + break; + } + + case (WeaponClasses.Rocket): + { + // For point defense, favor turrets and RoF + ModuleWeapon Rocket = item.Current as ModuleWeapon; + float candidateRocketAccel = Rocket.thrust / Rocket.rocketMass; + float candidateRPM = Rocket.roundsPerMinute / 2; + bool candidatePFuzed = Rocket.proximityDetonation; + float candidateYTraverse = Rocket.yawRange; + float candidatePTraverse = Rocket.maxPitch; + float candidateMinrange = Rocket.engageRangeMin; + float candidateMaxRange = Rocket.engageRangeMax; + Transform fireTransform = Rocket.fireTransforms[0]; + + if (Rocket.BurstFire && Rocket.RoundsRemaining > 0 && Rocket.RoundsRemaining < Rocket.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + + if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) continue; + if (Rocket.choker || Rocket.impulseWeapon) continue; + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE / 2; + } + bool compareRocketRPM = false; + + if (candidateYTraverse > 0 || candidatePTraverse > 0) + { + ModuleTurret turret = Rocket.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Rocket) ? 2.0f : TargetInCustomTurretRange(Rocket, 5, default) ? 2.0f : 0.01f; // weight selection towards turrets that can fire on missile + } + if (targetRocketAccel < candidateRocketAccel) + { + candidateRPM *= 1.5f; //weight towards faster rockets + } + if (!candidatePFuzed) + { + candidateRPM *= 0.01f; //negatively weight against contact-fuze rockets + } + 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 ((targetWeapon != null) && targetWeapon.GetWeaponClass() == WeaponClasses.Gun) + { + compareRocketRPM = true; + } + if ((targetWeapon != null) && (targetWeaponRPM > candidateRPM)) + continue; //dont replace better guns (but do replace missiles) + if ((compareRocketRPM && (targetWeaponRPM * 2) < candidateRPM) || (!compareRocketRPM && (targetWeaponRPM) < candidateRPM)) + { + targetWeapon = item.Current; + targetRocketAccel = candidateRocketAccel; + targetWeaponRPM = candidateRPM; + } + break; + } + } + + if (candidateClass == WeaponClasses.Missile) + { + 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 + int candidatePriority = 0; + float candidateTDPS = 0f; + + 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 ordnance + candidateDetDist = mlauncher.DetonationDistance; + candidateAccel = mlauncher.thrust / mlauncher.part.mass; //for anti-missile, prioritize proxidetonation and accel + bool EMP = mlauncher.warheadType == MissileBase.WarheadTypes.EMP; + candidatePriority = Mathf.RoundToInt(mlauncher.priority); + + if (EMP) continue; + if (vessel.Splashed && FlightGlobals.getAltitudeAtPos(mlauncher.transform.position) < -10) continue; //we aren't going to surface in time (and are under no threat from the missile while underwater0 so don't baother + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + if (candidateDetDist + candidateAccel > targetWeaponTDPS) + { + candidateTDPS = candidateDetDist + candidateAccel; // weight selection faster missiles and larger proximity detonations that might catch an incoming missile in the blast + } + */ + continue; //Missile interception for MissileLauncher-based missiles handled in the point defense logic + } + else + { //is modular missile + BDModularGuidance mm = item.Current as BDModularGuidance; //need to work out priority stuff for MMGs + candidateTDPS = 5000; + + candidateDetDist = mm.warheadYield; + //candidateAccel = (((MissileLauncher)item.Current).thrust / ((MissileLauncher)item.Current).part.mass); + candidateAccel = 1; + candidatePriority = Mathf.RoundToInt(mm.priority); + + if (vessel.Splashed && FlightGlobals.getAltitudeAtPos(mlauncher.transform.position) < -5) continue; + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + if (candidateDetDist + candidateAccel > targetWeaponTDPS) + { + candidateTDPS = candidateDetDist + candidateAccel; + } + } + if (distance < ((EngageableWeapon)item.Current).engageRangeMin) + candidateTDPS *= -1f; // if within min range, negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo + + if ((targetWeapon != null) && (((distance < gunRange) && targetWeapon.GetWeaponClass() == WeaponClasses.Gun || targetWeapon.GetWeaponClass() == WeaponClasses.Rocket || targetWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) || (targetWeaponTDPS > candidateTDPS))) + continue; //dont replace guns or better missiles + targetWeapon = item.Current; + targetWeaponTDPS = candidateTDPS; + targetWeaponPriority = candidatePriority; + } + } + } + + //else if (!target.isLanded) + else if (target.isFlying && !target.isMissile) + { + // iterate over weaponTypesAir and pick suitable one based on engagementRange (and dynamic launch zone for missiles) + // Prioritize by: + // 1. AA missiles (if we're flying, otherwise use guns if we're within gun range) + // 1. Lasers + // 2. Guns + // 3. rockets + // 4. unguided missiles + using (List.Enumerator item = weaponTypesAir.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + + // candidate, check engagement envelope + if (!CheckEngagementEnvelope(item.Current, distance, targetVessel)) continue; + // weapon usable, if missile continue looking for lasers/guns, else take it + WeaponClasses candidateClass = item.Current.GetWeaponClass(); + // any rocketpods work? + switch (candidateClass) + { + case (WeaponClasses.Bomb): //hardly ideal, but if it's the only thing you have, then just maybe... + { + if (!vessel.Splashed || (vessel.Splashed && vessel.altitude > targetVessel.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 ordnance + //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; + } + } + } + } + } + break; + } + case (WeaponClasses.Rocket): + { + //for AA, favor higher accel and proxifuze + ModuleWeapon Rocket = item.Current as ModuleWeapon; + float candidateRocketAccel = Rocket.thrust / Rocket.rocketMass; + float candidateRPM = Rocket.roundsPerMinute; + bool candidatePFuzed = Rocket.proximityDetonation; + 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]; + if (vessel.LandedOrSplashed && candidatePTraverse <= 0) continue; //not going to hit a flier with fixed guns + if (vessel.Splashed && BDArmorySettings.BULLET_WATER_DRAG && (!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0) continue; + Vector3 aimDirection = fireTransform.forward; + float targetCosAngle = Rocket.FiringSolutionVector != null ? Vector3.Dot(aimDirection, (Vector3)Rocket.FiringSolutionVector) : Vector3.Dot(aimDirection, (vessel.CoM - fireTransform.position).normalized); + bool outsideFiringCosAngle = targetCosAngle < Rocket.targetAdjustedMaxCosAngle; + + if (Rocket.BurstFire && Rocket.RoundsRemaining > 0 && Rocket.RoundsRemaining < Rocket.RoundsPerMag) //if we're in the middle of firing a burst-fire weapon, keep that selected until it's done firing + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + if ((Rocket.choker || Rocket.electroLaser) && target.isDebilitated) continue; //don't keep shooting target with shutdown guns if shutdown + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //dont replace a higher priority weapon with a lower priority one + + if (candidateYTraverse > 0 || candidatePTraverse > 0) + { + ModuleTurret turret = Rocket.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Rocket) ? 2.0f : TargetInCustomTurretRange(Rocket, 5, default) ? 2.0f : 0.01f; // weight selection towards turrets that can face the right direction + } + + if (targetRocketAccel < candidateRocketAccel) + { + candidateRPM *= 1.5f; //weight towards faster rockets + } + if (candidatePFuzed) + { + candidateRPM *= 1.5f; // weight selection towards flak ammo + } + else + { + candidateRPM *= 0.5f; + } + if (outsideFiringCosAngle) + { + 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 || 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 + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetRocketAccel = candidateRocketAccel; + targetWeaponPriority = candidatePriority; + } + if (targetWeaponPriority == candidatePriority) //if equal priority, use standard weighting + { + if (targetWeapon != null && (targetWeapon.GetWeaponClass() == WeaponClasses.Missile) && (targetWeaponTDPS > 0)) + continue; //dont replace missiles within their engage range + if (targetWeaponRPM < candidateRPM) //or best gun + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetRocketAccel = candidateRocketAccel; + targetWeaponPriority = candidatePriority; + } + } + break; + } + //Guns have higher priority than rockets; selected gun will override rocket selection + case (WeaponClasses.Gun): + { + // For AtA, generally favour higher RPM and turrets + //prioritize weapons with priority, then: + //if shooting fighter-sized targets, prioritize RPM + //if shooting larger targets - bombers/zeppelins/Ace Combat Wunderwaffen - prioritize biggest caliber + ModuleWeapon Gun = item.Current as ModuleWeapon; + float candidateRPM = Gun.roundsPerMinute; + bool candidateGimbal = Gun.maxPitch > Gun.minPitch && Gun.maxPitch > 20; //not going to hit air with low elevation, unless they're flying very low + float candidateTraverse = Gun.yawRange; + bool candidatePFuzed = Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Proximity || Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Flak; + bool candidateVTFuzed = Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Timed || Gun.eFuzeType == PooledBullet.BulletFuzeTypes.Flak; + float Cannistershot = Gun.ProjectileCount; + float candidateMinrange = Gun.engageRangeMin; + float candidateMaxRange = Gun.engageRangeMax; + int candidatePriority = Mathf.RoundToInt(Gun.priority); + float candidateRadius = targetVessel.GetRadius(Gun.fireTransforms[0].forward, target.bounds); + float candidateCaliber = Gun.caliber; + + if (Gun.BurstFire && Gun.RoundsRemaining > 0 && Gun.RoundsRemaining < Gun.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + if ((Gun.Impulse != 0 && Gun.electroLaser) && target.isDebilitated) continue; //don't keep shooting target with non-damaging shutdown guns if shutdown + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + Transform fireTransform = Gun.fireTransforms[0]; + if ((vessel.situation == Vessel.Situations.LANDED || vessel.situation == Vessel.Situations.SPLASHED) && !candidateGimbal) continue; //not going to hit fliers with fixed guns + if ((!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) continue; //don't select guns on sinking ships, but allow gun selection on subs + + Vector3 aimDirection = fireTransform.forward; + float targetCosAngle = Gun.FiringSolutionVector != null ? Vector3.Dot(aimDirection, (Vector3)Gun.FiringSolutionVector) : Vector3.Dot(aimDirection, (vessel.CoM - fireTransform.position).normalized); + bool outsideFiringCosAngle = targetCosAngle < Gun.targetAdjustedMaxCosAngle; + + if (targetWeapon != null && targetWeaponPriority > candidatePriority) continue; //keep higher priority weapon + + if (candidateRadius > 8) //most fighters are, what, at most 15m in their largest dimension? That said, maybe make this configurable in the weapon PAW... + {//weight selection towards larger caliber bullets, modified by turrets/fuzes/range settings when shooting bombers + if (candidateGimbal = true && candidateTraverse > 0) + { + ModuleTurret turret = Gun.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Gun) ? 2.0f : TargetInCustomTurretRange(Gun, 5, default) ? 2.0f : 0.01f; // weight selection towards turrets that can face the right direction + } + if (candidatePFuzed || candidateVTFuzed) + { + candidateCaliber *= 1.5f; // weight selection towards flak ammo + } + if (outsideFiringCosAngle) + { + 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 || 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 + } + candidateRPM = candidateCaliber * 10; + } + else //weight selection towards RoF, modified by turrets/fuzes/shot quantity/range + { + if (candidateGimbal = true && candidateTraverse > 0) + { + ModuleTurret turret = Gun.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Gun) ? 2.0f : TargetInCustomTurretRange(Gun, 5, default) ? 2.0f : 0.01f; // weight selection towards turrets that can face the right direction + } + if (candidatePFuzed || candidateVTFuzed) + { + candidateRPM *= 1.5f; // weight selection towards flak ammo + } + if (Cannistershot > 1) + { + candidateRPM *= (1 + ((Cannistershot / 2) / 100)); // weight selection towards cluster ammo based on submunition count + } + if (outsideFiringCosAngle) + { + 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 || 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; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeaponRPM < candidateRPM) + { + if ((targetWeapon != null) && (targetWeapon.GetWeaponClass() == WeaponClasses.Missile) && (targetWeaponTDPS > 0)) + continue; //dont replace missiles within their engage range + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + } + break; + } + //if lasers, lasers will override gun selection + case (WeaponClasses.DefenseLaser): + { + // For AA, favour higher power/turreted + ModuleWeapon Laser = item.Current as ModuleWeapon; + float candidateRPM = Laser.roundsPerMinute; + bool candidateGimbal = Laser.maxPitch > Laser.minPitch && Laser.maxPitch > 20; + 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; + float candidatePower = electrolaser ? Laser.ECPerShot / (pulseLaser ? 50 : 1) : Laser.laserDamage / (pulseLaser ? 50 : 1); + + Transform fireTransform = Laser.fireTransforms[0]; + + if (Laser.BurstFire && Laser.RoundsRemaining > 0 && Laser.RoundsRemaining < Laser.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + + if ((vessel.situation == Vessel.Situations.LANDED || vessel.situation == Vessel.Situations.SPLASHED) && !candidateGimbal) continue; //not going to hit fliers with fixed lasers + if ((!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && vessel.Splashed && BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0) continue; + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + + if (electrolaser = true && target.isDebilitated) continue; // don't select EMP weapons if craft already disabled + + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + + candidateRPM *= candidatePower; + + if (candidateGimbal = true && candidateTraverse > 0) + { + ModuleTurret turret = Laser.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Laser) ? 1.5f : TargetInCustomTurretRange(Laser, 5, default) ? 1.3f : 0.01f; // weight selection towards turrets that can face the right direction + } + 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; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeapon != null && (targetWeapon.GetWeaponClass() == WeaponClasses.Missile) && (targetWeaponTDPS > 0)) + continue; //dont replace missiles within their engage range + if (targetWeaponRPM < candidateRPM) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + } + break; + } + //projectile weapon selected, any missiles that take precedence? + case (WeaponClasses.Missile): + { + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + float candidateDetDist = 0; + float candidateTurning = 0; + int candidatePriority = 0; + float candidateTDPS = 0f; + + 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 ordnance + candidateDetDist = mlauncher.DetonationDistance; + candidateTurning = mlauncher.maxTurnRateDPS; //for anti-aircraft, prioritize detonation dist and turn capability. Rejigger to use kinematic missile perf. based on missile maxAoA/maxG/optimalAirspeed instead of arbitrary static value? + candidatePriority = Mathf.RoundToInt(mlauncher.priority); + bool EMP = mlauncher.warheadType == MissileBase.WarheadTypes.EMP; + // Should probably turn this into a switch + bool heat = mlauncher.TargetingMode == MissileBase.TargetingModes.Heat; + bool radar = mlauncher.TargetingMode == MissileBase.TargetingModes.Radar; + bool inertial = mlauncher.TargetingMode == MissileBase.TargetingModes.Inertial; + bool antiRad = mlauncher.TargetingMode == MissileBase.TargetingModes.AntiRad; + float heatThresh = mlauncher.heatThreshold; + if (EMP && target.isDebilitated) continue; + if (vessel.Splashed && (!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(mlauncher.transform.position) < -10)) continue; //allow submarine-mounted missiles; new launch depth check in launchAuth + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + + if (candidateTurning > targetWeaponTDPS) + { + candidateTDPS = candidateTurning; // weight selection towards more maneuverable missiles + } + if (candidateDetDist > 0) + { + candidateTDPS += candidateDetDist; // weight selection towards misiles with proximity warheads + } + //if (heat && heatTarget.exists && heatTarget.signalStrength * + // ((BDArmorySettings.ASPECTED_IR_SEEKERS && Vector3.Dot(guardTarget.vesselTransform.up, mlauncher.transform.forward) > 0.25f) ? + // mlauncher.frontAspectHeatModifier : 1) < heatThresh) //heatTarget doesn't get found until *after* a heater is selected + if (heat) + { + // Note this doesn't consider flares... + if (targetHeatSignature < 0) + (targetHeatSignature, Part tempPart) = BDATargetManager.GetVesselHeatSignature(targetVessel, targetDir * 50f + vessel.CoM); + if (targetHeatSignature * ((BDArmorySettings.ASPECTED_IR_SEEKERS && Vector3.Dot(targetVessel.vesselTransform.up, mlauncher.GetForwardTransform()) > 0.25f) ? mlauncher.frontAspectHeatModifier : 1) < heatThresh) + candidateTDPS *= 0.0001f; //Heatseeker, but IR sig is below missile threshold, skip to something else unless nothing else available + //candidateTDPS *= 0.0001f; //Heatseeker, but IR sig is below missile threshold, skip to something else unless nothing else available + //if (mlauncher.frontAspectHeatModifier < 0.15f) continue; + //candidateTDPS *= mlauncher.frontAspectHeatModifier / 100; + } + if (radar) + { + if (!skipRadarLockCheck) + { + radarLocked = CheckLockStatus(targetVessel, true, ref radarDetected, ref skipRadarDetectionCheck); + skipRadarLockCheck = true; + } + + if (!radarLocked) + { + if (!mlauncher.radarLOAL) candidateTDPS *= 0.001f; //no radar lock, skip to something else unless nothing else available + else + { + if (mlauncher.seekerTimeout < ((distance - mlauncher.activeRadarRange) / mlauncher.optimumAirspeed)) candidateTDPS *= 0.5f; //outside missile self-lock zone + } + } + } + if (inertial) + { + if (!skipRadarDetectionCheck) + { + radarDetected = CheckDetectionStatus(targetVessel, true, ref radarLocked, ref skipRadarLockCheck); + + skipRadarDetectionCheck = true; + } + + if (!radarDetected) + { + candidateTDPS *= 0.001f; + } + } + if (antiRad && rwr && rwr.enabled) + { + if (!skipRWRCheck) + { + CheckAntiRadStatus(targetVessel, RWRTypes); + skipRWRCheck = true; + } + + bool foundAntiRad = false; + foreach (RadarWarningReceiver.RWRThreatTypes type in mlauncher.antiradTargets) + { + if (RWRTypes[(int)type]) + { + foundAntiRad = true; + break; + } + } + + if (!foundAntiRad) candidateTDPS *= 0.001f; + } + if (mlauncher.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) + { + candidateTDPS *= 0.001f; //no laserdot, skip to something else unless nothing else available + } + float fovAngle = VectorUtils.Angle(mlauncher.GetForwardTransform(), targetVessel.CoM - mlauncher.transform.position); + if (fovAngle > mlauncher.missileFireAngle && mlauncher.missileFireAngle < mlauncher.maxOffBoresight * 0.75f) + { + candidateTDPS *= mlauncher.missileFireAngle / fovAngle; //missile is clamped to a narrow boresight - do we have anything with a wider FoV we should start with? + } + } + else + { //is modular missile + BDModularGuidance mm = item.Current as BDModularGuidance; //need to work out priority stuff for MMGs + candidateTDPS = 5000; + candidateDetDist = mm.warheadYield; + //candidateTurning = ((MissileLauncher)item.Current).maxTurnRateDPS; //for anti-aircraft, prioritize detonation dist and turn capability + candidatePriority = Mathf.RoundToInt(mm.priority); + + if ((!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(mlauncher.transform.position) < 0)) continue; + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + + //if (candidateTurning > targetWeaponTDPS) + //{ + // candidateTDPS = candidateTurning; // need a way of calculating this... + //} + if (candidateDetDist > 0) + { + candidateTDPS += candidateDetDist; // weight selection towards misiles with proximity warheads + } + } + if (distance < ((EngageableWeapon)item.Current).engageRangeMin || firedMissiles >= maxMissilesOnTarget || (unguidedWeapon && distance > ((EngageableWeapon)item.Current).engageRangeMax / 10)) + 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; + targetWeaponTDPS = candidateTDPS; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeaponTDPS < candidateTDPS) + { + targetWeapon = item.Current; + targetWeaponTDPS = candidateTDPS; + targetWeaponPriority = candidatePriority; + } + } + } + break; + } + } + } + } + else if (target.isUnderwater) + { + // iterate over weaponTypesSLW (Ship Launched Weapons) and pick suitable one based on engagementRange + // Prioritize by: + // 1. Depth Charges + // 2. Torpedos + using (List.Enumerator item = weaponTypesSLW.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + if (!CheckEngagementEnvelope(item.Current, distance, targetVessel)) continue; + + WeaponClasses candidateClass = item.Current.GetWeaponClass(); + switch (candidateClass) + { + case (WeaponClasses.SLW): + { + MissileLauncher SLW = item.Current as MissileLauncher; + if (SLW.TargetingMode == MissileBase.TargetingModes.Radar && (!_sonarsEnabled && !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 ordnance + float candidateYield = SLW.GetBlastRadius(); + float candidateTurning = SLW.maxTurnRateDPS; + float candidateTDPS = 0f; + bool EMP = SLW.warheadType == MissileBase.WarheadTypes.EMP; + bool heat = SLW.TargetingMode == MissileBase.TargetingModes.Heat; + bool radar = SLW.TargetingMode == MissileBase.TargetingModes.Radar; + bool inertial = SLW.TargetingMode == MissileBase.TargetingModes.Inertial; + float heatThresh = SLW.heatThreshold; + int candidatePriority = Mathf.RoundToInt(SLW.priority); + + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + if (EMP && target.isDebilitated) continue; + + if (!vessel.Splashed || (vessel.Splashed && vessel.altitude > targetVessel.altitude)) //if surfaced or sumberged, but above target, try depthcharges + { + if (item.Current.GetMissileType().ToLower() == "depthcharge") + { + if (distance < candidateYield) continue; //could add in prioritization for bigger boom, but how many different options for depth charges are there? + targetWeapon = item.Current; + targetWeaponPriority = candidatePriority; + break; + } + } + + if (item.Current.GetMissileType().ToLower() != "torpedo") continue; + + if (distance < candidateYield) continue; //don't use explosives within their blast radius + //if(firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + if (SLW.TargetingMode == MissileBase.TargetingModes.Heat && SLW.activeRadarRange < 0 && (rwr && rwr.rwrEnabled)) //we have passive acoustic homing? see if anything has active sonar + { + if (!skipRWRCheck) + { + CheckAntiRadStatus(targetVessel, in RWRTypes); + skipRWRCheck = true; + } + + if (RWRTypes[6]) candidateYield *= 1.5f; // Prioritize PAH Torps for hostile sonar sources + } + + if (candidateTurning + candidateYield > targetWeaponTDPS) + { + candidateTDPS = candidateTurning + candidateYield; // weight selection towards more maneuverable missiles + } + //if (candidateDetDist > 0) + //{ + // 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 (!skipRadarLockCheck) + { + radarLocked = CheckLockStatus(targetVessel, false, ref radarDetected, ref skipRadarDetectionCheck); + skipRadarDetectionCheck = true; + } + + if (!radarLocked) + { + if (!SLW.radarLOAL) candidateTDPS *= 0.001f; //no radar/sonar lock, skip to something else unless nothing else available + else + { + if (SLW.seekerTimeout < ((distance - SLW.activeRadarRange) / SLW.optimumAirspeed)) candidateTDPS *= 0.5f; //outside missile self-lock zone + } + } + } + if (inertial) + { + if (!skipRadarDetectionCheck) + { + radarDetected = CheckDetectionStatus(targetVessel, false, ref radarLocked, ref skipRadarLockCheck); + skipRadarDetectionCheck = true; + } + + if (!radarDetected) + { + candidateTDPS *= 0.001f; //no radar/sonar, skip to something else unless nothing else available + } + } + if (distance < ((EngageableWeapon)item.Current).engageRangeMin || firedMissiles >= maxMissilesOnTarget || (unguidedWeapon && distance > ((EngageableWeapon)item.Current).engageRangeMax / 10)) + candidateTDPS *= -1f; // if within min range, negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo + if ((!vessel.Splashed) || ((distance > gunRange) && (vessel.LandedOrSplashed))) // If we're not airborne, we want to prioritize guns + { + if ((distance <= 500 || distance < candidateYield || candidateTDPS < 1) && targetWeapon != null) continue; //torp 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; + targetWeaponTDPS = candidateTDPS; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeaponTDPS < candidateTDPS) + { + targetWeapon = item.Current; + targetWeaponTDPS = candidateTDPS; + targetWeaponPriority = candidatePriority; + } + } + } + break; + //MMG torpedo support... ? + } + case (WeaponClasses.Rocket): + { + ModuleWeapon Rocket = item.Current as ModuleWeapon; + float candidateRocketPower = Rocket.blastRadius; + float CandidateEndurance = Rocket.thrustTime; + int candidateRanking = Mathf.RoundToInt(Rocket.priority); + Transform fireTransform = Rocket.fireTransforms[0]; + + if (Rocket.BurstFire && Rocket.RoundsRemaining > 0 && Rocket.RoundsRemaining < Rocket.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponPriority = 99; + continue; + } + if ((Rocket.choker || Rocket.electroLaser) && target.isDebilitated) continue; //don't keep shooting target with non-damaging shutdown guns if shutdown + if (vessel.Splashed && FlightGlobals.getAltitudeAtPos(fireTransform.position) < -5)//if underwater, rockets might work, at close range + { + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if ((distance > 500 * CandidateEndurance)) continue; + } + if (targetWeaponPriority > candidateRanking) + continue; //don't select a lower priority weapon over a higher priority one + + if (targetWeaponPriority < candidateRanking) //use priority gun + { + if (distance < candidateRocketPower) continue;// don't fire rockets when within blast radius + targetWeapon = item.Current; + targetRocketPower = candidateRocketPower; + targetWeaponPriority = candidateRanking; + } + else //if equal priority, use standard weighting + { + if (targetRocketPower < candidateRocketPower) //don't replace higher yield rockets + { + if (distance < candidateRocketPower) continue;// don't drop bombs when within blast radius + targetWeapon = item.Current; + targetRocketPower = candidateRocketPower; + targetWeaponPriority = candidateRanking; + } + } + } + break; + } + case (WeaponClasses.DefenseLaser): + { + // For STS, favour higher power/turreted + ModuleWeapon Laser = item.Current as ModuleWeapon; + float candidateRPM = Laser.roundsPerMinute; + bool candidateGimbal = Laser.maxPitch > Laser.minPitch && Laser.maxPitch > 20; + 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; + float candidatePower = electrolaser ? Laser.ECPerShot / (pulseLaser ? 50 : 1) : Laser.laserDamage / (pulseLaser ? 50 : 1); + + Transform fireTransform = Laser.fireTransforms[0]; + if (Laser.BurstFire && Laser.RoundsRemaining > 0 && Laser.RoundsRemaining < Laser.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + if (vessel.Splashed && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)//if underwater, lasers should work, at close range + { + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if (distance > candidateMaxrange / 10) continue; + } + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + + if (electrolaser) continue; // don't use lightning guns underwater + + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + + candidateRPM *= candidatePower; + + if (candidateGimbal = true && candidateTraverse > 0) + { + ModuleTurret turret = Laser.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Laser) ? 1.5f : TargetInCustomTurretRange(Laser, 5, default) ? 1.5f : 0.01f; // weight selection towards turrets that can face the right direction + } + 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; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeaponRPM < candidateRPM) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + } + } + break; + } + } + } + } + // Note: this is an "else" instead of an "else if" because craft spawned to the runway from the SPH don't have their Landed state set for some reason. + else // if (target.isLandedOrSurfaceSplashed) //for targets on surface/above 10m depth + { + // iterate over weaponTypesGround and pick suitable one based on engagementRange (and dynamic launch zone for missiles) + // Prioritize by: + // 1. ground attack missiles (cruise, gps, unguided) if target not moving + // 2. ground attack missiles (guided) if target is moving + // 3. Bombs / Rockets + // 4. Guns + + using (List.Enumerator item = weaponTypesGround.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + // candidate, check engagement envelope + if (!CheckEngagementEnvelope(item.Current, distance, targetVessel)) continue; + // weapon usable, if missile continue looking for lasers/guns, else take it + WeaponClasses candidateClass = item.Current.GetWeaponClass(); + switch (candidateClass) + { + case (WeaponClasses.DefenseLaser): //lasers would be a suboptimal choice for strafing attacks, but if nothing else available... + { + // For Atg, favour higher power/turreted + ModuleWeapon Laser = item.Current as ModuleWeapon; + float candidateRPM = Laser.roundsPerMinute; + bool candidateGimbal = Laser.maxPitch > Laser.minPitch; + 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; + bool HEpulses = Laser.HEpulses; + float candidatePower = electrolaser ? Laser.ECPerShot / (pulseLaser ? 50 : 1) : Laser.laserDamage / (pulseLaser ? 50 : 1); + + if (Laser.BurstFire && Laser.RoundsRemaining > 0 && Laser.RoundsRemaining < Laser.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + + Transform fireTransform = Laser.fireTransforms[0]; + + if ((!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) continue; //new ModuleWeapon depth check for sub-mounted rockets + + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + + if (electrolaser = true && target.isDebilitated) continue; // don't select EMP weapons if craft already disabled + + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + + candidateRPM *= candidatePower / 1000; + + if (candidateGimbal = true && candidateTraverse > 0) + { + ModuleTurret turret = Laser.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Laser) ? 1.5f : TargetInCustomTurretRange(Laser, 5, default) ? 1.3f : 0.01f; // weight selection towards turrets that can face the right direction + } + if (HEpulses) + { + candidateRPM *= 1.5f; // weight selection towards lasers that can do blast damage + } + 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; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeapon != null && targetWeapon.GetWeaponClass() == WeaponClasses.Rocket || targetWeapon.GetWeaponClass() == WeaponClasses.Gun) continue; + if (targetWeaponImpact < candidateRPM) //don't replace bigger guns + { + targetWeapon = item.Current; + targetWeaponImpact = candidateRPM; + targetWeaponPriority = candidatePriority; + } + } + break; + } + + case (WeaponClasses.Gun): //iterate through guns, if nothing else, use found gun + { + if ((distance > gunRange) && (targetWeapon != null)) + continue; + // For Ground Attack, favour higher blast strength + ModuleWeapon Gun = item.Current as ModuleWeapon; + float candidateRPM = Gun.roundsPerMinute; + float candidateImpact = Gun.bulletMass * Gun.bulletVelocity; + int candidatePriority = Mathf.RoundToInt(Gun.priority); + bool candidateGimbal = Gun.maxPitch > Gun.minPitch; + float candidateMinrange = Gun.engageRangeMin; + float candidateMaxRange = Gun.engageRangeMax; + float candidateTraverse = Gun.yawRange * (Gun.maxPitch - Gun.minPitch); + float candidateRadius = targetVessel.GetRadius(Gun.fireTransforms[0].forward, target.bounds); + float candidateCaliber = Gun.caliber; + Transform fireTransform = Gun.fireTransforms[0]; + + if (Gun.BurstFire && Gun.RoundsRemaining > 0 && Gun.RoundsRemaining < Gun.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponRPM = candidateRPM; + targetWeaponPriority = 99; + continue; + } + if ((Gun.Impulse != 0 && Gun.electroLaser) && target.isDebilitated) continue; //don't keep shooting target with non-damaging shutdown guns if shutdown + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if ((!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && vessel.Splashed && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0) continue; + if (candidateCaliber < 75 && FlightGlobals.getAltitudeAtPos(target.position) + targetVessel.GetRadius() < 0) continue; //vessel completely submerged, and not using rounds big enough to survive water impact + } + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + candidateRPM = BDArmorySettings.FIRE_RATE_OVERRIDE; + } + + if (targetWeaponPriority > candidatePriority) + continue; //dont replace better guns or missiles within their engage range + + if (candidateRadius > 4) //smmall vees target with high-ROF weapons to improve hit chance, bigger stuff use bigger guns + { + candidateRPM = candidateImpact * candidateRPM; + } + if (candidateGimbal && candidateTraverse > 0) + { + ModuleTurret turret = Gun.turret; + candidateRPM *= TargetInTurretRange(turret, 5, default, Gun) ? 1.5f : TargetInCustomTurretRange(Gun, 5, default) ? 1.5f : 0.01f; // weight selection towards turrets that can face the right direction + } + 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; + targetWeaponImpact = candidateRPM; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetWeapon != null && targetWeapon.GetWeaponClass() == WeaponClasses.Rocket) continue; + if (targetWeaponImpact < candidateRPM) //don't replace bigger guns + { + targetWeapon = item.Current; + targetWeaponImpact = candidateRPM; + targetWeaponPriority = candidatePriority; + } + } + break; + } + //Any rockets we can use instead of guns? + case (WeaponClasses.Rocket): + { + ModuleWeapon Rocket = item.Current as ModuleWeapon; + float candidateRocketPower = Rocket.blastRadius; + float CandidateEndurance = Rocket.thrustTime; + int candidateRanking = Mathf.RoundToInt(Rocket.priority); + Transform fireTransform = Rocket.fireTransforms[0]; + + if (Rocket.BurstFire && Rocket.RoundsRemaining > 0 && Rocket.RoundsRemaining < Rocket.RoundsPerMag) + { + targetWeapon = item.Current; + targetWeaponPriority = 99; + continue; + } + if ((Rocket.choker || Rocket.electroLaser) && target.isDebilitated) continue; //don't keep shooting target with non-damaging shutdown guns if shutdown + if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) + { + if (distance > 100 * CandidateEndurance) continue; + } + + if (targetWeaponPriority > candidateRanking) + continue; //don't select a lower priority weapon over a higher priority one + + if (targetWeaponPriority < candidateRanking) //use priority gun + { + if (distance < candidateRocketPower) continue;// don't drop bombs when within blast radius + targetWeapon = item.Current; + targetRocketPower = candidateRocketPower; + targetWeaponPriority = candidateRanking; + } + else //if equal priority, use standard weighting + { + if (targetRocketPower < candidateRocketPower) //don't replace higher yield rockets + { + if (distance < candidateRocketPower) continue;// don't drop bombs when within blast radius + targetWeapon = item.Current; + targetRocketPower = candidateRocketPower; + targetWeaponPriority = candidateRanking; + } + } + break; + } + //Bombs are good. any of those we can use over rockets? + case (WeaponClasses.Bomb): + { + if (vessel.Splashed && vessel.altitude < targetVessel.altitude) continue; //I guess depth charges would sorta apply here, but those are SLW instead + MissileLauncher Bomb = item.Current as MissileLauncher; + if (Bomb.reloadableRail != null && (Bomb.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordnance + //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); + double srfSpeed = targetVessel.horizontalSrfSpeed; + + 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) + { + // Priority Sequence: + // - guided (JDAM) + // - by blast strength + // - find way to implement cluster bomb selection priority? + + if (Bomb.GuidanceMode != MissileBase.GuidanceModes.AGMBallistic) + { + 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 (srfSpeed > 1) //prioritize cluster bombs for moving targets + { + candidateYield *= (candidateCluster * 2); + 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; + } + } + } + if (Bomb.GuidanceMode == MissileBase.GuidanceModes.AGMBallistic) + { + 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; + } + } + } + } + break; + } + //Missiles are the preferred method of ground attack. use if available over other options + case (WeaponClasses.Missile): //don't use missiles underwater. That's what torpedoes are for + { + // Priority Sequence: + // - Antiradiation + // - 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? + int candidatePriority; + float candidateYield; + double srfSpeed = targetVessel.horizontalSrfSpeed; + MissileLauncher Missile = item.Current as MissileLauncher; + if (Missile != null) + { + //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 ordnance + if (vessel.Splashed && (!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && FlightGlobals.getAltitudeAtPos(item.Current.GetPart().transform.position) < -10) 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 (Missile.TargetingMode == MissileBase.TargetingModes.Gps || + Missile.GuidanceMode == MissileBase.GuidanceModes.Cruise || + Missile.GuidanceMode == MissileBase.GuidanceModes.AGMBallistic || + Missile.TargetingMode == MissileBase.TargetingModes.Inertial || + 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 + if (!skipRWRCheck) + { + CheckAntiRadStatus(targetVessel, RWRTypes); + skipRWRCheck = true; + } + + foreach (RadarWarningReceiver.RWRThreatTypes type in Missile.antiradTargets) + { + if (RWRTypes[(int)type]) + { + candidateAntiRad = true; + candidateYield *= 2; // Prioritize anti-rad missiles for hostile radar sources + break; + } + } + + if (candidateAntiRad) + { + if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom + //targetYield = candidateYield; + //targetWeapon = item.Current; + //targetWeaponPriority = candidatePriority; + candidateAGM = true; + } + } + else if (Missile.TargetingMode == MissileBase.TargetingModes.Laser) + { + if (candidateAntiRad) continue; //keep antirad missile; + if (targetingPods.Count <= 0 || (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude > Mathf.Max(100, 0.013f * (float)targetVessel.srfSpeed * (float)targetVessel.srfSpeed))) 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) + { + if (!skipRadarLockCheck) + { + radarLocked = CheckLockStatus(targetVessel, true, ref radarDetected, ref skipRadarDetectionCheck); + skipRadarLockCheck = true; + } + + if (!radarLocked) + { + if (!Missile.radarLOAL) candidateYield *= 0.1f; //no radar lock, skip to something else unless nothing else available + else + { + if (Missile.seekerTimeout < ((distance - Missile.activeRadarRange) / Missile.optimumAirspeed)) candidateYield *= 0.5f; //outside missile self-lock zone + } + } + } + + if (Missile.TargetingMode == MissileBase.TargetingModes.Inertial) + { + if (!skipRadarDetectionCheck) + { + radarDetected = CheckDetectionStatus(targetVessel, true, ref radarLocked, ref skipRadarLockCheck); + skipRadarDetectionCheck = true; + } + + if (!radarDetected) + { + candidateYield *= 0.1f; + } + } + if (targetWeapon != null && targetYield > candidateYield) continue; + //targetYield = candidateYield; + //targetWeapon = item.Current; + //targetWeaponPriority = candidatePriority; + } + } + float fovAngle = VectorUtils.Angle(Missile.GetForwardTransform(), targetVessel.CoM - Missile.transform.position); + if (fovAngle > Missile.missileFireAngle && Missile.missileFireAngle < Missile.maxOffBoresight * 0.75f) + { + candidateYield *= Missile.missileFireAngle / fovAngle; //missile is clamped to a narrow boresight - do we have anything with a wider FoV we should start with? + } + if (distance < ((EngageableWeapon)item.Current).engageRangeMin || firedMissiles >= maxMissilesOnTarget || (unguidedWeapon && distance > ((EngageableWeapon)item.Current).engageRangeMax / 10)) + candidateYield *= -1f; // if within min range, negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo + if (!vessel.LandedOrSplashed || (vessel.LandedOrSplashed && (distance > gunRange || targetWeapon == null || (distance <= gunRange && targetWeapon != null && (targetWeapon.GetWeaponClass() != WeaponClasses.Rocket || targetWeapon.GetWeaponClass() != WeaponClasses.Gun))))) // If we're not airborne, we want to prioritize guns + { + if (distance <= gunRange && candidateYield < 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; + targetYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetYield < candidateYield) + { + targetWeapon = item.Current; + targetYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + } + } + } + else //modular missile + { + 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 ((!surfaceAI || surfaceAI.SurfaceType != AIUtils.VehicleMovementType.Submarine) && vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(mm.transform.position) < -10)) 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.TargetingMode == MissileBase.TargetingModes.Inertial || + 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 (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 (mm.TargetingMode == MissileBase.TargetingModes.Radar && (!_radarsEnabled || (vesselRadarData != null && !vesselRadarData.locked)) && !mm.radarLOAL) candidateYield *= 0.1f; + if (mm.TargetingMode == MissileBase.TargetingModes.Inertial && !(_radarsEnabled || _irstsEnabled)) candidateYield *= 0.1f; + if (targetWeapon != null && targetYield > candidateYield) continue; + //targetYield = candidateYield; + //targetWeapon = item.Current; + //targetWeaponPriority = candidatePriority; + } + } + if (distance < ((EngageableWeapon)item.Current).engageRangeMin || firedMissiles >= maxMissilesOnTarget) + candidateYield *= -1f; // if within min range, negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo + + if (!vessel.LandedOrSplashed || (vessel.LandedOrSplashed && (distance > gunRange || targetWeapon == null || (distance <= gunRange && targetWeapon != null && (targetWeapon.GetWeaponClass() != WeaponClasses.Rocket || targetWeapon.GetWeaponClass() != WeaponClasses.Gun))))) // If we're not airborne, we want to prioritize guns + { + if (distance <= gunRange && candidateYield < 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; + targetYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetYield < candidateYield) + { + targetWeapon = item.Current; + targetYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + } + } + } + break; + } + + // TargetInfo.isLanded includes splashed but not underwater, for whatever reasons. + // If target is splashed, and we have torpedoes, use torpedoes, because, obviously, + // torpedoes are the best kind of sausage for splashed targets, + // almost as good as STS missiles, which we don't have. + case (WeaponClasses.SLW): + { + if (!target.isSplashed) continue; + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + MissileLauncher SLW = item.Current as MissileLauncher; + if (item.Current.GetMissileType().ToLower() == "depthcharge") continue; // don't use depth charges against surface ships + if (SLW.reloadableRail != null && (SLW.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordnance + float candidateYield = SLW.GetBlastRadius() * 4; + bool EMP = SLW.warheadType == MissileBase.WarheadTypes.EMP; + int candidatePriority = Mathf.RoundToInt(SLW.priority); + + if (EMP && target.isDebilitated) continue; + // not sure on the desired selection priority algorithm, so placeholder By Yield for now + + if (SLW.TargetingMode == MissileBase.TargetingModes.Heat && SLW.activeRadarRange < 0 && (rwr && rwr.rwrEnabled)) //we have passive acoustic homing? see if anything has active sonar + { + if (!skipRWRCheck) + { + CheckAntiRadStatus(targetVessel, in RWRTypes); + skipRWRCheck = true; + } + + if (RWRTypes[6]) candidateYield *= 2; // Prioritize PAH Torps for hostile sonar sources + } + + if (distance < ((EngageableWeapon)item.Current).engageRangeMin || firedMissiles >= maxMissilesOnTarget || ((unguidedWeapon && vessel.Splashed) && distance > ((EngageableWeapon)item.Current).engageRangeMax / 10)) //don't penalize air-dropped unguided torps + candidateYield *= -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 ((distance <= gunRange || distance < candidateYield || candidateYield < 1) && targetWeapon != null) continue; //torp are within min range/can't lock, don't replace existing gun if in gun range + if ((distance < candidateYield || candidateYield < 1) && targetWeapon != null) continue; //torp are within min range/can't lock, use something else; else, prioritize SLW, as those are the best option + + if (targetWeaponPriority < candidatePriority) //use priority gun + { + targetWeapon = item.Current; + targetYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetYield < candidateYield) + { + targetWeapon = item.Current; + targetYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + } + } + break; + } + } + } + } + + // return result of weapon selection + if (targetWeapon != null) + { + //update the legacy lists & arrays, especially selectedWeapon and weaponIndex + selectedWeapon = targetWeapon; + // find it in weaponArray + for (int i = 1; i < weaponArray.Length; i++) + { + weaponIndex = i; + if (selectedWeapon.GetShortName() == weaponArray[weaponIndex].GetShortName() && targetWeapon.GetEngageRange() == weaponArray[weaponIndex].GetEngageRange() && targetWeapon.GetEngageFOV() == weaponArray[weaponIndex].GetEngageFOV()) + { + break; + } + } + + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Selected weapon {selectedWeapon.GetShortName()}"); + } + + PrepareWeapons(); + SetDeployableWeapons(); + DisplaySelectedWeaponMessage(); + return true; + } + else + { + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - No weapon selected for target {targetVessel.vesselName}"); + // Debug.Log("DEBUG target isflying:" + target.isFlying + ", isLorS:" + target.isLandedOrSurfaceSplashed + ", isUW:" + target.isUnderwater); + // if (target.isFlying) + // foreach (var weapon in weaponTypesAir) + // { + // var engageableWeapon = weapon as EngageableWeapon; + // Debug.Log("DEBUG flying target:" + targetVessel + ", weapon:" + weapon + " can engage:" + CheckEngagementEnvelope(weapon, distance) + ", engageEnabled:" + engageableWeapon.engageEnabled + ", min/max:" + engageableWeapon.GetEngagementRangeMin() + "/" + engageableWeapon.GetEngagementRangeMax()); + // } + // if (target.isLandedOrSurfaceSplashed) + // foreach (var weapon in weaponTypesAir) + // { + // var engageableWeapon = weapon as EngageableWeapon; + // Debug.Log("DEBUG landed target:" + targetVessel + ", weapon:" + weapon + " can engage:" + CheckEngagementEnvelope(weapon, distance) + ", engageEnabled:" + engageableWeapon.engageEnabled + ", min/max:" + engageableWeapon.GetEngagementRangeMin() + "/" + engageableWeapon.GetEngagementRangeMax()); + // } + } + selectedWeapon = null; + weaponIndex = 0; + return false; + } + } + + // extension for feature_engagementenvelope: check engagement parameters of the weapon if it can be used against the current target + bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget, Vessel targetVessel) + { + EngageableWeapon engageableWeapon = weaponCandidate as EngageableWeapon; + + if (engageableWeapon == null) return true; + if (!engageableWeapon.engageEnabled) return true; + try + { + //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() + (float)vessel.speed * 2)) return false; //have AI preemptively begin to lead 2s out from max weapon range + //take target vel into account? //if you're going 250m/s, that's only an extra 500m to the maxRange; if the enemy is closing towards you at 250m/s, that's 250m addition + //Max 1.5x engagement, or engagementRange + vel*4? + //min 2x engagement, or engagement + 2000m? + if (weaponCandidate.GetWeaponClass() != WeaponClasses.Bomb) + { + if (weaponCandidate.GetWeaponClass() != WeaponClasses.Missile || ((MissileBase)weaponCandidate).UseStaticMaxLaunchRange) + if (distanceToTarget > engageableWeapon.GetEngagementRangeMax() + Mathf.Max(1000, (float)(vessel.srf_velocity - targetVessel.srf_velocity).magnitude * 2)) return false; //have AI preemptively begin to lead 2s out from max weapon range + } + switch (weaponCandidate.GetWeaponClass()) + { + case WeaponClasses.DefenseLaser: + { + ModuleWeapon laser = (ModuleWeapon)weaponCandidate; + // check overheat + if (laser.isOverheated) + return false; + if (laser.BurstFire && laser.RoundsRemaining > 0 && laser.RoundsRemaining < laser.RoundsPerMag) + { + if (CheckAmmo(laser)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - continuing to fire {weaponCandidate.GetShortName()}"); + return true; + } + return false; + } + if (distanceToTarget < laser.minSafeDistance) return false; + + // check yaw range of turret + ModuleTurret turret = laser.turret; + float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; + if (turret != null) + if (!TargetInTurretRange(turret, gimbalTolerance)) + return false; + if (laser.customTurret.Count > 0) + { + if (!TargetInCustomTurretRange(laser, gimbalTolerance)) return false; + } + if (laser.isReloading || !laser.hasGunner) + return false; + + // check ammo + if (CheckAmmo(laser)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); + } + return true; + } + break; + } + + case WeaponClasses.Gun: + { + ModuleWeapon gun = (ModuleWeapon)weaponCandidate; + if (gun.isOverheated) return false; + if (gun.BurstFire && gun.RoundsRemaining > 0 && gun.RoundsRemaining < gun.RoundsPerMag) + { + if (CheckAmmo(gun)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - continuing to fire {weaponCandidate.GetShortName()}"); + return true; + } + return false; + } + if (distanceToTarget < gun.minSafeDistance) return false; + + // check yaw range of turret + //ModuleTurret turret = gun.turret; + //float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; + //if (turret != null) + //if (!TargetInTurretRange(turret, gimbalTolerance, default, gun)) + //return false; + + // check overheat, reloading, ability to fire soon + if (gun.isReloading || !gun.hasGunner) + return false; + if (!gun.CanFireSoon()) + return false; + // check ammo + if (CheckAmmo(gun)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); + } + return true; + } + break; + } + + case WeaponClasses.Missile: + { + MissileBase ml = (MissileBase)weaponCandidate; + //if (distanceToTarget < engageableWeapon.GetEngagementRangeMin()) return false; //handled by bool smartWeapon select + + bool readyMissiles = false; + using (var msl = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (msl.MoveNext()) + { + if (msl.Current == null) continue; + if (msl.Current.GetWeaponChannel() > weaponChannel) continue; + if (msl.Current.launched) continue; + readyMissiles = true; + break; + } + if (!readyMissiles) return false; + // lock radar if needed + if (ml.TargetingMode == MissileBase.TargetingModes.Radar) + { + if (results.foundAntiRadiationMissile && DynamicRadarOverride) return false; // Don't try to fire radar missiles while we have an incoming anti-rad missile + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.canLock && rd.Current.sonarMode == ModuleRadar.SonarModes.None) + { + if (results.foundAntiRadiationMissile && rd.Current.DynamicRadar) continue; + rd.Current.EnableRadar(); + _radarsEnabled = true; + } + } + } + if (ml.TargetingMode == MissileBase.TargetingModes.Inertial) + { + if (!results.foundAntiRadiationMissile || !DynamicRadarOverride) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.sonarMode == ModuleRadar.SonarModes.None) + { + if (rd.Current.DynamicRadar && results.foundAntiRadiationMissile) continue; //don't enable radar if incoming HARM, unless radar is specifically set to be used regardless + float scanSpeed = (rd.Current.locked && rd.Current.lockedTarget.vessel == targetVessel) ? rd.Current.multiLockFOV : rd.Current.radarAzFOV / rd.Current.scanRotationSpeed * 2; + if (GpsUpdateMax > 0 && scanSpeed < GpsUpdateMax) GpsUpdateMax = scanSpeed; + rd.Current.EnableRadar(); + if (ml.GetWeaponClass() != WeaponClasses.SLW) _radarsEnabled = true; + else _sonarsEnabled = true; + } + } + } + if (!_radarsEnabled) + { + using (List.Enumerator rd = irsts.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null) + { + float scanSpeed = rd.Current.directionalFieldOfView / rd.Current.scanRotationSpeed * 2; + if (GpsUpdateMax > 0 && scanSpeed < GpsUpdateMax) GpsUpdateMax = scanSpeed; + rd.Current.EnableIRST(); + _irstsEnabled = true; + } + _irstsEnabled = true; + } + } + + } + if (ml.TargetingMode == MissileBase.TargetingModes.Laser || ml.TargetingMode == MissileBase.TargetingModes.Gps) + { + 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(); + } + } + else + { + if (ml.TargetingMode == MissileBase.TargetingModes.Gps) + { + if (results.foundAntiRadiationMissile && DynamicRadarOverride) return false; + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.canLock && rd.Current.sonarMode == ModuleRadar.SonarModes.None) + { + if (results.foundAntiRadiationMissile && rd.Current.DynamicRadar) continue; + rd.Current.EnableRadar(); + _radarsEnabled = true; + } + } + } + } + } + MissileLauncher mlauncher = ml as MissileLauncher; + + unguidedWeapon = UnguidedMissile(ml, distanceToTarget); + + // check DLZ + float fireFOV = -1; + + if (mlauncher != null) //else MMGs throw an Error here + fireFOV = mlauncher.missileTurret ? mlauncher.missileTurret.turret.yawRange : mlauncher.multiLauncher && mlauncher.multiLauncher.turret ? mlauncher.multiLauncher.turret.turret.yawRange : -1; + else + fireFOV = ml.customTurret.Count > 0 ? 5 : -1; + MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(ml, targetVessel.Velocity(), targetVessel.CoM, fireFOV, unguidedWeapon); + if (vessel.srfSpeed > ml.minLaunchSpeed && distanceToTarget < dlz.maxLaunchRange && distanceToTarget > dlz.minLaunchRange) + { + //old radar/ins special conditions would prevent these missile types from ever being dumbfired, now covered by above unguidedWeapon condition + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); + } + return true; + } + if (BDArmorySettings.DEBUG_MISSILES) + { + 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) // TODO: bomb always allowed? + using (var bomb = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (bomb.MoveNext()) + { + if (bomb.Current == null) continue; + if (bomb.Current.GetWeaponChannel() > weaponChannel) continue; + if (bomb.Current.launched) continue; + return true; + } + break; + + case WeaponClasses.Rocket: + { + ModuleWeapon rocket = (ModuleWeapon)weaponCandidate; + + if (rocket.isOverheated) + return false; + if (rocket.BurstFire && rocket.RoundsRemaining > 0 && rocket.RoundsRemaining < rocket.RoundsPerMag) + { + if (CheckAmmo(rocket)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - continuing to fire {weaponCandidate.GetShortName()}"); + return true; + } + return false; + } + if (distanceToTarget < rocket.minSafeDistance) return false; + // check yaw range of turret + ModuleTurret turret = rocket.turret; + float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; + if (turret != null) + if (!TargetInTurretRange(turret, gimbalTolerance, default, rocket)) + return false; + if (rocket.customTurret.Count > 0) + { + if (!TargetInCustomTurretRange(rocket, gimbalTolerance)) return false; + } + //check reloading and crewed + if (rocket.isReloading || !rocket.hasGunner) + return false; + + // check ammo + if (CheckAmmo(rocket)) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); + } + return true; + } + break; + } + + case WeaponClasses.SLW: + { + MissileBase ml = (MissileBase)weaponCandidate; + if (distanceToTarget < engageableWeapon.GetEngagementRangeMin()) return false; + + // Enable sonar, or radar, if no sonar is found. + if (((MissileBase)weaponCandidate).TargetingMode == MissileBase.TargetingModes.Radar) + { + if (results.foundTorpedo && results.foundHeatMissile && DynamicRadarOverride) return false; // Don't try to fire active sonar torps while we have an incoming passive sonar torp + + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.sonarMode == ModuleRadar.SonarModes.Active) + { + if (results.foundTorpedo && results.foundHeatMissile && rd.Current.DynamicRadar) continue; + rd.Current.EnableRadar(); + _sonarsEnabled = true; + } + + } + } + if (((MissileBase)weaponCandidate).TargetingMode == MissileBase.TargetingModes.Inertial) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.sonarMode != ModuleRadar.SonarModes.None) + { + if (rd.Current.sonarMode == ModuleRadar.SonarModes.Active && results.foundTorpedo && results.foundHeatMissile && rd.Current.DynamicRadar) continue; + rd.Current.EnableRadar(); + _sonarsEnabled = true; + } + float scanSpeed = (rd.Current.locked && rd.Current.lockedTarget.vessel == targetVessel) ? rd.Current.multiLockFOV : rd.Current.radarAzFOV / rd.Current.scanRotationSpeed * 2; + if (GpsUpdateMax > 0 && scanSpeed < GpsUpdateMax) GpsUpdateMax = scanSpeed; + } + } + MissileLauncher mlauncher = ml as MissileLauncher; + + unguidedWeapon = ml.GuidanceMode == MissileBase.GuidanceModes.None || UnguidedMissile(ml, distanceToTarget); + return true; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + catch (Exception e) + { + var excDetails = e.Message; +#if DEBUG + excDetails += $"\n{e.StackTrace}"; // Add a stacktrace of the original exception for debug builds. +#endif + Debug.LogError($"[BDArmory.MissileFire]: Exception thrown while checking the engagement envelope of {(weaponCandidate != null ? weaponCandidate.GetPartName() : "NULL weapon")} against {(targetVessel != null ? targetVessel.vesselName : "NULL vessel")}: {excDetails}"); + } + return false; + } + + public void SetTarget(TargetInfo target) + { + if (target) // We have a target + { + if (currentTarget) + { + currentTarget.Disengage(this); + } + target.Engage(this); + if (target != null && !target.isMissile) + { + var pilotAI = PilotAI; + if (pilotAI && pilotAI.IsExtending && target.Vessel != pilotAI.extendTarget) + { + pilotAI.StopExtending($"changed target from {pilotAI.extendTarget.GetName()} to {target.Vessel.GetName()}"); // Only stop extending if the target is different from the extending target + } + } + currentTarget = target; + guardTarget = target.Vessel; + if (multiTargetNum > 1 || multiMissileTgtNum > 1) + { + SmartFindSecondaryTargets(); + using (List.Enumerator secTgt = targetsAssigned.GetEnumerator()) + while (secTgt.MoveNext()) + { + if (secTgt.Current == null) continue; + if (secTgt.Current == currentTarget) continue; + secTgt.Current.Engage(this); + } + using (List.Enumerator mslTgt = missilesAssigned.GetEnumerator()) + while (mslTgt.MoveNext()) + { + if (mslTgt.Current == null) continue; + if (mslTgt.Current == currentTarget) continue; + mslTgt.Current.Engage(this); + } + } + MissileBase ml = CurrentMissile; + MissileBase pMl = PreviousMissile; + if (!ml && pMl) ml = PreviousMissile; //if fired missile, then switched to guns or something + + if (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 && !ml.radarLOAL) //switch active lock instead of clearing locks for SARH missiles + { + //vesselRadarData.UnlockCurrentTarget(); + vesselRadarData.TryLockTarget(guardTarget); + } + else + vesselRadarData.SwitchActiveLockedTarget(guardTarget); + } + else + { + if (PreviousMissile != null && PreviousMissile.ActiveRadar && PreviousMissile.targetVessel != null) //previous missile has gone active, don't need that lock anymore + { + vesselRadarData.UnlockSelectedTarget(PreviousMissile.targetVessel.Vessel); + } + vesselRadarData.TryLockTarget(guardTarget); + } + */ + + if (!vesselRadarData.SwitchActiveLockedTarget(guardTarget)) + vesselRadarData.TryLockTarget(guardTarget); + } + } + } + else // No target, disengage + { + if (currentTarget) + { + currentTarget.Disengage(this); + } + guardTarget = null; + currentTarget = null; + staleTarget = false; //reset staletarget bool if no target + } + } + + #endregion Smart Targeting + public float detectedTargetTimeout = 0; + public bool staleTarget = false; + + FloatCurve SurfaceVisionOffset = null; + + public bool CanSeeTarget(TargetInfo target, bool checkForNonVisualDetection = true, 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/IRST + // 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 :) + // 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 teammates provide vision? Could count scouted threats as stale to prevent precise targeting, but at least let AI know something is out there + + if (target == null || target.Vessel == null) return false; + + // First check for radar/IRST detection, because that's the cheapest + if (checkForNonVisualDetection) + { + //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 && hasAntiRadiationOrdnance)//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].signalType) && (rwr.pingsData[i].position - target.position).sqrMagnitude < 20f * 20f) + { + detectedTargetTimeout = 0; + staleTarget = false; + return true; + } + } + } + } + + // can we get a visual sight of the target? + + VesselCloakInfo vesselcamo = target.Vessel.gameObject.GetComponent(); + float viewModifier = 1; + if (vesselcamo && vesselcamo.cloakEnabled) + { + viewModifier = vesselcamo.opticalReductionFactor; + } + //Can the target be seen? + float visDistance = guardRange; + if (BDArmorySettings.UNDERWATER_VISION && (this.vessel.IsUnderwater() || target.Vessel.IsUnderwater())) visDistance = 100; + visDistance *= viewModifier; + float objectPermanenceThreshold = (target.Vessel.LandedOrSplashed && target.Vessel.srfSpeed < 10) ? 30 * (10 - (float)target.Vessel.srfSpeed) : 30; //have slow/stationary targets have much longer timeouts since they are't going anywhere. + //needs to use lastGoodVesselVel, not current speed, since if we can't see it, we can't know how fast it's going + if ((target.Vessel.CoM - vessel.CoM).sqrMagnitude < (visDistance * visDistance) && + VectorUtils.Angle(-vessel.ReferenceTransform.forward, target.Vessel.CoM - vessel.CoM) < (guardAngle * 1.1f) / 2) + { + if ((target.Vessel.LandedOrSplashed && vessel.LandedOrSplashed) && ((target.Vessel.CoM - vessel.CoM).sqrMagnitude > 2250000f)) //land Vee vs land Vee will have a max of ~1.8km viewDist, due to curvature of Kerbin + { + Vector3 targetDirection = (target.Vessel.CoM - vessel.CoM).ProjectOnPlanePreNormalized(vessel.up); + if (RadarUtils.TerrainCheck(target.Vessel.CoM + ((target.Vessel.vesselSize.y / 2) * vessel.up), vessel.CoM + (SurfaceVisionOffset.Evaluate((target.Vessel.CoM - vessel.CoM).magnitude) * vessel.up), FlightGlobals.currentMainBody) + || RadarUtils.TerrainCheck(vessel.CoM + targetDirection, vessel.CoM, FlightGlobals.currentMainBody)) ////target more than 1.5km away, do a paired raycast looking straight, and a raycast using an offset to adjust the horizonpoint to the target, should catch majority of intervening terrain. Clamps to 10km; beyond that, spotter (air)craft will be needed to share vision + { + if (target.detectedTime.TryGetValue(Team, out float detectedTime) && Time.time - detectedTime < Mathf.Max(objectPermanenceThreshold, targetScanInterval)) //intervening terrain, has an ally seen the target? + { + //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; + } + } + else//target/vessel is flying, or ground Vees are within 1.5km of each other, standard LoS checks + { + if (RadarUtils.TerrainCheck((vessel.LandedOrSplashed ? target.Vessel.CoM + (vessel.up * (target.Vessel.vesselSize.y / 2)) : target.Vessel.CoM), vessel.CoM, FlightGlobals.currentMainBody)) + { + if (target.detectedTime.TryGetValue(Team, out float detectedTime) && Time.time - detectedTime < Mathf.Max(objectPermanenceThreshold, 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; + } + + //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(objectPermanenceThreshold, 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; + } + + /// + /// Check to see if an incoming missile is visible based on guardRange and missile state + /// + /// + /// + public bool CanSeeTarget(MissileBase target) + { + // can we get a visual sight of the target? + 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.vessel.CoM - vessel.CoM).sqrMagnitude < visrange * visrange) + { + if (RadarUtils.TerrainCheck(target.vessel.CoM, vessel.CoM, FlightGlobals.currentMainBody)) + { + return false; + } + + return true; + } + + return false; + } + + void SearchForRadarSource() + { + antiRadTargetAcquired = false; + antiRadiationTarget = Vector3.zero; + if (rwr && rwr.rwrEnabled) + { + float closestAngle = 360; + MissileBase missile = CurrentMissile; + + if (!missile) return; + + float maxOffBoresight = missile.maxOffBoresight; + + if (missile.TargetingMode != MissileBase.TargetingModes.AntiRad) return; + + MissileLauncher ml = CurrentMissile as MissileLauncher; + //Debug.Log($"antiradTgt count: {(ml.antiradTargets != null ? ml.antiradTargets.Length : "null")}"); + //if (ml.antiradTargets == null) ml.ParseAntiRadTargetTypes(); + for (int i = 0; i < rwr.pingsData.Length; i++) + { + if (rwr.pingsData[i].exists && ml.antiradTargets.Contains(rwr.pingsData[i].signalType)) + { + Vector3 position = rwr.pingsData[i].position; + float angle = VectorUtils.Angle(position - missile.transform.position, missile.GetForwardTransform()); + + if (angle < closestAngle && angle < maxOffBoresight) + { + closestAngle = angle; + antiRadiationTarget = position; + antiRadTargetAcquired = true; + //Debug.Log($"antiradTgt count: antiRad target found: {rwr.pingsData[i].vessel.vesselName}"); + } + } + } + } + } + + void SearchForLaserPoint() + { + MissileBase ml = CurrentMissile; + if (!ml || !(ml.TargetingMode == MissileBase.TargetingModes.Laser || ml.TargetingMode == MissileBase.TargetingModes.Gps)) + { + return; + } + + MissileLauncher launcher = ml as MissileLauncher; + if (launcher != null) + { + foundCam = BDATargetManager.GetLaserTarget(launcher, + launcher.GuidanceMode == MissileBase.GuidanceModes.BeamRiding, Team); + } + else + { + foundCam = BDATargetManager.GetLaserTarget((BDModularGuidance)ml, false, Team); + } + + if (foundCam) + { + laserPointDetected = true; + } + else + { + laserPointDetected = false; + } + } + + void SearchForHeatTarget(MissileBase currMissile, TargetInfo targetMissile = null) + { + if (currMissile != null) + { + if (!currMissile || currMissile.TargetingMode != MissileBase.TargetingModes.Heat) + { + return; + } + float scanRadius = currMissile.lockedSensorFOV * 0.5f; + float maxOffBoresight = currMissile.maxOffBoresight * 0.85f; + + if (vesselRadarData) // && !currMissile.IndependantSeeker) //missile with independantSeeker can't get targetdata from radar/IRST + { + if (currMissile.GuidanceMode != MissileBase.GuidanceModes.SLW || currMissile.GuidanceMode == MissileBase.GuidanceModes.SLW && currMissile.activeRadarRange > 0) //heatseeking missiles/torps + { + if (_irstsEnabled) + { + if (targetMissile == null) + heatTarget = vesselRadarData.activeIRTarget(guardTarget, this); //point seeker at active target's IR return + else + heatTarget = vesselRadarData.activeIRTarget(targetMissile.Vessel, this); + } + else + { + if (vesselRadarData.locked) + { + if (targetMissile == null) //uncaged radar lock + heatTarget = vesselRadarData.lockedTargetData.targetData; + else //since it's probable that the Wm is locked to the current guardTarget, but not that incoming missile we're trying to acquire for intercept + { + List possibleTargets = vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == targetMissile.Vessel) + { + heatTarget = possibleTargets[i]; + break; + } + } + } + } + } + } + else //active sonar torps + { + heatTarget = vesselRadarData.detectedRadarTarget(guardTarget, this); //get initial direction for passive sonar torps from passive/non-locking sonar return + } + } + Vector3 forward = currMissile.GetForwardTransform(); + Vector3 missilePos = currMissile.MissileReferenceTransform.position; + Vector3 direction = + heatTarget.exists && VectorUtils.Angle(heatTarget.predictedPosition - missilePos, forward) < maxOffBoresight ? + heatTarget.predictedPosition - missilePos + : forward; + // remove AI target check/move to a missile .cfg option to allow older gen heaters? + if (currMissile.GuidanceMode != MissileBase.GuidanceModes.SLW || currMissile.GuidanceMode == MissileBase.GuidanceModes.SLW && currMissile.activeRadarRange > 0) + heatTarget = BDATargetManager.GetHeatTarget(vessel, vessel, new Ray(missilePos + (50 * forward), direction), TargetSignatureData.noTarget, scanRadius, currMissile.heatThreshold, currMissile.frontAspectHeatModifier, currMissile.uncagedLock, currMissile.targetCoM, currMissile.lockedSensorFOVBias, currMissile.lockedSensorVelocityBias, currMissile.lockedSensorVelocityMagnitudeBias, currMissile.lockedSensorMinAngularVelocity, this, targetMissile != null ? targetMissile : guardMode ? currentTarget : null, IFF: currMissile.hasIFF); + else heatTarget = BDATargetManager.GetAcousticTarget(vessel, vessel, new Ray(missilePos + (50 * forward), direction), TargetSignatureData.noTarget, scanRadius, currMissile.heatThreshold, currMissile.targetCoM, currMissile.lockedSensorFOVBias, currMissile.lockedSensorVelocityBias, currMissile.lockedSensorVelocityMagnitudeBias, currMissile.lockedSensorMinAngularVelocity, this, targetMissile != null ? targetMissile : guardMode ? currentTarget : null, IFF: currMissile.hasIFF); + } + } + + bool CrossCheckWithRWR(TargetInfo v) + { + bool matchFound = false; + if (rwr && rwr.rwrEnabled) + { + for (int i = 0; i < rwr.pingsData.Length; i++) + { + if (rwr.pingsData[i].exists && (rwr.pingsData[i].position - v.position).sqrMagnitude < 20f * 20f) + { + matchFound = true; + break; + } + } + } + + return matchFound; + } + public class TargetData + { + public Vector3 targetGEOPos = Vector3.zero; + public float TimeOfLastINS = 0f; + public float INStimetogo = 0f; + + public TargetData() { } + + public TargetData(Vector3 targetGEOPosIn) + { + targetGEOPos = targetGEOPosIn; + } + public TargetData(Vector3 targetGEOPosIn, float TimeOfLastINSIn, float INStimetogoIn) + { + targetGEOPos = targetGEOPosIn; + TimeOfLastINS = TimeOfLastINSIn; + INStimetogo = INStimetogoIn; + } + } + + public void SendTargetDataToMissile(MissileBase ml, Vessel targetVessel, bool clearHeat = true, TargetData targetData = null, bool getTarget = true) + { //TODO BDModularGuidance: implement all targetings on base + bool dumbfire = false; + bool validTarget = false; + //if (targetVessel == null) + // targetVessel = guardTarget; + switch (ml.TargetingMode) + { + case MissileBase.TargetingModes.Laser: + { + if (laserPointDetected) + { + ml.lockedCamera = foundCam; + ml.TargetAcquired = true; + if (guardMode && guardTarget != null && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude < 10 * 10) validTarget = true; //*highly* unlikely laser-guided missiles used for missile interception, so leaving these guardTarget + } + else + { + dumbfire = true; + validTarget = true; + } + break; + } + case MissileBase.TargetingModes.Gps: + { + if (getTarget && targetVessel) + { + if ((designatedGPSInfo.worldPos - targetVessel.CoM).sqrMagnitude > 100) + { + ml.targetGPSCoords = designatedGPSCoords; + ml.TargetAcquired = true; + validTarget = true; + } + else if (foundCam && (foundCam.groundTargetPosition - targetVessel.CoM).sqrMagnitude > Mathf.Max(400, 0.013f * (float)targetVessel.srfSpeed * (float)targetVessel.srfSpeed)) + { + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(foundCam.groundTargetPosition, vessel.mainBody); + ml.TargetAcquired = true; + validTarget = true; + } + else if (vesselRadarData && vesselRadarData.locked) + { + List possibleTargets = vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == targetVessel) + { + ml.targetGPSCoords = possibleTargets[i].geoPos; + ml.TargetAcquired = true; + validTarget = true; + break; + } + } + } + } + + if (!validTarget) + { + if (targetData != null) + { + ml.targetGPSCoords = targetData.targetGEOPos; + ml.TargetAcquired = true; + } + else if (designatedGPSCoords != Vector3d.zero) + { + ml.targetGPSCoords = designatedGPSCoords; + ml.TargetAcquired = true; + } + else if (ml.GetWeaponClass() == WeaponClasses.Bomb) + { + dumbfire = true; + validTarget = true; + } + + if (laserPointDetected) + ml.lockedCamera = foundCam; + if (guardMode && GPSDistanceCheck(VectorUtils.GetWorldSurfacePostion(ml.targetGPSCoords, vessel.mainBody), targetVessel)) validTarget = true; + } + + if (vesselRadarData) + { + ml.vrd = vesselRadarData; + } + break; + } + case MissileBase.TargetingModes.Heat: + { + if (heatTarget.exists) + { + ml.heatTarget = heatTarget; + if (clearHeat) heatTarget = TargetSignatureData.noTarget; + + var heatTgtVessel = ml.heatTarget.vessel.gameObject; + if (heatTgtVessel) ml.targetVessel = heatTgtVessel.GetComponent(); + } + break; + } + case MissileBase.TargetingModes.Radar: + { + if (vesselRadarData && vesselRadarData.locked)//&& radar && radar.lockedTarget.exists) + { + if (targetVessel != null) + { + List possibleTargets = vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == targetVessel) + { + ml.radarTarget = possibleTargets[i]; //send correct targetlock if firing multiple SARH missiles + break; + } + } + } + else + ml.radarTarget = vesselRadarData.lockedTargetData.targetData; + ml.vrd = vesselRadarData; + vesselRadarData.LastMissile = ml; + + if (ml.radarTarget.vessel) + { + var radarTgtvessel = ml.radarTarget.vessel.gameObject; + if (radarTgtvessel) ml.targetVessel = radarTgtvessel.GetComponent(); + } + } + else + { + dumbfire = true; + validTarget = true; + } + break; + } + case MissileBase.TargetingModes.AntiRad: + { + if (antiRadTargetAcquired && antiRadiationTarget != Vector3.zero) + { + ml.TargetAcquired = true; + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(antiRadiationTarget, vessel.mainBody); + ml.lastPingTime = Time.time; + if (AntiRadDistanceCheck()) validTarget = true; + } + break; + } + case MissileBase.TargetingModes.None: + { + ml.TargetAcquired = true; + validTarget = true; + break; + } + case MissileBase.TargetingModes.Inertial: + { + if (vesselRadarData) + { + // If manual launch + if (targetVessel == null) + { + if (vesselRadarData.locked) //grab target from primary lock + { + targetVessel = vesselRadarData.lockedTargetData.targetData.vessel; + validTarget = true; + vesselRadarData.LastMissile = ml; + } + else if (_irstsEnabled) //or brightest ping on IRST + { + targetVessel = vesselRadarData.activeIRTarget(null, this).vessel; + validTarget = targetVessel; + } + } + // If GMR and we want to recalculate the target + else if (getTarget) + { + TargetSignatureData INSTarget = TargetSignatureData.noTarget; + if (ml.GetWeaponClass() == WeaponClasses.SLW) + { + if (_sonarsEnabled) + INSTarget = vesselRadarData.detectedRadarTarget(targetVessel, this); //detected by sonar scan? + } + else + { + if (_radarsEnabled) + INSTarget = vesselRadarData.detectedRadarTarget(targetVessel, this); //detected by radar scan? + if (!INSTarget.exists && _irstsEnabled) + INSTarget = vesselRadarData.activeIRTarget(null, this); //how about IRST? + } + if (INSTarget.exists) + { + validTarget = targetVessel; + } + } + + // Check if we've grabbed a valid target + if (validTarget) + { + Vector3 TargetLead = MissileGuidance.GetAirToAirFireSolution(ml, targetVessel, out ml.INStimetogo); + //designatedGPSInfo = new GPSTargetInfo(VectorUtils.WorldPositionToGeoCoords(TargetLead, targetVessel.mainBody), targetVessel.vesselName.Substring(0, Mathf.Min(12, targetVessel.vesselName.Length))); + designatedINSCoords = VectorUtils.WorldPositionToGeoCoords(TargetLead, targetVessel.mainBody); + ml.TimeOfLastINS = Time.time; + ml.TargetAcquired = true; + } + else + { + // If we haven't then first check coords from GMR under AI control + if (targetData != null) + { + designatedINSCoords = targetData.targetGEOPos; + ml.TimeOfLastINS = targetData.TimeOfLastINS; + ml.INStimetogo = targetData.INStimetogo; + ml.TargetAcquired = true; + validTarget = true; + } + else + { + // If they don't exist then dumbfire + dumbfire = true; + if (ml.GetWeaponClass() == WeaponClasses.Bomb) + { + validTarget = true; + ml.TargetAcquired = false; + break; + } + else + { + //designatedGPSInfo = new GPSTargetInfo(VectorUtils.WorldPositionToGeoCoords(ml.MissileReferenceTransform.position + ml.MissileReferenceTransform.forward * 10000, vessel.mainBody), "null target"); + designatedINSCoords = VectorUtils.WorldPositionToGeoCoords(ml.MissileReferenceTransform.position + ml.MissileReferenceTransform.forward * 10000, vessel.mainBody); + ml.TargetAcquired = true; + } + } + } + + // Set data + ml.targetGPSCoords = designatedINSCoords; + if (targetVessel != null) + ml.TargetINSCoords = VectorUtils.WorldPositionToGeoCoords(targetVessel.CoM, vessel.mainBody); + else + ml.TargetINSCoords = designatedINSCoords; + } + designatedINSCoords = Vector3d.zero; + break; + } + default: + { + if (ml.GetWeaponClass() == WeaponClasses.Bomb) + { + validTarget = true; + } + break; + } + } + if (validTarget && targetVessel != null) + { + ml.targetVessel = targetVessel.gameObject ? targetVessel.gameObject.GetComponent() : null; + } + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileData]: Sending targetInfo to {(dumbfire ? "dumbfire " : "")}{Enum.GetName(typeof(MissileBase.TargetingModes), ml.TargetingMode)} Missile..."); + if (ml.targetVessel != null) Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: firing missile at {(targetVessel != null ? targetVessel.GetName() : "null target")}"); + } + + #endregion Targeting + + #region Guard + + public void ResetGuardInterval() + { + targetScanTimer = 0; + } + + void GuardMode() + { + if (BDArmorySettings.PEACE_MODE) return; + + UpdateGuardViewScan(); + + //setting turrets to guard mode + if (selectedWeapon != null && selectedWeapon != previousSelectedWeapon && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) + { + //make this not have to go every frame + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) //want to find all weapons in WeaponGroup, rather than all weapons of parttype + { + 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. + { + 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 + weapon.Current.FiringTolerance = AutoFireCosAngleAdjustment; + } + } + + if (!guardTarget && selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) + { + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + if (weapon.Current.isAPS) continue; + // if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; + weapon.Current.autoFire = false; + weapon.Current.autoFireFailReason = "Deselected"; + weapon.Current.autofireShotCount = 0; + weapon.Current.visualTargetVessel = null; + weapon.Current.visualTargetPart = null; + } + } + //if (missilesAway < 0) + // missilesAway = 0; + + if (missileIsIncoming) + { + if (!isLegacyCMing) + { + // StartCoroutine(LegacyCMRoutine()); // Deprecated + } + + targetScanTimer -= Time.fixedDeltaTime; //advance scan timing (increased urgency) + } + + // Update target priority UI + if ((targetPriorityEnabled) && (currentTarget)) + UpdateTargetPriorityUI(currentTarget); + + //scan and acquire new target + if (Time.time - targetScanTimer > targetScanInterval) + { + targetScanTimer = Time.time; + + if (!guardFiringMissile)// || (firedMissiles >= maxMissilesOnTarget && multiMissileTgtNum > 1 && BDATargetManager.TargetList(Team).Count > 1)) //grab new target, if possible + { + SmartFindTarget(); + + if (guardTarget == null || selectedWeapon == null) + { + SetCargoBays(); + SetDeployableWeapons(); + return; + } + + //firing + if (weaponIndex > 0) + { + if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW) + { + if (CurrentMissile != null) // Reloadable rails can give a null missile. + { + bool launchAuthorized = true; + bool pilotAuthorized = true; + //(!pilotAI || pilotAI.GetLaunchAuthorization(guardTarget, this)); + + if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile && vessel.Splashed && vessel.altitude < -10) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire] missile below launch depth"); + launchAuthorized = false; //submarine below launch depth + } + Vector3 missileReferencePosition = CurrentMissile.MissileReferenceTransform.position; + if (selectedWeapon.GetWeaponClass() == WeaponClasses.SLW && !vessel.Splashed) + { + BDModulePilotAI pilotAI = null; + BDModuleVTOLAI vtolAI = null; + var ai = AI; + if (ai != null && ai.pilotEnabled) switch (ai.aiType) + { + case AIType.PilotAI: pilotAI = ai as BDModulePilotAI; break; + case AIType.VTOLAI: vtolAI = ai as BDModuleVTOLAI; break; + } + if (pilotAI && vessel.altitude > pilotAI.finalBombingAlt * 1.2f) launchAuthorized = false; //don't torpedo bomb from high up, the torp's won't survive water impact + //if flying with air-drop torps, adjust aimer pos based on predicted water impact point. torps aren't AAMs + if (vtolAI && vessel.altitude > 120) launchAuthorized = false; + Vector3 torpImpactPos = missileReferencePosition + vessel.srf_vel_direction * (vessel.horizontalSrfSpeed * bombFlightTime); //might need a projectonPlane, check what srf_vel_dir actually outputs - parallel to surface, or vel direction when !orbit + missileReferencePosition = torpImpactPos - ((float)FlightGlobals.getAltitudeAtPos(torpImpactPos) * VectorUtils.GetUpDirection(torpImpactPos)); + } + //float targetAngle = VectorUtils.Angle(-transform.forward, guardTarget.transform.position - transform.position); + float targetAngle = VectorUtils.Angle(CurrentMissile.MissileReferenceTransform.forward, guardTarget.CoM - missileReferencePosition); + float targetDistance = Vector3.Distance(currentTarget.position, missileReferencePosition); + if (selectedWeapon.GetWeaponClass() == WeaponClasses.SLW && !vessel.Splashed) + { + if (targetDistance < vessel.horizontalSrfSpeed * bombFlightTime) launchAuthorized = false; //too close, dropped torp will overshoot + } + if (CurrentMissile.GuidanceMode != MissileBase.GuidanceModes.Cruise && CurrentMissile.GuidanceMode != MissileBase.GuidanceModes.Kappa && CurrentMissile.GuidanceMode != MissileBase.GuidanceModes.AAMLoft && CurrentMissile.GuidanceMode != MissileBase.GuidanceModes.AGMBallistic) + { + if (RadarUtils.TerrainCheck(guardTarget.CoM, missileReferencePosition)) //vessel behind terrain. exception for missiles which can (probably) sort that out + { + launchAuthorized = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire] target behind terrain, CurrentMissile: {CurrentMissile.shortName}, GuidanceMode: {CurrentMissile.GuidanceMode}"); + } + } + MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(CurrentMissile, guardTarget.Velocity(), guardTarget.CoM, -1, unguidedWeapon); // (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 (unguidedWeapon && targetDistance > (CurrentMissile.GetEngagementRangeMax() / 10) + (CurrentMissile.GetWeaponClass() == WeaponClasses.SLW && !vessel.Splashed ? vessel.horizontalSrfSpeed * bombFlightTime : 0)) launchAuthorized = false; //account for distance covered while dropping torps + if (engagedTargets > multiMissileTgtNum) launchAuthorized = false; //already fired on max allowed targets + // 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, CurrentMissile) || (ml is not null && (ml.missileTurret || (ml.multiLauncher && ml.multiLauncher.turret)))); + + + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} launchAuth={launchAuthorized}, pilotAut={pilotAuthorized}, missilesAway/Max={firedMissiles}/{maxMissilesOnTarget}"); + + if (firedMissiles < maxMissilesOnTarget) + { + if (CurrentMissile.TargetingMode == MissileBase.TargetingModes.Radar && (CurrentMissile.GetWeaponClass() == WeaponClasses.SLW ? _sonarsEnabled : _radarsEnabled) && !CurrentMissile.radarLOAL && (!vesselRadarData || !vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != guardTarget && (MaxRadarLocks + vesselRadarData.MaxRadarLocksExternal) <= vesselRadarData.numLockedTargets))) + { + 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(guardTarget, CurrentMissile)); + } + } + else if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} waiting for missile to be ready..."); + } + + // if (!launchAuthorized || !pilotAuthorized || missilesAway >= maxMissilesOnTarget) + // { + // targetScanTimer -= 0.5f * targetScanInterval; + // } + } + } + else if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) + { + bool launchAuthorized = true; + var pilotAI = PilotAI; + if (pilotAI && pilotAI.divebombing && vessel.altitude > (guardTarget.LandedOrSplashed ? pilotAI.minAltitude + ((pilotAI.defaultAltitude - pilotAI.minAltitude) / 2) : pilotAI.finalBombingAlt + 500)) launchAuthorized = false; //don't release dive bombs unless already dived more than half the distance between bombing alt and min alt, or 500m above aerial divebomb alt + MissileLauncher ml = selectedWeapon as MissileLauncher; + if (ml && vessel.altitude < ml.GetBlastRadius()) launchAuthorized = false; + if (!guardFiringMissile && launchAuthorized) + { + StartCoroutine(GuardBombRoutine()); + } + } + else if (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || + selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || + selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + StartCoroutine(GuardTurretRoutine()); + } + } + } + SetCargoBays(); + SetDeployableWeapons(); + } + + if (overrideTimer > 0) + { + overrideTimer -= TimeWarp.fixedDeltaTime; + } + else + { + overrideTimer = 0; + overrideTarget = null; + } + } + + void UpdateGuardViewScan() + { + results = RadarUtils.GuardScanInDirection(this, transform, guardAngle, guardRange, rwr); + incomingThreatVessel = null; + if (results.foundMissile) + { + if (rwr && (rwr.omniDetection || results.foundRadarMissile)) //enable omniRWRs for all incoming threats. Moving this here as RWRs would be detecting missiles before they reached danger close + { + if (!rwr.rwrEnabled) rwr.EnableRWR(); + if (rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; + } + } + if (results.foundMissile && (results.incomingMissiles[0].distance < guardRange || results.incomingMissiles[0].time < Mathf.Max(cmThreshold, evadeThreshold))) //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.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"); + } + missileIsIncoming = true; + incomingMissileLastDetected = Time.time; + // Assign the closest missile as the main threat. FIXME In the future, we could do something more complex to handle all the incoming missiles. + incomingMissileDistance = results.incomingMissiles[0].distance; + incomingMissileTime = results.incomingMissiles[0].time; + incomingThreatPosition = results.incomingMissiles[0].position; + incomingThreatVessel = results.incomingMissiles[0].vessel; + incomingMissileVessel = results.incomingMissiles[0].vessel; + //radar missiles + if (!results.foundTorpedo) + { + //if (results.foundMissile) //make sure underAttack still gets called if incoming missile is GPS/INS + StartCoroutine(UnderAttackRoutine()); + if (results.foundRadarMissile) //have this require an RWR? + { + if (rwr || incomingMissileDistance <= guardRange * 0.33f) //within ID range? + //StartCoroutine(UnderAttackRoutine()); + { + FireChaff(); + FireECM(10); + } + } + //laser missiles + if (results.foundAGM) //Assume Laser Warning Receiver regardless of omniDetection? Or move laser missiles to the passive missiles section? + { + //StartCoroutine(UnderAttackRoutine()); + + FireSmoke(); + if (targetMissiles && guardTarget == null) + { + //targetScanTimer = Mathf.Min(targetScanInterval, Time.time - targetScanInterval + 0.5f); + targetScanTimer -= targetScanInterval / 2; + } + } + //passive missiles + if (results.foundHeatMissile || results.foundAntiRadiationMissile || results.foundGPSMissile) + { + if (rwr && rwr.omniDetection) + { + if (results.foundHeatMissile) + { + FireFlares(); + FireOCM(true); + } + if (results.foundAntiRadiationMissile) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && (rd.Current.DynamicRadar || DynamicRadarOverride)) + rd.Current.DisableRadar(); + _radarsEnabled = false; + } + FireECM(0); //disable jammers + } + + if (results.foundGPSMissile) + FireECM(cmThreshold); + //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.DynamicRadar || DynamicRadarOverride)) + rd.Current.DisableRadar(); + _radarsEnabled = false; + } + } + + if (incomingMissileDistance <= guardRange * 0.33f) //within ID range? + { + if (results.foundGPSMissile) + FireECM(cmThreshold); //try to jam datalink to launcher/GPS + if (results.foundHeatMissile) //AtG heater!? Flares! + { + FireFlares(); + FireOCM(true); + } + } + } + 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 if (results.foundGPSMissile) + { + FireECM(cmThreshold); + } + 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.DynamicRadar || DynamicRadarOverride) + rd.Current.DisableRadar(); + _radarsEnabled = false; + } + } + FireECM(0);//uh oh, blip ECM! + } + } + else //assume heater + { + FireFlares(); + FireOCM(true); + } + } + //StartCoroutine(UnderAttackRoutine()); + } + } + } + else + { + StartCoroutine(UnderAttackRoutine()); + if (results.foundHeatMissile) //standin for passive acoustic homing. Will have to expand this if facing *actual* heat-seeking torpedoes + { + if ((rwr && rwr.omniDetection) || (incomingMissileDistance <= Mathf.Min(guardRange * 0.33f, 2500))) //within ID range? + { + if (radars.Count > 0) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.sonarMode == ModuleRadar.SonarModes.Active) //kill active sonar + rd.Current.DisableRadar(); + } + _sonarsEnabled = false; + } + FireECM(0); // kill active noisemakers + FireDecoys(); + } + } + if (results.foundRadarMissile) //standin for active sonar + { + FireBubbles(); + FireECM(10); + } + if (results.foundGPSMissile) //not really sure what you'd do vs a wireguided/INS torpedo - kill engines and active sonar + fire decoys to try and break detection by op4 passive sonar? fire bubblers to garble active sonar detection? + { + } + } + } + else + { + // FIXME these shouldn't be necessary if all checks against them are guarded by missileIsIncoming. + incomingMissileDistance = float.MaxValue; + incomingMissileTime = float.MaxValue; + incomingMissileVessel = null; + } + + if (results.firingAtMe) + { + if (!missileIsIncoming) // Don't override incoming missile threats. FIXME In the future, we could do something more complex to handle all incoming threats. + { + incomingThreatPosition = results.threatPosition; + incomingThreatVessel = results.threatVessel; + } + if (priorGunThreatVessel == results.threatVessel) + { + incomingMissTime += Time.fixedDeltaTime; + } + else + { + priorGunThreatVessel = results.threatVessel; + incomingMissTime = 0f; + } + incomingThreatDistanceSqr = (results.threatPosition - vessel.CoM).sqrMagnitude; + var pilotAI = PilotAI; + var ai = AI; + if ((pilotAI && incomingMissTime >= pilotAI.evasionTimeThreshold && incomingMissDistance < pilotAI.evasionThreshold) || ai != null && ai.aiType != AIType.PilotAI) // If we haven't been under fire long enough, ignore gunfire + { + FireOCM(false); //enable visual countermeasures if under fire + } + if (results.threatWeaponManager != null) + { + incomingMissDistance = results.missDistance + results.missDeviation; + TargetInfo nearbyFriendly = BDATargetManager.GetClosestFriendly(this); + TargetInfo nearbyThreat = BDATargetManager.GetTargetFromWeaponManager(results.threatWeaponManager); + var nearbyFriendlyWM = nearbyFriendly != null ? nearbyFriendly.WeaponManager : null; + var nearbyThreatWM = nearbyThreat != null ? nearbyThreat.WeaponManager : null; + + if (nearbyThreat != null && nearbyThreatWM != null && nearbyFriendly != null && nearbyFriendlyWM != null) + { + if (Team.IsEnemy(nearbyThreatWM.Team) && nearbyFriendlyWM.Team == Team) + //turns out that there's no check for AI on the same team going after each other due to this. Who knew? + { + if (nearbyThreat == currentTarget && nearbyFriendlyWM.currentTarget != null) + //if being attacked by the current target, switch to the target that the nearby friendly was engaging instead + { + SetOverrideTarget(nearbyFriendlyWM.currentTarget); + nearbyFriendlyWM.SetOverrideTarget(nearbyThreat); + 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 + nearbyFriendlyWM.SetOverrideTarget(nearbyThreat); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} called for help from {nearbyFriendly.Vessel.vesselName}"); + } + } + } + } + StartCoroutine(UnderAttackRoutine()); //this seems to be firing all the time, not just when bullets are flying towards craft...? + StartCoroutine(UnderFireRoutine()); + } + else + { + incomingMissTime = 0f; // Reset incoming fire time + } + } + + public void ForceScan() + { + targetScanTimer = -100; + } + + public void StartGuardTurretFiring() + { + if (!guardTarget) return; + if (selectedWeapon == null) return; + int TurretID = 0; + int MissileTgtID = 0; + List firedTargets = []; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) + { + if (weapon.Current.turret != null && (weapon.Current.ammoCount > 0 || BDArmorySettings.INFINITE_AMMO)) // Other turrets can just generally aim at the currently targeted vessel. + { + weapon.Current.visualTargetVessel = guardTarget; + } + continue; + } + + if (multiTargetNum > 1) + { + if (weapon.Current.turret && (weapon.Current.maxPitch > weapon.Current.minPitch || weapon.Current.yawRange > 0)) + { + if (TurretID >= Mathf.Min((targetsAssigned.Count), multiTargetNum)) + { + TurretID = 0; //if more turrets than targets, loop target list + } + if (targetsAssigned.Count > 0 && targetsAssigned[TurretID].Vessel != null) + { + if (((weapon.Current.engageAir && targetsAssigned[TurretID].isFlying) || + (weapon.Current.engageGround && targetsAssigned[TurretID].isLandedOrSurfaceSplashed) || + (weapon.Current.engageSLW && targetsAssigned[TurretID].isUnderwater)) //check engagement envelope + && TargetInTurretRange(weapon.Current.turret, 7, targetsAssigned[TurretID].Vessel.CoM, weapon.Current)) + { + weapon.Current.visualTargetVessel = targetsAssigned[TurretID].Vessel; // if target within turret fire zone, assign + firedTargets.Add(targetsAssigned[TurretID]); + } + else //else try remaining targets + { + using (List.Enumerator item = targetsAssigned.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current.Vessel == null) continue; + if ((weapon.Current.engageAir && !item.Current.isFlying) || + (weapon.Current.engageGround && !item.Current.isLandedOrSurfaceSplashed) || + (weapon.Current.engageSLW && !item.Current.isUnderwater)) continue; + if (TargetInTurretRange(weapon.Current.turret, 7, item.Current.Vessel.CoM, weapon.Current)) + { + weapon.Current.visualTargetVessel = item.Current.Vessel; + firedTargets.Add(item.Current); + break; + } + } + } + TurretID++; + } + if (MissileTgtID >= Mathf.Min((missilesAssigned.Count), multiTargetNum)) + { + MissileTgtID = 0; //if more turrets than targets, loop target list + } + if (missilesAssigned.Count > 0 && missilesAssigned[MissileTgtID].Vessel != null) //if missile, override non-missile target + { + if (weapon.Current.engageMissile) + { + if (TargetInTurretRange(weapon.Current.turret, 7, missilesAssigned[MissileTgtID].Vessel.CoM, weapon.Current)) + { + weapon.Current.visualTargetVessel = missilesAssigned[MissileTgtID].Vessel; // if target within turret fire zone, assign + firedTargets.Add(missilesAssigned[MissileTgtID]); + } + else //assigned target outside turret arc, try the other targets on the list + { + using (List.Enumerator item = missilesAssigned.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current.Vessel == null) continue; + if (TargetInTurretRange(weapon.Current.turret, 7, item.Current.Vessel.CoM, weapon.Current)) + { + weapon.Current.visualTargetVessel = item.Current.Vessel; + firedTargets.Add(item.Current); + break; + } + } + } + } + MissileTgtID++; + } + } + else + { + //weapon.Current.visualTargetVessel = guardTarget; + weapon.Current.visualTargetVessel = targetsAssigned.Count > 0 && targetsAssigned[0].Vessel != null ? targetsAssigned[0].Vessel : guardTarget; //make sure all guns targeting the same target, to ensure the leadOffest is the same, and that the Ai isn't trying to use the leadOffset from a turret + //Debug.Log("[BDArmory.MTD]: target from list was null, defaulting to " + guardTarget.name); + } + } + else + { + weapon.Current.visualTargetVessel = guardTarget; + //Debug.Log("[BDArmory.MTD]: non-turret, assigned " + guardTarget.name); + } + weapon.Current.targetCOM = targetCoM; + using (List.Enumerator Tgt = targetsAssigned.GetEnumerator()) + while (Tgt.MoveNext()) + { + if (!firedTargets.Contains(Tgt.Current)) + Tgt.Current.Disengage(this); + } + using (List.Enumerator Tgt = missilesAssigned.GetEnumerator()) + while (Tgt.MoveNext()) + { + if (!firedTargets.Contains(Tgt.Current)) + Tgt.Current.Disengage(this); + } + if (targetCoM) + { + weapon.Current.targetCockpits = false; + weapon.Current.targetEngines = false; + weapon.Current.targetWeapons = false; + weapon.Current.targetMass = false; + weapon.Current.targetRandom = false; + } + else + { + weapon.Current.targetCockpits = targetCommand; + weapon.Current.targetEngines = targetEngine; + weapon.Current.targetWeapons = targetWeapon; + weapon.Current.targetMass = targetMass; + weapon.Current.targetRandom = targetRandom; + } + + weapon.Current.autoFireTimer = Time.time; + //weapon.Current.autoFireLength = 3 * targetScanInterval / 4; + weapon.Current.autoFireLength = (fireBurstLength < 0.01f) ? targetScanInterval / 2f : fireBurstLength; + weapon.Current.autofireShotCount = 0; + } + } + int MissileID = 0; + public void PointDefenseTurretFiring() + { + if (!IsPrimaryWM) return; + // Note: this runs in the Earlyish timing stage, before bullets have moved. + int TurretID = 0; + int ballisticTurretID = 0; + int rocketTurretID = 0; + PDMslTgts.Clear(); + PDBulletTgts.Clear(); + PDRktTgts.Clear(); + MslTurrets.Clear(); + //missileTarget = null; + int APScount = pointDefenseWeaponArray.Length; + int missileCount = 0; + TargetInfo interceptiontarget = null; + Vector3 closestTarget = Vector3.zero; + Vector3 kbCorrection = BDKrakensbane.IsActive ? BDKrakensbane.FloatingOriginOffsetNonKrakensbane : Vector3.zero; // Correction for Krakensbane for bullets and rockets, which haven't been updated yet. + foreach (var weapon in pointDefenseWeaponArray) + { + if (weapon == null) continue; + //if (weapon.isAPS || weapon.dualModeAPS) + //{ + //APScount++; + if (weapon.ammoCount <= 0 && !BDArmorySettings.INFINITE_AMMO) continue; + if ((weapon.eAPSType == ModuleWeapon.APSTypes.Missile || weapon.eAPSType == ModuleWeapon.APSTypes.Omni) && !guardMode) + { + interceptiontarget = BDATargetManager.GetClosestMissileThreat(this); + if (interceptiontarget != null) PDMslTgts.Add(interceptiontarget); + + using (List.Enumerator target = BDATargetManager.FiredRockets.GetEnumerator()) + while (target.MoveNext()) + { + if (target.Current == null) continue; + if (target.Current.team == teamString) continue; + if (PDRktTgts.Contains(target.Current)) continue; + Vector3 targetPosition = target.Current.currentPosition - kbCorrection; + float threatDirectionFactor = (vessel.CoM - targetPosition).DotNormalized(target.Current.currentVelocity - vessel.Velocity()); + if (threatDirectionFactor < 0.95) continue; //if incoming round is heading this way + if (targetPosition.CloserToThan(weapon.fireTransforms[0].position, weapon.maxTargetingRange * 2)) + { + if (RadarUtils.TerrainCheck(targetPosition, vessel.CoM)) + { + continue; + } + else + { + if (closestTarget == Vector3.zero || (targetPosition - weapon.fireTransforms[0].position).sqrMagnitude < (closestTarget - weapon.fireTransforms[0].position).sqrMagnitude) + { + + closestTarget = targetPosition; + PDRktTgts.Add(target.Current); + } + } + } + } + } + if (weapon.eAPSType == ModuleWeapon.APSTypes.Ballistic || weapon.eAPSType == ModuleWeapon.APSTypes.Omni) + { + using (List.Enumerator target = BDATargetManager.FiredBullets.GetEnumerator()) + while (target.MoveNext()) + { + if (target.Current == null) continue; + if (target.Current.team == teamString) continue; + if (PDBulletTgts.Contains(target.Current)) continue; + Vector3 targetPosition = target.Current.currentPosition - kbCorrection; + float threatDirectionFactor = (vessel.CoM - targetPosition).DotNormalized(target.Current.currentVelocity - vessel.Velocity()); + if (threatDirectionFactor < 0.95) continue; //if incoming round is heading this way + if (targetPosition.CloserToThan(weapon.fireTransforms[0].position, weapon.maxTargetingRange * 2)) + { + if (RadarUtils.TerrainCheck(targetPosition, vessel.CoM)) + { + continue; + } + else + { + if (closestTarget == Vector3.zero || (targetPosition - weapon.fireTransforms[0].position).sqrMagnitude < (closestTarget - weapon.fireTransforms[0].position).sqrMagnitude) + { + closestTarget = targetPosition; + PDBulletTgts.Add(target.Current); + } + } + } + } + } + //} + } + + foreach (MissileBase missile in pointDefenseMissileArray) + { + if (missile == null) continue; + //if (!missile.engageMissile) continue; + if (missile.HasFired || missile.launched) continue; + MissileLauncher ml = missile as MissileLauncher; + if (ml && ml.multiLauncher && ml.multiLauncher.turret) + { + if (ml.multiLauncher.missileSpawner.ammoCount == 0 && !BDArmorySettings.INFINITE_ORDINANCE) continue; + if (!ml.multiLauncher.turret.turretEnabled) + ml.multiLauncher.turret.EnableTurret(missile, false); + //missileCount += Mathf.CeilToInt(ml.multiLauncher.missileSpawner.ammoCount / ml.multiLauncher.salvoSize); + missileCount++; // Technically we don't need an accurate count here, so we can be a bit more efficient like this + } + else missileCount++; + if (!guardMode) + { + interceptiontarget = BDATargetManager.GetClosestMissileThreat(this); + if (interceptiontarget != null && !PDMslTgts.Contains(interceptiontarget)) PDMslTgts.Add(interceptiontarget); + } + } + + //We already have a list of incoming threat missiles, just use that + if (guardMode && missileIsIncoming) + { + foreach (var incomingMissile in results.incomingMissiles) + { + if (incomingMissile.vessel != null && incomingMissile.vessel.gameObject.TryGetComponent(out var tInfo)) + PDMslTgts.Add(tInfo); + } + } + //Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}] tgtcount: {PDBulletTgts.Count + PDRktTgts.Count + PDMslTgts.Count}, APS count: {APScount}; interceptor count: {missileCount}"); + if (APScount + missileCount <= 0) + { + PDScanTimer = -100; + return; + } + + if (APScount > 0) + foreach (ModuleWeapon weapon in pointDefenseWeaponArray) + { + if (weapon == null) continue; + //if (weapon.isAPS || weapon.dualModeAPS) + //{ + if (weapon.eAPSType == ModuleWeapon.APSTypes.Ballistic || weapon.eAPSType == ModuleWeapon.APSTypes.Omni) + { + if (PDBulletTgts.Count > 0) + { + if (ballisticTurretID >= PDBulletTgts.Count) + { + if (weapon.isReloading || weapon.isOverheated || weapon.baseDeviation > 0.05 && (weapon.eWeaponType == ModuleWeapon.WeaponTypes.Ballistic || (weapon.eWeaponType == ModuleWeapon.WeaponTypes.Laser && weapon.pulseLaser))) + //if more APS turrets than targets, and APS is a rotary weapon using volume of fire instead of precision, roll over target list to assign multiple turrets to the incoming shell + ballisticTurretID = 0; + //else assign one turret per target, and hold fire on the rest + } + if (ballisticTurretID < PDBulletTgts.Count) + { + if (PDBulletTgts[ballisticTurretID] != null && (PDBulletTgts[ballisticTurretID].currentPosition - kbCorrection).FurtherFromThan(weapon.fireTransforms[0].position, weapon.engageRangeMax * 2)) ballisticTurretID = 0; //reset cycle so out of range guns engage closer targets + if (PDBulletTgts[ballisticTurretID] != null) //second check in case of turretID reset + { + if (TargetInTurretRange(weapon.turret, 7, PDBulletTgts[ballisticTurretID].currentPosition - kbCorrection, weapon)) + { + weapon.tgtShell = PDBulletTgts[ballisticTurretID]; // if target within turret fire zone, assign + } + else //else try remaining targets + { + using (List.Enumerator item = PDBulletTgts.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + if (TargetInTurretRange(weapon.turret, 7, item.Current.currentPosition - kbCorrection, weapon)) + { + weapon.tgtShell = item.Current; + break; + } + } + } + ballisticTurretID++; + } + } + else weapon.tgtShell = null; + } + else weapon.tgtShell = null; + } + if (weapon.eAPSType == ModuleWeapon.APSTypes.Missile || weapon.eAPSType == ModuleWeapon.APSTypes.Omni) + { + if (PDRktTgts.Count > 0) + { + if (rocketTurretID >= PDRktTgts.Count) + { + if ((weapon.isReloading || weapon.isOverheated) || weapon.baseDeviation > 0.05 && (weapon.eWeaponType == ModuleWeapon.WeaponTypes.Ballistic || (weapon.eWeaponType == ModuleWeapon.WeaponTypes.Laser && weapon.pulseLaser))) + rocketTurretID = 0; + } + if (rocketTurretID < PDRktTgts.Count) + { + if (PDRktTgts[rocketTurretID] != null && (PDRktTgts[rocketTurretID].currentPosition - kbCorrection).FurtherFromThan(weapon.fireTransforms[0].position, weapon.engageRangeMax * 2f)) rocketTurretID = 0; //reset cycle so out of range guns engage closer targets + if (PDRktTgts[rocketTurretID] != null) + { + bool viableTarget = true; + if (BDArmorySettings.BULLET_WATER_DRAG && weapon.eWeaponType == ModuleWeapon.WeaponTypes.Ballistic && FlightGlobals.getAltitudeAtPos(PDRktTgts[rocketTurretID].currentPosition - kbCorrection) < 0) viableTarget = false; + if (viableTarget && TargetInTurretRange(weapon.turret, 7, PDRktTgts[rocketTurretID].currentPosition - kbCorrection, weapon)) + { + weapon.tgtRocket = PDRktTgts[rocketTurretID]; // if target within turret fire zone, assign + weapon.tgtShell = null; + } + else //else try remaining targets + { + using (List.Enumerator item = PDRktTgts.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + if (!viableTarget) continue; + if (TargetInTurretRange(weapon.turret, 7, item.Current.currentPosition - kbCorrection, weapon)) + { + weapon.tgtRocket = item.Current; + weapon.tgtShell = null; + break; + } + } + } + rocketTurretID++; + } + } + } + else weapon.tgtRocket = null; + if (TurretID >= PDMslTgts.Count) TurretID = 0; + if (PDMslTgts.Count > 0) + { + if (PDMslTgts[TurretID].Vessel != null && PDMslTgts[TurretID].transform.position.FurtherFromThan(weapon.fireTransforms[0].position, weapon.engageRangeMax * 1.25f)) TurretID = 0; //reset cycle so out of range guns engage closer targets + if (PDMslTgts[TurretID].Vessel != null) + { + bool viableTarget = true; + if (BDArmorySettings.BULLET_WATER_DRAG && weapon.eWeaponType == ModuleWeapon.WeaponTypes.Ballistic && PDMslTgts[TurretID].Vessel.Splashed) viableTarget = false; + if (viableTarget && TargetInTurretRange(weapon.turret, 7, PDMslTgts[TurretID].Vessel.CoM, weapon)) + { + weapon.visualTargetPart = PDMslTgts[TurretID].Vessel.rootPart; // if target within turret fire zone, assign + //Debug.Log($"[BDArmory.MissileLauncher] assigning missile target {PDMslTgts[TurretID].Vessel.name}, ID {TurretID}"); + weapon.tgtShell = null; + weapon.tgtRocket = null; + } + else //else try remaining targets + { + using (List.Enumerator item = PDMslTgts.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current.Vessel == null) continue; + if (!viableTarget) continue; + if (TargetInTurretRange(weapon.turret, 7, item.Current.Vessel.CoM, weapon)) + { + weapon.visualTargetPart = item.Current.Vessel.rootPart; + //Debug.Log($"[BDArmory.MissileLauncher] assigning missile target {PDMslTgts[TurretID].Vessel.name} as secondary target, ID {TurretID}"); + weapon.tgtShell = null; + weapon.tgtRocket = null; + break; + } + } + } + TurretID++; + } + } + else + { + if (guardTarget == null) + { + weapon.visualTargetPart = null; + //Debug.Log($"[BDArmory.MissileLauncher] No Target! nulling visaltargetpart!"); + } + // weapon.Current.tgtShell = null; // FIXME These were wiping Omni type APS shell and rocket targets. + // weapon.Current.tgtRocket = null; + } + } + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}]: PDMslTgts: {PDMslTgts.Count}; {weapon.shortName} assigned shell:{(weapon.tgtShell != null ? "true" : "false")}; rocket: {(weapon.tgtRocket != null ? "true" : "false")}; missile:{(weapon.visualTargetPart != null ? weapon.visualTargetPart.vessel.GetName() : "null")}"); + weapon.autoFireTimer = Time.time; + weapon.autoFireLength = (fireBurstLength < 0.01f) ? targetScanInterval / 2f : fireBurstLength; + weapon.autofireShotCount = 0; + //} + } + + if (guardMode && missileCount > 0 && PDMslTgts.Count > 0 && !guardFiringMissile) + { + //bool logging = BDArmorySettings.DEBUG_MISSILES && BDArmorySettings.DEBUG_AI; + + int skipIRindex = 0; + bool skipRadarCheck = false; + bool radarLocked = false; + bool INSDetected = false; + bool skipDetectionCheck = false; + bool inLaserRange = false; + bool inARHRange = false; + bool changeTargets = true; + + int PDMslTgtsCount = PDMslTgts.Count; + // Check to ensure our first target actually is a valid target before doing + // all sorts of expensive processing... + if (!GetNextPDMslTgt(MissileID, PDMslTgtsCount)) return; + + Vessel targetVessel = PDMslTgts[MissileID].Vessel; + float targetDist = Vector3.Distance(vessel.CoM, targetVessel.CoM); + + //if (logging) + // Debug.Log($"[PD Missile Debug - {vessel.GetName()}] PDMslTgt size: {PDMslTgts.Count}; missile count: {missileCount}"); + foreach (MissileBase currMissile in pointDefenseMissileArray) //have guardMode requirement? + { + if (currMissile == null) continue; + MissileLauncher launcher = currMissile as MissileLauncher; + //if (!currMissile.engageMissile) continue; + if (currMissile.HasFired || currMissile.launched) continue; + + // If we've attempted... + if (targetDist > 0 && + (!pointDefenseMissileHasInertial || (skipDetectionCheck && !INSDetected)) && // An INS launch + (!pointDefenseMissileHasRadar || (skipRadarCheck && !radarLocked)) && // And a radar launch + (pointDefenseIRMissileCount == 0 || skipIRindex > 0) && // And an IR launch + (!pointDefenseMissileHasLaser || !inLaserRange) // And a laser launch + && !inARHRange) // And a maddog ARH launch + { + // Swap targets + int tempIndex = MissileID; + GetNextPDMslTgt(++MissileID, PDMslTgtsCount); + if (MissileID == tempIndex) return; + + // Otherwise, reset all values and check the current target + skipRadarCheck = false; + skipDetectionCheck = false; + inLaserRange = false; + inARHRange = false; + targetDist = -1f; + skipIRindex = 0; + // Doesn't need to be reset, as it'll be set within the loop, but in case the function is changed such + // that this is required... + //radarLocked = false; + + // Since we've swapped targets mid-way through, don't change targets at the end + changeTargets = false; + } + // Technically speaking, we could've potentially skipped a more capable IR missile in the above code... + + if (targetDist < 0) + { + targetVessel = PDMslTgts[MissileID].Vessel; + int tempIndex = MissileID; + while (targetVessel == null) + { + GetNextPDMslTgt(++MissileID, PDMslTgtsCount); + // All targets are null for some reason + if (MissileID == tempIndex) return; + + targetVessel = PDMslTgts[MissileID].Vessel; + } + + // If current target > max range + if (targetDist > pointDefenseMissileMaxRange) + { + tempIndex = MissileID; + // Find closer target + GetNextPDMslTgt(0, PDMslTgtsCount); + // If closest target is the current target, return + if (tempIndex == MissileID) return; + else + { + // Otherwise, reset all values and check the current target + skipRadarCheck = false; + skipDetectionCheck = false; + inLaserRange = false; + inARHRange = false; + targetVessel = PDMslTgts[MissileID].Vessel; + skipIRindex = 0; + // Doesn't need to be reset, as it'll be set within the loop, but in case the function is changed such + // that this is required... + //radarLocked = false; + } + } + + // Technically should be relative to the missile, but relative to CoM is close enough + targetDist = Vector3.Distance(vessel.CoM, targetVessel.CoM); + inLaserRange = maxTargetingLaserRange > targetDist; + inARHRange = pointDefenseMissileMaxARH > targetDist; + } + + //if (logging) + // Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}]: PD processing for missile: {currMissile.shortName}, for target: {(targetVessel != null ? targetVessel.GetName() : "null")} with UUID: {(targetVessel != null ? targetVessel.id : "null")}"); + + if (targetDist < currMissile.engageRangeMin) continue; + bool viableTarget = true; + + if (!CheckEngagementEnvelope(currMissile, targetDist, targetVessel)) continue; + bool torpedo = launcher && launcher.torpedo; //TODO - work out MMG torpedo support? + if (targetVessel.Splashed && !torpedo) viableTarget = false; + + switch (currMissile.TargetingMode) + { + case MissileBase.TargetingModes.Radar: + { + // If we should do the radar check + if (!skipRadarCheck && vesselRadarData != null) + { + if (!vesselRadarData.locked) + { + radarLocked = vesselRadarData.TryLockTarget(targetVessel, true); + } + else if (vesselRadarData.lockedTargetData.vessel == targetVessel) + radarLocked = true; + else + { + if (vesselRadarData.SwitchActiveLockedTarget(targetVessel)) + radarLocked = true; + else + radarLocked = vesselRadarData.TryLockTarget(targetVessel, true); + } + + // Once we've performed the check we can skip it + skipRadarCheck = true; + if (!INSDetected) + INSDetected = radarLocked; + } + + //if (logging) + // Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}]: PD skipRadarCheck: {skipRadarCheck}, radarLocked: {radarLocked}"); + + // If we're not locked, the radars/sonars are on and the missile isn't ARH LOAL + if (!radarLocked && (torpedo ? _sonarsEnabled : _radarsEnabled) && (currMissile.activeRadarRange <= 0 || !currMissile.radarLOAL)) continue; //don't have available radar lock, move to next missile + } + break; + case MissileBase.TargetingModes.Inertial: + { + // If we should do the radar check + if (!skipDetectionCheck && vesselRadarData != null) + { + TargetSignatureData INSTarget = TargetSignatureData.noTarget; + bool tempRadarLocked = false; + + // First check the radar for a detection + if (_radarsEnabled) + (INSTarget, tempRadarLocked) = vesselRadarData.detectedRadarTargetLock(targetVessel, this); //detected by radar scan? + + // If detected on radar + if (INSTarget.exists) + if (tempRadarLocked) + { + radarLocked = true; + skipRadarCheck = true; + } + else if (_irstsEnabled) + INSTarget = vesselRadarData.activeIRTarget(null, this); //how about IRST? + + skipDetectionCheck = true; + + INSDetected = (INSTarget.exists && INSTarget.vessel == targetVessel); + } + + if (!INSDetected) continue; + } + break; + case MissileBase.TargetingModes.Heat: + { + // If we've already checked all IR missile types... + if (skipIRindex >= pointDefenseIRMissileCount) continue; + + for (int i = 0; i < skipIRindex; i++) + // If we've already checked the current type of missile and failed... + if (pointDefenseIRMissileSkipArr[i] == currMissile.shortName) + { + //if (logging) + // Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}]: PD skipping IR missile: {currMissile.shortName}"); + continue; + } + // Look for a better way to do this... + SearchForHeatTarget(currMissile, PDMslTgts[MissileID]); + // If we haven't gotten a heat target, continue + if (!heatTarget.exists || + (heatTarget.vessel != targetVessel) || + heatTarget.signalStrength * ((BDArmorySettings.ASPECTED_IR_SEEKERS && Vector3.Dot(targetVessel.vesselTransform.up, currMissile.transform.forward) > 0.25f) ? currMissile.frontAspectHeatModifier : 1) < currMissile.heatThreshold) + { + // Write down the missile type that failed to lock + //if (logging) + // Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}]: PD IR missile: {currMissile.shortName}, failed to lock. Noted as skipIRindex: {skipIRindex}"); + pointDefenseIRMissileSkipArr[skipIRindex] = currMissile.shortName; + skipIRindex++; + continue; + } + } + break; + case MissileBase.TargetingModes.Laser: + { + if (!inLaserRange) continue; + } + break; + } + //need to see if missile is turreted (and is a unique turret we haven't seen yet); if so, check if target is within traverse, else see if target is within boresight + bool turreted = false; + MissileTurret mT = null; + if (launcher && (launcher.missileTurret || launcher.multiLauncher && launcher.multiLauncher.turret)) + { + mT = launcher.missileTurret ? launcher.missileTurret : launcher.multiLauncher.turret; + if (!MslTurrets.Contains(mT)) + { + turreted = true; + mT.EnableTurret(currMissile, false); + MslTurrets.Add(mT); //don't try to assign two different targets to a turret, so treat remaining missiles on the turret as boresight launch + mT.slavedTargetPosition = targetVessel.CoM; + mT.slaved = true; + mT.SlavedAim(); + } + } + if (currMissile.customTurret.Count> 0) + { + for (int i = 0; i < currMissile.customTurret.Count; i++) + { + if (currMissile.customTurret[i] == null) continue; + if (currMissile.customTurret[i].vessel != vessel) continue; + currMissile.customTurret[i].manuallyControlled = false; + currMissile.customTurret[i].slavedTargetPosition = targetVessel.CoM; + currMissile.customTurret[i].AimToTarget(currMissile.customTurret[i].slavedTargetPosition); + } + //TODO - don't assign two missiles on the same custom turret to two different targets check + } + //Debug.Log($"[PD Missile Debug - {vessel.GetName()}]viable: {viableTarget}; turreted: {turreted}; inRange: {(turreted ? TargetInTurretRange(mT.turret, mT.fireFOV, targetVessel.CoM) : GetLaunchAuthorization(targetVessel, this, currMissile))}"); + if (viableTarget && turreted ? TargetInTurretRange(mT.turret, mT.fireFOV, targetVessel.CoM) : GetLaunchAuthorization(targetVessel, this, currMissile)) + { + //missileTarget = targetVessel; + //if (logging) + // Debug.Log($"[BDArmory.MissileFire] firing interceptor missile: {currMissile.shortName} at {targetVessel.name}"); + StartCoroutine(GuardMissileRoutine(targetVessel, currMissile)); + break; + } + else + // If we can't fire at the current target, swap to a different missile. We assume later missiles are more capable than previous missiles + continue; + + } + // Move to the next target for the next scan... + if (changeTargets) + GetNextPDMslTgt(++MissileID, PDMslTgtsCount); + } + } + + public bool GetNextPDMslTgt(int currIndex, int tgtCount) + { + if (currIndex >= tgtCount) currIndex = 0; + int temp = currIndex; + while (GetMissilesAway(PDMslTgts[currIndex])[0] >= maxMissilesOnTarget) + { + currIndex++; + if (currIndex >= tgtCount) currIndex = 0; + // If we've searched all missiles and all targets are accounted for, return + if (currIndex == temp) + return false; + } + MissileID = currIndex; + //if (BDArmorySettings.DEBUG_MISSILES && BDArmorySettings.DEBUG_AI) + // Debug.Log($"[BDArmory.MissileFire - {(this.vessel != null ? vessel.GetName() : "null")}]: PD Selected target: {(PDMslTgts[currIndex].Vessel != null ? PDMslTgts[currIndex].Vessel.GetName() : "null")} has: {GetMissilesAway(PDMslTgts[currIndex])[0]} interceptors targeted"); + return true; + } + + public void SetOverrideTarget(TargetInfo target) + { + overrideTarget = target; + targetScanTimer = -100; + } + + public void UpdateMaxGuardRange() + { + var rangeEditor = (UI_FloatSemiLogRange)Fields["guardRange"].uiControlEditor; + rangeEditor.UpdateLimits(rangeEditor.minValue, BDArmorySettings.MAX_GUARD_VISUAL_RANGE); + } + + /// + /// Update the max gun range in-flight. + /// + public void UpdateMaxGunRange(Vessel v) + { + if (v != vessel || vessel == null || !vessel.loaded || !part.isActiveAndEnabled) return; + VesselModuleRegistry.OnVesselModified(v); + List gunLikeClasses = [WeaponClasses.Gun, WeaponClasses.DefenseLaser, WeaponClasses.Rocket]; + maxGunRange = 10f; + foreach (var weapon in VesselModuleRegistry.GetModules(vessel)) + { + if (weapon == null) continue; + if (weapon.GetWeaponChannel() > weaponChannel) continue; + if (gunLikeClasses.Contains(weapon.GetWeaponClass())) + { + maxGunRange = Mathf.Max(maxGunRange, weapon.maxEffectiveDistance); + } + } + if (BDArmorySetup.Instance.textNumFields != null && BDArmorySetup.Instance.textNumFields.ContainsKey("gunRange")) { BDArmorySetup.Instance.textNumFields["gunRange"].maxValue = maxGunRange; } + var oldGunRange = gunRange; + gunRange = Mathf.Min(gunRange, maxGunRange); + if (BDArmorySettings.DEBUG_AI && gunRange != oldGunRange) Debug.Log($"[BDArmory.MissileFire]: Updating gun range of {v.vesselName} to {gunRange} of {maxGunRange} from {oldGunRange}"); + } + + /// + /// Update the max gun range in the editor. + /// + public void UpdateMaxGunRange(Part eventPart) + { + if (EditorLogic.fetch.ship == null) return; + List gunLikeClasses = [WeaponClasses.Gun, WeaponClasses.DefenseLaser, WeaponClasses.Rocket]; + var rangeEditor = (UI_FloatPowerRange)Fields["gunRange"].uiControlEditor; + maxGunRange = rangeEditor.minValue; + foreach (var p in EditorLogic.fetch.ship.Parts) + { + foreach (var weapon in p.FindModulesImplementing()) + { + if (weapon == null) continue; + if (gunLikeClasses.Contains(weapon.GetWeaponClass())) + { + maxGunRange = Mathf.Max(maxGunRange, weapon.maxEffectiveDistance); + } + } + } + if (gunRange == 0 || gunRange > rangeEditor.maxValue - 1) { gunRange = maxGunRange; } + rangeEditor.UpdateLimits(rangeEditor.minValue, maxGunRange); + var oldGunRange = gunRange; + gunRange = Mathf.Min(gunRange, maxGunRange); + if (BDArmorySettings.DEBUG_AI && gunRange != oldGunRange) Debug.Log($"[BDArmory.MissileFire]: Updating gun range of {EditorLogic.fetch.ship.shipName} to {gunRange} of {maxGunRange} from {oldGunRange}"); + } + + /// + /// Update the max rangeSqr of engaging a visual target with guns. + /// + public void UpdateVisualGunRangeSqr(BaseField field, object obj) + { + maxVisualGunRangeSqr = Mathf.Min(gunRange * gunRange, guardRange * guardRange); + } + + public float ThreatClosingTime(Vessel threat) + { + float closureTime = 3600f; // Default closure time of one hour + if (threat) // If we weren't passed a null + { + closureTime = vessel.TimeToCPA(threat, closureTime); + } + return closureTime; + } + + // moved from pilot AI, as it does not really do anything AI related? + public bool GetLaunchAuthorization(Vessel targetV, MissileFire mf, MissileBase missile) + { + bool launchAuthorized = false; + MissileLauncher mlauncher = missile as MissileLauncher; + if (missile != null && targetV != null) + { + Vector3 target = targetV.CoM; + + if (missile.GetWeaponClass() == WeaponClasses.SLW && !vessel.Splashed && (targetV.CoM - vessel.CoM).sqrMagnitude < ((vessel.horizontalSrfSpeed * bombFlightTime) * (vessel.horizontalSrfSpeed * bombFlightTime))) + { + return launchAuthorized; //don't drop torps if closer than horizontal drop dist + } + if (targetV.speed > 1) //target is moving + { + if (missile.customTurret.Count > 0 || (mlauncher && (mlauncher.missileTurret || (mlauncher.multiLauncher && mlauncher.multiLauncher.turret)))) + target = MissileGuidance.GetAirToAirFireSolution(missile, targetV.CoM, targetV.Velocity()); + else + target = MissileGuidance.GetAirToAirFireSolution(missile, targetV); + } + + float boresightAngle = missile.maxOffBoresight * ((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 (unguidedWeapon || missile.TargetingMode == MissileBase.TargetingModes.None) // Override boresightAngle based on blast radius for unguidedWeapons or weapons with no targeting mode + { + if (missile.customTurret.Count > 0 || (mlauncher && (mlauncher.missileTurret || (mlauncher.multiLauncher && mlauncher.multiLauncher.turret)))) + boresightAngle = 1f; + else + boresightAngle = Mathf.Max(Mathf.Rad2Deg * Mathf.Atan(missile.GetBlastRadius() / (target - missile.transform.position).magnitude) / 3, 1f); // 1deg - within 1/3 of blast radius + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} boresight angle for unguided {missile.shortName} is {boresightAngle}."); + } + + // Check that target is within maxOffBoresight now and in future time fTime if we are in atmosphere + launchAuthorized = missile.maxOffBoresight >= 180 || VectorUtils.Angle(missile.GetForwardTransform(), target - missile.transform.position) < Mathf.Min(missile.missileFireAngle, boresightAngle); // Launch is possible now + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} final boresight check {(launchAuthorized ? "passed" : "failed")}, boresight angle {VectorUtils.Angle(missile.GetForwardTransform(), target - missile.transform.position)} of {missile.missileFireAngle}/{boresightAngle}."); + if (launchAuthorized && !vessel.InVacuum()) + { + //float fTime = 2 - Mathf.Min(missile.dropTime, 1); + float fTime = MissileLaunchParams.GetMissileActiveTime(missile, vessel.LandedOrSplashed); + Vector3 futurePos = target + (targetV.Velocity() * fTime); + Vector3 myFuturePos = vessel.CoM + (vessel.Velocity() * fTime); + launchAuthorized = launchAuthorized && ((!unguidedWeapon && missile.maxOffBoresight >= 180) || VectorUtils.Angle(missile.GetForwardTransform(), futurePos - myFuturePos) < boresightAngle); // Launch is likely also possible at fTime + } + } + + return launchAuthorized; + } + + /// + /// Check if missile is currently unable to be guided, does not apply for intentionally unguided missiles + /// + /// true if missile is unguided + public bool UnguidedMissile(MissileBase ml, float distanceToTarget) + { + MissileLauncher mlauncher = ml as MissileLauncher; + bool torpedo = mlauncher != null ? mlauncher.torpedo : false; //TODO - work out MMG torpedo support? + + bool unguidedWeapon = + ml.TargetingMode switch + { + MissileBase.TargetingModes.Laser => BDATargetManager.ActiveLasers.Count <= 0, + MissileBase.TargetingModes.Radar => !(torpedo ? _sonarsEnabled : _radarsEnabled) && (!ml.radarLOAL || (mlauncher != null && ml.seekerTimeout < ((distanceToTarget - ml.activeRadarRange) / mlauncher.optimumAirspeed))), + MissileBase.TargetingModes.Inertial => !((torpedo ? _sonarsEnabled : _radarsEnabled) || _irstsEnabled), + MissileBase.TargetingModes.Gps => BDATargetManager.ActiveLasers.Count <= 0 && !_radarsEnabled, + _ => false + }; //unify unguidedWeapon conditions + + return unguidedWeapon; + } + + /// + /// Clamps max missile engage range to max range of radar/laser/visual range + /// + /// max missile range clamped to targeting method range + public float MaxMissileRange(MissileBase ml, bool unguidedGuidedMissile = false) + { + // Clamp missile range to targeting method range + float maxEngageRange = ml.engageRangeMax; + if (unguidedGuidedMissile) + return 0.1f * maxEngageRange; + + switch (ml.TargetingMode) + { + case MissileBase.TargetingModes.Radar: + { + float radarRange = 0.1f * maxEngageRange; + VesselRadarData vrd = vessel.gameObject.GetComponent(); + if (vrd) + radarRange = Mathf.Max(vrd.MaxRadarRange(), ml.radarLOAL ? ml.activeRadarRange : 0f); + return Mathf.Min(maxEngageRange, radarRange); + } + case MissileBase.TargetingModes.Heat: + { + float irstRange = 0.1f * maxEngageRange; + VesselRadarData vrd = vessel.gameObject.GetComponent(); + if (vrd) + irstRange = vrd.MaxIRSTRange(); + irstRange = Mathf.Max(guardRange, irstRange); + return Mathf.Min(maxEngageRange, irstRange); + } + case MissileBase.TargetingModes.Laser: + { + if (targetingPods.Count > 0) + { + float maxPodRange = maxTargetingLaserRange; + //using (List.Enumerator tgp = targetingPods.GetEnumerator()) + // while (tgp.MoveNext()) + // { + // if (tgp.Current == null) continue; + // maxPodRange = Mathf.Max(tgp.Current.maxRayDistance, maxPodRange); + // } + return Mathf.Min(maxPodRange, maxEngageRange); + } + else + return 0.1f * maxEngageRange; + } + default: + return Mathf.Min(maxEngageRange, guardRange); + } + } + + /// + /// Check if AI is online and can target the current guardTarget with direct fire weapons + /// + /// true if AI might fire + bool AIMightDirectFire() + { + var ai = AI; + return ai != null && ai.pilotEnabled && ai.CanEngage() && guardTarget && ai.IsValidFixedWeaponTarget(guardTarget); + } + + #endregion Guard + + #region Turret + + int CheckTurret(float distance) + { + if (weaponIndex == 0 || selectedWeapon == null || + !(selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || + selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser || + selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket)) + { + return 2; + } + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.MissileFire]: Checking turrets"); + } + float finalDistance = distance; + //vessel.LandedOrSplashed ? distance : distance/2; //decrease distance requirement if airborne + + var ai = AI; + using var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator(); + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; + float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; + if (((ai != null && ai.pilotEnabled && ai.CanEngage()) || (TargetInTurretRange(weapon.Current.turret, gimbalTolerance, default, weapon.Current))) && weapon.Current.maxEffectiveDistance >= finalDistance) + { + if (weapon.Current.isOverheated) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} is overheated!"); + } + return -1; + } + if (weapon.Current.isReloading) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} is reloading!"); + } + return -1; + } + if (!weapon.Current.hasGunner) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} has no gunner!"); + } + return -1; + } + if (CheckAmmo(weapon.Current) || BDArmorySettings.INFINITE_AMMO) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} is valid!"); + } + return 1; + } + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} has no ammo."); + } + return -1; + } + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} cannot reach target ({distance} vs {weapon.Current.maxEffectiveDistance}, yawRange: {weapon.Current.yawRange}). Continuing."); + } + //else return 0; + } + return 2; + } + + bool TargetInTurretRange(ModuleTurret turret, float tolerance, Vector3 gTarget = default(Vector3), ModuleWeapon weapon = null) + { + if (!turret) + { + return false; + } + if (gTarget == default && !guardTarget) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.MissileFire]: Checking turret range but no guard target"); + } + return false; + } + if (gTarget == default) gTarget = guardTarget.CoM; + if (weapon != null && (gTarget - weapon.fireTransforms[0].transform.position).sqrMagnitude > (weapon.engageRangeMax * 1.25f) * (weapon.engageRangeMax * 1.25f)) return false; //target too far away + Transform turretTransform = turret.yawTransform.parent; + Vector3 direction = gTarget - turretTransform.position; + if (weapon != null && weapon.bulletDrop) // Account for bullet drop (rough approximation not accounting for target movement). + { + switch (weapon.GetWeaponClass()) + { + case WeaponClasses.Gun: + { + (float distance, Vector3 dir) = direction.MagNorm(); + var effectiveBulletSpeed = (turret.part.rb.velocity + BDKrakensbane.FrameVelocityV3f + weapon.bulletVelocity * dir).magnitude; + var timeOfFlight = distance / effectiveBulletSpeed; + direction -= 0.5f * FlightGlobals.getGeeForceAtPosition(vessel.CoM) * timeOfFlight * timeOfFlight; + break; + } + case WeaponClasses.Rocket: + { + (float distance, Vector3 dir) = direction.MagNorm(); + var effectiveRocketSpeed = (turret.part.rb.velocity + BDKrakensbane.FrameVelocityV3f + (weapon.thrust * weapon.thrustTime / weapon.rocketMass) * dir).magnitude; + var timeOfFlight = distance / effectiveRocketSpeed; + direction -= 0.5f * FlightGlobals.getGeeForceAtPosition(vessel.CoM) * timeOfFlight * timeOfFlight; + break; + } + } + } + Vector3 directionYaw = direction.ProjectOnPlanePreNormalized(turretTransform.up); + + float angleYaw = VectorUtils.Angle(turretTransform.forward, directionYaw); + float signedAnglePitch = 90 - VectorUtils.Angle(turretTransform.up, direction); + bool withinPitchRange = (signedAnglePitch >= turret.minPitch - tolerance && signedAnglePitch <= turret.maxPitch + tolerance); + + if (angleYaw < (turret.yawRange / 2) + tolerance && withinPitchRange) + { + 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}"); + } + return true; + } + else + { + 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}"); + } + return false; + } + } + + bool TargetInCustomTurretRange(ModuleWeapon weapon, float tolerance, Vector3 gTarget = default(Vector3)) + { + if (!weapon || weapon.customTurret.Count == 0) + { + return false; + } + if (gTarget == default && !guardTarget) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.MissileFire]: Checking turret range but no guard target"); + } + return false; + } + if (gTarget == default) gTarget = guardTarget.CoM; + if (weapon != null && (gTarget - weapon.fireTransforms[0].transform.position).sqrMagnitude > (weapon.engageRangeMax * 1.25f) * (weapon.engageRangeMax * 1.25f)) return false; //target too far away + Transform turretTransform = weapon.customTurret[0].bottomTransform; //might be an issue if grabbing non-servo; servos are proper Z+ forward Y+ up that turrets are, hinges... + Vector3 direction = gTarget - turretTransform.position; + if (weapon != null && weapon.bulletDrop) // Account for bullet drop (rough approximation not accounting for target movement). + { + switch (weapon.GetWeaponClass()) + { + case WeaponClasses.Gun: + { + (float distance, Vector3 dir) = direction.MagNorm(); + var effectiveBulletSpeed = (weapon.part.rb.velocity + BDKrakensbane.FrameVelocityV3f + weapon.bulletVelocity * dir).magnitude; + var timeOfFlight = distance / effectiveBulletSpeed; + direction -= 0.5f * FlightGlobals.getGeeForceAtPosition(vessel.CoM) * timeOfFlight * timeOfFlight; + break; + } + case WeaponClasses.Rocket: + { + (float distance, Vector3 dir) = direction.MagNorm(); + var effectiveRocketSpeed = (weapon.part.rb.velocity + BDKrakensbane.FrameVelocityV3f + (weapon.thrust * weapon.thrustTime / weapon.rocketMass) * dir).magnitude; + var timeOfFlight = distance / effectiveRocketSpeed; + direction -= 0.5f * FlightGlobals.getGeeForceAtPosition(vessel.CoM) * timeOfFlight * timeOfFlight; + break; + } + } + } + Vector3 directionYaw = weapon.fireTransforms[0].transform.up; + float angleYaw = 0; + float yawRange = 0; + bool withinPitchRange = false; + using (List.Enumerator servo = weapon.customTurret.GetEnumerator()) + while (servo.MoveNext()) + { + if (servo.Current == null || servo.Current.vessel != vessel) continue; + if (servo.Current.isYawRotor) + { + directionYaw = direction.ProjectOnPlanePreNormalized(servo.Current.yawTransform.up); + angleYaw = VectorUtils.Angle(turretTransform.forward, directionYaw); + yawRange = servo.Current.fullRotation ? 360 : servo.Current.maxYaw - servo.Current.minYaw; + } + else + { + float signedAnglePitch = 90 - VectorUtils.Angle(servo.Current.yawNormal, direction); + withinPitchRange = (signedAnglePitch >= servo.Current.minPitch - tolerance && signedAnglePitch <= servo.Current.maxPitch + tolerance); + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.MissileFire]: Checking turret range - target is INSIDE gimbal limits! signedAnglePitch: {signedAnglePitch}, minPitch: {servo.Current.minPitch}, maxPitch: {servo.Current.maxPitch}, tolerance: {tolerance}"); + } + } + } + if (angleYaw < (yawRange / 2) + tolerance && withinPitchRange) + { + return true; + } + else + { + return false; + } + } + + public bool CheckAmmo(ModuleWeapon weapon) + { + string ammoName = weapon.ammoName; + if (ammoName == "ElectricCharge") return true; // Electric charge is almost always rechargable, so weapons that use it always have ammo. + if (BDArmorySettings.INFINITE_AMMO) //check for infinite ammo + { + return true; + } + else + { + /* + using (List.Enumerator p = vessel.parts.GetEnumerator()) + while (p.MoveNext()) + { + if (p.Current == null) continue; + // using (IEnumerator resource = p.Current.Resources.GetEnumerator()) + using (var resource = p.Current.Resources.dict.Values.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + if (resource.Current.resourceName != ammoName) continue; + if (resource.Current.amount >= weapon.requestResourceAmount) + { + return true; + } + } + } + */ + vessel.GetConnectedResourceTotals(weapon.ResID_Ammo, out double ammoCurrent, out double ammoMax); + if (ammoCurrent >= weapon.requestResourceAmount) + { + if (weapon.secondaryAmmoPerShot > 0) + { + if (weapon.secondaryAmmoName == "ElectricCharge") return true; + vessel.GetConnectedResourceTotals(weapon.ResID_SecAmmo, out double secAmmoCurrent, out double secAmmoMax); + if (secAmmoCurrent < weapon.secondaryAmmoPerShot) return false; + } + return true; + } + + return false; + } + } + + 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 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 (!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 (weapon.GetWeaponChannel() > weaponChannel) continue; + hasWeapons = true; + if (weapon.GetWeaponClass() == WeaponClasses.Gun || weapon.GetWeaponClass() == WeaponClasses.Rocket || weapon.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + var gun = weapon.GetWeaponModule(); + if (gun.isAPS && !gun.dualModeAPS) continue; //ignore non-dual purpose APS weapons, they can't attack + if (gun.ammoName == "ElectricCharge") { hasWeaponsAndAmmo = true; break; } + 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() + { // 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 (weapon.GetWeaponChannel() > weaponChannel) continue; + if (weapon.GetWeaponClass() == WeaponClasses.Gun || weapon.GetWeaponClass() == WeaponClasses.Rocket || weapon.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + var gun = weapon.GetWeaponModule(); + if (gun.isAPS && !gun.dualModeAPS) continue; //ignore non-dual purpose APS weapons, they can't attack + if (gun.ammoName == "ElectricCharge") { 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; + } + + + void ToggleTurret() + { + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetWeaponChannel() > weaponChannel) continue; + if (selectedWeapon == null) + { + 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. + { + weapon.Current.StandbyWeapon(); + } + else + { + weapon.Current.DisableWeapon(); + } + } + else + { + weapon.Current.EnableWeapon(); + if (weapon.Current.dualModeAPS) + { + weapon.Current.isAPS = false; + if (!guardMode) weapon.Current.aiControlled = false; + } + } + } + } + + #endregion Turret + + #region Aimer + bool ShowBoreRing(bool visible) + { + if (vessel == null || !vessel.isActiveVessel) return false; // We're not in control. + if (boreRing != null) boreRing.SetActive(visible); + return true; // We're in control. + } + + string bombAimerDebugString = ""; + float BombAimer() + { + var bomb = selectedWeapon; // Avoid repeated calls to selectedWeapon.get(). + if (bomb == null || bomb.GetWeaponClass() != WeaponClasses.Bomb) + { + showBombAimer = false; + bombAimerPosition = vessel.CoM + vessel.Velocity() * 2; //reset bombAimerPosition + return 0; //null conditions for running bombAimer, return + //go with an approximation for drop time. Will cause inaccuracies if, say, there's an NPC bomber dropping parachute bombs or something, but better than returning 0, and good enough for things like extending for a bombing run + } + var bombPart = bomb.GetPart(); // We know the selected weapon is a bomb at this point. + showBombAimer = bombPart != null && vessel.verticalSpeed < 50 && AltitudeTrigger(); // Situational conditions for showing the aimer. + if (!showBombAimer) + { + bombAimerPosition = vessel.CoM + vessel.Velocity() * 2; //reset bombAimerPosition + return 0f; + } + if (!BDArmorySettings.DRAW_AIMERS || !vessel.isActiveVessel || MapView.MapIsEnabled) //don't draw the aimer in these circumstances, but running the bombAimerPosition sim is still necessary + { + showBombAimer = false; + } + MissileBase ml = bombPart.GetComponent(); + + float simDeltaTime = 5f * Time.fixedDeltaTime; + float simTime = 0; + Vector3 simVelocity = (bombPart.rb != null ? bombPart.rb : bombPart.parent.rb).velocity + BDKrakensbane.FrameVelocityV3f; // bombs on reloadable rails don't have a rigid body. + Vector3 currPos = ml.MissileReferenceTransform.position + Time.fixedDeltaTime * simVelocity; // Start on the next frame. + Vector3 prevPos = currPos; + Vector3 closestPos = currPos; + Vector3 simAcceleration = Vector3.zero; + MissileLauncher launcher = ml as MissileLauncher; + if (launcher != null) + { + if (launcher.multiLauncher && launcher.multiLauncher.salvoSize > 1) + currPos += ((launcher.multiLauncher.salvoSize / 2 * (60 / launcher.multiLauncher.rippleRPM)) + launcher.multiLauncher.deploySpeed) * vessel.Velocity(); //add an offset for bomblet dispensers, etc, to have them start deploying before target to carpet bomb + simVelocity += launcher.decoupleSpeed * (launcher.decoupleForward ? launcher.MissileReferenceTransform.forward : -launcher.MissileReferenceTransform.up); + } + else + { //TODO: BDModularGuidance review this value + simVelocity += 5 * -ml.MissileReferenceTransform.up; + } + + bombAimerTrajectory.Clear(); + bombAimerPosition = Vector3.zero; + + // FIXME values for MMG missiles (launcher == null) need calculating. + // FIXME mk82 bombs on reloadable rails behave like mk83 bombs. + float ordnanceMass = launcher != null && launcher.multiLauncher ? launcher.multiLauncher.missileMass : ml.part.partInfo.partPrefab.mass; + float ordnanceThrust = launcher != null ? launcher.cruiseThrust : 0; + float ordnanceBoost = launcher != null ? launcher.thrust : 0; + float thrustTime = launcher != null ? launcher.cruiseTime + launcher.boostTime : 0; + Vector3 pointingDirection = ml.MissileReferenceTransform.forward; + var upDirection = VectorUtils.GetUpDirection(currPos); + float dragArea = launcher != null ? launcher.simpleDrag : 0; + float liftArea; + float liftForce = 0; + float dragForce = 0; + float AoA = 0; + float atmDensity; + float simSpeedSquared; + var simStartTime = Time.realtimeSinceStartup; + float CoDOffset = launcher != null ? Mathf.Abs(launcher.simpleCoD.z) : 0; + float CoDOffsetSqrt = launcher != null ? BDAMath.Sqrt(CoDOffset) : 0; + float blastRadiusThreshold = CurrentMissile.GetBlastRadius() * 0.68f; //single bomb modifier for blast radius in GuardBomBRoutine is 0.68, so target dist needs to be at least this + StringBuilder logstring = new(); + //bombAimerTerrainNormal = upDirection; + while (true) // Basic forward Euler, which should be good enough for this. + { + atmDensity = (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currPos), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody); + simSpeedSquared = simVelocity.sqrMagnitude; + upDirection = VectorUtils.GetUpDirection(currPos); + var simVelocityDir = simVelocity.normalized; + var lastSimSpeed = simVelocity.magnitude; + + // Position update before the velocity update so that they're in sync. + prevPos = currPos; + currPos += simDeltaTime * simVelocity; + bombAimerTrajectory.Add(currPos); + + if (Mathf.Floor(simVelocity.magnitude / 10f) != Mathf.Floor(lastSimSpeed / 10f)) logstring.Append($"; {simVelocity.magnitude}: {AoA}, {liftForce}, {dragForce}"); + + var (distance, direction) = (currPos - prevPos).MagNorm(); + Ray ray = new(prevPos, direction); + if (Physics.Raycast(ray, out RaycastHit hitInfo, distance, simTime < ml.dropTime ? (int)LayerMasks.Scenery : (int)(LayerMasks.Scenery | LayerMasks.Parts | LayerMasks.EVA))) // Only consider scenery during the drop time to avoid self hits. + { + bombAimerPosition = hitInfo.point; + //bombAimerTerrainNormal = hitInfo.normal; + simTime += (distance - hitInfo.distance) / distance * simDeltaTime; + bombAimerCPA = guardTarget ? AIUtils.PredictPosition(prevPos, simVelocity, simAcceleration, AIUtils.TimeToCPA(prevPos - guardTarget.CoM, simVelocity - guardTarget.Velocity(), simAcceleration - (guardTarget.Splashed ? Vector3.zero : guardTarget.acceleration_immediate))) : bombAimerPosition; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) bombAimerDebugString = $"Scenery / part hit at {simTime:0.00}s"; + break; + } + else + { + var currentAlt = FlightGlobals.getAltitudeAtPos(currPos); + if (currentAlt < 0) + { + bombAimerPosition = currPos - currentAlt * upDirection; + var prevAlt = FlightGlobals.getAltitudeAtPos(prevPos); + simTime += prevAlt / (prevAlt - currentAlt) * simDeltaTime; + bombAimerCPA = guardTarget ? AIUtils.PredictPosition(prevPos, simVelocity, simAcceleration, AIUtils.TimeToCPA(prevPos - guardTarget.CoM, simVelocity - guardTarget.Velocity(), simAcceleration - (guardTarget.Splashed ? Vector3.zero : guardTarget.acceleration_immediate))) : bombAimerPosition; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) bombAimerDebugString = $"Water hit at {simTime:0.00}s"; + break; + } + } + if (guardTarget) + { + var guardPos = AIUtils.PredictPosition(guardTarget, simTime, false); + float targetDist = Vector3.Distance(currPos, guardPos) - guardTarget.GetRadius(); + var currentAlt = FlightGlobals.getAltitudeAtPos(currPos); + if (targetDist < blastRadiusThreshold && currentAlt < guardTarget.altitude) //adjusting bombaimer pos based on guardTarget proximity should only occur for targeting above ground targets, so the AI knows when to release/where to aim vs something on top of a building or bridge, or trying to bomb an ArsenalBird or similar + { + var timeToCPA = AIUtils.TimeToCPA(currPos - guardPos, simVelocity - guardTarget.Velocity(), simAcceleration - (guardTarget.Splashed ? Vector3.zero : guardTarget.acceleration_immediate)); + bombAimerCPA = AIUtils.PredictPosition(currPos, simVelocity, simAcceleration, timeToCPA); + (distance, direction) = (bombAimerCPA - currPos).MagNorm(); + if (Physics.Raycast(currPos, direction, out hitInfo, distance, simTime < ml.dropTime ? (int)LayerMasks.Scenery : (int)(LayerMasks.Scenery | LayerMasks.Parts | LayerMasks.EVA))) // Only consider scenery during the drop time to avoid self hits. + bombAimerPosition = hitInfo.point; // Check for scenery hit + else bombAimerPosition = bombAimerCPA; + simTime += timeToCPA; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) bombAimerDebugString = $"Target CPA at {simTime:0.00}s"; + break; + } + else //else "too close to bomb" will get triggered the moment the bombaimer passes the target if target alt > 200, be it due to legitimate overshoot, or momentary twitch as AI maneuvers + {// maybe base on target within FOV/vector3.Dot(guardTarget, vessel.CoM) > 0 ? + if (!guardTarget.LandedOrSplashed && currentAlt < (float)guardTarget.altitude) + { + bombAimerPosition = currPos - ((float)guardTarget.altitude - currentAlt) * upDirection; + var prevAlt = FlightGlobals.getAltitudeAtPos(prevPos); + simTime += prevAlt / (prevAlt - currentAlt) * simDeltaTime; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) bombAimerDebugString = $"below target at {simTime}s"; + break; + } + } + } + simAcceleration = FlightGlobals.RefFrameIsRotating ? (Vector3)FlightGlobals.getGeeForceAtPosition(currPos) : Vector3.zero; + + if (launcher != null) + { + dragArea = launcher.aero ? launcher.currDragArea : (simTime > launcher.deployTime ? launcher.deployedDrag : launcher.simpleDrag) * (0.008f * ordnanceMass); + liftArea = launcher.currLiftArea; + } + else + { + //TODO: BDModularGuidance drag and lift calculations + dragArea = ml.vessel.parts.Sum(p => p.dragScalar); + liftArea = 0.015f; + } + + // AoA varies wildly for some bombs, e.g., JDAM (10—30°), B-83 (4—3.5°). The following is a rough approx from fitting data points from a JDAM and a B-83. + AoA = liftArea > 0 && launcher != null && simTime > launcher.dropTime ? + Mathf.Min(launcher.maxAoA, (170f / CoDOffsetSqrt / (1 + simSpeedSquared / 1200f) + 2f / CoDOffset) * Mathf.Clamp01(simTime - launcher.dropTime)) : + 0; + pointingDirection = Vector3.RotateTowards(simVelocityDir, upDirection, Mathf.Deg2Rad * AoA, 0); + + if (launcher != null && launcher.aero) + { + // Lift + liftForce = 0.5f * atmDensity * simSpeedSquared * liftArea * BDArmorySettings.GLOBAL_LIFT_MULTIPLIER * Mathf.Max(MissileGuidance.DefaultLiftCurve.Evaluate(AoA), 0); + simAcceleration += liftForce / ordnanceMass * -simVelocity.ProjectOnPlanePreNormalized(pointingDirection).normalized; + + // Drag + dragForce = 0.5f * atmDensity * simSpeedSquared * dragArea * BDArmorySettings.GLOBAL_DRAG_MULTIPLIER * Mathf.Max(MissileGuidance.DefaultDragCurve.Evaluate(AoA), 0f); + simAcceleration += dragForce / ordnanceMass * -simVelocityDir; + } + else // Simple drag. This works fairly well for mk82 and mk82-brake bombs, but not JDAMs or B-83. + { + dragForce = 0.5f * atmDensity * simSpeedSquared * dragArea; + simAcceleration += dragForce / ordnanceMass * -simVelocityDir; + } + + // Thrusting + if (launcher != null && thrustTime > 0 && simTime <= thrustTime) + { + if (simTime < launcher.boostTime) + { + simAcceleration += ordnanceBoost / ordnanceMass * pointingDirection; + } + else + { + simAcceleration += ordnanceThrust / ordnanceMass * pointingDirection; + } + } + + simVelocity += simDeltaTime * simAcceleration; + simTime += simDeltaTime; + + if (Time.realtimeSinceStartup - simStartTime >= 0.1f) + { + bombAimerPosition = currPos; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) bombAimerDebugString = $"Time's up at {simTime:0.00}s"; + break; + } + } + if ((BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) && guardTarget) bombAimerDebugString += $", distance: {(bombAimerPosition - guardTarget.CoM).magnitude:0}m, radius: {boreRing.transform.localScale.x:0}m"; + + //debug lines + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && showBombAimer) + BombAimerRender(); + return simTime; + } + + void BombAimerRender() + { + if (bombAimerTrajectory.Count == 0) { lr.enabled = false; return; } + lr = GetComponent(); + if (!lr) { lr = gameObject.AddComponent(); } + lr.enabled = true; + lr.startWidth = 1f; + lr.endWidth = 1f; + lr.positionCount = bombAimerTrajectory.Count; + int i = 0; + foreach (var point in bombAimerTrajectory) + lr.SetPosition(i++, point); + } + + // Check GPS target is within 20m for stationary targets, and a scaling distance based on target speed for targets moving faster than ~175 m/s + bool GPSDistanceCheck(Vector3 pos, Vessel targetVessel) + { + if (targetVessel == null) return false; + return (targetVessel.CoM - pos).sqrMagnitude < Mathf.Max(400, 0.013f * (float)targetVessel.srfSpeed * (float)targetVessel.srfSpeed); + } + + // Check antiRad target is within 20m for stationary targets, and a scaling distance based on target speed for targets moving faster than ~175 m/s + bool AntiRadDistanceCheck() + { + if (!guardTarget) return false; + return (VectorUtils.WorldPositionToGeoCoords(antiRadiationTarget, vessel.mainBody) - VectorUtils.WorldPositionToGeoCoords(guardTarget.CoM, vessel.mainBody)).sqrMagnitude < Mathf.Max(400, 0.013f * (float)guardTarget.srfSpeed * (float)guardTarget.srfSpeed); + } + + bool AltitudeTrigger() + { + const float maxAlt = 10000; + double asl = vessel.mainBody.GetAltitude(vessel.CoM); + double agl = asl - vessel.terrainAltitude; + return agl < maxAlt || asl < maxAlt; + } + + #endregion Aimer + } +} \ No newline at end of file diff --git a/BDArmory/Modules/ModuleWingCommander.cs b/BDArmory/Control/ModuleWingCommander.cs similarity index 79% rename from BDArmory/Modules/ModuleWingCommander.cs rename to BDArmory/Control/ModuleWingCommander.cs index 3a7b02ddf..87bb95c50 100644 --- a/BDArmory/Modules/ModuleWingCommander.cs +++ b/BDArmory/Control/ModuleWingCommander.cs @@ -1,20 +1,30 @@ 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.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Extensions; + +namespace BDArmory.Control { public class ModuleWingCommander : PartModule { - public MissileFire weaponManager; + public MissileFire WeaponManager + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; + } + } + MissileFire _weaponManager; public List friendlies; @@ -71,9 +81,9 @@ public override void OnStart(StartState state) StartCoroutine(StartupRoutine()); GameEvents.onGameStateSave.Add(SaveWingmen); - GameEvents.onVesselLoaded.Add(OnVesselLoad); - GameEvents.onVesselDestroy.Add(OnVesselLoad); - GameEvents.onVesselGoOnRails.Add(OnVesselLoad); + GameEvents.onVesselLoaded.Add(OnVesselLoaded); + GameEvents.onVesselDestroy.Add(OnVesselLoaded); + GameEvents.onVesselGoOnRails.Add(OnVesselLoaded); MissileFire.OnChangeTeam += OnToggleTeam; screenMessage = new ScreenMessage("", 2, ScreenMessageStyle.LOWER_CENTER); @@ -93,8 +103,6 @@ IEnumerator StartupRoutine() yield return null; } - weaponManager = part.FindModuleImplementing(); - RefreshFriendlies(); RefreshWingmen(); LoadWingmen(); @@ -102,17 +110,14 @@ IEnumerator StartupRoutine() void OnDestroy() { - if (HighLogic.LoadedSceneIsFlight) - { - GameEvents.onGameStateSave.Remove(SaveWingmen); - GameEvents.onVesselLoaded.Remove(OnVesselLoad); - GameEvents.onVesselDestroy.Remove(OnVesselLoad); - GameEvents.onVesselGoOnRails.Remove(OnVesselLoad); - MissileFire.OnChangeTeam -= OnToggleTeam; - } + GameEvents.onGameStateSave.Remove(SaveWingmen); + GameEvents.onVesselLoaded.Remove(OnVesselLoaded); + GameEvents.onVesselDestroy.Remove(OnVesselLoaded); + GameEvents.onVesselGoOnRails.Remove(OnVesselLoaded); + MissileFire.OnChangeTeam -= OnToggleTeam; } - void OnVesselLoad(Vessel v) + void OnVesselLoaded(Vessel v) { if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed) { @@ -123,47 +128,30 @@ void OnVesselLoad(Vessel v) void RefreshFriendlies() { - if (!weaponManager) return; + var wm = WeaponManager; + if (!wm) return; friendlies = new List(); using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) while (vs.MoveNext()) { if (vs.Current == null) continue; - if (!vs.Current.loaded || vs.Current == vessel) continue; + if (!vs.Current.loaded || vs.Current == vessel || VesselModuleRegistry.IgnoredVesselTypes.Contains(vs.Current.vesselType)) continue; - IBDAIControl pilot = null; - MissileFire wm = null; - List.Enumerator ps = vs.Current.FindPartModulesImplementing().GetEnumerator(); - while (ps.MoveNext()) - { - if (ps.Current == null) continue; - pilot = ps.Current; - break; - } - ps.Dispose(); - - if (pilot == null) continue; - List.Enumerator ws = vs.Current.FindPartModulesImplementing().GetEnumerator(); - while (ws.MoveNext()) - { - // TODO: JDK: note that this assigns the last module found. Is that what we want? - wm = ws.Current; - } - ws.Dispose(); - - if (!wm || wm.Team != weaponManager.Team) continue; - friendlies.Add(pilot); + var ac = vs.Current.ActiveController(); + if (ac == null) continue; // Since this is called on vessel destroy, we need to check that the vessel module isn't null. + if (ac.AI == null) continue; + if (ac.WM == null || ac.WM.Team != wm.Team) continue; + friendlies.Add(ac.AI); } //TEMPORARY - wingmen = new List(); - List.Enumerator fs = friendlies.GetEnumerator(); + wingmen = []; + using var fs = friendlies.GetEnumerator(); while (fs.MoveNext()) { if (fs.Current == null) continue; wingmen.Add(fs.Current); } - fs.Dispose(); } void RefreshWingmen() @@ -175,7 +163,8 @@ void RefreshWingmen() focusIndexes.Clear(); return; } - wingmen.RemoveAll(w => w == null || (w.weaponManager && w.weaponManager.Team != weaponManager.Team)); + var wm = WeaponManager; + if (wm != null) wingmen.RemoveAll(wingman => wingman == null || (wingman.WeaponManager && wingman.WeaponManager.Team != wm.Team)); List uniqueIndexes = new List(); List.Enumerator fIndexes = focusIndexes.GetEnumerator(); @@ -188,7 +177,7 @@ void RefreshWingmen() } } fIndexes.Dispose(); - focusIndexes = new List(uniqueIndexes); + focusIndexes = [.. uniqueIndexes]; } void SaveWingmen(ConfigNode cfg) @@ -219,18 +208,11 @@ void LoadWingmen() using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) while (vs.MoveNext()) { - if (vs.Current == null) continue; - if (!vs.Current.loaded) continue; + if (vs.Current == null || !vs.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(vs.Current.vesselType)) continue; if (vs.Current.id.ToString() != wingIDs.Current) continue; - List.Enumerator pilots = vs.Current.FindPartModulesImplementing().GetEnumerator(); - while (pilots.MoveNext()) - { - if (pilots.Current == null) continue; - wingmen.Add(pilots.Current); - break; - } - pilots.Dispose(); + var pilot = vs.Current.ActiveController().AI; + if (pilot != null) wingmen.Add(pilot); } } wingIDs.Dispose(); @@ -273,8 +255,11 @@ 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(); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectWingCommander.position); + BDArmorySetup.WindowRectWingCommander = GUI.Window(1293293, BDArmorySetup.WindowRectWingCommander, WingmenWindow, StringUtils.Localize("#LOC_BDArmory_WingCommander_Title"),//"WingCommander" BDArmorySetup.BDGuiSkin.window); + BDArmorySetup.SetGUIOpacity(false); if (showAGWindow) AGWindow(); } @@ -284,10 +269,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); @@ -330,33 +315,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), @@ -365,7 +350,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), @@ -377,7 +362,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) @@ -428,12 +413,11 @@ void CommandButton(CommandFunction func, string buttonLabel, ref float buttonLin if (commandSelf) { - List.Enumerator ai = vessel.FindPartModulesImplementing().GetEnumerator(); - while (ai.MoveNext()) - { - func(ai.Current, -1, data); - } - ai.Dispose(); + using (var ai = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ai.MoveNext()) + { + func(ai.Current, -1, data); // Note: this commands *all* AIs on the vessel. + } } } else @@ -467,6 +451,14 @@ public void CommandAllFollow() i++; } } + public int GetFreeWingIndex() + { + RefreshFriendlies(); + int freeIndex = 0; + while (friendlies.Select(f => f.commandFollowIndex).Contains(freeIndex)) + ++freeIndex; + return freeIndex; + } void CommandAG(IBDAIControl wingman, int index, object ag) { @@ -502,7 +494,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; @@ -554,7 +546,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/VesselSpawner.cs b/BDArmory/Control/VesselSpawner.cs deleted file mode 100644 index 605b17c12..000000000 --- a/BDArmory/Control/VesselSpawner.cs +++ /dev/null @@ -1,1723 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using KSP.UI.Screens; -using UnityEngine; -using BDArmory.Core; -using BDArmory.Modules; -using BDArmory.Misc; -using BDArmory.UI; - -namespace BDArmory.Control -{ - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class VesselSpawner : MonoBehaviour - { - public static VesselSpawner Instance; - - // Interesting spawn locations on Kerbin. - public static string spawnLocationsCfg = "GameData/BDArmory/spawn_locations.cfg"; - [VesselSpawnerField] public static List spawnLocations; - - private string message; - void Awake() - { - if (Instance) - Destroy(Instance); - Instance = this; - } - - void Start() - { - VesselSpawnerField.Load(); - spawnLocationCamera = new GameObject("StationaryCameraParent"); - spawnLocationCamera = (GameObject)Instantiate(spawnLocationCamera, Vector3.zero, Quaternion.identity); - spawnLocationCamera.SetActive(false); - } - - void OnDestroy() - { - VesselSpawnerField.Save(); - Destroy(spawnLocationCamera); - } - - private void OnGUI() - { - } - - #region Camera Adjustment - GameObject spawnLocationCamera; - Transform originalCameraParentTransform; - float originalCameraNearClipPlane; - public void ShowSpawnPoint(double latitude, double longitude, double altitude = 0, float distance = 100, bool spawning = false) - { - if (!spawning) - { - FlightGlobals.fetch.SetVesselPosition(FlightGlobals.currentMainBody.flightGlobalsIndex, latitude, longitude, Math.Max(5, altitude), FlightGlobals.ActiveVessel.vesselType == VesselType.Plane ? 0 : 90, 0, true, true); - FlightCamera.fetch.SetDistance(distance); - } - else - { - FlightGlobals.fetch.SetVesselPosition(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.9f ? 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.position = spawnPoint + cameraPosition; - flightCamera.transform.rotation = Quaternion.LookRotation(-flightCamera.transform.position, radialUnitVector); - flightCamera.SetDistance(distance); - } - } - - public void RevertSpawnLocationCamera(bool keepTransformValues = true) - { - if (!spawnLocationCamera.activeSelf) return; - 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 - - public enum SpawnFailureReason { None, NoCraft, NoTerrain, InvalidVessel, VesselLostParts, VesselFailedToSpawn, TimedOut }; - public SpawnFailureReason spawnFailureReason = SpawnFailureReason.None; - public bool vesselsSpawning = false; - public bool vesselSpawnSuccess = false; - public int spawnedVesselCount = 0; - - // Cancel all spawning modes. - public void CancelVesselSpawn() - { - // Single spawn - if (vesselsSpawning) - { - vesselsSpawning = false; - message = "Vessel spawning cancelled."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[VesselSpawner]: " + message); - } - if (spawnAllVesselsOnceCoroutine != null) - { - StopCoroutine(spawnAllVesselsOnceCoroutine); - spawnAllVesselsOnceCoroutine = null; - } - - // Continuous spawn - if (vesselsSpawningContinuously) - { - vesselsSpawningContinuously = false; - if (continuousSpawningScores != null) - DumpContinuousSpawningScores(); - continuousSpawningScores = null; - message = "Continuous vessel spawning cancelled."; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - BDACompetitionMode.Instance.ResetCompetitionScores(); - } - if (spawnVesselsContinuouslyCoroutine != null) - { - StopCoroutine(spawnVesselsContinuouslyCoroutine); - spawnVesselsContinuouslyCoroutine = null; - } - - // Continuous single spawn - if (vesselsSpawningOnceContinuously) - { - vesselsSpawningOnceContinuously = false; - message = "Continuous single spawning cancelled."; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - } - if (spawnAllVesselsOnceContinuouslyCoroutine != null) - { - StopCoroutine(spawnAllVesselsOnceContinuouslyCoroutine); - spawnAllVesselsOnceContinuouslyCoroutine = null; - } - - RevertSpawnLocationCamera(true); - } - - #region Single spawning - public void SpawnAllVesselsOnce(double latitude, double longitude, double altitude = 0, float distance = 10f, bool absDistanceOrFactor = false, float easeInSpeed = 1f, bool killEverythingFirst = true, bool assignTeams = true, string spawnFolder = null, List craftFiles = null) - { - SpawnAllVesselsOnce(new SpawnConfig(latitude, longitude, altitude, distance, absDistanceOrFactor, easeInSpeed, killEverythingFirst, assignTeams, spawnFolder, craftFiles)); - } - - public void SpawnAllVesselsOnce(SpawnConfig spawnConfig) - { - //Reset gravity - if (BDArmorySettings.GRAVITY_HACKS) - { - PhysicsGlobals.GraviticForceMultiplier = 1d; - VehiclePhysics.Gravity.Refresh(); - } - - 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); - RevertSpawnLocationCamera(true); - spawnAllVesselsOnceCoroutine = StartCoroutine(SpawnAllVesselsOnceCoroutine(spawnConfig)); - Debug.Log("[VesselSpawner]: Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m."); - } - - 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(double latitude, double longitude, double altitude, float spawnDistanceFactor, bool absDistanceOrFactor, float easeInSpeed, bool killEverythingFirst = true, bool assignTeams = true, string folder = null, List craftFiles = null) - { - yield return SpawnAllVesselsOnceCoroutine(new SpawnConfig(latitude, longitude, altitude, spawnDistanceFactor, absDistanceOrFactor, easeInSpeed, killEverythingFirst, assignTeams, folder, craftFiles)); - } - private IEnumerator SpawnAllVesselsOnceCoroutine(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(Environment.CurrentDirectory + $"/AutoSpawn/{spawnConfig.folder}").Where(f => f.EndsWith(".craft")).ToList(); - if (spawnConfig.craftFiles.Count == 0) - { - message = "Vessel spawning: found no craft files in " + Environment.CurrentDirectory + $"/AutoSpawn/{spawnConfig.folder}"; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = false; - spawnFailureReason = SpawnFailureReason.NoCraft; - yield break; - } - spawnConfig.craftFiles.Shuffle(); // Randomise the spawn order. - spawnedVesselCount = 0; // Reset our spawned vessel count. - spawnConfig.altitude = Math.Max(2, spawnConfig.altitude); // Don't spawn too low. - 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("[VesselSpawner]: " + 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.ResetCompetitionScores(); // Reset competition scores. - BDACompetitionMode.Instance.RemoveDebrisNow(); // Remove debris and space junk. - } - yield return new WaitForFixedUpdate(); - #endregion - - #region Pre-spawning - if (spawnConfig.killEverythingFirst) - { - // Kill all vessels (including debris). - var vesselsToKill = FlightGlobals.Vessels.Where(v => v.vesselType != VesselType.SpaceObject).ToList(); - foreach (var vessel in vesselsToKill) - RemoveVessel(vessel); - } - while (removeVesselsPending > 0) - yield return new 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). - ShowSpawnPoint(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."; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - var startTime = Planetarium.GetUniversalTime(); - double lastStableTimeStart = startTime; - double stableTime = 0; - do - { - lastTerrainDistance = terrainDistance; - yield return new WaitForFixedUpdate(); - terrainDistance = Physics.Raycast(ray, out hit, 2f * (float)(spawnConfig.altitude + distanceToCoMainBody), 1 << 15) ? 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!"; - Debug.Log("[VesselSpawner]: " + 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("[VesselSpawner]: Spawning vessels..."); - var spawnedVessels = new Dictionary>(); - Vector3d craftGeoCoords; - Vector3 craftSpawnPosition; - var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.9f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. - string failedVessels = ""; - var shipFacility = EditorFacility.None; - 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 = SpawnVesselFromCraftFile(craftUrl, craftGeoCoords, 0, 0f, out shipFacility); // SPAWN - } - catch { vessel = null; } - if (vessel == null) - { - var craftName = craftUrl.Substring((Environment.CurrentDirectory + $"/AutoSpawn/{spawnConfig.folder}").Length); - Debug.Log("[VesselSpawner]: 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.GetName())) - vessel.vesselName += "_" + spawnedVesselCount; - spawnedVessels.Add(vessel.GetName(), 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; - } - if (failedVessels != "") - { - message += "Some vessels failed to spawn: " + failedVessels; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[VesselSpawner]: " + message); - } - - // Wait for an update so that the vessels' parts list gets updated. - yield return new 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 new WaitForFixedUpdate(); - - // Now rotate them and put them at the right altitude. - var finalSpawnPositions = new Dictionary(); - var finalSpawnRotations = new Dictionary(); - foreach (var vesselName in spawnedVessels.Keys) - { - var vessel = spawnedVessels[vesselName].Item1; - 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 (Physics.Raycast(ray, out hit, (float)(spawnConfig.altitude + distanceToCoMainBody), 1 << 15)) - { - distance = hit.distance; - localSurfaceNormal = hit.normal; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[VesselSpawner]: found terrain for spawn adjustments"); - } - else - { - distance = FlightGlobals.getAltitudeAtPos(craftSpawnPosition) - (float)terrainAltitude; // If the raycast fails, use the value from FlightGlobals and terrainAltitude of the original spawn point. - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[VesselSpawner]: failed to find terrain for spawn adjustments"); - } - - // Fix control point orientation by setting the reference transformation to that of the root part. - spawnedVessels[vesselName].Item1.SetReferenceTransform(spawnedVessels[vesselName].Item1.rootPart); - - if (!spawnAirborne) - { - 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. - { - finalSpawnPositions[vesselName] += (float)distanceUnderWater * localRadialUnitVector; - if (!spawnAirborne) - vessel.Splashed = true; // Set the vessel as splashed. - } - } - vessel.SetPosition(finalSpawnPositions[vesselName]); - Debug.Log("[VesselSpawner]: Vessel " + vessel.vesselName + " spawned!"); - } - #endregion - - #region Post-spawning - yield return new WaitForFixedUpdate(); - // Revert the camera and focus on one as it lowers to the terrain. - RevertSpawnLocationCamera(true); - if (FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD) - { - 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 new 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("[VesselSpawner]: Invalid vessels: " + string.Join(", ", invalidVessels.Select(t => t.Item1 + ":" + t.Item2))); - spawnFailureReason = SpawnFailureReason.InvalidVessel; - } - else - { - do - { - yield return new WaitForFixedUpdate(); - - // 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?.vesselName)); - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[VesselSpawner]: " + 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 = vessel.FindPartModuleImplementing(); - if (weaponManager != null && weaponManagers.Contains(weaponManager)) // The weapon manager has been added, let's go! - { - // Turn on the brakes. - spawnedVessels[vessel.GetName()].Item1.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); // Disable them first to make sure they trigger on toggling. - spawnedVessels[vessel.GetName()].Item1.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - - vesselsToCheck.Remove(vessel); - } - } - if (vesselsToCheck.Count == 0) - allWeaponManagersAssigned = true; - - if (allWeaponManagersAssigned) - 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 be 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) - { - spawnedVessels[vesselName].Item1.SetPosition(finalSpawnPositions[vesselName]); - spawnedVessels[vesselName].Item1.SetRotation(finalSpawnRotations[vesselName]); - } - - if (allWeaponManagersAssigned) - { - if (!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 new WaitForFixedUpdate(); - foreach (var vesselName in spawnedVessels.Keys) - { - if (vesselsHaveLanded[vesselName] == 0 && Vector3.Dot(spawnedVessels[vesselName].Item1.srf_velocity, radialUnitVector) < 0) // Check that vessel has started moving. - vesselsHaveLanded[vesselName] = 1; - if (vesselsHaveLanded[vesselName] == 1 && Vector3.Dot(spawnedVessels[vesselName].Item1.srf_velocity, radialUnitVector) >= 0) // Check if the vessel has landed. - { - vesselsHaveLanded[vesselName] = 2; - spawnedVessels[vesselName].Item1.Landed = true; // Tell KSP that the vessel is landed. - } - if (vesselsHaveLanded[vesselName] == 1 && spawnedVessels[vesselName].Item1.srf_velocity.sqrMagnitude > spawnConfig.easeInSpeed) // While the vessel hasn't landed, prevent it from moving too fast. - spawnedVessels[vesselName].Item1.SetWorldVelocity(0.99 * spawnConfig.easeInSpeed * spawnedVessels[vesselName].Item1.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?.vesselName)); - 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 < 10 + spawnConfig.altitude / spawnConfig.easeInSpeed); // Give the vessels up to (10 + 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?.vesselName)); - BDACompetitionMode.Instance.competitionStatus.Add(message); - spawnFailureReason = SpawnFailureReason.VesselLostParts; - } - else - { - foreach (var vessel in spawnedVessels.Select(v => v.Value.Item1)) - { - var weaponManager = vessel.FindPartModuleImplementing(); - 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 (!vessel.FindPartModulesImplementing().Any(engine => engine.EngineIgnited)) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. - { - Debug.Log("[VesselSpawner]: " + vessel.GetName() + " didn't activate engines on AG10! Activating ALL their engines."); - foreach (var engine in vessel.FindPartModulesImplementing()) - engine.Activate(); - } - } - - 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 - { - if (spawnConfig.assignTeams) - { - // Assign the vessels to their own teams. - Debug.Log("[VesselSpawner]: Assigning each vessel to its own team."); - LoadedVesselSwitcher.Instance.MassTeamSwitch(true); - yield return new WaitForFixedUpdate(); - } - } - #endregion - - Debug.Log("[VesselSpawner]: Vessel spawning " + (vesselSpawnSuccess ? "SUCCEEDED!" : "FAILED! " + spawnFailureReason)); - vesselsSpawning = false; - } - - private bool vesselsSpawningOnceContinuously = false; - public Coroutine spawnAllVesselsOnceContinuouslyCoroutine = null; - - public void SpawnAllVesselsOnceContinuously(double latitude, double longitude, double altitude = 0, float distance = 10f, bool absDistanceOrFactor = false, float easeInSpeed = 1f, bool killEverythingFirst = true, string spawnFolder = null, List craftFiles = null) - { - SpawnAllVesselsOnceContinuously(new SpawnConfig(latitude, longitude, altitude, distance, absDistanceOrFactor, easeInSpeed, killEverythingFirst, true, spawnFolder, craftFiles)); - } - public void SpawnAllVesselsOnceContinuously(SpawnConfig spawnConfig) - { - vesselsSpawningOnceContinuously = true; - if (spawnAllVesselsOnceContinuouslyCoroutine != null) - StopCoroutine(spawnAllVesselsOnceContinuouslyCoroutine); - spawnAllVesselsOnceContinuouslyCoroutine = StartCoroutine(SpawnAllVesselsOnceContinuouslyCoroutine(spawnConfig)); - Debug.Log("[VesselSpawner]: 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 new WaitForFixedUpdate(); - if (!vesselSpawnSuccess) - { - vesselsSpawningOnceContinuously = false; - yield break; - } - yield return new WaitForFixedUpdate(); - - // NOTE: runs in separate coroutine - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - 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; - message = "Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - while (BDACompetitionMode.Instance.competitionStarting) - yield return new 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("[VesselSpawner]: " + 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) < 10d) - { - BDACompetitionMode.Instance.competitionStatus.Add("Waiting " + (10d - (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. - } - #endregion - - #region Continuous spawning - public bool vesselsSpawningContinuously = false; - int continuousSpawnedVesselCount = 0; - public void SpawnVesselsContinuously(double latitude, double longitude, double altitude = 1000, float spawnDistanceFactor = 20f, bool absDistanceOrFactor = false, bool killEverythingFirst = true, string spawnFolder = null) - { - //Reset gravity - if (BDArmorySettings.GRAVITY_HACKS) - { - PhysicsGlobals.GraviticForceMultiplier = 1d; - VehiclePhysics.Gravity.Refresh(); - } - - vesselsSpawningContinuously = true; - spawnFailureReason = SpawnFailureReason.None; - continuousSpawningScores = new Dictionary(); - if (spawnVesselsContinuouslyCoroutine != null) - StopCoroutine(spawnVesselsContinuouslyCoroutine); - RevertSpawnLocationCamera(true); - spawnVesselsContinuouslyCoroutine = StartCoroutine(SpawnVesselsContinuouslyCoroutine(latitude, longitude, altitude, spawnDistanceFactor, absDistanceOrFactor, killEverythingFirst, spawnFolder)); - Debug.Log("[VesselSpawner]: Triggering continuous vessel spawning at " + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.ToString("G6") + " at altitude " + altitude + "m."); - } - 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(double latitude, double longitude, double altitude, float distance, bool absDistanceOrFactor, bool killEverythingFirst, string spawnFolder = null) - { - yield return SpawnVesselsContinuouslyCoroutine(new SpawnConfig(latitude, longitude, altitude, distance, absDistanceOrFactor, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, killEverythingFirst, true, spawnFolder)); - } - 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(Environment.CurrentDirectory + $"/AutoSpawn/{spawnConfig.folder}").Where(f => f.EndsWith(".craft")).ToList(); - if (spawnConfig.craftFiles.Count == 0) - { - message = "Vessel spawning: found no craft files in " + Environment.CurrentDirectory + $"/AutoSpawn/{spawnConfig.folder}"; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = 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("[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.ResetCompetitionScores(); // Reset competition scores. - } - vesselsToActivate.Clear(); // Clear any pending vessel activations. - yield return new WaitForFixedUpdate(); - #endregion - - #region Pre-spawning - if (spawnConfig.killEverythingFirst) - { - // Kill all vessels (including debris). - var vesselsToKill = FlightGlobals.Vessels.Where(v => v.vesselType != VesselType.SpaceObject).ToList(); - foreach (var vessel in vesselsToKill) - RemoveVessel(vessel); - } - while (removeVesselsPending > 0) - yield return new 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; - 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). - ShowSpawnPoint(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."; - Debug.Log("[VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - var startTime = Planetarium.GetUniversalTime(); - double lastStableTimeStart = startTime; - double stableTime = 0; - do - { - lastTerrainDistance = terrainDistance; - yield return new WaitForFixedUpdate(); - terrainDistance = Physics.Raycast(ray, out hit, 2f * (float)(spawnConfig.altitude + distanceToCoMainBody), 1 << 15) ? 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 activeWeaponManagersByCraftURL = new Dictionary(); - var invalidVesselCount = new Dictionary(); - Vector3d craftGeoCoords; - Vector3 craftSpawnPosition; - var shipFacility = EditorFacility.None; - var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.9f ? 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(); - var duplicateCraftCounter = 0; - 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) || (activeWeaponManagersByCraftURL.ContainsKey(craftURL) && (activeWeaponManagersByCraftURL[craftURL] == null || activeWeaponManagersByCraftURL[craftURL].vessel == null))))) - { - spawnQueue.Enqueue(craftURL); - ++spawnCounts[craftURL]; - } - 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("[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 => v.FindPartModuleImplementing() != null).Select(v => v.GetName()).ToList().Contains(craftURLToVesselName[craftURL])).ToList(); - if (missing.Count > 0) - { - Debug.Log("[VesselSpawner]: MISSING vessels: " + string.Join(", ", craftURLToVesselName.Where(c => missing.Contains(c.Key)).Select(c => c.Value))); - Debug.Log("[VesselSpawner]: MISSING active: " + string.Join(", ", activeWeaponManagersByCraftURL.Where(c => c.Value != null).Select(c => c.Value.vessel.vesselName + ":" + c.Value.vessel.vesselType + ":" + BDACompetitionMode.Instance.IsValidVessel(c.Value.vessel)))); - } - } - if (craftToSpawn.Count > 0) - { - // Spawn the craft in a downward facing ring. - string failedVessels = ""; - foreach (var craftURL in craftToSpawn) - { - if (activeWeaponManagersByCraftURL.ContainsKey(craftURL)) - activeWeaponManagersByCraftURL.Remove(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 = SpawnVesselFromCraftFile(craftURL, craftGeoCoords, 0, 0f, out shipFacility); // SPAWN - } - catch { vessel = null; } - if (vessel == null) - { - var craftName = craftURL.Substring((Environment.CurrentDirectory + $"/AutoSpawn/{spawnConfig.folder}").Length); - Debug.Log("[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.GetName())) // Avoid duplicate names. - vessel.vesselName += "_" + (++duplicateCraftCounter); - craftURLToVesselName.Add(craftURL, vessel.GetName()); // 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.ContainsKey(vessel.vesselName)) - { - BDACompetitionMode.Instance.Scores[vessel.vesselName] = new ScoringData { vesselRef = vessel, lastFiredTime = Planetarium.GetUniversalTime(), previousPartCount = vessel.parts.Count }; // Note: we can't assign the weaponManagerRef yet as it may not be updated. - if (!BDACompetitionMode.Instance.DeathOrder.ContainsKey(vessel.vesselName)) // Temporarily add the vessel to the DeathOrder to prevent it from being detected as newly dead until it's finished spawning. - BDACompetitionMode.Instance.DeathOrder.Add(vessel.vesselName, new Tuple(BDACompetitionMode.Instance.DeathOrder.Count, 0)); - } - if (!vesselsToActivate.Contains(vessel)) - vesselsToActivate.Add(vessel); - if (!continuousSpawningScores.ContainsKey(vessel.GetName())) - continuousSpawningScores.Add(vessel.GetName(), new ContinuousSpawningScores()); - continuousSpawningScores[vessel.GetName()].vessel = vessel; // Update some values in the scoring structure. - continuousSpawningScores[vessel.GetName()].outOfAmmoTime = 0; - ++continuousSpawnedVesselCount; - continuousSpawnedVesselCount %= spawnSlots.Count; - Debug.Log("[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("[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 new WaitForFixedUpdate(); - yield return new 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.ReferenceTransform == null || vessel.rootPart?.GetReferenceTransform() == null) && ++count < 5) yield return new WaitForFixedUpdate(); - if (vessel == null) continue; // In case the vessel got destroyed in the mean time. - vessel.SetReferenceTransform(vessel.rootPart); - 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. - } - } - // 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 new WaitForFixedUpdate(); - - LoadedVesselSwitcher.Instance.UpdateList(); - 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) - { - // 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.GetName()]; - 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."; - if (activeWeaponManagersByCraftURL.ContainsKey(craftURL)) activeWeaponManagersByCraftURL.Remove(craftURL); // Shouldn't occur, but just to be sure. - activeWeaponManagersByCraftURL.Add(craftURL, null); // Indicate to the spawning routine that the craft is effectively dead. - 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("[VesselSpawner]: " + message); - vesselsToActivate.Remove(vessel); - RemoveVessel(vessel); // Remove the vessel - } - continue; - } - - // Check if the weapon manager has been added to the weapon managers list. - var weaponManager = vessel.FindPartModuleImplementing(); - 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 (!vessel.FindPartModulesImplementing().Any(engine => engine.EngineIgnited)) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. - { - Debug.Log("[VesselSpawner]: " + vessel.GetName() + " didn't activate engines on AG10! Activating ALL their engines."); - foreach (var engine in vessel.FindPartModulesImplementing()) - engine.Activate(); - } - // 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())); - var craftURL = craftURLToVesselName.ToDictionary(i => i.Value, i => i.Key)[vessel.GetName()]; - if (activeWeaponManagersByCraftURL.ContainsKey(craftURL)) activeWeaponManagersByCraftURL.Remove(craftURL); // Shouldn't occur, but just to be sure. - activeWeaponManagersByCraftURL.Add(craftURL, weaponManager); - // Enable guard mode if a competition is active. - if (BDACompetitionMode.Instance.competitionIsActive) - if (!weaponManager.guardMode) - weaponManager.ToggleGuardMode(); - weaponManager.AI.ReleaseCommand(); - vessel.altimeterDisplayState = AltimeterDisplayState.AGL; - if (BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN && BDACompetitionMode.Instance.competitionIsActive) - DumpContinuousSpawningScores(); - // Adjust BDACompetitionMode's scoring structures. - UpdateCompetitionScores(vessel, true); - ++continuousSpawningScores[vessel.GetName()].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) - { - if (!BDACompetitionMode.Instance.rammingInformation.ContainsKey(vessel.GetName())) // Vessel information hasn't been added to rammingInformation datastructure yet. - { - BDACompetitionMode.Instance.rammingInformation.Add(vessel.GetName(), new BDACompetitionMode.RammingInformation { vesselName = vessel.GetName(), targetInformation = new Dictionary() }); - foreach (var otherVesselName in BDACompetitionMode.Instance.rammingInformation.Keys) - { - if (otherVesselName == vessel.GetName()) continue; - BDACompetitionMode.Instance.rammingInformation[vessel.GetName()].targetInformation.Add(otherVesselName, new BDACompetitionMode.RammingTargetInformation { vessel = BDACompetitionMode.Instance.rammingInformation[otherVesselName].vessel }); - } - } - BDACompetitionMode.Instance.rammingInformation[vessel.GetName()].vessel = vessel; - BDACompetitionMode.Instance.rammingInformation[vessel.GetName()].partCount = vessel.parts.Count; - BDACompetitionMode.Instance.rammingInformation[vessel.GetName()].radius = BDACompetitionMode.GetRadius(vessel); - foreach (var otherVesselName in BDACompetitionMode.Instance.rammingInformation.Keys) - { - if (otherVesselName == vessel.GetName()) continue; - BDACompetitionMode.Instance.rammingInformation[otherVesselName].targetInformation[vessel.GetName()] = new BDACompetitionMode.RammingTargetInformation { vessel = vessel }; - } - } - vesselsToActivate.Remove(vessel); - 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); - } - } - } - } - - // 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 (removeVesselsPending > 0) - yield return new 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 new WaitForFixedUpdate(); - } - else - { - yield return new WaitForSeconds(1); // 1s between checks. Nothing much happens if nothing needs spawning. - } - } - #endregion - vesselsSpawningContinuously = false; - Debug.Log("[VesselSpawner]: Continuous vessel spawning ended."); - } - - // 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 List deathTimes = new List(); - public Dictionary scoreData = new Dictionary(); - public Dictionary cleanKilledBy = new Dictionary(); - public Dictionary cleanRammedBy = new Dictionary(); - public Dictionary cleanMissileKilledBy = new Dictionary(); - public double cumulativeTagTime = 0; - public int cumulativeHits = 0; - public int cumulativeDamagedPartsDueToRamming = 0; - public int cumulativeDamagedPartsDueToMissiles = 0; - }; - public Dictionary continuousSpawningScores; - public void UpdateCompetitionScores(Vessel vessel, bool newSpawn = false) - { - var vesselName = vessel.GetName(); - 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 (newSpawn && BDACompetitionMode.Instance.DeathOrder.ContainsKey(vesselName)) - { - continuousSpawningScores[vesselName].deathTimes.Add(BDACompetitionMode.Instance.DeathOrder[vesselName].Item2); - BDACompetitionMode.Instance.DeathOrder.Remove(vesselName); - } - if (BDACompetitionMode.Instance.Scores.ContainsKey(vesselName)) - { - scoreData[spawnCount] = BDACompetitionMode.Instance.Scores[vesselName]; // Save the Score instance for the vessel. - if (newSpawn) - { - BDACompetitionMode.Instance.Scores[vesselName] = new ScoringData { vesselRef = vessel, weaponManagerRef = vessel.FindPartModuleImplementing(), lastFiredTime = Planetarium.GetUniversalTime(), previousPartCount = vessel.parts.Count(), tagIsIt = scoreData[spawnCount].tagIsIt }; - continuousSpawningScores[vesselName].cumulativeTagTime = scoreData.Sum(kvp => kvp.Value.tagTotalTime); - continuousSpawningScores[vesselName].cumulativeHits = scoreData.Sum(kvp => kvp.Value.Score); - continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToRamming = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToRamming); - continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToMissiles = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToMissiles); - // Re-insert some information needed for Tag. - switch (scoreData[spawnCount].LastDamageWasFrom()) - { - case DamageFrom.Bullet: - BDACompetitionMode.Instance.Scores[vesselName].lastHitTime = scoreData[spawnCount].lastHitTime; - BDACompetitionMode.Instance.Scores[vesselName].lastPersonWhoHitMe = scoreData[spawnCount].lastPersonWhoHitMe; - break; - case DamageFrom.Ram: - BDACompetitionMode.Instance.Scores[vesselName].lastRammedTime = scoreData[spawnCount].lastRammedTime; - BDACompetitionMode.Instance.Scores[vesselName].lastPersonWhoRammedMe = scoreData[spawnCount].lastPersonWhoRammedMe; - break; - case DamageFrom.Missile: - BDACompetitionMode.Instance.Scores[vesselName].lastMissileHitTime = scoreData[spawnCount].lastMissileHitTime; - BDACompetitionMode.Instance.Scores[vesselName].lastPersonWhoHitMeWithAMissile = scoreData[spawnCount].lastPersonWhoHitMeWithAMissile; - break; - default: - break; - } - } - } - if (BDACompetitionMode.Instance.whoCleanShotWho.ContainsKey(vesselName)) - { - continuousSpawningScores[vesselName].cleanKilledBy[spawnCount] = BDACompetitionMode.Instance.whoCleanShotWho[vesselName]; - if (newSpawn) BDACompetitionMode.Instance.whoCleanShotWho.Remove(vesselName); - } - if (BDACompetitionMode.Instance.whoCleanRammedWho.ContainsKey(vesselName)) - { - continuousSpawningScores[vesselName].cleanRammedBy[spawnCount] = BDACompetitionMode.Instance.whoCleanRammedWho[vesselName]; - if (newSpawn) BDACompetitionMode.Instance.whoCleanRammedWho.Remove(vesselName); - } - if (BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles.ContainsKey(vesselName)) - { - continuousSpawningScores[vesselName].cleanMissileKilledBy[spawnCount] = BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles[vesselName]; - if (newSpawn) BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles.Remove(vesselName); - } - } - - 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("[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("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: Name:" + vesselName); - logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: DEATHCOUNT:" + (vesselScore.spawnCount - 1 + (vesselsToActivate.Contains(vesselScore.vessel) || !LoadedVesselSwitcher.Instance.weaponManagers.SelectMany(teamManager => teamManager.Value, (teamManager, weaponManager) => weaponManager.vessel).Contains(vesselScore.vessel) ? 1 : 0))); // Account for vessels that haven't respawned yet. - if (vesselScore.deathTimes.Count > 0) logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: DEATHTIMES:" + string.Join(",", vesselScore.deathTimes.Select(d => d.ToString("0.0")))); - 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("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSHOTME:" + whoShotMeScores); - var whoDamagedMeWithBulletsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromBullets.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromBullets.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); - if (whoDamagedMeWithBulletsScores != "") logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHBULLETS:" + whoDamagedMeWithBulletsScores); - 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("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHORAMMEDME:" + whoRammedMeScores); - var whoShotMeWithMissilesScores = 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 (whoShotMeWithMissilesScores != "") logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSHOTMEWITHMISSILES:" + whoShotMeWithMissilesScores); - 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("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHMISSILES:" + whoDamagedMeWithMissilesScores); - var otherKills = string.Join(", ", scoreData.Where(kvp => kvp.Value.gmKillReason != GMKillReason.None).Select(kvp => kvp.Key + ":" + kvp.Value.gmKillReason)); - if (otherKills != "") logStrings.Add("[esselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: OTHERKILL:" + otherKills); - if (vesselScore.cleanKilledBy.Count > 0) logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANKILL:" + string.Join(", ", vesselScore.cleanKilledBy.Select(kvp => kvp.Key + ":" + kvp.Value))); - if (vesselScore.cleanRammedBy.Count > 0) logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANRAM:" + string.Join(", ", vesselScore.cleanRammedBy.Select(kvp => kvp.Key + ":" + kvp.Value))); - if (vesselScore.cleanMissileKilledBy.Count > 0) logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANMISSILEKILL:" + string.Join(", ", vesselScore.cleanMissileKilledBy.Select(kvp => kvp.Key + ":" + kvp.Value))); - if (scoreData.Sum(kvp => kvp.Value.shotsFired) > 0) logStrings.Add("[VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: ACCURACY:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.shotsFired > 0).Select(kvp => kvp.Key + ":" + kvp.Value.Score + "/" + kvp.Value.shotsFired))); - if (BDArmorySettings.TAG_MODE) - { - if (scoreData.Sum(kvp => kvp.Value.tagScore) > 0) logStrings.Add("[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("[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("[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("[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 = Environment.CurrentDirectory + "/GameData/BDArmory/Logs"; - if (!Directory.Exists(folder)) - Directory.CreateDirectory(folder); - File.WriteAllLines(Path.Combine(folder, BDACompetitionMode.Instance.CompetitionID.ToString() + (tag != "" ? "-" + tag : "") + ".log"), logStrings); - } - // Also dump the results to the normal log. - foreach (var line in logStrings) - Debug.Log(line); - } - #endregion - - [Serializable] - public class SpawnConfig - { - public SpawnConfig(double latitude, double longitude, double altitude, float distance, bool absDistanceOrFactor, float easeInSpeed = 1f, bool killEverythingFirst = true, bool assignTeams = true, string folder = null, List craftFiles = null) - { - 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.folder = folder; - this.craftFiles = craftFiles; - } - public SpawnConfig(SpawnConfig other) - { - 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.folder = other.folder; - this.craftFiles = other.craftFiles?.ToList(); - } - 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 string folder = null; - public List craftFiles = null; - } - #region Team Spawning - 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. - 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.latitude, spawnConfig.longitude, spawnConfig.altitude, spawnConfig.distance, spawnConfig.absDistanceOrFactor, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, killAllFirst, false, spawnConfig.folder, spawnConfig.craftFiles); - if (!vesselSpawnSuccess) - { - message = "Vessel spawning failed, aborting."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[VesselSpawner]: " + 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 new WaitForFixedUpdate(); - LoadedVesselSwitcher.Instance.MassTeamSwitch(false, spawnCounts); // Assign teams. - 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 new WaitForFixedUpdate(); - } - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - if (startCompetitionNow) - { - yield return new WaitForFixedUpdate(); - BDACompetitionMode.Instance.StartCompetitionNow(); - } - } - } - #endregion - - // Stagger the spawn slots to avoid consecutive craft being launched too close together. - private static 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; - } - - private 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; - ++removeVesselsPending; - StartCoroutine(RemoveVesselCoroutine(vessel)); - } - private IEnumerator RemoveVesselCoroutine(Vessel vessel) - { - vessel.Die(); // Kill the vessel - yield return new 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 new WaitForFixedUpdate(); - --removeVesselsPending; - } - - 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 = vessel.FindPartModuleImplementing(); - 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("[VesselSpawner]: " + m); - if (BDACompetitionMode.Instance.Scores.ContainsKey(vesselName)) - { - BDACompetitionMode.Instance.Scores[vesselName].gmKillReason = GMKillReason.OutOfAmmo; // Indicate that it was us who killed it and remove any "clean" kills. - if (BDACompetitionMode.Instance.whoCleanShotWho.ContainsKey(vesselName)) BDACompetitionMode.Instance.whoCleanShotWho.Remove(vesselName); - if (BDACompetitionMode.Instance.whoCleanRammedWho.ContainsKey(vesselName)) BDACompetitionMode.Instance.whoCleanRammedWho.Remove(vesselName); - if (BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles.ContainsKey(vesselName)) BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles.Remove(vesselName); - } - RemoveVessel(vessel); - } - } - } - - #region Actual spawning of individual craft - // THE FOLLOWING STOLEN FROM VESSEL MOVER via BenBenWilde's autospawn (and tweaked slightly) - private Vessel SpawnVesselFromCraftFile(string craftURL, Vector3d gpsCoords, float heading, float pitch, out EditorFacility shipFacility, List crewData = null) - { - VesselData newData = new VesselData(); - - newData.craftURL = craftURL; - newData.latitude = gpsCoords.x; - newData.longitude = gpsCoords.y; - newData.altitude = gpsCoords.z; - - newData.body = FlightGlobals.currentMainBody; - newData.heading = heading; - newData.pitch = pitch; - newData.orbiting = false; - newData.flagURL = HighLogic.CurrentGame.flagURL; - newData.owned = true; - newData.vesselType = VesselType.Ship; - - newData.crew = new List(); - - return SpawnVessel(newData, out shipFacility, crewData); - } - - private Vessel SpawnVessel(VesselData vesselData, out EditorFacility shipFacility, List crewData = null) - { - shipFacility = EditorFacility.None; - //Set additional info for landed vessels - bool landed = false; - if (!vesselData.orbiting) - { - landed = true; - if (vesselData.altitude == null || vesselData.altitude < 0) - { - vesselData.altitude = 35; - } - - Vector3d pos = vesselData.body.GetRelSurfacePosition(vesselData.latitude, vesselData.longitude, vesselData.altitude.Value); - - vesselData.orbit = new Orbit(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, vesselData.body); - vesselData.orbit.UpdateFromStateVectors(pos, vesselData.body.getRFrmVel(pos), vesselData.body, Planetarium.GetUniversalTime()); - } - else - { - vesselData.orbit.referenceBody = vesselData.body; - } - - ConfigNode[] partNodes; - ShipConstruct shipConstruct = null; - if (!string.IsNullOrEmpty(vesselData.craftURL)) - { - var craftNode = ConfigNode.Load(vesselData.craftURL); - shipConstruct = new ShipConstruct(); - if (!shipConstruct.LoadShip(craftNode)) - { - Debug.LogError("Ship file error!"); - return null; - } - - // Set the name - if (string.IsNullOrEmpty(vesselData.name)) - { - vesselData.name = shipConstruct.shipName; - } - - // Set some parameters that need to be at the part level - uint missionID = (uint)Guid.NewGuid().GetHashCode(); - uint launchID = HighLogic.CurrentGame.launchID++; - foreach (Part p in shipConstruct.parts) - { - p.flightID = ShipConstruction.GetUniqueFlightID(HighLogic.CurrentGame.flightState); - p.missionID = missionID; - p.launchID = launchID; - p.flagURL = vesselData.flagURL ?? HighLogic.CurrentGame.flagURL; - - // Had some issues with this being set to -1 for some ships - can't figure out - // why. End result is the vessel exploding, so let's just set it to a positive - // value. - p.temperature = 1.0; - } - - //add minimal crew - //bool success = false; - Part part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity); - - // Add the crew member - if (part != null) - { - // Create the ProtoCrewMember - ProtoCrewMember crewMember = HighLogic.CurrentGame.CrewRoster.GetNextOrNewKerbal(ProtoCrewMember.KerbalType.Crew); - crewMember.gender = UnityEngine.Random.Range(0, 100) > 50 - ? ProtoCrewMember.Gender.Female - : ProtoCrewMember.Gender.Male; - 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); - } - - // 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 - // slightly different format than those required for a ProtoVessel (seriously - // Squad?!?). - ConfigNode empty = new ConfigNode(); - ProtoVessel dummyProto = new ProtoVessel(empty, null); - Vessel dummyVessel = new Vessel(); - dummyVessel.parts = shipConstruct.Parts; - dummyProto.vesselRef = dummyVessel; - - // Create the ProtoPartSnapshot objects and then initialize them - foreach (Part p in shipConstruct.parts) - { - dummyVessel.loaded = false; - p.vessel = dummyVessel; - - dummyProto.protoPartSnapshots.Add(new ProtoPartSnapshot(p, dummyProto, true)); - } - foreach (ProtoPartSnapshot p in dummyProto.protoPartSnapshots) - { - p.storePartRefs(); - } - - // Create the ship's parts - List partNodesL = new List(); - foreach (ProtoPartSnapshot snapShot in dummyProto.protoPartSnapshots) - { - ConfigNode node = new ConfigNode("PART"); - snapShot.Save(node); - partNodesL.Add(node); - } - partNodes = partNodesL.ToArray(); - } - else - { - // Create crew member array - ProtoCrewMember[] crewArray = new ProtoCrewMember[vesselData.crew.Count]; - int i = 0; - foreach (CrewData cd in vesselData.crew) - { - // Create the ProtoCrewMember - ProtoCrewMember crewMember = HighLogic.CurrentGame.CrewRoster.GetNextOrNewKerbal(ProtoCrewMember.KerbalType.Crew); - if (cd.name != null) - { - crewMember.KerbalRef.name = cd.name; - } - 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). - - crewArray[i++] = crewMember; - } - - // Create part nodes - uint flightId = ShipConstruction.GetUniqueFlightID(HighLogic.CurrentGame.flightState); - partNodes = new ConfigNode[1]; - partNodes[0] = ProtoVessel.CreatePartNode(vesselData.craftPart.name, flightId, crewArray); - - // Default the size class - //sizeClass = UntrackedObjectClass.A; - - // Set the name - if (string.IsNullOrEmpty(vesselData.name)) - { - vesselData.name = vesselData.craftPart.name; - } - } - - // Create additional nodes - ConfigNode[] additionalNodes = new ConfigNode[0]; - - // Create the config node representation of the ProtoVessel - ConfigNode protoVesselNode = ProtoVessel.CreateVesselNode(vesselData.name, vesselData.vesselType, vesselData.orbit, 0, partNodes, additionalNodes); - - // Additional settings for a landed vessel - if (!vesselData.orbiting) - { - Vector3d norm = vesselData.body.GetRelSurfaceNVector(vesselData.latitude, vesselData.longitude); - - bool splashed = false;// = landed && terrainHeight < 0.001; - - // Create the config node representation of the ProtoVessel - // Note - flying is experimental, and so far doesn't work - protoVesselNode.SetValue("sit", (splashed ? Vessel.Situations.SPLASHED : landed ? - Vessel.Situations.LANDED : Vessel.Situations.FLYING).ToString()); - protoVesselNode.SetValue("landed", (landed && !splashed).ToString()); - protoVesselNode.SetValue("splashed", splashed.ToString()); - protoVesselNode.SetValue("lat", vesselData.latitude.ToString()); - protoVesselNode.SetValue("lon", vesselData.longitude.ToString()); - protoVesselNode.SetValue("alt", vesselData.altitude.ToString()); - protoVesselNode.SetValue("landedAt", vesselData.body.name); - - // Figure out the additional height to subtract - float lowest = float.MaxValue; - if (shipConstruct != null) - { - foreach (Part p in shipConstruct.parts) - { - foreach (Collider collider in p.GetComponentsInChildren()) - { - if (collider.gameObject.layer != 21 && collider.enabled) - { - lowest = Mathf.Min(lowest, collider.bounds.min.y); - } - } - } - } - else - { - foreach (Collider collider in vesselData.craftPart.partPrefab.GetComponentsInChildren()) - { - if (collider.gameObject.layer != 21 && collider.enabled) - { - lowest = Mathf.Min(lowest, collider.bounds.min.y); - } - } - } - - if (lowest == float.MaxValue) - { - lowest = 0; - } - - // 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 rotation = Quaternion.identity; - float heading = vesselData.heading; - if (shipConstruct == null) - { - rotation = rotation * Quaternion.FromToRotation(Vector3.up, Vector3.back); - } - else if (shipConstruct.shipFacility == EditorFacility.SPH) - { - rotation = rotation * Quaternion.FromToRotation(Vector3.forward, -Vector3.forward); - heading += 180.0f; - } - else - { - rotation = rotation * Quaternion.FromToRotation(Vector3.up, Vector3.forward); - rotation = Quaternion.FromToRotation(Vector3.up, -Vector3.up) * rotation; - - vesselData.heading = 0; - vesselData.pitch = 0; - } - - rotation = rotation * Quaternion.AngleAxis(heading, Vector3.back); - rotation = rotation * Quaternion.AngleAxis(vesselData.roll, Vector3.down); - rotation = rotation * Quaternion.AngleAxis(vesselData.pitch, Vector3.left); - - // Set the height and rotation - if (landed || splashed) - { - float hgt = (shipConstruct != null ? shipConstruct.parts[0] : vesselData.craftPart.partPrefab).localRoot.attPos0.y - lowest; - hgt += vesselData.height + 35; - protoVesselNode.SetValue("hgt", hgt.ToString(), true); - } - protoVesselNode.SetValue("rot", KSPUtil.WriteQuaternion(normal * rotation), true); - - // Set the normal vector relative to the surface - Vector3 nrm = (rotation * Vector3.forward); - protoVesselNode.SetValue("nrm", nrm.x + "," + nrm.y + "," + nrm.z, true); - protoVesselNode.SetValue("prst", false.ToString(), true); - } - - // Add vessel to the game - ProtoVessel protoVessel = HighLogic.CurrentGame.AddVessel(protoVesselNode); - - // Set the vessel size (FIXME various other vessel fields appear to not be set, e.g. CoM) - protoVessel.vesselRef.vesselSize = shipConstruct.shipSize; - shipFacility = shipConstruct.shipFacility; - switch (shipFacility) - { - case EditorFacility.SPH: - protoVessel.vesselRef.vesselType = VesselType.Plane; - break; - case EditorFacility.VAB: - protoVessel.vesselRef.vesselType = VesselType.Ship; - break; - default: - break; - } - - // Store the id for later use - vesselData.id = protoVessel.vesselRef.id; - // StartCoroutine(PlaceSpawnedVessel(protoVessel.vesselRef)); - - //destroy prefabs - foreach (Part p in FindObjectsOfType()) - { - if (!p.vessel) - { - Destroy(p.gameObject); - } - } - - return protoVessel.vesselRef; - } - - private IEnumerator PlaceSpawnedVessel(Vessel v) - { - v.isPersistent = true; - v.Landed = false; - v.situation = Vessel.Situations.FLYING; - while (v.packed) - { - yield return null; - } - v.SetWorldVelocity(Vector3d.zero); - - // yield return null; - // FlightGlobals.ForceSetActiveVessel(v); - yield return null; - v.Landed = true; - v.situation = Vessel.Situations.PRELAUNCH; - v.GoOffRails(); - // v.IgnoreGForces(240); - - StageManager.BeginFlight(); - } - - internal class CrewData - { - public string name = null; - public ProtoCrewMember.Gender? gender = null; - public bool addToRoster = true; - - public CrewData() { } - public CrewData(CrewData cd) - { - name = cd.name; - gender = cd.gender; - addToRoster = cd.addToRoster; - } - } - private class VesselData - { - public string name = null; - public Guid? id = null; - public string craftURL = null; - public AvailablePart craftPart = null; - public string flagURL = null; - public VesselType vesselType = VesselType.Ship; - public CelestialBody body = null; - public Orbit orbit = null; - public double latitude = 0.0; - public double longitude = 0.0; - public double? altitude = null; - public float height = 0.0f; - public bool orbiting = false; - public bool owned = false; - public List crew = new List(); - public PQSCity pqsCity = null; - public Vector3d pqsOffset = Vector3d.zero; - public float heading = 0f; - public float pitch = 0f; - public float roll = 0f; - } - #endregion - } -} \ No newline at end of file diff --git a/BDArmory/Control/VesselSpawnerField.cs b/BDArmory/Control/VesselSpawnerField.cs deleted file mode 100644 index e4bf81318..000000000 --- a/BDArmory/Control/VesselSpawnerField.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using BDArmory.Core; -using BDArmory.Control; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Control -{ - public class SpawnLocation - { - public string name; - public Vector2d location; - - public SpawnLocation(string _name, Vector2d _location) { name = _name; location = _location; } - public override string ToString() { return name + ", " + location.ToString("G6"); } - } - - [AttributeUsage(AttributeTargets.Field)] - public class VesselSpawnerField : Attribute - { - public VesselSpawnerField() { } - - static List defaultLocations = new List{ - new SpawnLocation("KSC", new Vector2d(-0.04762, -74.8593)), - new SpawnLocation("Inland KSC", new Vector2d(20.5939, -146.567)), - new SpawnLocation("Desert Runway", new Vector2d(-6.44958, -144.038)), - new SpawnLocation("Ice field", new Vector2d(80.3343, -32.0119)), - new SpawnLocation("Canyon", new Vector2d(-52.7592, -4.71081)), - new SpawnLocation("Big Canyon", new Vector2d(6.97865, -170.804)), - new SpawnLocation("Manley Valley", new Vector2d(45.6, -137.3)), - new SpawnLocation("Half-pipe", new Vector2d(-21.1388, 72.6437)), - new SpawnLocation("Kurgan's spot", new Vector2d(-28.4595, -9.15156)), - new SpawnLocation("Bowl 1", new Vector2d(35.6559, -77.4941)), - new SpawnLocation("Bowl 2", new Vector2d(3.8744, -78.0039)), - new SpawnLocation("Bowl 3", new Vector2d(0.268284, -80.5195)), - new SpawnLocation("Pyramids", new Vector2d(-6.4743, -141.662)), - }; - - public static void Save() - { - ConfigNode fileNode = ConfigNode.Load(VesselSpawner.spawnLocationsCfg); - if (fileNode == null) - fileNode = new ConfigNode(); - - if (!fileNode.HasNode("BDASpawnLocations")) - fileNode.AddNode("BDASpawnLocations"); - - ConfigNode settings = fileNode.GetNode("BDASpawnLocations"); - - settings.ClearValues(); - foreach (var spawnLocation in VesselSpawner.spawnLocations) - settings.AddValue("LOCATION", spawnLocation.ToString()); - - fileNode.Save(VesselSpawner.spawnLocationsCfg); - } - - public static void Load() - { - VesselSpawner.spawnLocations = new List(); - ConfigNode fileNode = ConfigNode.Load(VesselSpawner.spawnLocationsCfg); - if (fileNode != null && fileNode.HasNode("BDASpawnLocations")) - { - ConfigNode settings = fileNode.GetNode("BDASpawnLocations"); - foreach (var spawnLocation in settings.GetValues("LOCATION")) - { - var parsedValue = (SpawnLocation)ParseValue(typeof(SpawnLocation), spawnLocation); - if (parsedValue != null) - { - VesselSpawner.spawnLocations.Add(parsedValue); - } - } - } - // Add defaults if nothing got loaded. - if (VesselSpawner.spawnLocations.Count == 0) - { - Debug.Log("[VesselSpawnerField]: No locations found in config file, adding defaults."); - VesselSpawner.spawnLocations = defaultLocations.ToList(); - } - } - - public static object ParseValue(Type type, string value) - { - try - { - if (type == typeof(string)) - { - return value; - } - else if (type == typeof(Vector2d)) - { - char[] charsToTrim = { '(', ')', ' ' }; - string[] strings = value.Trim(charsToTrim).Split(','); - if (strings.Length == 2) - { - double x = double.Parse(strings[0]); - double y = double.Parse(strings[1]); - return new Vector2d(x, y); - } - } - else if (type == typeof(SpawnLocation)) - { - var parts = value.Split(new char[] { ',' }, 2); - if (parts.Length == 2) - { - var name = (string)ParseValue(typeof(string), parts[0]); - var location = (Vector2d)ParseValue(typeof(Vector2d), parts[1]); - if (name != null && location != null) - return new SpawnLocation(name, location); - } - } - } - catch (Exception e) - { - Debug.LogException(e); - } - Debug.LogError("[VesselSpawnerField]: Failed to parse settings field of type " + type + " and value " + value); - return null; - } - } -} diff --git a/BDArmory/Control/_description b/BDArmory/Control/_description new file mode 100644 index 000000000..9671420e1 --- /dev/null +++ b/BDArmory/Control/_description @@ -0,0 +1,2 @@ +Control logic for vessels. +FIXME AI and WM modules should probably be moved here along with various other modules, e.g., WingCommander. diff --git a/BDArmory/CounterMeasure/CMBubble.cs b/BDArmory/CounterMeasure/CMBubble.cs new file mode 100644 index 000000000..a1ce6b671 --- /dev/null +++ b/BDArmory/CounterMeasure/CMBubble.cs @@ -0,0 +1,74 @@ +using System.Collections; +using UnityEngine; + +using BDArmory.Utils; + +namespace BDArmory.CounterMeasure +{ + public class CMBubble : MonoBehaviour + { + public Vector3 velocity; + + void OnEnable() + { + StartCoroutine(BubbleRoutine()); + } + + IEnumerator BubbleRoutine() + { + yield return new WaitForSecondsFixed(10); + + gameObject.SetActive(false); + } + + void FixedUpdate() + { + //physics + //atmospheric drag (stock) + float simSpeedSquared = velocity.sqrMagnitude; + Vector3 currPos = transform.position; + float mass = 0.01f; + float drag = 5f; + Vector3 dragForce = (0.008f * mass) * drag * 0.5f * simSpeedSquared * + ((float) + FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currPos), + FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody) * 10) * + velocity.normalized; + + velocity -= (dragForce / mass) * Time.fixedDeltaTime; + if (FlightGlobals.getAltitudeAtPos(currPos) < -1) + velocity += FlightGlobals.getGeeForceAtPosition(currPos) * Time.fixedDeltaTime; + + transform.position += velocity * Time.fixedDeltaTime; + } + public static float RaycastBubblescreen(Ray ray) + { + float fieldStrength = 1; + if (!CMDropper.bubblePool) + { + return fieldStrength; + } + float falloffFactor = 0.4f; + for (int i = 0; i < CMDropper.bubblePool.size; i++) + { + Transform bubbleTf = CMDropper.bubblePool.GetPooledObject(i).transform; + if (bubbleTf.gameObject.activeInHierarchy) + { + Plane bubblePlane = new Plane((ray.origin - bubbleTf.position).normalized, bubbleTf.position); + float enter; + if (bubblePlane.Raycast(ray, out enter)) + { + float dist = (ray.GetPoint(enter) - bubbleTf.position).sqrMagnitude; + if (dist < 24 * 24) + { + fieldStrength -= 1 * falloffFactor; + falloffFactor *= 0.7f; + } + } + } + } + + return Mathf.Clamp01(fieldStrength); + } + } +} diff --git a/BDArmory/CounterMeasure/CMChaff.cs b/BDArmory/CounterMeasure/CMChaff.cs index 4d8696b98..98ea18898 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 @@ -35,10 +36,15 @@ void OnEnable() gameObject.SetActive(false); return; } - + pe.useWorldSpace = false; // Don't use worldspace, so that we can move the FX properly. StartCoroutine(LifeRoutine()); } + void OnDisable() + { + body = null; + } + IEnumerator LifeRoutine() { geoPos = VectorUtils.WorldPositionToGeoCoords(transform.position, body); @@ -46,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, body) * 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/CMDecoy.cs b/BDArmory/CounterMeasure/CMDecoy.cs new file mode 100644 index 000000000..e2a781e7c --- /dev/null +++ b/BDArmory/CounterMeasure/CMDecoy.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; + +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Targeting; + +namespace BDArmory.CounterMeasure +{ + public class CMDecoy : MonoBehaviour + { + List pEmitters; + + Light[] lights; + float startTime; + + public bool alive = true; + + Vector3 upDirection; + + public Vector3 velocity; + + public float acousticSig; + public float audio; + //float minAudio; + //float startAudio; + + float lifeTime = 30; + + public void SetAcoustics(Vessel sourceVessel) + { + // generate decoy sound prodile within spectrum of emitting vessel's acoustic signature, but narrow range for low heats + acousticSig = BDATargetManager.GetVesselAcousticSignature(sourceVessel, Vector3.zero).Item1; + audio = acousticSig; + float audioMinMult = Mathf.Clamp(((0.00093f * acousticSig * acousticSig - 1.4457f * acousticSig + 1141.95f) / 1000f), 0.65f, 0.8f); // Equivalent to above, but uses polynomial for speed + audio *= UnityEngine.Random.Range(audioMinMult, Mathf.Max(BDArmorySettings.FLARE_FACTOR, 0f) - audioMinMult + 0.8f); + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log("[BDArmory.CMDecoy]: New decoy generated from " + sourceVessel.GetName() + ":" + acousticSig.ToString("0.0") + ", decoy sig: " + audio.ToString("0.0")); + } + + void OnEnable() + { + //startAudio = audio; + //minAudio = startAudio * 0.34f; + if (pEmitters == null) + { + pEmitters = new List(); + + using (var pe = gameObject.GetComponentsInChildren().Cast().GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + { + EffectBehaviour.AddParticleEmitter(pe.Current); + pEmitters.Add(pe.Current); + } + } + } + + EnableEmitters(); + + ++BDArmorySetup.numberOfParticleEmitters; + + if (lights == null) + { + lights = gameObject.GetComponentsInChildren(); + } + + using (IEnumerator lgt = lights.AsEnumerable().GetEnumerator()) + while (lgt.MoveNext()) + { + if (lgt.Current == null) continue; + lgt.Current.enabled = true; + } + startTime = Time.time; + + BDArmorySetup.Decoys.Add(this); + + upDirection = VectorUtils.GetUpDirection(transform.position); + + this.transform.localScale = Vector3.one; + } + + void FixedUpdate() + { + if (!gameObject.activeInHierarchy) return; + + //floating origin and velocity offloading corrections + if (BDKrakensbane.IsActive) + { + transform.localPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + + if (velocity != Vector3.zero) + { + transform.localRotation = Quaternion.LookRotation(velocity, upDirection); + } + + //turbulence + using (var pEmitter = pEmitters.GetEnumerator()) + while (pEmitter.MoveNext()) + { + if (pEmitter.Current == null) continue; + try + { + pEmitter.Current.worldVelocity = 2 * ParticleTurbulence.flareTurbulence; + } + catch (NullReferenceException e) + { + Debug.LogWarning("[BDArmory.CMDecoy]: NRE setting worldVelocity: " + e.Message); + } + + try + { + if (FlightGlobals.ActiveVessel && FlightGlobals.ActiveVessel.atmDensity <= 0) + { + pEmitter.Current.emit = false; + } + } + catch (NullReferenceException e) + { + Debug.LogWarning("[BDArmory.CMDecoy]: NRE checking density: " + e.Message); + } + } + // + + if (Time.time - startTime > lifeTime) //stop emitting after lifeTime seconds + { + alive = false; + transform.localScale = Vector3.zero; + BDArmorySetup.Decoys.Remove(this); + 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; + gameObject.SetActive(false); + return; + } + transform.localPosition += velocity * Time.fixedDeltaTime; + if (FlightGlobals.getAltitudeAtPos(transform.position) > 0) + velocity += FlightGlobals.getGeeForceAtPosition(transform.position) * 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; + emitter.Current.emit = true; + } + } + } +} \ No newline at end of file diff --git a/BDArmory/CounterMeasure/CMDropper.cs b/BDArmory/CounterMeasure/CMDropper.cs index 316a57b9b..0e985dfbf 100644 --- a/BDArmory/CounterMeasure/CMDropper.cs +++ b/BDArmory/CounterMeasure/CMDropper.cs @@ -2,12 +2,16 @@ 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; +using BDArmory.Extensions; +using BDArmory.Weapons.Missiles; +using BDArmory.VesselSpawning; + namespace BDArmory.CounterMeasure { public class CMDropper : PartModule @@ -15,12 +19,16 @@ public class CMDropper : PartModule public static ObjectPool flarePool; public static ObjectPool chaffPool; public static ObjectPool smokePool; + public static ObjectPool decoyPool; + public static ObjectPool bubblePool; public enum CountermeasureTypes { - Flare, - Chaff, - Smoke + Flare = 1 << 0, + Chaff = 1 << 1, + Smoke = 1 << 2, + Decoy = 1 << 3, + Bubbles = 1 << 4 } public CountermeasureTypes cmType = CountermeasureTypes.Flare; @@ -30,7 +38,12 @@ public enum CountermeasureTypes UI_FloatRange(controlEnabled = true, scene = UI_Scene.Editor, minValue = 1f, maxValue = 200f, stepIncrement = 1f)] public float ejectVelocity = 30; - [KSPField] public string ejectTransformName; + [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 = "cmTransform"; Transform ejectTransform; [KSPField] public string effectsTransformName = string.Empty; @@ -42,40 +55,76 @@ public enum CountermeasureTypes string resourceName; + public bool isMissileCM = false; + VesselChaffInfo vci; - [KSPAction("Fire Countermeasure")] + public BDStagingAreaGauge gauge; + public bool hasGauge = false; + public int cmCount = 0; + public int maxCMCount = 1; + VesselCMDropperInfo vesselCMs; + + + [KSPAction("#LOC_BDArmory_FireCountermeasure")] public void AGDropCM(KSPActionParam param) { - DropCM(); + if (!isMissileCM) + 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(); + + case CountermeasureTypes.Decoy: + return LaunchDecoy(); + + case CountermeasureTypes.Bubbles: + return DropBubbles(); } + return false; } public override void OnStart(StartState state) { + if (part.FindModuleImplementing() != null) + { + isMissileCM = true; + Events["EventDropCM"].guiActive = false; + Fields["ejectVelocity"].guiActive = false; + Fields["priority"].guiActive = false; + Fields["ejectVelocity"].guiActiveEditor = false; + Fields["priority"].guiActiveEditor = false; + } + else if (SpawnUtils.IsModularMissilePart(part)) + { + isMissileCM = true; + Events["EventDropCM"].guiActive = false; + } + if (HighLogic.LoadedSceneIsFlight) { SetupCM(); ejectTransform = part.FindModelTransform(ejectTransformName); + if (ejectTransform == null) // Create an eject transform that has ejectTransform.forward in the part.transform.up direction + { + ejectTransform = new GameObject().transform; + ejectTransform.SetParent(part.transform); + ejectTransform.localRotation = Quaternion.AngleAxis(-90, Vector3.right); + } if (effectsTransformName != string.Empty) { @@ -84,13 +133,20 @@ public override void OnStart(StartState state) part.force_activate(); - audioSource = gameObject.AddComponent(); - audioSource.minDistance = 1; - audioSource.maxDistance = 1000; - audioSource.spatialBlend = 1; + if (!isMissileCM) + { + SetupAudio(); + } + EnsureVesselCMs(); + vesselCMs.AddCMDropper(this); - UpdateVolume(); - BDArmorySetup.OnVolumeChange += UpdateVolume; + PartResource cmResource = GetCMResource(); + if (cmResource != null) + { + cmCount = (int)cmResource.amount; + maxCMCount = (int)cmResource.maxAmount; + } + GameEvents.onVesselsUndocking.Add(OnVesselsUndocking); } else { @@ -110,19 +166,29 @@ void UpdateVolume() void OnDestroy() { BDArmorySetup.OnVolumeChange -= UpdateVolume; + GameEvents.onVesselsUndocking.Remove(OnVesselsUndocking); + if (vesselCMs != null) vesselCMs.RemoveCMDropper(this); } - public override void OnUpdate() + void OnVesselsUndocking(Vessel v1, Vessel v2) + { + if (vessel != v1 && vessel != v2) return; // Not us. + if (countermeasureType.ToLower() == "chaff" && !vessel.gameObject.GetComponent()) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CMDropper]: {vessel.vesselName} didn't have VesselChaffInfo on undocking ({v1.vesselName} — {v2.vesselName})"); + SetupCM(); // Re-setup countermeasures at least one of the vessels would have lost the VesselModule when they docked. + } + } + + void Update() { if (audioSource) + audioSource.dopplerLevel = vessel.isActiveVessel ? 0 : 1; + if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && vessel.IsControllable) { - if (vessel.isActiveVessel) - { - audioSource.dopplerLevel = 0; - } - else + if (vessel.isActiveVessel && hasGauge) { - audioSource.dopplerLevel = 1; + gauge.UpdateCMMeter((vesselCMs.cmCounts[cmType] >= 1 ? (float)vesselCMs.cmCounts[cmType] : 0) / (float)vesselCMs.cmMaxCounts[cmType], cmType); } } } @@ -166,9 +232,36 @@ void SetupCMType() case "smoke": cmType = CountermeasureTypes.Smoke; break; + + case "decoy": + cmType = CountermeasureTypes.Decoy; + break; + + case "bubble": + cmType = CountermeasureTypes.Bubbles; + break; + } + } + + public void UpdateVCI() + { + vci = vessel.gameObject.GetComponent(); + if (!vci) + { + vci = vessel.gameObject.AddComponent(); } } + public void SetupAudio() + { + audioSource = gameObject.AddComponent(); + audioSource.minDistance = 1; + audioSource.maxDistance = 1000; + audioSource.spatialBlend = 1; + UpdateVolume(); + BDArmorySetup.OnVolumeChange += UpdateVolume; + } + void SetupCM() { countermeasureType = countermeasureType.ToLower(); @@ -176,7 +269,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,12 +279,15 @@ 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) + if (!isMissileCM) { - vci = vessel.gameObject.AddComponent(); + vci = vessel.gameObject.GetComponent(); + if (!vci) + { + vci = vessel.gameObject.AddComponent(); + } } if (!chaffPool) { @@ -201,45 +297,76 @@ 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) { SetupSmokePool(); } break; + + case "decoy": + cmType = CountermeasureTypes.Decoy; + cmSound = SoundUtils.GetAudioClip("BDArmory/Sounds/decoySound"); + if (!decoyPool) + { + SetupDecoyPool(); + } + resourceName = "CMDecoy"; + break; + + case "bubble": + cmType = CountermeasureTypes.Bubbles; + cmSound = SoundUtils.GetAudioClip("BDArmory/Sounds/smokeEject"); + resourceName = "CMBubbleCurtain"; + if (!bubblePool) + { + SetupBubblePool(); + } + break; } } - void DropFlare() + bool DropFlare() { - PartResource cmResource = GetCMResource(); - if (cmResource == null || !(cmResource.amount >= 1)) return; - cmResource.amount--; + if (!BDArmorySettings.INFINITE_COUNTERMEASURES) + { + PartResource cmResource = GetCMResource(); + if (cmResource == null || !(cmResource.amount >= 1)) return false; + cmResource.amount--; + vesselCMs.cmCounts[cmType]--; + cmCount--; + } audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); audioSource.PlayOneShot(cmSound); GameObject cm = flarePool.GetPooledObject(); - cm.transform.position = transform.position; + cm.transform.position = ejectTransform.position; CMFlare cmf = cm.GetComponent(); cmf.velocity = part.rb.velocity - + Krakensbane.GetFrameVelocityV3f() - + (ejectVelocity * transform.up) - + (UnityEngine.Random.Range(-3f, 3f) * transform.forward) - + (UnityEngine.Random.Range(-3f, 3f) * transform.right); + + BDKrakensbane.FrameVelocityV3f + + (ejectVelocity * ejectTransform.forward) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.up) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.right); cmf.SetThermal(vessel); cm.SetActive(true); FireParticleEffects(); + return true; } - void DropChaff() + bool DropChaff() { - PartResource cmResource = GetCMResource(); - if (cmResource == null || !(cmResource.amount >= 1)) return; - cmResource.amount--; + if (!BDArmorySettings.INFINITE_COUNTERMEASURES) + { + PartResource cmResource = GetCMResource(); + if (cmResource == null || !(cmResource.amount >= 1)) return false; + cmResource.amount--; + vesselCMs.cmCounts[cmType]--; + cmCount--; + } audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); audioSource.PlayOneShot(cmSound); @@ -251,34 +378,39 @@ void DropChaff() GameObject cm = chaffPool.GetPooledObject(); CMChaff chaff = cm.GetComponent(); - chaff.Emit(ejectTransform.position, ejectVelocity * ejectTransform.forward); + chaff.Emit(ejectTransform.position, ejectVelocity * ejectTransform.forward + vessel.Velocity()); FireParticleEffects(); + return true; } - void PopSmoke() + bool PopSmoke() { - PartResource smokeResource = GetCMResource(); - if (smokeResource.amount >= 1) + if (!BDArmorySettings.INFINITE_COUNTERMEASURES) { + PartResource smokeResource = GetCMResource(); + if (smokeResource == null || !(smokeResource.amount >= 1)) return false; smokeResource.amount--; - audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); - audioSource.PlayOneShot(cmSound); + vesselCMs.cmCounts[cmType]--; + cmCount--; + } + audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); + audioSource.PlayOneShot(cmSound); - StartCoroutine(SmokeRoutine()); + StartCoroutine(SmokeRoutine()); - FireParticleEffects(); - } + FireParticleEffects(); + return true; } 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) + - (UnityEngine.Random.Range(-3f, 3f) * transform.forward) + - (UnityEngine.Random.Range(-3f, 3f) * transform.right); + smoke.velocity = part.rb.velocity + (ejectVelocity * ejectTransform.forward) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.up) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.right); smokeCMObject.SetActive(true); smokeCMObject.transform.position = ejectTransform.position + (10 * ejectTransform.forward); float longestLife = 0; @@ -292,10 +424,83 @@ IEnumerator SmokeRoutine() } audioSource.PlayOneShot(smokePoofSound); - yield return new WaitForSeconds(longestLife); + yield return new WaitForSecondsFixed(longestLife); smokeCMObject.SetActive(false); } + bool LaunchDecoy() + { + if (!BDArmorySettings.INFINITE_COUNTERMEASURES) + { + PartResource cmResource = GetCMResource(); + if (cmResource == null || !(cmResource.amount >= 1)) return false; + cmResource.amount--; + vesselCMs.cmCounts[cmType]--; + cmCount--; + } + audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); + audioSource.PlayOneShot(cmSound); + + GameObject cm = decoyPool.GetPooledObject(); + cm.transform.position = ejectTransform.position; + CMDecoy cmd = cm.GetComponent(); + cmd.velocity = part.rb.velocity + + BDKrakensbane.FrameVelocityV3f + + (ejectVelocity * ejectTransform.forward) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.up) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.right); + cmd.SetAcoustics(vessel); + + cm.SetActive(true); + + FireParticleEffects(); + return true; + } + + bool DropBubbles() + { + if (!BDArmorySettings.INFINITE_COUNTERMEASURES) + { + PartResource bubbleResource = GetCMResource(); + if (bubbleResource == null || !(bubbleResource.amount >= 1)) return false; + bubbleResource.amount--; + vesselCMs.cmCounts[cmType]--; + cmCount--; + } + audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); + audioSource.PlayOneShot(cmSound); + + StartCoroutine(BubbleRoutine()); + + FireParticleEffects(); + return true; + } + + IEnumerator BubbleRoutine() + { + yield return new WaitForSecondsFixed(0.2f); + GameObject bubbleCMObject = bubblePool.GetPooledObject(); + CMBubble bubble = bubbleCMObject.GetComponent(); + bubble.velocity = part.rb.velocity + (ejectVelocity * ejectTransform.forward) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.up) + + (UnityEngine.Random.Range(-3f, 3f) * ejectTransform.right); + bubbleCMObject.SetActive(true); + bubbleCMObject.transform.position = ejectTransform.position + (10 * ejectTransform.forward); + float longestLife = 0; + using (IEnumerator emitter = bubbleCMObject.GetComponentsInChildren().Cast().GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + EffectBehaviour.AddParticleEmitter(emitter.Current); + emitter.Current.Emit(); + if (emitter.Current.maxEnergy > longestLife) longestLife = emitter.Current.maxEnergy; + } + + yield return new WaitForSecondsFixed(longestLife); + bubbleCMObject.SetActive(false); + } + + void SetupFlarePool() { GameObject cm = GameDatabase.Instance.GetModel("BDArmory/Models/CMFlare/model"); @@ -303,7 +508,19 @@ void SetupFlarePool() cm.AddComponent(); flarePool = ObjectPool.CreateObjectPool(cm, 10, true, true); } - + public static void ResetFlarePool() + { + if (CMDropper.flarePool != null) + { + foreach (var flareObj in CMDropper.flarePool.pool) + if (flareObj.activeInHierarchy) + { + var flare = flareObj.GetComponent(); + if (flare == null) continue; + flare.EnableEmitters(); + } + } + } void SetupSmokePool() { GameObject cm = GameDatabase.Instance.GetModel("BDArmory/Models/CMSmoke/cmSmokeModel"); @@ -320,6 +537,83 @@ void SetupChaffPool() chaffPool = ObjectPool.CreateObjectPool(cm, 10, true, true); } + void SetupDecoyPool() + { + GameObject cm = GameDatabase.Instance.GetModel("BDArmory/Models/CMDecoy/model"); + cm.SetActive(false); + cm.AddComponent(); + decoyPool = ObjectPool.CreateObjectPool(cm, 10, true, true); + } + + void SetupBubblePool() + { + GameObject cm = GameDatabase.Instance.GetModel("BDArmory/Models/CMBubble/cmSmokeModel"); + cm.SetActive(false); + cm.AddComponent(); + bubblePool = ObjectPool.CreateObjectPool(cm, 10, true, true); + } + + public static void DisableAllCMs() + { + if (flarePool != null && flarePool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CMDropper]: Setting {flarePool.pool.Count(flare => flare != null & flare.activeInHierarchy)} flare CMs inactive."); + foreach (var flare in flarePool.pool) + { + if (flare == null) continue; + flare.SetActive(false); + } + } + if (smokePool != null && smokePool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CMDropper]: Setting {smokePool.pool.Count(smoke => smoke != null & smoke.activeInHierarchy)} smoke CMs inactive."); + foreach (var smoke in smokePool.pool) + { + if (smoke == null) continue; + smoke.SetActive(false); + } + } + if (chaffPool != null && chaffPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CMDropper]: Setting {chaffPool.pool.Count(chaff => chaff != null & chaff.activeInHierarchy)} chaff CMs inactive."); + foreach (var chaff in chaffPool.pool) + { + if (chaff == null) continue; + chaff.SetActive(false); + } + } + if (decoyPool != null && decoyPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CMDropper]: Setting {decoyPool.pool.Count(decoy => decoy != null & decoy.activeInHierarchy)} decoy CMs inactive."); + foreach (var decoy in decoyPool.pool) + { + if (decoy == null) continue; + decoy.SetActive(false); + } + } + if (bubblePool != null && bubblePool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CMDropper]: Setting {bubblePool.pool.Count(bubble => bubble != null & bubble.activeInHierarchy)} bubble CMs inactive."); + foreach (var bubble in bubblePool.pool) + { + if (bubble == null) continue; + bubble.SetActive(false); + } + } + } + void EnsureVesselCMs() + { + if (!vesselCMs || vesselCMs.vessel != vessel) + { + vesselCMs = vessel.gameObject.GetComponent(); + if (!vesselCMs) + { + vesselCMs = vessel.gameObject.AddComponent(); + } + } + + vesselCMs.DelayedCleanList(); + } // RMB info in editor public override string GetInfo() { diff --git a/BDArmory/CounterMeasure/CMFlare.cs b/BDArmory/CounterMeasure/CMFlare.cs index ede8e58b2..4d003ab92 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(); - 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) - Debug.Log("[BDArmory]: New flare generated from " + sourceVessel.GetDisplayName() + ":" + BDATargetManager.GetVesselHeatSignature(sourceVessel).ToString("0.0") + ", heat: " + thermal.ToString("0.0") + " mult: " + thermalMinMult + "-" + thermalMaxMult); + 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, 1.75f - thermalMinMult + 0.65f); + thermal *= UnityEngine.Random.Range(thermalMinMult, Mathf.Max(BDArmorySettings.FLARE_FACTOR, 0f) - thermalMinMult + 0.8f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: 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.GetName() + ":" + 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,48 +119,46 @@ 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 //downforce - Vector3 downForce = (Mathf.Clamp(velocity.magnitude, 0.1f, 150) / 150) * 20 * -upDirection; + Vector3 downForce = (Mathf.Clamp(velocity.magnitude, 0.1f, 150) / 150) * 20 * upDirection; //smoke should rise, not fall... //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) - { - Debug.LogWarning("CMFlare NRE setting worldVelocity"); - } + 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) - { - Debug.LogWarning("CMFlare NRE checking density"); - } - } - 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.FlareSmoke; + 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..b2b6a6bdc --- /dev/null +++ b/BDArmory/CounterMeasure/ModuleCloakingDevice.cs @@ -0,0 +1,289 @@ +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; + if (part != null && part.vessel != null) + { + using (var Part = 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(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Modules/ModuleECMJammer.cs b/BDArmory/CounterMeasure/ModuleECMJammer.cs similarity index 71% rename from BDArmory/Modules/ModuleECMJammer.cs rename to BDArmory/CounterMeasure/ModuleECMJammer.cs index 393d3062a..e2fb3e7db 100644 --- a/BDArmory/Modules/ModuleECMJammer.cs +++ b/BDArmory/CounterMeasure/ModuleECMJammer.cs @@ -1,7 +1,9 @@ -using System.Text; -using BDArmory.CounterMeasure; +using BDArmory.UI; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; +using System.Text; -namespace BDArmory.Modules +namespace BDArmory.CounterMeasure { public class ModuleECMJammer : PartModule { @@ -11,8 +13,12 @@ public class ModuleECMJammer : PartModule [KSPField] public float rcsReductionFactor = 0.75f; + [KSPField] public float rcsOverride = -1; + [KSPField] public double resourceDrain = 5; + [KSPField] public string resourceName = "ElectricCharge"; + [KSPField] public bool alwaysOn = false; [KSPField] public bool signalSpam = true; @@ -21,15 +27,27 @@ 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; + + public bool isMissileECM = false; + + private int resourceID; + + private float cooldownTimer = 0; + VesselECMJInfo vesselJammer; + private BDStagingAreaGauge gauge; + [KSPAction("Enable")] public void AGEnable(KSPActionParam param) { - if (!jammerEnabled) + if (!jammerEnabled && !isMissileECM) { EnableJammer(); } @@ -38,7 +56,7 @@ public void AGEnable(KSPActionParam param) [KSPAction("Disable")] public void AGDisable(KSPActionParam param) { - if (jammerEnabled) + if (jammerEnabled && !isMissileECM) { DisableJammer(); } @@ -47,7 +65,8 @@ public void AGDisable(KSPActionParam param) [KSPAction("Toggle")] public void AGToggle(KSPActionParam param) { - Toggle(); + if (!isMissileECM) + Toggle(); } [KSPEvent(guiActiveEditor = false, guiActive = true, guiName = "#LOC_BDArmory_Toggle")]//Toggle @@ -62,13 +81,23 @@ 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(); + if (part.FindModuleImplementing() != null || SpawnUtils.IsModularMissilePart(part)) + { + isMissileECM = true; + Events["Toggle"].guiActive = false; + } + + gauge = (BDStagingAreaGauge)part.AddModule("BDStagingAreaGauge"); GameEvents.onVesselCreate.Add(OnVesselCreate); } @@ -79,11 +108,13 @@ void OnDestroy() void OnVesselCreate(Vessel v) { - EnsureVesselJammer(); + if (v == vessel) + EnsureVesselJammer(); } public void EnableJammer() { + if (cooldownTimer > 0) return; EnsureVesselJammer(); vesselJammer.AddJammer(this); jammerEnabled = true; @@ -95,11 +126,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) { @@ -112,6 +146,14 @@ public override void OnFixedUpdate() DrainElectricity(); } + if (cooldownTimer > 0) + { + cooldownTimer -= TimeWarp.fixedDeltaTime; + if (vessel.isActiveVessel) + { + gauge.UpdateHeatMeter(cooldownTimer / cooldownInterval); + } + } } void EnsureVesselJammer() @@ -151,7 +193,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/VesselCMDropperInfo.cs b/BDArmory/CounterMeasure/VesselCMDropperInfo.cs new file mode 100644 index 000000000..cdb5f93a1 --- /dev/null +++ b/BDArmory/CounterMeasure/VesselCMDropperInfo.cs @@ -0,0 +1,217 @@ +using BDArmory.UI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BDArmory.CounterMeasure +{ + public class VesselCMDropperInfo : MonoBehaviour + { + List droppers = new List(); + public Vessel vessel; + public Dictionary cmCounts = new Dictionary(); + public Dictionary cmMaxCounts = new Dictionary(); + public Dictionary hasCMGauge = new Dictionary(); + bool cleaningRequired = false; + + void Start() + { + if (!Setup()) + { + Destroy(this); + return; + } + vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; + GameEvents.onVesselPartCountChanged.Add(OnVesselPartCountChanged); + GameEvents.onVesselChange.Add(OnVesselSwitched); + if (vessel.isActiveVessel) + StartCoroutine(DelayedCleanListRoutine()); + else cleaningRequired = true; + } + + + bool Setup() + { + if (!HighLogic.LoadedSceneIsFlight) return false; + if (!vessel) vessel = GetComponent(); + if (!vessel) + { + Debug.Log("[BDArmory.VesselCMDropperInfo]: VesselCMDropperInfo was added to an object with no vessel component"); + return false; + } + hasCMGauge = Enum.GetValues(typeof(CMDropper.CountermeasureTypes)).Cast().ToDictionary(cm => cm, cm => false); + cmCounts = Enum.GetValues(typeof(CMDropper.CountermeasureTypes)).Cast().ToDictionary(cm => cm, cm => 0); + cmMaxCounts = Enum.GetValues(typeof(CMDropper.CountermeasureTypes)).Cast().ToDictionary(cm => cm, cm => 0); + return true; + } + + void OnDestroy() + { + if (vessel) vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; + GameEvents.onVesselPartCountChanged.Remove(OnVesselPartCountChanged); + GameEvents.onVesselChange.Remove(OnVesselSwitched); + } + + void AboutToBeDestroyed() + { + Destroy(this); + } + + void OnVesselPartCountChanged(Vessel v) + { + if (gameObject.activeInHierarchy && v == vessel && vessel.isActiveVessel) + StartCoroutine(DelayedCleanListRoutine()); + else cleaningRequired = true; + } + void OnVesselSwitched(Vessel v) + { + if (gameObject.activeInHierarchy && v == vessel && cleaningRequired) + CleanList(); + } + public void AddCMDropper(CMDropper CM) + { + if (droppers is null && !Setup()) + { + Destroy(this); + return; + } + + if (!droppers.Contains(CM)) + { + droppers.Add(CM); + } + + DelayedCleanList(); + } + + public void RemoveCMDropper(CMDropper CM) + { + if (droppers is null && !Setup()) + { + Destroy(this); + return; + } + + droppers.Remove(CM); + CleanList(); + } + + public void DelayedCleanList() + { + StartCoroutine(DelayedCleanListRoutine()); + } + IEnumerator DelayedCleanListRoutine() + { + var wait = new WaitForFixedUpdate(); + yield return wait; + yield return wait; + CleanList(); + } + + void CleanList() + { + vessel = GetComponent(); + if (!vessel) + { + Destroy(this); + } + droppers.RemoveAll(j => j == null); + droppers.RemoveAll(j => j.vessel != vessel); //cull destroyed CM boxes, if any, refresh Gauges on remainder + cmCounts.Clear(); + cmMaxCounts.Clear(); + foreach (var cmType in Enum.GetValues(typeof(CMDropper.CountermeasureTypes)).Cast()) + { + cmCounts.Add(cmType, 0); + cmMaxCounts.Add(cmType, 0); + } + foreach (CMDropper p in droppers) + { + if (p.vessel != this.vessel) continue; + cmCounts[p.cmType] += p.cmCount; + cmMaxCounts[p.cmType] += p.maxCMCount; + + if (hasCMGauge[p.cmType] || p.hasGauge) + { + hasCMGauge[p.cmType] = true; + continue; + } + p.gauge = (BDStagingAreaGauge)p.part.AddModule("BDStagingAreaGauge"); + hasCMGauge[p.cmType] = true; + p.hasGauge = true; + p.gauge.AmmoName = p.cmType switch + { + CMDropper.CountermeasureTypes.Flare => "Flares", + CMDropper.CountermeasureTypes.Chaff => "Chaff", + CMDropper.CountermeasureTypes.Smoke => "Smoke", + CMDropper.CountermeasureTypes.Decoy => "Decoys", + + CMDropper.CountermeasureTypes.Bubbles => "Bubbles", + _ => "???" + }; + /* + bool addGauge = false; + switch (p.cmType) + { + case CMDropper.CountermeasureTypes.Flare: + { + if (!(hasCMGauge[p.cmType] || p.hasGauge)) + addGauge = true; + hasCMGauge[p.cmType] = true; + } + break; + case CMDropper.CountermeasureTypes.Chaff: + { + if (!(hasCMGauge[p.cmType] || p.hasGauge)) + addGauge = true; + hasCMGauge[p.cmType] = true; + } + break; + case CMDropper.CountermeasureTypes.Smoke: + { + if (!(hasCMGauge[p.cmType] || p.hasGauge)) + addGauge = true; + hasCMGauge[p.cmType] = true; + } + break; + case CMDropper.CountermeasureTypes.Decoy: + { + if (!(hasCMGauge[p.cmType] || p.hasGauge)) + addGauge = true; + hasCMGauge[p.cmType] = true; + } + break; + case CMDropper.CountermeasureTypes.Bubbles: + { + if (!(hasCMGauge[p.cmType] || p.hasGauge)) + addGauge = true; + hasCMGauge[p.cmType] = true; + } + break; + } + if (addGauge) + { + p.gauge = (BDStagingAreaGauge)p.part.AddModule("BDStagingAreaGauge"); + p.hasGauge = true; + hasCMGauge[p.cmType] = true; + p.gauge.AmmoName = p.cmType switch + { + CMDropper.CountermeasureTypes.Flare => "Flares", + CMDropper.CountermeasureTypes.Chaff => "Chaff", + CMDropper.CountermeasureTypes.Smoke => "Smoke", + CMDropper.CountermeasureTypes.Decoy => "Decoys", + CMDropper.CountermeasureTypes.Bubbles => "Bubbles", + _ => "???" + }; + } + */ + } + foreach (var cmType in Enum.GetValues(typeof(CMDropper.CountermeasureTypes)).Cast()) + { + hasCMGauge[cmType] = false; + } + cleaningRequired = false; + } + } +} diff --git a/BDArmory/CounterMeasure/VesselChaffInfo.cs b/BDArmory/CounterMeasure/VesselChaffInfo.cs index 62492e88c..b3dbc3c00 100644 --- a/BDArmory/CounterMeasure/VesselChaffInfo.cs +++ b/BDArmory/CounterMeasure/VesselChaffInfo.cs @@ -1,8 +1,8 @@ -using UnityEngine; +using BDArmory.Extensions; +using UnityEngine; namespace BDArmory.CounterMeasure { - [RequireComponent(typeof(Vessel))] public class VesselChaffInfo : MonoBehaviour { Vessel vessel; @@ -15,25 +15,21 @@ public class VesselChaffInfo : MonoBehaviour const float minMult = 0.1f; float chaffScalar = 500; - void Awake() + void Start() { vessel = GetComponent(); if (!vessel) { - Debug.Log("[BDArmory]: VesselChaffInfo was added to an object with no vessel component"); + Debug.Log("[BDArmory.VesselChaffInfo]: VesselChaffInfo was added to an object with no vessel component"); Destroy(this); return; } - vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; } void OnDestroy() { - if (vessel) - { - vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; - } + if (vessel) vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; } void AboutToBeDestroyed() @@ -53,8 +49,9 @@ public void Chaff() void FixedUpdate() { + float speedOrAccel = (!vessel.InVacuum()) ? (float)vessel.srfSpeed : Mathf.Abs((float)vessel.acceleration_immediate.magnitude); chaffScalar = Mathf.MoveTowards(chaffScalar, chaffMax, - Mathf.Clamp(speedRegenMult * (float)vessel.srfSpeed, minRegen, maxRegen) * Time.fixedDeltaTime); + Mathf.Clamp(speedRegenMult * speedOrAccel, minRegen, maxRegen) * Time.fixedDeltaTime); } } } diff --git a/BDArmory/CounterMeasure/VesselCloakInfo.cs b/BDArmory/CounterMeasure/VesselCloakInfo.cs new file mode 100644 index 000000000..769fd877f --- /dev/null +++ b/BDArmory/CounterMeasure/VesselCloakInfo.cs @@ -0,0 +1,161 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Utils; + +namespace BDArmory.CounterMeasure +{ + 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 Start() + { + vessel = GetComponent(); + if (!vessel) + { + Debug.Log("[BDArmory.VesselCloakInfo]: VesselCloakInfo was added to an object with no vessel component"); + Destroy(this); + return; + } + cloaks = new List(); + vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; + GameEvents.onVesselCreate.Add(OnVesselCreate); + GameEvents.onPartJointBreak.Add(OnPartJointBreak); + GameEvents.onPartDie.Add(OnPartDie); + } + + void OnDestroy() + { + if (vessel) 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(); + } + } +} \ No newline at end of file diff --git a/BDArmory/CounterMeasure/VesselECMJInfo.cs b/BDArmory/CounterMeasure/VesselECMJInfo.cs index b156b22c1..9222eb4ce 100644 --- a/BDArmory/CounterMeasure/VesselECMJInfo.cs +++ b/BDArmory/CounterMeasure/VesselECMJInfo.cs @@ -1,17 +1,20 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Modules; using UnityEngine; +using BDArmory.Utils; +using BDArmory.Targeting; +using BDArmory.Radar; + namespace BDArmory.CounterMeasure { - [RequireComponent(typeof(Vessel))] public class VesselECMJInfo : MonoBehaviour { List jammers; public Vessel vessel; - + private TargetInfo ti; bool jEnabled; + bool cleaningRequired = false; public bool jammerEnabled { @@ -38,21 +41,34 @@ public float rcsReductionFactor { get { return rcsr; } } - - void Awake() + void Start() { - jammers = new List(); - vessel = GetComponent(); - + if (!Setup()) + { + Destroy(this); + return; + } vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; GameEvents.onVesselCreate.Add(OnVesselCreate); GameEvents.onPartJointBreak.Add(OnPartJointBreak); GameEvents.onPartDie.Add(OnPartDie); } + bool Setup() + { + if (!vessel) vessel = GetComponent(); + if (!vessel) + { + Debug.Log("[BDArmory.VesselECMJInfo]: VesselECMJInfo was added to an object with no vessel component"); + return false; + } + if (jammers is null) jammers = new List(); + return true; + } + void OnDestroy() { - vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; + if (vessel) vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; GameEvents.onVesselCreate.Remove(OnVesselCreate); GameEvents.onPartJointBreak.Remove(OnPartJointBreak); GameEvents.onPartDie.Remove(OnPartDie); @@ -63,32 +79,19 @@ void AboutToBeDestroyed() Destroy(this); } - void OnPartDie(Part p = null) - { - if (gameObject.activeInHierarchy) - { - StartCoroutine(DelayedCleanJammerListRoutine()); - } - } + void OnPartDie() => OnPartDie(null); + void OnPartDie(Part p) => cleaningRequired = true; + void OnVesselCreate(Vessel v) => cleaningRequired = true; + void OnPartJointBreak(PartJoint j, float breakForce) => cleaningRequired = true; - void OnVesselCreate(Vessel v) - { - if (gameObject.activeInHierarchy) - { - StartCoroutine(DelayedCleanJammerListRoutine()); - } - } - - void OnPartJointBreak(PartJoint j, float breakForce) + public void AddJammer(ModuleECMJammer jammer) { - if (gameObject.activeInHierarchy) + if (jammers is null && !Setup()) { - StartCoroutine(DelayedCleanJammerListRoutine()); + Destroy(this); + return; } - } - public void AddJammer(ModuleECMJammer jammer) - { if (!jammers.Contains(jammer)) { jammers.Add(jammer); @@ -99,13 +102,24 @@ public void AddJammer(ModuleECMJammer jammer) public void RemoveJammer(ModuleECMJammer jammer) { + if (jammers is null && !Setup()) + { + Destroy(this); + return; + } + jammers.Remove(jammer); UpdateJammerStrength(); } - void UpdateJammerStrength() + public void UpdateJammerStrength(TargetInfo tInfoIn = null) { + if (jammers is null && !Setup()) + { + Destroy(this); + return; + } jEnabled = jammers.Count > 0; if (!jammerEnabled) @@ -121,6 +135,8 @@ void UpdateJammerStrength() float rcsrTotal = 1; float rcsrCount = 0; + float rcsOverride = -1; + List.Enumerator jammer = jammers.GetEnumerator(); while (jammer.MoveNext()) { @@ -139,6 +155,7 @@ void UpdateJammerStrength() { rcsrTotal *= jammer.Current.rcsReductionFactor; rcsrCount++; + if (rcsOverride < jammer.Current.rcsOverride) rcsOverride = jammer.Current.rcsOverride; } } jammer.Dispose(); @@ -148,23 +165,72 @@ 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; + rcsr = 1f; } - } + if (tInfoIn == null) + ti = RadarUtils.GetVesselRadarSignature(vessel, false); + else + ti = tInfoIn; + + if (rcsOverride > 0) ti.radarBaseSignature = rcsOverride; + ti.radarRCSReducedSignature = ti.radarBaseSignature; + ti.radarModifiedSignature = ti.radarBaseSignature; + ti.radarLockbreakFactor = 1f; + //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) / 100f) + 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 / 100f) + 1.0f) * totaljStrength; + //4) lockbreaking strength relative to jammer's lockbreak strength in relation to vessel rcs signature: + // lockbreak_factor = baseSig/modifiedSig x (1 - lockBreakStrength/baseSig/100) + // Use clamp to prevent RCS reduction resulting in increased lockbreak factor, which negates value of RCS reduction) + ti.radarLockbreakFactor = (ti.radarRCSReducedSignature == 0f) ? 0f : + Mathf.Max(Mathf.Clamp01(ti.radarRCSReducedSignature / ti.radarModifiedSignature) * (1f - (totalLBstrength / ti.radarRCSReducedSignature / 100f)), 0f); // 0 is minimum lockbreak factor + } + void OnFixedUpdate() + { + if (UI.BDArmorySetup.GameIsPaused) return; + //Debug.Log($"[ECMDebug]: jammer on {vessel.GetName()} active! Jammer strength: {jStrength}"); + if (jEnabled && jStrength > 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 < jStrength * 10) + { + RadarWarningReceiver.PingRWR(loadedvessels.Current, vessel.CoM, RadarWarningReceiver.RWRThreatTypes.Jamming, 0.2f); + //Debug.Log($"[ECMDebug]: jammer on {vessel.GetName()} active! Pinging RWR on {loadedvessels.Current.GetName()}"); + } + } + } + if (cleaningRequired) + { + StartCoroutine(DelayedCleanJammerListRoutine()); + cleaningRequired = false; // Set it false here instead of in CleanJammerList to allow it to be triggered on consecutive frames. + } + } public void DelayedCleanJammerList() { - StartCoroutine(DelayedCleanJammerListRoutine()); + cleaningRequired = true; } IEnumerator DelayedCleanJammerListRoutine() { - yield return null; - yield return null; + var wait = new WaitForFixedUpdate(); + yield return wait; + yield return wait; CleanJammerList(); } @@ -179,16 +245,15 @@ void CleanJammerList() jammers.RemoveAll(j => j == null); jammers.RemoveAll(j => j.vessel != vessel); - List.Enumerator jam = vessel.FindPartModulesImplementing().GetEnumerator(); - while (jam.MoveNext()) - { - if (jam.Current == null) continue; - if (jam.Current.jammerEnabled) + using (var jam = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (jam.MoveNext()) { - AddJammer(jam.Current); + if (jam.Current == null) continue; + if (jam.Current.jammerEnabled) + { + AddJammer(jam.Current); + } } - } - jam.Dispose(); UpdateJammerStrength(); } } diff --git a/BDArmory/CounterMeasure/_description b/BDArmory/CounterMeasure/_description new file mode 100644 index 000000000..d9fce1a67 --- /dev/null +++ b/BDArmory/CounterMeasure/_description @@ -0,0 +1 @@ +Countermeasures to missiles and potentially other things. \ No newline at end of file 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 57% rename from BDArmory.Core/Services/DamageService.cs rename to BDArmory/Damage/DamageService.cs index 622e7151a..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); @@ -10,11 +22,18 @@ public abstract class DamageService : NotificableService public abstract void AddDamageToPart_svc(Part p, float damage); + public abstract void AddHealthToPart_svc(Part p, float damage, bool overcharge = false); + public abstract void AddDamageToKerbal_svc(KerbalEVA kerbal, float damage); public abstract float GetPartDamage_svc(Part p); public abstract float GetPartArmor_svc(Part p); + public abstract float GetPartMaxArmor_svc(Part p); + + public abstract float GetArmorDensity_svc(Part p); + + public abstract float GetArmorStrength_svc(Part p); public abstract float GetMaxPartDamage_svc(Part p); diff --git a/BDArmory/Damage/HitpointTracker.cs b/BDArmory/Damage/HitpointTracker.cs new file mode 100644 index 000000000..1d959e840 --- /dev/null +++ b/BDArmory/Damage/HitpointTracker.cs @@ -0,0 +1,1687 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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 = -1f; //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 isProcWheel = false; + private bool isVariantPart = 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; + + public 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 = -1f; + + [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 radarReflectivity; + [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; + + private float hullRadarReturnFactor = 1; + private float armorRadarReturnFactor = 1; + + public Dictionary defaultShader = []; + public Dictionary 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(); + if (BDArmorySettings.MAX_ARMOR_LIMIT >= 0) + { + maxSupportedArmor = Mathf.Min(BDArmorySettings.MAX_ARMOR_LIMIT, maxSupportedArmor); + Armor = Mathf.Min(Armor, maxSupportedArmor); + } + + 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; + } + if (part.Modules.Contains("KSPWheelBase")) + { + isProcWheel = true; + } + if (part.Modules.Contains("ModuleB9PartSwitch") || part.Modules.Contains("ModulePartVariants")) + { + isVariantPart = 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 (HullTypeNum < 1 || HullTypeNum > HullInfo.materialNames.Count) + { + Debug.LogWarning($"[BDArmory.HitpointTracker]: Invalid HullTypeNum found on {part.partInfo.name} on {part.vessel.vesselName}. Resetting to Aluminium."); + HullTypeNum = 2; // Invalid hull type number, revert to default Aluminium + } + 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 (ArmorThickness < 0) ArmorThickness = part.IsMissile() ? 2 : 10; + 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.Modules.Contains("ModuleReactiveArmor")) + { + Fields["Armor"].guiActiveEditor = false; + Fields["ArmorTypeNum"].guiActiveEditor = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + } + 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() || 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) //Mod part set to start with armor, or armor panel. > 10, since less than 10mm of armor can't be considered 'startsArmored' + { + startsArmored = true; + if (Armor < 0) // armor amount modified in SPH/VAB and does not = either the default nor the .cfg thickness + Armor = ArmorThickness;//set Armor amount to .cfg value + //See also ln 1183-1186 + } + else + { + if (Armor < 0) Armor = ArmorThickness; //10 for parts, 2 for missiles, from ln 347 + 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(); + 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 + partSize = CalcPartBounds(this.part, this.transform).size; + 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... + if (part.IsAero()) + { + if (!isProcWing) //procWings handled elsewhere + { + if (!FerramAerospace.CheckForFAR()) + { + if ((float)part.Modules.GetModule().deflectionLiftCoeff < (Mathf.Max(partSize.x, partSize.y) * Mathf.Max(partSize.y, partSize.z) / 3.52f)) + { + sizeAdjust = 0.5f; //wing is triangular + } + } + else + { + if (FerramAerospace.GetFARWingSweep(part) > 0) sizeAdjust = 0.5f; //wing isn't rectangular + } + } + } + else + sizeAdjust = 0.5f; //armor on one side, otherwise will have armor thickness on both sides of the panel, nonsensical + double weight + } + 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 (!isProcWing) //moving this here so any dynamic texture adjustment post spawn (TURD/TUFX/etc) will be grabbed by the defaultShader census + { //have this be done by RadarUtils when doing RCS snapshots instead? + var r = part.GetComponentsInChildren(); + for (int i = 0; i < r.Length; i++) + { + if (r[i].GetComponentInParent() != part) continue; // Don't recurse to child parts. + int key = r[i].material.GetInstanceID(); // The instance ID is unique for each object (not just component or gameObject). + if (defaultShader.ContainsKey(key)) continue; + defaultShader.Add(key, r[i].material.shader); //This doesn't grab part variants - parts with variants that are switched to will not register in defaultShader, and render as black in the RCS window + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: ARMOR: part shader on {r[i].GetComponentInParent().partInfo.name} is {r[i].material.shader.name}"); + if (r[i].material.HasProperty("_Color")) + { + if (!defaultColor.ContainsKey(key)) defaultColor.Add(key, r[i].material.color); + } + } + } + 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 || isProcWheel || isVariantPart) + { + 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 their 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 + if (isProcWing || isProcPart || isProcWheel || isVariantPart) + { + float Safetymass = 0; + var SST = part.GetComponent(); + if (SST != null) + { Safetymass = SST.FBmass + SST.FISmass; } + partMass = part.mass - armorMass - HullMassAdjust - Safetymass; + if (isVariantPart) + { + var r = part.GetComponentsInChildren(); + for (int i = 0; i < r.Length; i++) + { + if (r[i].GetComponentInParent() != part) continue; // Don't recurse to child parts. + int key = r[i].material.GetInstanceID(); // The instance ID is unique for each object (not just component or gameObject). + if (!defaultShader.ContainsKey(key)) + { + defaultShader.Add(key, r[i].material.shader); //grab materials for part variant variants when switching to that variant + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: ARMOR: part shader on {r[i].GetComponentInParent().partInfo.name} is {r[i].material.shader.name}"); + } + if (r[i].material.HasProperty("_Color")) + { + if (!defaultColor.ContainsKey(key)) defaultColor.Add(key, r[i].material.color); + } + } + } + } + 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 || isProcWheel || isVariantPart) + { + 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, float.MaxValue); + 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 (part.IsAero() && !isProcWing) + { + 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 = (partMass * 14000) * FerramAerospace.GetFARMassMult(part); //FAR massMult doubles stock masses (stock mass at 0.5 Mass-Strength; stock wings 700 HP per unit of Lift + } + else + 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 || isProcWheel || isVariantPart) + { + 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) * (isProcWheel ? 2.6f : 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) + { + 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 should scale with stock wings correctly (and if used as thicker structural elements, should scale with tanks of similar size) + armorVolume = ProceduralWing.GetPWingArea(part); + if (!part.name.Contains("B9.Aero.Wing.Procedural.Panel")) + { + previousEdgeLift = false; + 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 = (aeroVolume * 1400) * 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 + } + else + 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 + } + else + { + hitpoints = aeroVolume * 1200; + if (HighLogic.LoadedSceneIsFlight) + { + if (!FerramAerospace.CheckForFAR()) + { + var lift = part.FindModuleImplementing(); + if (lift != null) lift.deflectionLiftCoeff = 0; + } + } + } + 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.HP_THRESHOLD >= 100 && hitpoints > BDArmorySettings.HP_THRESHOLD) + { + var scale = BDArmorySettings.HP_THRESHOLD / (Mathf.Exp(1) - 1); + hitpoints = Mathf.Min(hitpoints, BDArmorySettings.HP_THRESHOLD * Mathf.Log(hitpoints / scale + 1)); + } + hitpoints = Mathf.Max(BDAMath.RoundToUnit(hitpoints, HpRounding), HpRounding); //fix ultralight parts like CM boxes having 0 HP + //hitpoints = Mathf.Round(hitpoints);//? + 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; + } + 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); + 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); + 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 * 1000 + "kg; 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 < reduceMass) reduceMass = totalArmorQty; //shouldn't be happening, but just in case + 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 (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[HPTracker] Debug: current Armor: " + Armor + "; ArmorRemaining: " + ArmorRemaining + "; ArmorPanel: " + ArmorPanel); + if (ArmorPanel) + { + Hitpoints = ArmorRemaining; // * armorVolume * 10; + if (Armor <= 0) + { + Debug.Log("[HPTracker] Debug: Armor integrity reduced to 0! Destroying panel"); + DestroyPart(); + } + } + totalArmorQty -= reduceMass; + armorMass = totalArmorQty * BDArmorySettings.ARMOR_MASS_MOD; //tons + if (armorMass <= 0) + { + armorMass = 0; + } + } + + public void overrideArmorSetFromConfig() + { + ArmorSet = true; + + if (ArmorThickness > 10 || ArmorPanel) //Mod part set to start with armor, or armor panel + { + startsArmored = true; + if (Armor < 0) // armor amount modified in SPH/VAB and does not = either the default nor the .cfg thickness + Armor = ArmorThickness;//set Armor amount to .cfg value + //See also ln 1183-1186 + } + if (maxSupportedArmor < 0) //hasn't been set in cfg + { + if (part.IsAero()) + { + if (isProcWing) + maxSupportedArmor = ProceduralWing.getPwingThickness(part); + else + maxSupportedArmor = 20; + } + else + { + maxSupportedArmor = ((Mathf.Min(partSize.x, partSize.y, partSize.z) / 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.MAX_ARMOR_LIMIT >= 0) + { + maxSupportedArmor = Mathf.Min(BDArmorySettings.MAX_ARMOR_LIMIT, maxSupportedArmor); + } + 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; + armorRadarReturnFactor = 1; + + 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 = part.IsMissile() ? 2 : 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; + armorRadarReturnFactor = armorInfo.radarReflectivity; + } + 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; + armorRadarReturnFactor = armorInfo.radarReflectivity; + } + CalculateRCSreduction(); + 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() || part.Modules.Contains("ModuleReactiveArmor")) 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 (isProcWing) + maxSupportedArmor = ProceduralWing.getPwingThickness(part); + if (BDArmorySettings.MAX_ARMOR_LIMIT >= 0) + { + maxSupportedArmor = Mathf.Min(BDArmorySettings.MAX_ARMOR_LIMIT, maxSupportedArmor); + } + 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(part)) + { + _hullConfigured = true; + part.gTolerance = (isAI || part.vesselType == VesselType.SpaceObject) ? 999 : ArmorPanel ? 50 : part.partInfo.partPrefab.gTolerance; //50 for now, armor panels should probably either be determined by armor material, or arbitrary 'weld/mounting bracket' strength + 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.IsFunctional() || 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; + hullRadarReturnFactor = hullInfo.radarMod; + hullType = hullInfo.name; + CalculateRCSreduction(); + 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 void CalculateRCSreduction() + { + if (ArmorTypeNum > 1 && Armor > 0) //if ArmorType != None and armor thickness != 0 + { + //float radarReflected = 1 - (armorRadarReturnFactor * (1 + Mathf.Log(Mathf.Max(Armor, 1), 100f))) //FIXME - this is busted, needs review + //vv less than ideal, but works for v1.0 + float radarReflected = Armor < 10 ? armorRadarReturnFactor + ((1 - armorRadarReturnFactor) / 10) * (10 - Armor) : armorRadarReturnFactor;//armor < 10 will have reduced radar absorbsion, else + //reflector armor/ translucent armor reflecting subsurface structure/RAM thicker than it needs to be, no change; + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker] radarReflectivity for {part.name} is {armorRadarReturnFactor}; radarRefected {radarReflected}"); + radarReflectivity = radarReflected; //radar return based on armor material + if (radarReflected > 1) //radar-translucent armor... + { + if (hullRadarReturnFactor < 1) // w/ radar absorbent structural elements + radarReflectivity = 1 - (hullRadarReturnFactor * (radarReflected - 1)); + if (hullRadarReturnFactor < 1) // w/ radar reflective structural elements + radarReflectivity = 1 - (hullRadarReturnFactor * (1 - radarReflected)); + } + } + else //(ArmorTypeNum < 1 || Armor < 1) //no armor, radar return based on hull material + { + radarReflectivity = hullRadarReturnFactor; + } + if (radarReflectivity > 2 || radarReflectivity < 0) // goes up to 2 in case of radar reflectors/anti-stealth coatings, etc + { + radarReflectivity = Mathf.Clamp(radarReflectivity, 0, 2); + } + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[ARMOR]: Radar return rating is " + radarReflectivity); + } + 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 71% rename from BDArmory.Core/Services/ModuleDamageService.cs rename to BDArmory/Damage/ModuleDamageService.cs index b493b82fa..7ad2d3bd5 100644 --- a/BDArmory.Core/Services/ModuleDamageService.cs +++ b/BDArmory/Damage/ModuleDamageService.cs @@ -1,15 +1,21 @@ -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) { var damageModule = p.Modules.GetModule(); + if (!damageModule) return; damageModule.ReduceArmor(armorMass); @@ -25,6 +31,7 @@ public override void ReduceArmor_svc(Part p, float armorMass) public override void SetDamageToPart_svc(Part p, float PartDamage) { var damageModule = p.Modules.GetModule(); + if (!damageModule) return; damageModule.SetDamage(PartDamage); @@ -40,6 +47,7 @@ public override void SetDamageToPart_svc(Part p, float PartDamage) public override void AddDamageToPart_svc(Part p, float PartDamage) { var damageModule = p.Modules.GetModule(); + if (!damageModule) return; damageModule.AddDamage(PartDamage); @@ -51,10 +59,25 @@ public override void AddDamageToPart_svc(Part p, float PartDamage) Operation = DamageOperation.Add }); } + public override void AddHealthToPart_svc(Part p, float PartDamage, bool overcharge) + { + var damageModule = p.Modules.GetModule(); + if (!damageModule) return; + damageModule.AddHealth(PartDamage, overcharge); + + PublishEvent(new DamageEventArgs() + { + VesselId = p.vessel.GetInstanceID(), + PartId = p.GetInstanceID(), + Damage = PartDamage, + Operation = DamageOperation.Add + }); + } public override void AddDamageToKerbal_svc(KerbalEVA kerbal, float damage) { var damageModule = kerbal.part.Modules.GetModule(); + if (!damageModule) return; damageModule.AddDamageToKerbal(kerbal, damage); @@ -77,7 +100,11 @@ public override float GetPartArmor_svc(Part p) float armor_ = Mathf.Max(1, p.Modules.GetModule().Armor); return armor_; } - + public override float GetPartMaxArmor_svc(Part p) + { + float armor_ = Mathf.Max(1, p.Modules.GetModule().StartingArmor); + return armor_; + } public override float GetMaxPartDamage_svc(Part p) { return p.Modules.GetModule().GetMaxHitpoints(); @@ -87,6 +114,14 @@ public override float GetMaxArmor_svc(Part p) { return p.Modules.GetModule().GetMaxArmor(); } + public override float GetArmorDensity_svc(Part p) + { + return p.Modules.GetModule().Density; + } + public override float GetArmorStrength_svc(Part p) + { + return p.Modules.GetModule().Strength; + } public override void DestroyPart_svc(Part p) { diff --git a/BDArmory/Damage/ModuleDrainEC.cs b/BDArmory/Damage/ModuleDrainEC.cs new file mode 100644 index 000000000..66cb92f7e --- /dev/null +++ b/BDArmory/Damage/ModuleDrainEC.cs @@ -0,0 +1,417 @@ +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using Expansions.Serenity; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BDArmory.Damage +{ + public class ModuleDrainEC : PartModule + { + public float incomingDamage = 0; //damage from EMP source + public float EMPDamage = 0; //total EMP buildup accrued + float EMPThreshold = 100; //craft get temporarily disabled + float BrickThreshold = 1000; //craft get permanently bricked + public bool softEMP = true; //can EMPdamage exceed EMPthreshold? + private bool disabled = false; //prevent further EMP buildup while rebooting + public bool bricked = false; //He's dead, jeb + public bool isMissile = false; + private float rebootTimer = 15; + //if for whatever reason players are manually firing EMPs at targets with AI/WM disabled, don't enable them when vessel reboots + private IBDAIControl activeAI = null; + private List activeWMs = []; + private List activeHinges = []; + private List activeServos = []; + List activeEngines = []; + List activeFSEngines = []; + public enum EMPbuildupTiers + { + None = 0, + Sensors = 1, + Engines = 2, + Controls = 3, + Weapons = 4, + Electrics = 5, + Command = 6 + } + public static readonly EMPbuildupTiers EMPTierMax = Enum.GetValues(typeof(EMPbuildupTiers)).Cast().Max(); + EMPbuildupTiers currentEMPBuildup = EMPbuildupTiers.None; + EMPbuildupTiers lastTierTriggered = EMPbuildupTiers.None; + float EMPTierThreshold = 10; + /// + /// So. basic idea is EMP base threshold determined by seat count - more command seats, more flight comps, more redundancy. + /// Probe cores look at SASServiceLevel, since that's a decent measure of how 'advanced' the probe is/what sort of electronics it'd have. + /// EMP Damage is then modified based on part mass and armor/hull materials (incl. that of the command part). + /// + void Start() + { + foreach (var moduleCommand in VesselModuleRegistry.GetModuleCommands(vessel)) + { + if (moduleCommand.part.CrewCapacity > 0) EMPThreshold += moduleCommand.part.CrewCapacity * 100; //cockpits worth 100 per seat + if (moduleCommand.minimumCrew == 0) + { + var CPULevel = moduleCommand.part.FindModuleImplementing(); + EMPThreshold += 10; + if (CPULevel != null) EMPThreshold += CPULevel.SASServiceLevel * 20; //drones worth 10-70, depending on capability + } + } + if (isMissile = vessel.IsMissile()) EMPThreshold = 5; + BrickThreshold = EMPThreshold * 5; + EMPTierThreshold = EMPThreshold / (int)EMPTierMax; + //EMPThreshold = (100 * (seatCount - ((1 - (vessel.GetTotalMass() / seatCount)) / 2)); + } + + void FixedUpdate() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDArmorySetup.GameIsPaused) return; + if (BDArmorySettings.PAINTBALL_MODE && !isMissile) + { + EMPDamage = 0; + incomingDamage = 0; + if (disabled) EnableVessel(EMPbuildupTiers.None); + return; + } + if (!bricked) + { + if (EMPDamage > 0 || incomingDamage > 0) + { + UpdateEMPLevel(); + } + } + + } + + void UpdateEMPLevel() + { + if ((!disabled || (disabled && !softEMP)) && incomingDamage > 0) + { + EMPDamage += incomingDamage; //only accumulate EMP damage if it's hard EMP or craft isn't disabled + incomingDamage = 0; //reset incoming damage amount + if (disabled && !softEMP) + { + if (rebootTimer > 0) + { + rebootTimer += incomingDamage / 100; //if getting hit by new sources of hard EMP, add to reboot timer + } + } + } + if (disabled) + { + //EMPDamage = Mathf.Clamp(EMPDamage - 5 * TimeWarp.fixedDeltaTime, 0, Mathf.Infinity); //speed EMP cooldown, if electrolaser'd takes about ~10 sec to reboot. may need to be reduced further + //fatal if fast+low alt, but higher alt or good glide ratio is survivable + if (rebootTimer > 0) + { + rebootTimer -= 1 * TimeWarp.fixedDeltaTime; + } + else + { + EMPDamage = 0; + } + } + else + { + EMPDamage = Mathf.Clamp(EMPDamage - 5 * TimeWarp.fixedDeltaTime, 0, Mathf.Infinity); //have EMP buildup dissipate over time + } + if (isMissile && EMPDamage > 10) + { + foreach (Part p in vessel.parts) + { + var MB = p.FindModuleImplementing(); + if (MB != null) + { + MB.guidanceActive = false; + } + } + bricked = true; + return; + } + //if (EMPDamage > EMPThreshold && !bricked && !disabled) //does the damage exceed the soft cap, but not the hard cap? + if (!bricked && !disabled && EMPDamage > 0) //does the damage exceed the soft cap, but not the hard cap? + { + currentEMPBuildup = (EMPbuildupTiers)Math.Min(Mathf.FloorToInt(EMPDamage / EMPTierThreshold), (int)EMPTierMax); + //Debug.Log($"[BDArmory.ModuleDrainEC]: currentEMPBuildup Tier on {vessel.GetName()}: {currentEMPBuildup}. last Tier Triggered: {lastTierTriggered}"); + if (currentEMPBuildup > lastTierTriggered) + { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Increasing EMP tier from {lastTierTriggered} to {currentEMPBuildup} on {vessel.GetName()}"); + DisableVessel(currentEMPBuildup); + } + if (currentEMPBuildup < lastTierTriggered) + { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Lowering EMP tier from {lastTierTriggered} to {currentEMPBuildup} on {vessel.GetName()}"); + EnableVessel(currentEMPBuildup); + } + } + + if (EMPDamage > BrickThreshold && !bricked) //does the damage exceed the hard cap? + { + bricked = true; //if so brick the craft + var message = vessel.vesselName + " is bricked!"; + 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; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleDrainEC]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + EnableVessel(EMPbuildupTiers.None); + } + } + private void DisableVessel(EMPbuildupTiers EMPbuildup) + { + if (EMPbuildup >= EMPbuildupTiers.Sensors && lastTierTriggered < EMPbuildupTiers.Sensors) //deactivate sensors + { + foreach (var radar in VesselModuleRegistry.GetModules(vessel)) + { + if (radar.radarEnabled) + radar.DisableRadar(); + } + foreach (var spaceRadar in VesselModuleRegistry.GetModules(vessel)) + { + if (spaceRadar.radarEnabled) + spaceRadar.DisableRadar(); + } + foreach (var camera in VesselModuleRegistry.GetModules(vessel)) + { + if (camera.cameraEnabled) + camera.DisableCamera(); + } + foreach (var IRST in VesselModuleRegistry.GetModules(vessel)) + { + if (IRST.enabled) + IRST.DisableIRST(); + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Disabling Sensors on {vessel.GetName()}"); + } + if (EMPbuildup >= EMPbuildupTiers.Engines && lastTierTriggered < EMPbuildupTiers.Engines) //deactivate Engines + { + foreach (var engine in VesselModuleRegistry.GetModuleEngines(vessel)) + { + engine.Shutdown(); + engine.allowRestart = false; + activeEngines.Add(engine); + } + activeFSEngines = FireSpitter.GetActiveEngines(vessel); + FireSpitter.SetActiveFSEngines(activeFSEngines, false); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Disabling Engines on {vessel.GetName()}"); + } + if (EMPbuildup >= EMPbuildupTiers.Controls && lastTierTriggered < EMPbuildupTiers.Controls) //deactivate control surfaces and other hydraulics + { + foreach (var ctrl in VesselModuleRegistry.GetModules(vessel)) + { + ctrl.authorityLimiter /= 10; //simpler than having to store all control surface values in a list somewhere + ctrl.ctrlSurfaceRange /= 10; + } + foreach (var turret in VesselModuleRegistry.GetModules(vessel)) + { + turret.yawSpeedDPS /= 100; + turret.pitchSpeedDPS /= 100; + } + foreach (var hinge in VesselModuleRegistry.GetModules(vessel)) + { + if (hinge.servoMotorIsEngaged) + hinge.servoMotorIsEngaged = false; + activeHinges.Add(hinge); + } + foreach (var servo in VesselModuleRegistry.GetModules(vessel)) + { + if (servo.servoMotorIsEngaged) + servo.servoMotorIsEngaged = false; + activeServos.Add(servo); + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Disabling ControlSurfaces on {vessel.GetName()}"); + } + if (EMPbuildup >= EMPbuildupTiers.Weapons && lastTierTriggered < EMPbuildupTiers.Weapons) //deactivate Weapons + { + foreach (var weapon in VesselModuleRegistry.GetModuleWeapons(vessel)) + { + weapon.weaponState = ModuleWeapon.WeaponStates.Locked; //prevent weapons from firing + } + foreach (var missile in VesselModuleRegistry.GetMissileBases(vessel)) + { + missile.engageRangeMax /= 10000; //prevent weapons from firing + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Disabling Weapons on {vessel.GetName()}"); + } + if (EMPbuildup >= EMPbuildupTiers.Electrics && lastTierTriggered < EMPbuildupTiers.Electrics) //drain electrics. + { + foreach (Part p in vessel.parts) + { + PartResource r = p.Resources.Where(pr => pr.resourceName == "ElectricCharge").FirstOrDefault(); + if (r != null) + { + if (r.amount >= 0) + { + p.RequestResource("ElectricCharge", r.amount); + //Random battery Fire if 'Fires' Battledamage enabled? + } + } + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleDrainEC]: Shorting Electrics on {vessel.GetName()}"); + } + if (EMPbuildup >= EMPbuildupTiers.Command && lastTierTriggered < EMPbuildupTiers.Command) //deactivate control + { + disabled = true; + foreach (var command in VesselModuleRegistry.GetModuleCommands(vessel)) + { + command.minimumCrew *= 10; //disable vessel control + } + // Store the active AI if there was one. + var AI = vessel.ActiveController().AI; + activeAI = (AI != null && AI.pilotEnabled) ? AI : null; + if (AI != null && AI.pilotEnabled) AI.DeactivatePilot(); //disable AI + + // Store the guardMode state of the WMs. + activeWMs.Clear(); + foreach (var wm in VesselModuleRegistry.GetMissileFires(vessel).Where(wm => wm != null)) + { + if (wm.guardMode) activeWMs.Add(wm); + wm.guardMode = false; //disable guardmode + wm.debilitated = true; //for weapon selection and targeting; + } + rebootTimer = BDArmorySettings.WEAPON_FX_DURATION; + var message = "Disabling " + vessel.vesselName + " for " + rebootTimer + "s due to EMP damage"; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleDrainEC]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + + var empFX = Instantiate(GameDatabase.Instance.GetModel("BDArmory/FX/Electroshock"), + vessel.rootPart.transform.position, Quaternion.identity); + empFX.SetActive(true); + empFX.transform.SetParent(vessel.rootPart.transform); + empFX.AddComponent(); + } + lastTierTriggered = EMPbuildup; + } + private void EnableVessel(EMPbuildupTiers EMPbuildup) + { + if (EMPbuildup < EMPbuildupTiers.Sensors && lastTierTriggered >= EMPbuildupTiers.Sensors) //Reactivate sensors + { + foreach (var radar in VesselModuleRegistry.GetModules(vessel)) + { + if (!radar.radarEnabled) + radar.EnableRadar(); + } + foreach (var spaceRadar in VesselModuleRegistry.GetModules(vessel)) + { + if (!spaceRadar.radarEnabled) + spaceRadar.EnableRadar(); + } + foreach (var camera in VesselModuleRegistry.GetModules(vessel)) + { + if (!camera.cameraEnabled) + camera.EnableCamera(); + } + foreach (var IRST in VesselModuleRegistry.GetModules(vessel)) + { + if (!IRST.enabled) + IRST.EnableIRST(); + } + } + if (EMPbuildup < EMPbuildupTiers.Engines && lastTierTriggered >= EMPbuildupTiers.Engines) //reactivate Engines + { + foreach (var engine in activeEngines.Where(e => e != null)) + { + engine.allowRestart = true; + var mme = engine.part.FindModuleImplementing(); + if (mme == null) engine.Activate(); + else + { + if (mme.runningPrimary) mme.PrimaryEngine.Activate(); + else mme.SecondaryEngine.Activate(); + } + } + activeEngines.Clear(); + FireSpitter.SetActiveFSEngines(activeFSEngines, true); + } + if (EMPbuildup < EMPbuildupTiers.Controls && lastTierTriggered >= EMPbuildupTiers.Controls) //reactivate control surfaces + { + foreach (var ctrl in VesselModuleRegistry.GetModules(vessel)) + { + ctrl.authorityLimiter *= 10; + ctrl.ctrlSurfaceRange *= 10; + } + foreach (var turret in VesselModuleRegistry.GetModules(vessel)) + { + turret.yawSpeedDPS *= 100; + turret.pitchSpeedDPS *= 100; + } + foreach (var servo in activeServos) servo.servoMotorIsEngaged = true; + foreach (var hinge in activeHinges) hinge.servoMotorIsEngaged = true; + } + if (EMPbuildup < EMPbuildupTiers.Weapons && lastTierTriggered >= EMPbuildupTiers.Weapons) //reactivate Weapons + { + foreach (var weapon in VesselModuleRegistry.GetModuleWeapons(vessel)) + { + if (weapon.isAPS) + weapon.EnableWeapon(); //reactivate APS + else + weapon.DisableWeapon(); //reset WeaponState + } + foreach (var missile in VesselModuleRegistry.GetMissileBases(vessel)) + { + missile.engageRangeMax *= 10000; + } + } + if (EMPbuildup < EMPbuildupTiers.Command && lastTierTriggered >= EMPbuildupTiers.Command) //reactivate control + { + foreach (var command in VesselModuleRegistry.GetModuleCommands(vessel)) + { + { + command.minimumCrew /= 10; //more elegant than a dict storing every crew part's cap to restore to original amount + } + } + // Previously enabled active AI is still attached, fire it up again. + if (activeAI != null && activeAI.vessel == vessel) + activeAI.ActivatePilot(); + activeAI = null; + + // Restore the guardMode state of the WMs. + foreach (var wm in VesselModuleRegistry.GetMissileFires(vessel).Where(wm => wm != null)) wm.debilitated = false; + foreach (var wm in activeWMs) wm.guardMode = true; + activeWMs.Clear(); + } + disabled = false; + lastTierTriggered = EMPbuildup; + } + } + + internal class EMPShock : MonoBehaviour + { + public void Start() + { + foreach (var pe in gameObject.GetComponentsInChildren()) + { + EffectBehaviour.AddParticleEmitter(pe); + pe.emit = true; + StartCoroutine(TimerRoutine()); + } + } + IEnumerator TimerRoutine() + { + yield return new WaitForSecondsFixed(5); + Destroy(gameObject); + } + + private void OnDestroy() + { + foreach (var pe in gameObject.GetComponentsInChildren()) + { + EffectBehaviour.RemoveParticleEmitter(pe); + } + + } + } +} diff --git a/BDArmory/Damage/ModuleDrainIntakes.cs b/BDArmory/Damage/ModuleDrainIntakes.cs new file mode 100644 index 000000000..c97b7ec7e --- /dev/null +++ b/BDArmory/Damage/ModuleDrainIntakes.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +using BDArmory.Utils; +using BDArmory.Control; + +namespace BDArmory.Damage +{ + public class ModuleDrainIntakes : PartModule + { + public float drainRate = 999; + public float drainDuration = 20; + private bool initialized = false; + + public void Update() + { + if (HighLogic.LoadedSceneIsFlight) + { + drainDuration -= Time.deltaTime; + if (drainDuration <= 0) + { + using (var intake = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (intake.MoveNext()) + { + if (intake.Current == null) continue; + intake.Current.intakeEnabled = true; + } + var WM = VesselModuleRegistry.GetMissileFire(vessel); + if (WM != null) + { + WM.debilitated = false; //for weapon selection and targeting; + } + part.RemoveModule(this); + } + } + if (!initialized) + { + //Debug.Log("[BDArmory.ModuleDrainIntakes]: " + this.part.name + "choked!"); + initialized = true; + using (var intake = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (intake.MoveNext()) + { + if (intake.Current == null) continue; + intake.Current.intakeEnabled = false; + } + var WM = VesselModuleRegistry.GetMissileFire(vessel); + if (WM != null) + { + WM.debilitated = true; //for weapon selection and targeting; + } + } + } + } +} + diff --git a/BDArmory/Damage/ModuleMassAdjust.cs b/BDArmory/Damage/ModuleMassAdjust.cs new file mode 100644 index 000000000..01b1ea47c --- /dev/null +++ b/BDArmory/Damage/ModuleMassAdjust.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +using BDArmory.UI; +using BDArmory.Settings; + +namespace BDArmory.Damage +{ + public class ModuleMassAdjust : PartModule, IPartMassModifier + { + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => massMod; + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.CONSTANTLY; + + public float massMod = 0f; //mass to add to part, in tons + public float duration = 15; //duration of effect, in seconds + private float startMass = 0; + private bool hasSetup = false; + + private void EndEffect() + { + massMod = 0; + part.RemoveModule(this); + //Debug.Log("[BDArmory.ModuleMassAdjust]: ME field expired, " + this.part.name + "mass: " + this.part.mass); + } + + void FixedUpdate() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDArmorySetup.GameIsPaused) return; + + duration -= TimeWarp.fixedDeltaTime; + + if (duration <= 0) + { + EndEffect(); + } + if (!hasSetup) + { + SetupME(); + } + } + + private void SetupME() + { + startMass = this.part.mass; + hasSetup = true; + 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 + { + massMod = Mathf.Clamp(massMod, -0.95f * startMass, 0); //clamp mod mass to min of 5% of original value to prevent negative mass and whatever Kraken that summons + } + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/.note b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/.note new file mode 100644 index 000000000..2f42be5df --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/.note @@ -0,0 +1,3 @@ +Note: +- Unity doesn't maintain backwards compatibility for shader bundles, so for shader bundles to work on KSP 1.9.1 and onwards, they need to be created with Unity 2019.2.2f1 or earlier. +- However, Unity has broken 2019.2.2f1 and probably won't fix it, so using Unity 2018.4.36f1 to generate new shader bundles seems to be the best option. \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_linux.bundle b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_linux.bundle index ef77b51c1..67ae93e2f 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_linux.bundle and b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_linux.bundle differ diff --git a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_macosx.bundle b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_macosx.bundle index d90daf302..7972e9654 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_macosx.bundle and b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_macosx.bundle differ diff --git a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_windows.bundle b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_windows.bundle index 0188d5c32..ed9c4a183 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_windows.bundle and b/BDArmory/Distribution/GameData/BDArmory/AssetBundles/bdarmoryshaders_windows.bundle differ diff --git a/BDArmory/Distribution/GameData/BDArmory/BDArmory.version b/BDArmory/Distribution/GameData/BDArmory/BDArmory.version index 233d2912f..9722fd61e 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, + "MINOR":12, "PATCH":0, - "BUILD":3 + "BUILD":0 }, "KSP_VERSION": { "MAJOR":1, - "MINOR":11, - "PATCH":0 + "MINOR":12, + "PATCH":5 }, "KSP_VERSION_MIN": { @@ -30,7 +30,7 @@ "KSP_VERSION_MAX": { "MAJOR":1, - "MINOR":11, + "MINOR":12, "PATCH":999 } } diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg new file mode 100644 index 000000000..2bc217bfb --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg @@ -0,0 +1,157 @@ +//////////////////////////////////////////////////////// +// Default Armor Config - Do Not Change +//////////////////////////////////////////////////////// + +ARMOR +{ + name = def // do not change this! + 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 + radarReflectivity = 1 //radar absorbsion, 0-1 scale, 0 is full radar absorbsion, 1 is full reflectivity + Cost = 100 //cost per cubic meter +} + +//////////////////////////////////////////////////////// +// End Default Armor Config +//////////////////////////////////////////////////////// +ARMOR +{ + name = None //based off of Aluminium, since most parts assumed to be made if it + 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 + radarReflectivity = 1 + 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 + radarReflectivity = 1 + Cost = 250 +} + +ARMOR +{ + name = Titanium + Density = 4506 + Strength = 552 + Hardness = 2000 + Yield = 330 + YoungModulus = 116 + Ductility = 0.58 + Diffusivity = 22 + SafeUseTemp = 703 //Titanium loses tensile strength above 430 C + radarReflectivity = 1 + Cost = 5000 +} + +ARMOR +{ + name = Beryllium + Density = 1850 + Strength = 370 + Hardness = 1320 + Yield = 240 + YoungModulus = 287 + Ductility = 0.07 + Diffusivity = 200 + SafeUseTemp = 1830 + radarReflectivity = 1 + Cost = 8000 +} + +ARMOR +{ + name = Aramid Fibre + Density = 1440 + Strength = 300 + Hardness = 10 + Yield = 300 + YoungModulus = 82.2 + Ductility = 0.035 + Diffusivity = 0.04 + SafeUseTemp = 770 + radarReflectivity = 1.1 + Cost = 10000 +} + +ARMOR +{ + name = S-Glass Composite + 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 + radarReflectivity = 1.2 //something something fiberlgass transparent to radar and letting sub-surface structure be reflected + Cost = 6600 +} + +ARMOR +{ + name = Depleted Uranium + Density = 19000 + Strength = 1720 + Hardness = 3850 + Yield = 965 + YoungModulus = 170 + Ductility = 0.15 + Diffusivity = 12 + SafeUseTemp = 1623 + radarReflectivity = 1 + 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 + radarReflectivity = 1 + Cost = 600 +} +ARMOR +{ + // Based on https://www.laird.com/sites/default/files/2021-10/RFP-DS-BSR%20MFS%2022092021.pdf + // List of other RAM products here: https://www.laird.com/sites/default/files/2023-02/Brochure-%20Absorber%20Infosheet_Military_Aerospace.pdf + name = Radar Absorbent Coating + Density = 4200 + Strength = 5 + Hardness = 514 // ~70 Shore hardness + Yield = 8 // Best guess from https://www.matweb.com/search/DataSheet.aspx?MatGUID=cbe7a469897a47eda563816c86a73520&ckck=1 + YoungModulus = 0.1 // Best guess from https://www.matweb.com/search/DataSheet.aspx?MatGUID=cbe7a469897a47eda563816c86a73520&ckck=1 + Ductility = 0.5 + Diffusivity = 4 // best guess from https://ui.adsabs.harvard.edu/abs/2020IJT....41...12J/abstract + SafeUseTemp = 443 + radarReflectivity = 0.1 // Will need to balance this, corresponds to -10 dB + Cost = 40000 +} diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg index 36627ef8e..836109247 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg @@ -5,16 +5,28 @@ BULLET { name = def // do not change this! + DisplayName = Default Bullet //human-readable name for RMB info/ammo selection/etc. caliber = 30 bulletVelocity = 1109 bulletMass = 0.3880 bulletDragTypeName = AnalyticEstimate apBulletMod = 1 //Armor penetration depth multiplier - subProjectileCount = 1 - //HE Bullet Values - explosive = True + projectileCount = 1 //projectiles fired per triggerpull, for shotguns/etc + //Bullet stats + 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) + beehive = false //round separates into multiple submunitions on Timed/Proximity Fuze detonation + subMunitionType = def; 1// only needed if beehive = true, else leave blank/don't add. integer after type specifies # of subprojectiles released + projectileTTL= 0.5 //time to live, used by submunitions deployed from beehive rounds to set how long they persist for + subProjectileDispersion = -1 //spread angle of subprojectiles, leave -1 for spread determined by vel and pellet quantity + massMod = 0 //mass (in tons) added to hit part. negative values will reduce mass instead + impulse = 0// impulse applied to hit part. negative values will pull instead of push tntMass = 0.001 - fuzeType = None // choose from None, Timed, Proximity, or Flak + fuzeType = None // choose from None, Timed, Proximity, Flak, Impact, Delay, or Penetrating + guidanceDPS = 0 //for homing bullets, turnrate/s in degrees + guidanceRange = -1 //for homing bullets, homing range in m (set to -1 for infinite range) //Tracer settings projectileColor = 255, 15, 0, 128//RGBA 0-255, final color of shot if fadeColor = True fadeColor = False //fade color from startColor to projectileColor? @@ -24,21 +36,22 @@ BULLET //////////////////////////////////////////////////////// // End Default Bullet Config //////////////////////////////////////////////////////// - BULLET { name = 7.62x39mmBullet + DisplayName = 7.62 Kalashnikov caliber = 7.62 bulletVelocity = 718 bulletMass = 0.0965 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 254, 185, 0, 160 //RGBA 0-255 fadeColor = False startColor = 254, 185, 25, 120 - subProjectileCount = 1 + projectileCount = 1 apBulletMod = 1 bulletDragTypeName = AnalyticEstimate } @@ -46,54 +59,60 @@ BULLET BULLET { name = 7.7x56mmBullet + 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 projectileColor = 145, 249, 163, 160 //RGBA 0-255 fadeColor = False startColor = 145, 249, 160, 120 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //15mm penetration steel bulletDragTypeName = AnalyticEstimate } BULLET { name = 7.92mmBullet + 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 projectileColor = 222, 249, 242, 160 //RGBA 0-255 fadeColor = False startColor = 222, 249, 242, 120 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //15mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 9mmBullet + DisplayName = 9mm Bullet caliber = 9 bulletVelocity = 380 bulletMass = 0.114 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 212, 145, 2, 160 //RGBA 0-255 fadeColor = False startColor = 212, 145, 2, 120 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //11mm bulletDragTypeName = AnalyticEstimate } @@ -101,15 +120,17 @@ BULLET { // RaufossMk211 name = 12.7mmBullet + 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 = None + projectileCount = 1 + fuzeType = Impact projectileColor = 255, 50, 0, 160 //RGBA 0-255 fadeColor = True startColor = 255, 105, 25, 120 @@ -117,17 +138,39 @@ BULLET BULLET { - name = 12.7mmHEBullet + name = 12.7mmAPIBullet + DisplayName = .50 API caliber = 12.7 bulletVelocity = 890 - bulletMass = .16 + bulletMass = .057 //HE Bullet Values - explosive = True - tntMass = .14 - apBulletMod = 0.8 + explosive = None + incendiary = True + tntMass = 0 + apBulletMod = 1.2 //36mm bulletDragTypeName = AnalyticEstimate - subProjectileCount = 1 + projectileCount = 1 fuzeType = None + projectileColor = 215, 175, 0, 128 + fadeColor = True + startColor = 255, 215, 25, 0 +} + +BULLET +{ + name = 12.7mmHEBullet + DisplayName = .50 HE + caliber = 12.7 + bulletVelocity = 890 + bulletMass = .052 + //HE Bullet Values + explosive = Standard + incendiary = False + tntMass = .008 + apBulletMod = 0.8 //23mm + bulletDragTypeName = AnalyticEstimate + projectileCount = 1 + fuzeType = Impact projectileColor = 255, 15, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 @@ -136,217 +179,342 @@ BULLET BULLET { name = 20mmBullet //USN 20mm + DisplayName = USN 20mm caliber = 20 bulletVelocity = 880 bulletMass = 0.168 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 255, 255, 180, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //40mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 20mmShortBullet //Mgeschoss + DisplayName = 20mm Mgeschoss caliber = 20 bulletVelocity = 810 bulletMass = 0.095 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.09 - fuzeType = None + fuzeType = Impact projectileColor = 206, 255, 10, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.46 //11mm bulletDragTypeName = AnalyticEstimate } BULLET { - name = 20x102mmBullet //Vulcan AP + 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 projectileColor = 255, 60, 0, 128 fadeColor = False startColor = 255, 105, 0, 64 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //38mm //6.4mm pen at 1000m bulletDragTypeName = AnalyticEstimate } 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 + projectileCount = 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 = None + fuzeType = Impact projectileColor = 255, 15, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.9 + projectileCount = 1 + apBulletMod = 0.75 //28mm bulletDragTypeName = AnalyticEstimate } +BULLET +{ + name = 20x102mmEMPBullet //Vulcan HE + DisplayName = BlueScreen + caliber = 20 + bulletVelocity = 950 + bulletMass = 0.1101 + //HE Bullet Values + explosive = None + incendiary = False + EMP = True + tntMass = 0 + fuzeType = Impact + projectileColor = 255, 15, 0, 128 + fadeColor = False + startColor = 128, 128, 128, 0 + projectileCount = 1 + apBulletMod = 0.1 + bulletDragTypeName = AnalyticEstimate +} +BULLET +{ + name = 20x102mmGravBullet //Vulcan HE + DisplayName = Gravitic + caliber = 20 + bulletVelocity = 950 + bulletMass = 0.1101 + //HE Bullet Values + explosive = None + incendiary = False + massMod = 0.1 + impulse = 2000 + tntMass = 0 + fuzeType = Impact + projectileColor = 255, 15, 0, 128 + fadeColor = False + startColor = 128, 128, 128, 0 + projectileCount = 1 + apBulletMod = 0.1 + bulletDragTypeName = AnalyticEstimate +} BULLET { name = 23x115mmBullet //Afansev-Makarov + DisplayName = 23mm Shell caliber = 23 bulletVelocity = 720 bulletMass = 0.1900 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.2534 - fuzeType = None + fuzeType = Impact projectileColor = 254, 35, 2, 160 //RGBA 0-255 fadeColor = False startColor = 245, 35, 2, 120 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 1 //25mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 23x152mmBullet //Vya-23 + DisplayName = 23x152mm Shell caliber = 23 bulletVelocity = 1020 bulletMass = 0.19 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.2534 - fuzeType = None + fuzeType = Impact projectileColor = 254, 35, 2, 160 //RGBA 0-255 fadeColor = False startColor = 245, 35, 2, 120 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //34mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 25mmBullet + DisplayName = 25mm Bullet caliber = 25 bulletVelocity = 1020 bulletMass = 0.195 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 254, 245, 21, 160 //RGBA 0-255 fadeColor = False startColor = 245, 245, 21, 120 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //41mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 25x115mmBullet + DisplayName = 25mm Shell caliber = 25 bulletVelocity = 720 bulletMass = 0.19 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.2534 - fuzeType = None + fuzeType = Impact projectileColor = 254, 35, 2, 160 //RGBA 0-255 fadeColor = False startColor = 245, 35, 2, 120 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //19mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 25x137mmBullet + DisplayName = 25mm Bushmaster caliber = 25 bulletVelocity = 1020 bulletMass = 0.19 //HE Bullet Values - explosive = True - tntMass = 0.2534 - fuzeType = None + explosive = Standard + incendiary = True + tntMass = 0.05 //Originally 0.2534 + fuzeType = Impact + projectileColor = 254, 35, 2, 160 //RGBA 0-255 + fadeColor = False + startColor = 245, 35, 2, 120 + projectileCount = 1 + apBulletMod = 0.8 //33mm + bulletDragTypeName = AnalyticEstimate +} + +BULLET +{ + name = 25x137mmAPEXBullet + DisplayName = 25mm APEX + caliber = 25 + bulletVelocity = 970 + bulletMass = 0.223 + //HE Bullet Values + explosive = Standard + incendiary = False + tntMass = 0.05 + fuzeType = Impact projectileColor = 254, 35, 2, 160 //RGBA 0-255 fadeColor = False startColor = 245, 35, 2, 120 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.95 //40mm bulletDragTypeName = AnalyticEstimate } +BULLET +{ + name = 25x173mmFlak //flak + DisplayName = 25mm FlaK + caliber = 25 + bulletVelocity = 1020 + bulletMass = 0.19 + //HE Bullet Values + explosive = Standard + incendiary = False + tntMass = 0.1 + fuzeType = Flak + projectileColor = 115, 115, 70, 128 + fadeColor = False + startColor = 128, 128, 128, 0 + projectileCount = 1 + apBulletMod = 0.7 + bulletDragTypeName = NumericalIntegration +} + BULLET { name = 30mmBullet //Mk 108 + DisplayName = 30mm Mgeschoss caliber = 30 bulletVelocity = 540 bulletMass = 0.33 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.12 - fuzeType = None + fuzeType = Impact projectileColor = 255, 130, 0, 128 fadeColor = False startColor = 128, 128, 128, 120 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 0.87 //15mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 30x165Bullet //gSH-301 + DisplayName = 30mm gSH caliber = 30 bulletVelocity = 870 bulletMass = 0.3880 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 255, 0, 0, 128 fadeColor = True startColor = 255, 255, 0, 120 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //41mm bulletDragTypeName = AnalyticEstimate } BULLET { - name = 30x173Bullet //GAU AP + name = 30x173Bullet //GAU API + DisplayName = 30mm API caliber = 30 bulletVelocity = 1109 bulletMass = 0.3880 //HE Bullet Values - explosive = False + explosive = None + incendiary = True //sure, why not? tntMass = 0 fuzeType = None projectileColor = 255, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 120 - subProjectileCount = 1 - apBulletMod = 1.1 + projectileCount = 1 + apBulletMod = 1.1 //65mm bulletDragTypeName = AnalyticEstimate } @@ -354,198 +522,244 @@ BULLET BULLET { name = 30x173HEBullet //GAU HE + DisplayName = 30mm HE caliber = 30 bulletVelocity = 1109 bulletMass = 0.3880 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.077 // originally 0.254 - fuzeType = None + fuzeType = Impact projectileColor = 255, 20, 0, 128 fadeColor = True startColor = 255, 30, 0, 32 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //47mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 30x173VTBullet //Goalkeeper + DisplayName = 30mm Airburst caliber = 30 bulletVelocity = 1109 bulletMass = 0.3880 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.0770 // originally 0.254 fuzeType = Timed projectileColor = 255, 20, 0, 128 fadeColor = True startColor = 255, 30, 0, 32 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //47 bulletDragTypeName = AnalyticEstimate } - BULLET { name = 35x228HEBullet //oerlikon + DisplayName = Oerlikon HE caliber = 35 bulletVelocity = 1175 - bulletMass = 0.550 + bulletMass = 0.51 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.098 + fuzeType = Impact + projectileColor = 255, 250, 0, 128 + fadeColor = True + startColor = 255, 120, 0, 32 + projectileCount = 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 = Standard + incendiary = False + beehive = True + subMunitionType = 35x228AHEADPellet; 30 //IRL 152! + tntMass = 0.01 fuzeType = Timed projectileColor = 255, 250, 0, 128 fadeColor = True startColor = 255, 240, 0, 32 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //55 bulletDragTypeName = AnalyticEstimate } BULLET { - name = 35x228AHEADBullet //oerlikon + name = 35x228AHEADPellet //oerlikon + DisplayName = AHEAD Submunition caliber = 10 - bulletVelocity = 1175 - bulletMass = 0.015 + bulletVelocity = 0 //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 + projectileTTL= 0.5 bulletDragTypeName = AnalyticEstimate - subProjectileCount = 50 //IRL 152! + projectileCount = 1 + subProjectileDispersion = 0.75 //degrees of spread } BULLET { name = 40x53HEBullet //40mm grenade? + DisplayName = 40mm Grenade caliber = 40 bulletVelocity = 242 bulletMass = 0.3500 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 0.25 - fuzeType = None + fuzeType = Impact projectileColor = 255, 242, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //2.8mm bulletDragTypeName = AnalyticEstimate } 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 = None + fuzeType = Impact projectileColor = 167, 218, 240, 128 fadeColor = False startColor = 167, 218, 240, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //13mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 57mmBullet - caliber = 70 + DisplayName = 57mm Shell + caliber = 57 bulletVelocity = 1035 bulletMass = 2.4 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 1.2 - fuzeType = None + fuzeType = Impact projectileColor = 240, 190, 0, 128 fadeColor = False startColor = 240, 190, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //73mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 75mmBullet + DisplayName = 75mm HE caliber = 75 bulletVelocity = 620 bulletMass = 6.8 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 5.44 - fuzeType = None + fuzeType = Impact projectileColor = 248, 248, 230, 128 fadeColor = False startColor = 248, 247, 230, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //48mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 76x636mmBullet + DisplayName = 76mm HE caliber = 62 bulletVelocity = 915 bulletMass = 6.8 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 5.44 - fuzeType = None + fuzeType = Impact projectileColor = 255, 242, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //88mm bulletDragTypeName = AnalyticEstimate } BULLET { name = TungstenBullet + DisplayName = Tungsten HVAP caliber = 105 bulletVelocity = 5000 bulletMass = 1.25 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 122, 170, 240, 128 fadeColor = False startColor = 122, 170, 240, 0 - subProjectileCount = 1 - apBulletMod = 1.5 + projectileCount = 1 + apBulletMod = 1 //500mm bulletDragTypeName = AnalyticEstimate } BULLET { name = WMDBullet + DisplayName = Atomic Shell caliber = 200 bulletVelocity = 3000 bulletMass = 500 //HE Bullet Values - explosive = True - tntMass = 400 - fuzeType = None + explosive = None + incendiary = False + nuclear = True + EMP = True + tntMass = 4 + fuzeType = Impact projectileColor = 255, 242, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //N/A bulletDragTypeName = AnalyticEstimate } @@ -553,18 +767,20 @@ BULLET BULLET { name = LaserBolt + DisplayName = Laser caliber = 30 bulletVelocity = 4000 bulletMass = 0.125 //HE Bullet Values - explosive = False + explosive = None + incendiary = True tntMass = 0 fuzeType = None projectileColor = 0, 240, 100, 128 fadeColor = False startColor = 0, 240, 100, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 0.1 //14mm bulletDragTypeName = AnalyticEstimate } @@ -572,18 +788,20 @@ BULLET BULLET { name = 90mmBullet + DisplayName = 90mm Bullet caliber = 90 bulletVelocity = 850 - bulletMass = 19 + bulletMass = 14 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 255, 242, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //133mm bulletDragTypeName = AnalyticEstimate } @@ -591,18 +809,20 @@ BULLET BULLET { name = 100mmBullet + 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 projectileColor = 255, 242, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //165mm bulletDragTypeName = AnalyticEstimate } @@ -610,53 +830,59 @@ BULLET BULLET { name = 105mmBullet //slug + 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 projectileColor = 255, 242, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //187mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 105mmBulletAE //mortar + DisplayName = 105mm Mortar caliber = 105 bulletVelocity = 150 bulletMass = 25 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 24.5 - fuzeType = None + fuzeType = Impact projectileColor = 120, 120, 230, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //6mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 105mmBulletNI //flak + DisplayName = 105mm Flak caliber = 105 bulletVelocity = 1070 bulletMass = 17.8 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 35 fuzeType = Flak projectileColor = 115, 115, 70, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 + projectileCount = 1 apBulletMod = 1 bulletDragTypeName = NumericalIntegration } @@ -664,36 +890,40 @@ BULLET BULLET { name = 105mmGSBullet //cannister + DisplayName = 105mm Cannister caliber = 15 bulletVelocity = 900 bulletMass = 0.096 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 250, 250, 230, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 20 - apBulletMod = 1 + projectileCount = 20 + apBulletMod = 1 //35mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 105mmHEBullet //explosive + DisplayName = 105mm HE caliber = 105 bulletVelocity = 1020 bulletMass = 19.6 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 15.68 - fuzeType = None + fuzeType = Impact projectileColor = 250, 250, 230, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //150mm bulletDragTypeName = AnalyticEstimate } @@ -701,18 +931,20 @@ BULLET BULLET { name = 4p5inchQFBullet + DisplayName = 4.5in HE caliber = 113 bulletVelocity = 746 bulletMass = 29.4 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 23.52 - fuzeType = None + fuzeType = Impact projectileColor = 250, 240, 90, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //111mm bulletDragTypeName = AnalyticEstimate } @@ -720,90 +952,120 @@ BULLET 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 projectileColor = 250, 240, 90, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1.6 + projectileCount = 1 + apBulletMod = 1.0 //216mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 120mmBulletHE + DisplayName = 120mm HEAT caliber = 120 - bulletVelocity = 800 - bulletMass = 19.6 + bulletVelocity = 1140 + bulletMass = 13.1 //HE Bullet Values - explosive = True - tntMass = 15.68 - fuzeType = None + explosive = Shaped + incendiary = False + tntMass = 1.5 + fuzeType = Impact projectileColor = 250, 130, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + 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 + projectileCount = 1 + apBulletMod = 0.9057 // 480 mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 120mmBulletSabot - caliber = 20 - bulletVelocity = 1750 - bulletMass = 9 + DisplayName = 120mm Sabot + caliber = 27 + bulletVelocity = 1690 + bulletMass = 4.92 //HE Bullet Values - explosive = False + explosive = None + incendiary = True //DU is pyrolic tntMass = 0 fuzeType = None projectileColor = 240, 240, 230, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 2.5 + projectileCount = 1 + apBulletMod = 0.7838 // 629mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 120mmBulletCannister + DisplayName = 120mm Cannister caliber = 20 bulletVelocity = 1200 bulletMass = 0.17 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 40 - apBulletMod = 0.7 + projectileCount = 40 + apBulletMod = 0.7 //40mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 122mmBullet + DisplayName = 122mm HE caliber = 122 bulletVelocity = 685 bulletMass = 22.3 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 17.84 - fuzeType = None + fuzeType = Impact projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //77mm bulletDragTypeName = AnalyticEstimate } @@ -811,18 +1073,20 @@ BULLET BULLET { name = 125mmBulletHE + DisplayName = 125mm HE caliber = 125 bulletVelocity = 915 bulletMass = 18.4 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 8.5 - fuzeType = None + fuzeType = Impact projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.9 + projectileCount = 1 + apBulletMod = 0.9 //124mm bulletDragTypeName = AnalyticEstimate } @@ -830,18 +1094,20 @@ BULLET 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 projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 2.5 + projectileCount = 1 + apBulletMod = 0.6115 // 580 mm bulletDragTypeName = AnalyticEstimate } @@ -849,18 +1115,20 @@ BULLET BULLET { name = 130Bullet + DisplayName = 130mm AP caliber = 130 bulletVelocity = 725 bulletMass = 25 //HE Bullet Values - explosive = False + explosive = None + incendiary = False tntMass = 0 fuzeType = None projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1 + projectileCount = 1 + apBulletMod = 1 //108mm bulletDragTypeName = AnalyticEstimate } @@ -868,18 +1136,20 @@ BULLET BULLET { name = QF5-25Bullet + DisplayName = 5-25 HE caliber = 133 bulletVelocity = 814 bulletMass = 36.29 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 29 - fuzeType = None + fuzeType = Delay projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //127mm bulletDragTypeName = AnalyticEstimate } @@ -887,18 +1157,20 @@ BULLET BULLET { name = 155mmBullet + DisplayName = 6.1in HE caliber = 155 bulletVelocity = 563 bulletMass = 90.7 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 72.56 - fuzeType = None + fuzeType = Impact projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //105mm bulletDragTypeName = AnalyticEstimate } @@ -906,18 +1178,20 @@ BULLET BULLET { name = 203HEBullet + DisplayName = 8in HE caliber = 203 bulletVelocity = 607 bulletMass = 100 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 80 - fuzeType = None + fuzeType = Delay projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 0.8 + projectileCount = 1 + apBulletMod = 0.8 //102mm bulletDragTypeName = AnalyticEstimate } @@ -925,18 +1199,20 @@ BULLET BULLET { name = 356ApBullet + DisplayName = 14in HE caliber = 356 bulletVelocity = 629 bulletMass = 636 //HE Bullet Values - explosive = True + explosive = Standard + incendiary = False tntMass = 508 - fuzeType = None + fuzeType = Penetrating projectileColor = 250, 70, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 - subProjectileCount = 1 - apBulletMod = 1.7 + projectileCount = 1 + apBulletMod = 1.7 //439mm bulletDragTypeName = AnalyticEstimate } 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..a3498c1c9 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Materials.cfg @@ -0,0 +1,96 @@ +//////////////////////////////////////////////////////// +// 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 + radarMod = 1 +} + +//////////////////////////////////////////////////////// +// 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 + radarMod = 0.7 +} +MATERIAL +{ + name = Aluminium //default part material + localizedName = #LOC_BDArmory_Aluminium + massMod = 1 + costMod = 1 + healthMod = 1 + ignitionTemp = -1 + maxTemp = -1 //934? + ImpactMod = 1 + radarMod = 1 +} +MATERIAL +{ + name = Steel + localizedName = #LOC_BDArmory_Steel + massMod = 2 + costMod = 2 + healthMod = 1.75 + ignitionTemp = -1 + maxTemp = 1694 + ImpactMod = 2 + radarMod = 1 +} +MATERIAL +{ + name = Titanium + localizedName = #LOC_BDArmory_Titanium + massMod = 1.5 + costMod = 1.5 + healthMod = 1.25 + ignitionTemp = -1 + maxTemp = 1941 + ImpactMod = 1.5 + radarMod = 1 +} + +MATERIAL +{ + name = Composites + localizedName = #LOC_BDArmory_Composites + massMod = 0.6 + costMod = 3 + healthMod = 0.5 + ignitionTemp = 750 + maxTemp = 993 + ImpactMod = 0.75 + radarMod = 0.8 +} +MATERIAL +{ + // Based on https://www.cfoam.com/spec-sheets/ + // Alternatives are https://www.laird.com/products/absorbers/microwave-absorbing-foams + // Non-foam alternative?: https://www.laird.com/sites/default/files/2021-01/DS%20Eccosorb%20MF500F.pdf + name = Radar Absorbent Foam + localizedName = #LOC_BDArmory_RAMFoam + massMod = 0.17 + costMod = 10 + healthMod = 0.01 + ignitionTemp = -1 //IMO FTP Pt I, is non-combustible + maxTemp = 643 // Claimed max temp of 370C, many of the Eccosorb products range from 170-270C max temps + ImpactMod = 0.01 + radarMod = 0.01 // Will need to balance this, corresponds to -20 dB +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Mutators.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Mutators.cfg new file mode 100644 index 000000000..c4e948054 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Mutators.cfg @@ -0,0 +1,347 @@ +//////////////////////////////////////////////////////// +// Default Mutator Gamemode Config - Do Not Change +//////////////////////////////////////////////////////// + +MUTATOR +{ + name = def // do not change this! + weaponMod = false //Does the mutator affect weapons + weaponType = def //change weapon to ballistic/rocket/laser, set to def for no change + bulletType = def // weapon now fires this, set to def for no change + RoF = -1 // weapon new fire rate, set to -1 for no change + MaxDeviation = -1//new weapon accuracy, set to -1 for no change + laserDamage = -1 //laser damage per hit, set to -1 for no change + //lasers are pulselasers, for beams just use maxdeviation = 0 and RoF = 3000 + instaGib = false //one shot, one kill + Vampirism = 0 //amount of HP restored to each part per hit, can exceed max + Regen = 0 //amount of HP restored to each part, per 5 sec inverval. Can be negative to do HP drain instead + EngineMult = 0 //engine thrust multiplier. Leave at -1 for no change. + Strength = 1 //Damage multiplier, weapons deal more (or less, if < 1) damage, but retain default blast radii. Leave at -1 for no change + Defense = 1 //Defense multiplier, craft receive less (or more,if < 1) damage from incoming attacks. Leave at -1 for no change + Vengeance = false //craft explodes on death in micro-nuclear explosion + MassMod = 0 //craft gains/loses mass, in tons + resourceSteal = false //will weapon steal resources + resourceTax = //foo; bar; string of resources drained/regenned while mutator active + resourceTaxRate = 0 // resource loss (regen, if negative) rate, per 5 sec. interval + + icon = IconTarget//mutator UI icon, from BDArmory/Textures/Mutators/ + iconColor = 255,0,0,255 //icon glow Color +} +//////////////////////////////////////////////////////// +// End Default Mutator Config +//////////////////////////////////////////////////////// + +MUTATOR +{ + name = Instagib + weaponMod = true + weaponType = laser + bulletType = 23x115mmBullet //a nice red color + RoF = 60 + MaxDeviation = 0.05 + laserDamage = 1600 + instaGib = true + Vampirism = 0 + Regen = 0 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconTarget + iconColor = 255,0,0,255 +} + +MUTATOR +{ + name = Regen + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 25 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconRegen + iconColor = 0,240,17,255 +} + +MUTATOR +{ + name = Strength + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = 1.25 + Strength = 2 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconAttack + iconColor = 184,50,255,255 +} + +MUTATOR +{ + name = Defense + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = -1 + Strength = -1 + Defense = 2 + Vengeance = false + MassMod = 3 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconDefense + iconColor = 0,107,204,255 +} + +MUTATOR +{ + name = Speed + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = 2 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconSpeed + iconColor = 112,245,216,255 +} + +MUTATOR +{ + name = Vengeance + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = -1 + Strength = -1 + Defense = 0.8 + Vengeance = true + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconSkull + iconColor = 166,102,26,255 +} + +MUTATOR +{ + name = Vampirism + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 10 + Regen = -25 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconVampire + iconColor = 160,25,0,255 +} + + +MUTATOR +{ + name = Assault Cannon Arena + weaponMod = true + weaponType = ballistic + bulletType = 57mmBullet + RoF = 300 + MaxDeviation = 0.65 + laserDamage = 1600 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = 1.1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconBallistic + iconColor = 222,146,0,255 +} + +MUTATOR +{ + name = Laser Arena + weaponMod = true + weaponType = laser + bulletType = LaserBolt //to set beam color + RoF = 700 + MaxDeviation = 0.24 + laserDamage = 1600 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconLaser + iconColor = 0,240,100,255 +} + +MUTATOR +{ + name = Rocket Arena + weaponMod = true + weaponType = rocket + bulletType = FFAR70 + RoF = 450 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconRocket + iconColor = 84,0,222,255 +} + +MUTATOR +{ + name = Onslaught + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = -1 + instaGib = false + Vampirism = 5 + Regen = 5 + EngineMult = 1.25 + Strength = 1.5 + Defense = 0.75 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = + resourceTaxRate = 0 + icon = IconAttack2 + iconColor = 255,204,29,255 +} + +MUTATOR +{ + name = Juggernaut + weaponMod = true + weaponType = ballistic + bulletType = 35x228HEBullet + RoF = 5500 + MaxDeviation = 0.77 + laserDamage = -1 + instaGib = false + Vampirism = 0 + Regen = 10 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = false + resourceTax = 20x102Ammo; 25x137Ammo; 30x173Ammo; 50CalAmmo; CannonShells; Rockets + resourceTaxRate = -20 + icon = IconAccuracy + iconColor = 255,50,45,255 +} + +MUTATOR +{ + name = Easy Come Easy Go + weaponMod = false + weaponType = def + bulletType = def + RoF = -1 + MaxDeviation = -1 + laserDamage = 1000 + instaGib = false + Vampirism = 0 + Regen = 0 + EngineMult = -1 + Strength = -1 + Defense = -1 + Vengeance = false + MassMod = 0 + resourceSteal = true + resourceTax = LiquidFuel; 20x102Ammo; 25x137Ammo; 30x173Ammo; 50CalAmmo; CannonShells; Rockets + resourceTaxRate = 5 + icon = IconRocket + iconColor = 192,192,45,255 +} + + diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg index 9fbb46440..4042cecd9 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg @@ -5,15 +5,27 @@ ROCKET { name = def // do not change this! - rocketMass = 0.01 - caliber = 70 + 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 - shaped = False - flak = False + 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. + 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 - tntMass = 1 - subProjectileCount = 1 + nuclear = False //use nuclear detonation instead of tnt + beehive = False //rocket deploys submunitions + subMunitionType = def; 1//name of rocket or bullet to be released as a submunition if beehive is true, and the integer quantity released, else leave blank + incendiary = false + tntMass = 1 //in kg + projectileCount = 1 thrustDeviation = 0.1 rocketModelPath = BDArmory/Parts/h70Launcher/h70Rocket/model } @@ -24,31 +36,137 @@ ROCKET ROCKET { - name = Hydra70 + name = Hydra70 //70mm M151 HE multipurpose + DisplayName = Hydra M151 HE rocketMass = 0.012 caliber = 70 thrust = 6.2 thrustTime = 1.1 shaped = False + flak = True + EMP = False + choker = False + gravitic = False + impulse = False + explosive = True + incendiary = false + tntMass = 1.33 + projectileCount = 1 + thrustDeviation = 0.1 + rocketModelPath = BDArmory/Parts/h70Launcher/h70Rocket/model +} + +ROCKET +{ + name = H70M247 //70mm M247 HEAT + DisplayName = Hydra M247 HEAT + rocketMass = 0.0121 + caliber = 70 + apMod = 0.437 + thrust = 6.2 + thrustTime = 1.1 + shaped = True flak = False + EMP = False + choker = False + gravitic = False + impulse = False explosive = True - tntMass = 1.04 - subProjectileCount = 1 - thrustDeviation = 0.05 + incendiary = false + tntMass = 1.33 + projectileCount = 1 + thrustDeviation = 0.06 rocketModelPath = BDArmory/Parts/h70Launcher/h70Rocket/model } + ROCKET { - name = 8KOMS - rocketMass = 0.009 + name = H70Mk67 //70mm Mk67 White Phosphorus + DisplayName = Hydra Mk67 WP + rocketMass = 0.012 caliber = 70 + //apMod = 0.437 + thrust = 6.2 + thrustTime = 1.1 + shaped = False + flak = False + EMP = False + choker = False + gravitic = False + impulse = False + explosive = True + incendiary = True + tntMass = 0.2 + projectileCount = 1 + thrustDeviation = 0.06 + rocketModelPath = BDArmory/Parts/h70Launcher/h70Rocket/model +} + +ROCKET +{ + name = 8KOMS //80mm shapedcharge HEAT + Displayname = S-8KOM HEAT + rocketMass = 0.0113 + caliber = 80 thrust = 5.49 thrustTime = 1 + shaped = True + apMod = 0.783 //~400mm v RHA + flak = False + EMP = False + choker = False + gravitic = False + impulse = False + explosive = True + incendiary = false + tntMass = 1.1 + projectileCount = 1 + thrustDeviation = 0.05 + rocketModelPath = BDArmory/Parts/s-8Launcher/s-8Rocket/model +} + +ROCKET +{ + name = 8DMS //80mm FAE + Displayname = S-8DM FAE + rocketMass = 0.0116 + caliber = 80 + thrust = 5.1 + thrustTime = 1 shaped = False flak = False + EMP = False + choker = False + gravitic = False + impulse = False explosive = True - tntMass = 0.53 - subProjectileCount = 1 - thrustDeviation = 0 + incendiary = False + tntMass = 5 + projectileCount = 1 + thrustDeviation = 0.1 rocketModelPath = BDArmory/Parts/s-8Launcher/s-8Rocket/model } + + +ROCKET +{ + name = FFAR70 + DisplayName = Folding-Fin Aerial Rocket + rocketMass = 0.0084 + caliber = 70 + thrust = 5 + thrustTime = 1.1 + shaped = False + flak = True + EMP = False + choker = False + gravitic = False + impulse = False + explosive = True + incendiary = false + tntMass = 2.7 + projectileCount = 1 + thrustDeviation = 0.2 + rocketModelPath = BDArmory/Parts/h70Launcher/h70Rocket/model +} + diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/Inventory.txt b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/Inventory.txt index 044e1c00b..ffc169370 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/Inventory.txt +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/Inventory.txt @@ -52,7 +52,7 @@ M65ShellAmmo 460Shells Type4Rocket ATRocket -Hades122rocket +Hades122Rocket /////////////////////////////////// Armor pack 1 diff --git a/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt b/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt index 5b2d177de..c5a80fa3b 100644 --- a/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt +++ b/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt @@ -1,4 +1,4125 @@ -v1.3.4 +v1.12.0.0 +IMPROVEMENTS / FIXES +- UI: + - Adjust the "Steel Equivalent Thickness" display in the armor GUI to show a more intuitive value. +- AI / WM: + - Fix broken ActiveController registry clean-up. + - Add an action group trigger to the WM for when the vessel comes under attack. + - Fixed some bugs in point-defense logic which could lead to the game freezing. + - Further improved point-defense logic, should behave slightly better against mixed barrages of missiles, some of which it cannot engage with missiles (like stealthy missiles). + - AI can no longer see incoming missiles through water. + - AI will fire missiles that can (probably) clear any terrain in front of them without LoS to the target. + - Slightly better AI radar management behavior (pending a bigger overhaul), AI will no longer try to lock targets if it'll unlock the radar's current radar target (due to it being outside of multiLockFOV) if it's being used for guidance. + - Improved AI weapon selection, AI should be a tiny bit smarter about what weapon it picks, as well as not dumping all anti-radiation missiles at once. + - Fix surface AI reversing behavior, surface AI should now reverse more smoothly. + - Fix DEBUG_LINES causing NREs for vessels attacking targets beyond visual range which have not previously seen a target within visual range. +- Sensors: + - Radar and RWR code optimizations. + - Fix smooth slewing of the targeting camera when clicking on the GUI buttons when a target is locked. + - Targeting Camera terrain collision detection no longer starts 50m forward of the lens. + - Targeting Camera will no longer lock on to its parent vessel or missiles fired by its parent vessel. + - Prevent errors in radar code from potentially causing game freezes. + - Radar 'auto-disable vs incoming Antirad missiles' default has been changed to Off. This can be toggled per-radar or turned on for the entire vessel from the Weapon Manager. +- Weapons: + - Add conical beam weapon implementation for creating microwave drone zappers, War of the Worlds heatrays, tractor beams, etc. + - Adds new weapon.cfg fields: 'conicAoE' = T/F to enable conic field of fire for Laser class weapons. + 'beamFOV' = n to set width of beam. + 'friendlyFire' = T/F to set of conic weapons affect ally craft in AoE, if firing into a melee. + - Rework EMP weapons. Craft EMP threshold before getting disabled is now dependent on cockpit seat quantity/drone core SAS level. + - EMP damage received now adjusted by craft armor/hull type and craft bulk. Electrolaser/lightning weapons resisted by non-conductive armor and hull material (e.g. not metal). + - EMP missile/Nuke EMP burst/conicAoE EMP lasers resisted by craft bulk and conductive armor occluding cockpits from emission source. + - EMP buildup now causes increasing penalties/subsystem shutdowns as it accrues in a vessel receiving EMP damage. + - Fix hypersonic rounds dealing self damage when fired into something at close range. + - Fix explosion damage affecting armored parts which should be able to resist them. + - Fix incorrect application of certain shaped charge logic to continuous rod warheads. + - External fires from incendiary ammo into flammable hull materials no longer suppressed by Inerted Tanks. + - Added a AIMING_VISUAL_MALUS setting (default=0) that applies a malus to aiming at targets with mk1 eyeballs if >0 and the target is within visual range. The malus decreases both with time since target acquisition and shots fired at the target, and resets when changing targets. + Weapons configs can be configured with: + - 'sightingAccuracy' (default=1 mrad) which represents the limit of visual aiming accounting for scopes and other visual aids. + - 'malusReductionPerShot' (default 0.1) which determines how rapidly the malus decreases with each shot at the target. + - Missiles: + - Add ordnance offset option to non-adjustableRail reloadable MultiMissileLaunchers that don't come with set missile types. + - Fix Nuclear ordnance applying multiple stacks of EMP damage to a single vessel during detonation. + - Improve "CLOSlead" debug visualization, now shows the proper, lead-adjusted beam, for easier debugging. + - Add new "adjustMissileVOffset = T/F field for MultimissileLauncher.cfgs to set if missiles should automatically adjust mount position based on missile diameter (missile rails, etc) or not (VLS cells). + - Fixed missile ECM behavior, previously changes to the missile's radar signature due to ECM could be overwritten by the missile going active/passive, causing ECM to seemingly not function. + - Missiles with a "cruiseRangeTrigger" will now also fire the cruise motor if the missile's airspeed drops below 75% of its optimum airspeed while in-atmosphere. + - AAMLoft settings for modular missiles are now correctly loaded when returning to the SPH. + - Fix "guidanceDelay" not working properly. + - Improve Kappa guidance loft termination, as a result Kappa no longer uses LoftRangeFac and LoftVertVelComp. + - Fix CLOS/CLOSthree/CLOSlead guidance failure due to inaccurate determination of "no progress" condition. + - "CruisePopup" setting now persistent and editable in the hangar when missile config has "CruisePopup = true". + - Missiles will now respect config value of "DetonateAtMinimumDistance" setting, if a missile config does not have this set to true, it cannot be edited/used. + - Un-deprecate "proxyDetonate" field, missiles with "proxyDetonate = false" in their configs will be *strictly* impact fuzed. + - Fix certain scaling issues with multi-missile launchers. + - Fix missile turret aiming under Guard Mode control, particularly, turrets with IR, laser and anti-radiation missiles will now pre-aim at the target to facilitate acquisition. + - Robotics: + - !!Experimental!! Adds ability to build custom turrets with Breaking Ground Hinges and Servos. + - BG hinges/servos now have a 'Custom Turret ID' slider; attaching a weapon as a child of the robotic part adds a similar 'Custom Turret ID' slider to the gun. + - Setting both to the same value will enable the gun to use BG robotics to slew/traverse the attached weapon (and any other parts attached to the robotics) as if it were a standard BDA turret. + - Turret ID = 0 is an Off value, and will not link weapons to a servo. + - Pressing F2 to bring up the Weapon Alignment in the SPH/VAB will draw a Pitch/yaw Blue/Green alignment line for all enabled servos. + - All servos in a custom turret should have aligned forward and Up directions. + - Attached weapons should be placed in the forward direction for optimal operation. +- Competition: + - Allow warping to morning for waypoints tournaments. + - Show the waypoint name (if it exists) in the VS status during waypoint competitions. + +v1.11.1.0 +IMPROVEMENTS / FIXES +- General: + - Fix some compatibility issues with pre-KSP v1.12 versions. +- UI: + - Make the AI GUI auto-resizing by default. Left clicking the resize handle switches to manual resizing, right clicking it switches to auto-resizing. +- AI / WM: + - Improved point-defense logic to better account for IR missiles, AI should no longer get stuck trying to fire an IR missile when it can't attain a lock. + - Add action group triggers for enabling/disabling guard mode on WMs instead of just toggling it. + - Add 'Full 3-Axis PID' as an option in the pilot AI for using separate PID values for each axis (without dynamic damping). +- Sensors: + - Fixed omnidirectional radars failing to detect targets west of them. + - Fix IRST display issues for IRST parts with an irstTransform. + - Fix issues with notch calculations. + - Add "radarCanNotch" field to radars to allow for notch calculations to affect chaff effectiveness without affecting track/lock. + - Fix bubble decoys being able to affect radars in rare cases. + - Radar code optimizations. + - Targeting camera radar slaving now affected by chaff. +- Weapons: + - Adjust weapon selection logic to account for craft spawned from the SPH not having their Landed state set. + - Missiles: + - Add "activeRadarCanNotch" field to missiles as a counterpart to "radarCanNotch". + - Missile RCS now correctly works until the end of the cruise stage even with cruiseRangeTrigger > 0. + - Added 4th value to maxTorque to allow for specifying a "coast" maxTorque for use when the missile is coasting between the boost and cruise stages (due to cruiseDelay or cruiseRangeTrigger). + - Allow for laser/targeting camera targeted CLOS missiles to use radar velocity data when radar slaved but not CoM locked. + - Fix 0 values for advanced (multiple value) maxTorque, liftArea, dragArea, maxTorqueAero and steerMult usage not being respected. + - Fix 0 values for terminalSeekerTimeout not being respected. + - Fix missing Cruise Popup menu for MMLs. +- Competition: + - Fixes to include any newly detached fighters during competition start-up. + - Fix vessel name deconfliction for teams tournaments that use the "Re-use Craft To Fill Teams" option. + - Note: If this option is being used and two craft with the same name are used in multiple teams then the scoring will be messed up. + - If not storing Team Icon colours, reset the colours at the start of a tournament / continuous spawn. +- Scripts: + - Fix default GLOBAL_LIFT_MULTIPLIER in BDArmoryMissileManeuverEnvelopePlotter.py. + +v1.11.0.0 +IMPROVEMENTS / FIXES +- General: + - Fix the g-limit overrides from needing to save the persistent savegame, which was leaving spawned craft in play when reverting to the SPH. + - Fix issues with armor damage when using "Armor Mass Multiplier" values other than 1. +- Part Changes: + - Added stack attachment nodes to all turret bases. + - Sanity-checked turret bulkhead sizes. + - Increased FoV settings for FLIR ball and Targeting Pod to take advantage of new zoom readouts. + - Missiles: + - AMRAAMs given INS/ARH guidance and AAM/Pronav guidance to bring its capabilities in line with modern BDA+ features. + - Sidewinder missile given Pronav guidance. + - Hellfire missile slightly buffed and given Kappa guidance, giving it top-attack capabilities. +- UI: + - Fix spelling of "ordnance" in localisation files (settings and modules retain the misspelling). + - Add competition marquee messages for fuel/ammo detonations from battle damage. + - Fix 'Disable Radar vs ARMs' WM GUI toggle being inverted. + - Tweak vessel switching weights to try to remain focused on vessels that are about to explode. + - Fix EMP disable messages on craft that have alraedy been disabled before claiming a negative float value time duration. + - Fix Radar Display not displaying correctly on radars using radarTransform defined pointing directions. + - Add an option in Team Icons to not store the team colours in the settings.cfg file. +- AI / WM: + - Add missing unclamped limits for 3-axis static damping. + - Adjust the release of the "Attack" command so that craft don't go dead-stick when under attack but not threatened enough to trigger evasion. + - Fix AttackVIP targeting priority impacting target priority weighting if no enemy VIP is present. + - When assigning turret targets, AI now checks if target is within max range of the turret. + - Pilot AI using divebombing will now break off the dive when bombs have been released. + - Add an option to disable the behaviour of disabling guard mode when vessels are spawned. + - Fix pilot AI's standby mode to automatically trigger AG10 and enable engines if necessary when a threat is detected. + - Fix behavior of Guard Mode with regards to datalinked radars, Guard Mode should no longer unlock targets when these locks are being used by a datalinked vessel to guide a missile. + - Slightly improved Guard Mode radar lock management, locks are only dropped as needed on radars that can lock on to the target. + - AI will automatically set radar to max range when toggled on, allowing it to make full use of the radars. + - Tweaks to the auto-tuning loss function to give a more stable roll-relevance. Show an auto-tuning summary to reduce confusion in the SPH about the auto-tuning results. + - Disable Battle Damage while auto-tuning to avoid wooden parts catching fire. + - Fix AI continuing to increase extend distance while extending away from a target for lining up a bombing run. + - Fix some errors in Dynamic Launch Zone math. + - Improve VTOL AI's calculation of bank angle. + - Overhaul of logic for which AI / WM is in control of a vessel and proper support for motherships / detachable fighters: + - The "primary" WM is the WM closest (in hierarchical distance) to the root part of the vessel (an "Is Primary" label is added to the top of the WM's PAW to allow checking this). + - Only a single AI is allowed to be active on a craft at any given time. Activating one will deactivate the others. + - Whenever a vessel detaches from or docks with another vessel or loses parts, the primary WM and AI automatically updates to reflect the new vessel configurations. + - The primary AI is chosen as the first active AI sorted by hierarchical distance from the root part with the precedence: pilot, surface, VTOL, orbital. + - E.g., if an active pilot AI is further from the root part than an active surface AI at the time that the vessels dock, the pilot AI will be chosen as the primary and the surface AI will get deactivated. + - A stable sort is used, so ties are decided by order in the craft file. + - If no AI are active prior to docking (or on spawn), then the primary AI is chosen as the AI closest to the root part regardless of type. + - Vessel type is also updated to reflect the AI in control of the vessel: + - Plane + - Ship (spaceship, boat, submarine) + - Rover (tank/buggy, amphibious) + - Lander (stationary) + - Base (WM with no AI) + - If a competition is active, then: + - Duplicately named vessels spawned via BDA's spawning routines get a "_1"-style suffix. + - Duplicately named fighters (vessels that detach from a parent vessel that has a WM) get a "_F1"-style suffix. + - Vessel name deconfliction respects custom VESSELNAMING tags, only applying suffixes as necessary for deconfliction. + - Suffixes applied to spawned vessels are persistent throughout tournaments and continuous spawn, but suffixes applied to fighters are dependent on the order that they detach from the parent. + - Detached fighters are automatically added to active competitions / tournaments on the same team as the parent vessel and behave independently from the parent vessel. + - If the parent craft is free or commanded to attack a location, then the fighters get the same command, otherwise they are commanded to follow the parent. + - Ranked tournaments use the combined score of a parent vessel and its fighters when determining the ranking. + - Access patterns for the AI / WM are optimised to be even faster than the previously used VesselModuleRegistry. + - Added a "Weapon Channel" mechanic to support separation of weapons on fighters from the mothership, active Weapon Managers can only control weapons with a lower or equal "Weapon Channel" value. +- Countermeasures: + - Add 'Ammo' gauges for Countermeasure dispensers for how many CMs they have remaining. +- Sensors: + - Add support for slaving turrets via targeting cameras. + - Fix targeting camera issues with moving targets. + - Targeting camera now displays actual zoom power. + - Fix 'Slave Turret' button occasionally locking the turret to the vessel's origin point or non-target point in space. + - Locking a target will no longer automatically slave active turrets to the target when manually controlling a craft. To slave turrets to a locked radar/camera, use the 'Slave Turret' button. + - Fix bug that allowed radars to lock on below the radar horizon when the appropriate settings are toggled. + - Fix datalinked radars not conveying lock information to all datalinked vessels. + - Fix Conformal Decals and part variants affecting craft RCS analysis. + - Fix radars not having an elevation limit. Radars can now be given a specified elevation limit via "elevationFOV". + - Non-omnidirectional radars are assumed to be roll stabilized and will scan "directionalFieldOfView" horizontally and "elevationFOV" vertically. Radars will scan the ENTIRE pitch FoV with every sweep, vertical scan limits are affected by vessel pitch, however! + - Omnidirectional radars scan about an up/down axis (this will at some point be corrected so that they scan about their own spin axis), limited by "elevationFOV". + - Default behavior for non-omnidirectional radars is to assume a square scan zone (i.e., equal "directionalFieldOfView" and "elevationFOV" unless otherwise specified). + - Default behavior for omnidirectional radars is to provide full spherical coverage, as they did previously. + - Radars can now have asymmetric scan zones, both "directionalFieldOfView" and "elevationFOV" can be given two values to specify a lower/upper scan limit. (I.e., a radar with "directionalFieldOfView = -40, 20" will scan up to 40 degrees to the left and 20 to the right.) + - Note this is a very WIP feature and visualizations may not be perfect! + - For debugging, the "DEBUG DETECTORS" setting may be used, which will output the radar's lower/upper limits, FoV limit and offset (of the scan centerline) for both the azimuth and elevation axes. + - Fix radar display not being pitch stabilized, position of targets should now remain static even when maneuvering in pitch. + - RWR display should no longer drift. +- Weapons: + - Remove maxEngagementRange (default 5000m) check from bombs when AI is selecting what weapon to use. + - EMP weapons (both guns and missiles) now capable of disabling missiles. + - Add a "Yaw Standby Angle" slider to gun turrets. + - Multi-barreled guns now no longer swap to the next ammo type in the belt until after all barrels have fired. + - Multi-barreled guns now fire tracers every tracerInterval rounds of each barrel, and will fire tracers simultaneously from all barrels. + - Fix aim lead offset for weapons with deploy animations. + - Fix display issue where "Advanced Ammo Config" button does not reflect the actual state of the weapon. + - Weapons fire SFX maximum distance it can be heard now scales based on weapon caliber. + - Added fuzeDelay and fuzeSensitivity functionality to bullets. Now, for penetrating and delay fuzes you can specify the fuze delay in seconds, and the fuze sensitivity in mm for penetrating fuzes, by listing them with commas, i.e., "fuzeType = Penetrating,0.01,20" for a 0.01 s delay which will be set off by 20 mm of armor (currently material differences are not considered). + - Fix bullet behavior when hitting parts with multiple colliders embedded in each other, bullets should now only hit such a part once, unless it exits the part and then hits the part again. Note the system in place cannot catch cases where another part is clipped into such a part. + - Fix checks for phantom water on planets without oceans. + - Changed continuous rod warhead damage area from 60°-90° to 30°-60°. + - Missiles: + - General Changes: + - Fix maddog ARH missile launches not turning on seeker. + - Heat seeking missiles now more realistically use angular velocity rather than vector velocity for flare rejection. + - Added a new FloatCurve "lockedSensorVelocityMagnitudeBias", weights target score based on the normalized angular velocity magnitude difference, where 1 is when the target's angular velocity magnitude matches the expected angular velocity magnitude and 0 is when the angular velocity magnitude is off by the expected angular velocity magnitude (either 0, or twice the expected). FloatCurve defaults to all 1s when lockedSensorVelocityBias is not specified and linear when specified. + - Added lockedSensorMinAngularVelocity field which sets the minimum detectable angular velocity magnitude for angular velocity magnitude differencing. + - Ensure laser-guided weapons have "TargetAcquired" properly set prior to firing. + - Added cruise specific missile deployment animations. + - Guidance: + - Add improved missile controller. + - Missiles will now better respect gLimit as well as guidance-commanded acceleration, note this means some parameters may need to be tweaked to retain previous performance, specifically pronavGain and weave guidance accelerations. + - Improve AAMloft and Kappa's ability to hit targets with high closure rates close to the launch point, particularly for VLS and other 0/0 launch conditions. + - Add WeaveUseAGMDescentRatio and WeaveRandomRange to weave guidance, when WeaveUseAGMDescentRatio is true it forces the use of agmDescentRatio to allow for sea-skimming weave missiles. WeaveRandomRange is a Vector3 (format is WeaveRandomRange = (x,y,z) ) that allows for randomization of weave horizontal/vertical g and frequency parameters. + - Loft guidance parameters are now no longer persistent and inaccessible in the SPH for non-modular missiles. Modders may still tune these parameters in-flight by enabling the "Debug Missiles" option in the BDArmory Settings menu. + - For modular missiles, parameters are persistent and editable in the SPH. Turning on "Debug Missiles" will also allow for in-flight adjustment of these parameters. + - Modular Missiles: + - Fix check for multi-mode engines being ignited and having fuel. + - MissileLauncher/Standard Missiles: + - Sanity checks for maxOffboresight and cruiseSpeed, maxOffboresight and max cruiseSpeed are loaded from the configuration file to ensure consistency with intended values. + - Fix buggy behavior caused by partial failure of missile warheads to detonate when a custom warhead and a standard warhead were used in conjunction with non-zero fuze failure settings. + - New "maxTorqueAero" field for missiles, provides a dynamic pressure (density times velocity squared) based component to maxTorque. This is intended to simulate aerodynamic control surfaces (as opposed to maxTorque, which now represents things like thrust-vectoring). + - New "maneuvergLimit" field for missiles, provides the gLimit for the various maneuvers a missile might conduct (not general guidance maneuvers). Specifically, the pull-up phase of lofted guidance types, like AAMloft and kappa, and the pop-up maneuver of cruise missiles. + - Several missile parameters can now be specified for different phases of flight with comma-separated values. + - "liftArea"/"dragArea" has the format: [boost], [cruise], [deltaDeploy], [deltaCruiseDeploy], separated by commas. The [cruise] value is used when "decoupleBoosters" is true, representing the change in aerodynamics after dropping the booster stage. The deltas represent the change in area when deploy animations trigger. A boolean field "deployedLiftInCruise" (defaults to true) allows you to keep [deltaDeploy] after the booster separates. + - "maxTorqueAero" follows the same format as above. + - "maxTorque" has the format: [boost], [cruise], [post-thrust], allowing for simulation of thrust-vectoring missiles. + - "steerMult" has the format: [boost], [cruise], allowing reduced "steerMult"s in cruise if desired. + - Add the gLimit, maxAoA and chaff/flare effectivity fields to the info box. + - Implement various command-to-line of sight guidance types, these all use the "beamCorrectionFactor" field and the "pronavGain" fields (with support for pronav gain scheduling). + - "CLOS" performs basic command-to-line of sight guidance with no beam-velocity compensation. + - "CLOSThree" performs command-to-line of sight guidance with beam-velocity compensation. + - "CLOSLead" performs command-to-line of sight guidance with selectable lead (using the "beamLeadFactor" field, 1.0 representing full lead and 0.0 representing three point guidance. Defaults to 0.5 or half-rectification guidance.) + - "beamCorrectionFactor" is multiplied with the lateral error from the specified line of sight to provide a lateral velocity command, higher values result in a faster snap to the line of sight line, but also more oscillation. + - "pronavGain" is used, in a similar manner as it used in pronav guidance, to multiply the velocity error and provide an acceleration command. Higher values result in a faster snap to the commanded velocity vector, but also more oscillation. + - Replaced "radarTimeout" field with general "seekerTimeout", which is now used for heat-seeking, active radar homing, inertial and anti-radiation homing. Added "terminalSeekerTimeout" field so this can be specified separately for the initial and terminal targeting types. + - Improved guidance for continuous rod missiles, offset direction now depends on if the missile is above/below the target rather than always being offset above. + - Launchers: + - Fix multi-missile launcher issues when railAmmo > launcher rails on the mode. + - Fix multi-missile launchers not properly calculating "FiringAngle" parameter. + - Fix certain missiles becoming ghost-like entities that ignored bullets and interceptor missiles when fired from MultiMissileLaunchers with "ignoreLauncherColliders = true". + - Added "Lofted Aimpoint" toggle to Missile Turrets, with the accompanying "Loft Velocity Factor". When toggled on, the turret will aim above the target to provide some lofting to the missile trajectory (though this requires the usage of pronav and a gain schedule). "Loft Velocity Factor" is used to scale the missile's "optimimumVelocity" for the ballistic simulation, a lower "Loft Velocity Factor" results in a higher loft angle. + - Added new "deployBlocksPitch", "deployBlocksYaw" and "deployBlocksReload" booleans to Missile Turrets, when toggled on, the turret will have to finish it's deploy animation before pitching/yawing, and will have to retract for reloading (respecting the pitch and yaw toggles as well). + - Allow multi-missile launcher to gracefully error in the event of the missile model not being found. A debug log entry will be made, warning the user of such an error, and if it is a launcher, it will attempt to find the correct model to use. + - Added "displayOrdinanceHasColliders" field to multi-missile launchers, defaults to true. When set to false disables the colliders on the dummy missiles granting a very minimal performance improvment. +- Effects: + - Add splash effects to underwater explosions (very WIP, feedback on size, behavior etc. would be appreciated). + - Bullet hits now attached to whatever collider they hit, so bullet hits on animated parts, like turrets, will now move with them. + - Fix floating bullet hits and fuel leaks from explosions (note this does NOT fix decals floating due to collider issues, that's a problem with the part model). +- GameModes: + - Don't spawn new asteroids in asteroid rain mode while in high time-warp. +- Competition: + - Add a "defaults" button to the scoring window to reset score weights to their default values. + - Updated parsing and plotting scripts. + - Using 'python parse_tournament_log_files.py -w cfg' reads the score weights from the score_weights.cfg file. + - Using 'python plot_summary.py -i regex' creates the plot ignoring any craft matching the regex string. + - Add a "Daylight" position on the "TimeWarp Between Rounds" tournament slider to warp to dawn whenever a round would start in darkness. + - Fix Proximity Fuzed bullets/rockets and bullet/rocket-based nukes from not registering hits/score. +- Spawning: + - Automatically adjust the PRE range if the spawn distance would put craft outside the physics range. +- Scripts: + - All python scripts have been moved to the folder "Scripts". + - Added the new "BDArmoryMissileManeuverEnvelopePlotter.py" and "KSPFloatCurveCalc.py" scripts. + - "KSPFloatCurveCalc.py" is a general utility, implementing KSP FloatCurves in python as a class, using numpy and some not-so-efficient-but-faster-than-opening-KSP code. + - "BDArmoryMissileManeuverEnvelopePlotter.py" is a new tool, using "KSPFloatCurveCalc.py", numpy and matplotlib, designed to allow modders to make the most of the new missile changes. It is a command-line utility that allows the user to plot the performance of a missile, allowing the modder to better tune the performance of their missile. + +v1.10.0.0 +IMPROVEMENTS / FIXES +- General: + - Fix colliders on the AWACS Radar when using legless variant. + - Update the logic for syncing advanced 'Game Difficulty' settings to respect custom G-limit settings. +- UI: + - Add an alternate layout to the entries in the vessel switcher, togglable via the ↔ button. + - Adjust colour of vessel status and target strings in the vessel switcher for better visibility. + - Update localisation synchronisation script and sync en-us to other languages. Missing entries are commented out and tagged with ??? and the English translation (to assist with user contributions). + - Fix the wrongly labeled "ru-ru" localisation to "ru" so that it loads properly. + - Add additional Chinese localisation thanks to user ZIYUXUAN201011. + - Updated German localisation from EzBro. + - Adjust waypoint ranking in the vessel switcher window to be based on the current score weights. + - Fix m/km units in Team Icons to be used appropriately. + - Automatically disable team scores in the score window when running FFA tournaments or continuous spawning. +- AI: + - Fix the PID auto-tuner's "Initial Learning Rate" slider in the AI GUI. + - Improvements to pilot follow mode to improve stability. + - Fix check for overriding terrain avoidance when ramming ground targets. + - Fix AI always seeing all LoS targets within 200km regardless of view range or radar detection. + - The AI targeting priority 'Aerial Target Preference' now considers craft flatspinning or otherwise possessing negligible horizontal velocity lower than 200m from the ground as semi-ground targets for target filtering. + - Adjust the "fixed field" toggles in the AI GUI for auto-tuning the pilot PID axes that are in use. + - AIs set to evade while running waypoint courses will now use evasion non-linearity, if enabled, to provide some dodge capability while still remaining on-course for the next gate. + - Tweaked gate targeting logic, should reduce last-second control jerk while passing through gates with a high deviation. + - Waypoint Race mode now works with Surface and VTOL AI. + - Add 3-axis static damping in the pilot PID. + - Dynamic damping and 3-axis damping are now independent toggles. + - The UI (both PAW and AI GUI) update to show only the active damping sliders. + - Craft that used 3-axis dynamic damping get automatically updated to use the independent toggles without losing their tunings (but updated craft are not backwards compatible with older installs). + - Clamp the strafing minAlt of the pilot AI to at most half the gun range to give at most a 30° dive slope for short range guns. + - Some minor performance optimisations. +- Countermeasures: + - Fix flare and smoke CMs ejecting sideways. +- Detectors: + - Display non-zero RCS <= 0.01m^2 as dBsm instead of square meters for more precision. +- Weapons: + - Removed the 'Engage Missile' toggle from APS/Dual-Purpose APS turrets as these weapons don't use it and it was confusing users. + - Fix custom ammo belts not cycling ammo type in some situations. + - Fix weapons using a secondary ammo consuming secondary ammo when primary ammo missing. + - Fix tracers on guns with tracerlength > 0 extending behind gun on firing. + - Radar slaving multiple turrets when turret multi-targeting > 1 now sees if a radar lock exists for each secondary target, instead of overriding multi-targeting and aiming all guns to the primary lock. + - Fix Point Defense guns not firing on incoming missiles. + - Add Aim Override option to set what weapon AI uses for aiming/target lead when using heterogeneous weapon groups/weapons with offsets from centerpoint. + - Fix 'is target behind terrain' check extending beyond the target, preventing weapons from firing if target is in front of a wall or hill. + - Missiles: + - Added "hasIFF" field to missiles allowing for missile seekers without IFF functionality. Defaults to true. + - Fix AAMLoft implementation for MMGs. + - Fix reload gauge for reloadable launchers. + - Add dynamic ordnance offset for reloadable rails so larger missiles no longer clip into rails/hardpoint mounts. + - Fix lock-less Radar missiles doing a loop instead of going straight. + - Fix Reload Gauge on reloadable launchers. + - Fix Account For Mass/Cost not applying to Missile magazines. + - Fix Modular Missiles not being able to intercept missiles. +- Competition: + - Adjust the default score weights for rockets to be more inline with that of bullets. + - Adds a Vessel Legality checker button to the BDA Utilities Tool GUI if 'Runway Project' or 'Use Comp Overrides' gamemodes options are enabled and a Comp_settings.cfg is added to GameData/BDArmory/PluginData/. This will check a craft in the SPH/VAB for part count/engine count/Lift-To-Weight/Thrust-Weight-Ratio/max allowed weaponry/banned parts/etc. Check GameData/BDArmory/PluginData/Comp_settings.cfg once loaded in KSP for more info. + - Switch score weights to reside in their own config file "score_weights.cfg" to simplify distribution of custom score weights in RWP. +- Spawning: + - Reset the custom craft browser internals if the user switches saves. + - Add caching of the filtered vessel list in the custom craft browser to reduce UI rendering time. + +v1.9.0.0 +IMPROVEMENTS / FIXES +- General: + - Fix potential stack overflow in cfg parsing of bullets, rockets, materials and mutators that can cause KSP to not load if BDBNIC is also installed. +- UI: + - Added lead target indicator to bombing reticule. + - Bomb aimer reticule now dynamically sized based on bomb yield. + - Reloading BDA settings now reloads the spawn locations.cfg. + - Temporarily show the mouse cursor by holding right shift (customizable). +- AI: + - Add target priority debug info to the telemetry and AI debugging. + - Tweak take-off climb rates slightly for smoother take-off. + - Surface AI now able to (mostly) navigate around buildings at the KSC/Island Airfield/etc. + - Re-add look-ahead check to Missile Launch authorization, AI now again checks if the target is predicted to still be within boresight 2s later, should cut down on incidents of missiles firing at bad angles the moment a target enters FOV. + - Missiles set to not use DLZ launch ranges now still use min range turn radius kinematics check to see if a missile can plausibly physically maneuver to reach the target at shorter ranges for determining when the AI should launch. + - AI beginning to lead target 2s before coming into gun range now properly takes relative closing velocity into account. + - Bombing improvements: + - Fix the requested extend distance prior to dropping bombs. + - Fix AI not dropping bombs despite being on target. + - Fix AI not properly leading moving targets when bombing with unguided bombs. + - Add a "Bombing Altitude" slider and switch related fly-to instructions to use this when performing level bombing runs. + - Implements Dive Bombing behavior, and toggle to swap between dive and level bombing routines. + - AI now properly dives to torpedo bombing altitude at start of bombing run. Torpedo Bomb Alt raised to 200m. + - AI will now temporarily inhibit gain alt behavior when doing low-level bombing if min alt has been set too high to prevent aborting runs due to 'gain altitude'. + - AI will no longer do an Immelmann turn to come about when bombing. + - Include the expected drop-time (for unguided bombs) in the initial extend distance request and lower the default A2G extend distance to compensate. + - Add pilot extending readout to the loaded vessel switcher. + - Fix the bomb aimer sim from stopping early when the bomb is within the explosion radius of the target and use the closest approach instead. + - Fix the bomb aiming overshoot logic to only abort a bombing run when overshooting by more than twice the blast radius or 200m (whichever is larger). + - Slightly improve torpedo lead when torpedo bombing vs moving targets. + - Slightly improve AI bombing ability vs elevated(target on rooftop/bridge)/flying targets. + - Fix AI not always prioritizing torpedoes over bombs for A2G vs naval targets. + - AI is now capable of bombing when not the focused vessel. + - Experimental support for Surface AI torpedo and VTOL AI bomb/torpedo runs to properly aim/abort/extend when using these weapons. +- Detectors: + - Airborne sonars no longer detect splashed/submerged targets for datalinked teammate radars. + - Fix submerged radars being able to detect things. + - Fix bug that would prevent Laser/GPS targeting cameras from targeting vessels with a vessel exterior to CoM distance greater than 14m. +- Competition: + - Add a hidden setting for the number of attempts of a heat during a tournament before using the "start despite failures" option (if enabled). + - Improve the reporting of damage / parts hit from explosions in competitions. (Set REPORT_DAMAGE_NOT_PARTS_HIT to False to revert.) Reported values may include nearby debris as long as the main vessel is also hit. + - Add gzip-compression to the tournament.state file, falling back to plain ASCII if needed. + - Add continuous spawn scoring to the score window. (Removes the option to disable dumping the logs every spawn.) + - Fix Pinata mode team swap breaking ground-spawn competitions. +- Weapons: + - Missiles: + - Fix kappa guidance when firing at receding targets. + - Add cruise missile popup behavior, using the fields "CruisePopup", "CruisePopupAngle", "CruisePopupAltitude" and "CruisePopupRange". + - Added new terminal weave guidance based on https://www.sciencedirect.com/science/article/pii/S1474667015333437, set up as with any other terminalHomingType, uses fields "WeaveVerticalG", "WeaveHorizontalG", "WeaveFrequency", "WeaveTerminalAngle" and "WeaveFactor". + - Fix bombs/missiles in cargo bay logic and have the cargo bays automatically open/close when selected manually. + - Fix NRE when trying to use Antirad missiles in reloadable rails/MultiMissileLaunchers + - Added some target filtering to antirad guidance logic based on ping type, frequency, and proximity to previous ping, they shouldn't get decoyed by the first radar signal they detect now. + - AntiradTargetType now suports type names as well as ints, e.g. 'antiradTargetTypes = SAM, Detection' vs 'antiradTargetTypes = 0,5'. + - Added a Fire Angle slider to missiles to allow setting the permitted launch angle to narrower than the missile boresight FOV, to control how lined up with the target the AI should be before triggering a launch. + - Fixed issue with AI not firing GPS-guided missiles in some conditions. + - Attempting to fire GPS missiles from a vessel with existant, but non-valid targeting cam will now see if a radar fix can be used instead of aborting. + - Add support for nuclear/kinetic warheads on Modular Missiles. + - Fix custom beehive warhead firing submunitions sideways. + - Fix MultiMissileLaunchers using 'ignoreColliders = true' not properly disabling colliders on spawned missiles and potentially resulting in RUD if the launcher is clipped into the vessel. + - MultiMissileLaunchers with multiple tubes/cells/rails now properly display the amount of ordnance loaded into the launcher when Ordnance Available set to lower than the number of tubes. + - Fix radar-guided missiles fired from a VLS detonating immediately after launch due to lost lock. + - Missile 'hasMissed' grace period now adjusted based on missile velocity and motor state, so things like slow VLS launches don't time out before they can tip over and get on target. + - Fix GPS missile NRE when trying to fire from beyond Targeting Camera max range using a missile turret. + - Fix damage from explosions sometimes being lower than it should when hitting large parts with multiple Colliders. + - Missiles can now be equipped with jammers and countermeasures, the missiles will enable their jammers and begin dropping countermeasures when closer than "missileCMRange", at an interval of "missileCMInterval" seconds. + - This includes modular missiles, sliders corresponding to the two fields have been added. Setting "Countermeasure Range" to > 0 will activate this system. Note that any countermeasures placed on modular missiles will NOT be accessible for normal use. + - Note that jammers will only be enabled once at "missileCMRange". CMDropper priority will function as it does for BD AI. + - Fix multi-missile launcher scaling bug, missiles should now scale correctly. + - Added multi-missile launcher offsetMax field, to allow increasing the offset limits from the default of 1m. + - Added multi-missile launcher deployTime field and slider, this allows changing how long the launcher is open for after launching from 0.5s to up to 5s. This is primarily intended for launching slower missiles out of VLS. + - Reloadable rails no longer do a reload when there's no ordnance remaining. + - Torpedos now use running depth (default -3m if unguided) and depth-keeping, instead of diving in a straight line towards target. + - Fix Stingray Torpedo buoyancy. + - Add AAM Loft as a guidance option for Modular Missile Guidance missiles. +- Spawning: + - Allow storing craft URLs in the custom spawn templates to simplify repeated use of the templates with specific craft. + +v1.8.0.1 +IMPROVEMENTS / FIXES +- Weapons: + - Missiles: + - Fix not being able to manually fire GPS-guided bombs/missiles without an AI-assigned target. + +v1.8.0.0 +IMPROVEMENTS / FIXES +- General: + - Adjust fuel leak FX to work better in space and low atmospheric density. + - Add the ability to add custom sub-categories to the BDA part categorizer. + - Add some new missile plume prefabs for jets/sustainer stages. +- UI: + - Improve prevention of click-through of various windows in the editor. + - Open sections of the AI GUI now remain open when switching between flight and editor. + - Competition UI text in the upper left corner now properly scales when adjusting UI_SCALE. + - Added an option to follow the stock UI scaling instead of BDA's one. + - Add ability to set teams' Allies to the Team Selector GUI. + - Add craft thumbnails to the custom craft browser. + - Add a button (in the Settings->UI section) when in the SPH/VAB to generate missing craft thumbnails (which can be found in KSP/thumbs). + - Generating thumbnails involves loading each craft and capturing a thumbnail. This can take some time if many are missing. + - Add "VAB Organizer" MM patch for BDA parts — from user Spartwo. +- AI: + - Fix AI not going into Ramming mode when guns possess ammo, but not enough to meet resourceRequestAmount of mounted weapons. + - Orbital AI now automatically disables gimbal on reverse thrust and RCS engines (gimbal directions for these are incorrect). + - Add missing sonar checks for AIs trying to fire torpedoes. + - Fix AI not turning on Active sonar when trying to fire INS torpedoes if no passive sonar on vessel. + - AI visual detection of incoming missiles within visual range no longer requires a RWR. RWR detection of incoming missiles still requires a RWR on the craft. + - Tweak AI detection and reaction to incoming GPS/INS missiles, they should detect and react to them a bit more consistantly now. + - AI can now make use of non-DynamicRadar radars for missile targeting when being actively targeted by incoming anti-radiation ordnance. + - AI can now do non-linear evasion vs long-range targets while aiming missiles. + - Add Extend Min Gain Rate slider to set extend abort condition if not gaining distance fast enough. + - AI now checks for available secondary resource as well as primary for weapons using 2 ammo types (e.g. railguns using bullets + EC) when doing weapon selection checks. + - AI can now use an ally laserdot for targeting laser-guided ordnance if it lacks a targetcam of its own. + - AI now uses a TWR factor in the climb rate limit calculations. + - Improvements to formation flying behaviour. Planes in follow mode should now hold their positions better and be more stable. + - Fix point-defense missile system, AI should now engage with the appropriate amount of missiles. + - AI will no longer drop locks when these are supporting semi-active radar homing missiles. +- Competition: + - Fix craft equipped with repulsors breaking ground spawn. + - Add support for using custom spawn templates with RNG style tournaments. + - The number of teams per heat and vessels per team per heat are restricted to at most those in the selected spawn template. + - Teams are considered as the per file / per folder option. + - The "Re-use Craft To Fill Teams" option applies for teams that lack sufficient craft to fill the spawn template. + - Both shuffled and ranked tournaments are supported. + - Add a hidden option to "smart assign" teams for custom spawn templates (when not running a tournament). + - Set "VESSEL_SPAWN_SMART_REASSIGN_TEAMS = True" in BDA's settings.cfg to enable it when "Reassign Teams" is enabled. + - The first vessel in the spawn template with a non-default team name (can be set in the SPH) that isn't already taken sets the name for the team. Other teams get their default letter. + - Updated plot_summary.py to adjust how the graph and legend fit in the window. +- Countermeasures: + - Fix NRE from countermeasure droppers missing an ejectTransform in the .cfg. + - Fix Acoustic Decoy launcher launch direction. + - Acoustic Decoys will no longer defy gravity and fly if launched out of water. + - Fix Bubble Curtain launcher NRE. + - "Infinite Ordnance" now also applies to countermeasure droppers. + - Adjust cmThreshold timing for SLW countermeasures to 5x time set in WM to account for torpedoes being substantially slower than missiles. + - This should allow proper CM behavior against both torpedoes and missiles without compromising one or the other. +- Detectors: + - RWR no longer clamped to visual FOV for detecting incoming missiles. + - Add new fieldOfView field to RadarWarningReceiver module to set custom RWR FOV. + - Fix AI not popping countermeasures for non-radar incoming missiles detected by RWR but outside visual range. + - Added notching and beaming behavior for both vessel radars and missile radars. + - Defaults to off. When toggled on, radars can be notched by an aircraft flying low and at 90° to the radar. + - Behavior is configurable using the activeRadarVelocityGate and activeRadarRangeGate floatcurves for missiles and radarVelocityGate and radarRangeGate floatcurves for radars. + - Also includes water blocking radars, forcing engagements to consider the horizon. + - Added a hidden "RADAR_ALLOW_SURFACE_WARFARE" option (default: true), which permits surface vessels to see each other over the horizon to simplify naval engagements. + - Fix the targeting window breaking a lot of scroll behaviours. + - Fix the targeting window's slewing along an axis and add a small quadratic component to allow faster slewing with faster mouse movement. + - Avoid firing missiles while slewing the targeting window if the mouse drifts outside the window. +- Weapons: + - Adjust bullet tracers to lead up to their current position instead of from their previous position. + - Reduce bullet tracer visual glitching due to krakensbane adjustments. + - Weapons will now properly report Ammo Depleted when ammo remaining is less than amount required to fire. + - Fix weapons refusing to fire when one shot left when using multiple ammo boxes with differing starting ammo amounts due to floating point errors. + - Explosive beehive submunition explosion FX now scaled based on submunition tntMass, not parent HE VFX. + - Fix weapons with reload animations adding an extra 1-20s to the reload time, based on fire anim length. + - Add new "postFireChargeAnim" field for weapons with charge animations to specify if the animation should play in reverse to deactivate the weapon after firing. + - Fix weapons with reload sounds using "oneShotSound = false" immediately stopping the SFX when switching to a different weapon. + - Fix weapons with spindown anims when they stop firing that have overheated under AI control not spinning down until they've cooled down. + - Laser weapons or other weapons using ElectricCharge can now fire if Infinite Electricity is on, even if Infinite Ammo is not. + - Adds fireAnimOverrideSpeed field to set custom fire animation play rate instead of basing it on rate of fire. + - Fix electrolaser NRE when firing on a target vessel. + - Added a hidden "WEAPONS_RESPECT_CROSSFEED" setting (default: true), which may be set to false to restore previous behavior where guns did not respect crossfeed rules. + - Improved performance of bullet firing code. + - Fixed issue where slaved turrets would fire at a stale radar contact. + - Added "guidanceRange" parameter for guided bullets, determines the range to the target at which the bullet will begin to maneuver, set to -1 for guidance immediately from the muzzle. + - Missiles: + - Add "cruiseRangeTrigger" field to missiles. Missiles using this will delay firing their cruise motor until within the set distance to target. + - Tweak acoustic propagation loss rate for passive acoustic torpedoes, should have a bit further effective detection range now. + - Fix INS guidance ordnance not configured with a datalink immediately triggering the lost lock condition and self-destructing. + - Passive Sonar torpedoes now home in on loudest part, not COM. Respects targetCoM override in the .cfg. + - Fix Terminal Guidance Passive Sonar torpedoes not entering terminal guidance. + - Fix laser guided ordnance locking on to enemy laser dots. + - Missile in-flight anims (flightAnimationName) now starts playing when boost motor ignites instead of post boost phase. + - Added "custom" warhead module, functions like a normal warhead, except it launches a bullet instead of exploding, integrated with missiles. + - Point-defense missile turrets will no longer follow mouse aim unless manually selected. + - Fixed booster mass missing from missile dV calculation. + - Fixed terminal radar guidance missiles lacking IFF. + - Inertial guidance missiles with terminal guidance that were dumbfired will now activate their terminal guidance. +- Armor: + - Armor should now be more consistent in terms of HE resistance. Adjusting the "Armor Explosion Resistance Multiplier" should now more directly affect the amount of armor penetration explosions do. + - Improved long-dart armor penetration mechanics, should have improved performance against consecutive plates in contact with each other. Improved residual velocity formula. + - Implemented "realistic" model for explosive reactive armor against kinetic projectiles, now using an interaction length-based model, supports symmetrical and open-faced sandwich configurations. + - Hits on the sides of explosive reactive armor now no longer provide additional armor protection. (For Modders: Ensure that the section.forward transform is normal to the strike-face of the ERA. Turning on Debug Lines will show you the forward transforms in blue, the green and red lines represent .right and .up respectively.) + - Now the specific explosive reactive armor section hit by bullets will explode instead of a random section. + - Implemented angle correction for explosive reactive armor hits, now penetration increases with increasing hit angle. Resistance described by armorModifier * 300 mm at 68 degrees. + - HEAT warheads now become unfocused as they penetrate through layers of armor, thicker layers will unfocus HEAT jets more than thinner layers. + - Added a hidden "HEAT_CONE_HALF_ANGLE" setting (default: 2.5), which allows the damage cone angle for HEAT warheads to be adjusted. + - Setting this value to -1 makes the HEAT damage area into a line instead of a cone. + - Added a hidden "KERBAL_ERA" setting (default: true), which can be set to false so that EVA Kerbals no longer provide infinite armor. + - Adjustable armor now scales breakingForce and breakingTorque based on values in the part.cfg file. +- RWP: + - For S6R10, GM kill craft that lack propulsion and are significantly beyond the asteroid field radius. + - Adjust attraction of asteroids beyond 90% of the field radius to not clump together at the centre so that they don't kill the sole surviving craft. + - Add craft spawn warning message if craft doesn't have AI/WM on root part or if the root part isn't a cockpit. +- P:S: + - Add max extend time limit clamp to override extend behavior that goes on for longer than time limit. + - Add support for automated AI/WM settings for competition rules compliance. To utilze, create a 'Comp_settings.cfg' with an 'AIWMChecks' node, containing one or more of the following settings: + - extensionCutoffTime = n + - extendDistanceAirToAir = n + - MONOCOCKPIT_VIEWRANGE = n + - DUALCOCKPIT_VIEWRANGE = n + - guardAngle = n + - collisionAvoidanceThreshold = n + - vesselCollisionAvoidanceLookAheadPeriod = n + - vesselCollisionAvoidanceStrength = n + - idleSpeed = n + - DISABLE_SAS = T/F + If the file is not present, or it is, but the Use AI/WM Overrides toggle in Game Modes is off, overrides will not be used. Overrides will apply on competition start or vessel spawn. + +v1.7.1.0 +IMPROVEMENTS / FIXES +- General: + - Add a SimpleRepaint MM patch to ignore incompatible parts. + - Fix typos and duplicates in the changelog. +- Armor: + - Add a max armor limit slider (under General Slider Settings) to limit the max thickness of armor. Default: unclamped. +- UI: + - Team Icon text now scales with icon scale. +- AI: + - Orbital AI: + - Adds ability to use controllable sidemounted engines as RCS thrusters. + - Add toggles to disable use of RCS for translation and rotation. + - Adds Zero-throttle firing range setting that will override maneuvering orientation in favor of pointing ship in attack Vector direction within set distance. + - AI now takes target acceleration/velocity into account when accelerating to engagement speed/killing velocity. + - Adjustments to the hasPropulsion check to properly detect when a vessel no longer has propulsion. +- Detectors / Countermeasures: + - Allow higher precision for CM intervals and delays in the WM right-click menu. + - Fix invalid C# syntax in the TWS radar .cfg file. +- Competition: + - Auto-generated tournaments use the last used world. + - Fix ramming scoring to also work for orbital AI. +- Game Modes: + - Sanitise the waypoint course index in the vessel spawner window in case the course list has changed. + - Asteroid Field: + - When resetting asteroid fields, destroy those that have switched SoI as they don't reset properly. + - Adjust the asteroid attraction to be 20% of the field radius instead of hard-coded to 1km. + - Fix asteroids not spawning at the correct location in orbit. + - Limit motion-reduction force applied to asteroids to a linear function above 1500m/s and the maximum centroidFactor for re-centering asteroids to avoid yeeting them when the average velocity suddenly changes due to vessels dying. + - Limit growth of the asteroid pool to twice the requested field size to prevent infinitely growing pool size when there are issues generating new asteroids. +- Weapons: + - Optimizations to bullets, should be a slight performance increase when firing lots of bullets. + - Beehive submunitions with proximity/flak fuzes now properly set fuxe distance based on pellet tntMass, not parent shell's. + - Fix nuclear/explosive beehive submunitions not inheriting velocity when detonating on space. + - Fix beehive ammo with nuclear submunitions not being able to get the explosion models. + - Fix APS going rapid-fire when running out of ammo. + - Fix HEAT bullets being reported as Missile damage in the Score parser. + - Fix issue with velocity calc that would result in penetrating rounds slowly drifing away from a hit ship in space. + - Fix laser hit position not accounting for Krakensbane. + - Fix EC usage requirements not appearing in infocards when weapons are using deprecated ECPerShot. + - Missiles: + - Update the KKV model with fixed textures thanks to Stardust. + - Don't evade missiles fired from teammates unless the missile is locked on to craft or craft is in the flight path of the missile. + - Log an error if the missile exhaust prefab doesn't exist without breaking OnStart. +- Spawning: + - Disable waiting for terrain to settle if the spawn altitude is >10km. + - Add a couple of 30s timeouts to spawning initialisation to avoid a potential soft-lock. + +1.7.0.0 +IMPROVEMENTS / FIXES +- General: + - Fixes for post-build event instructions on Windows to allow for different 7zip executables. + - Downgrade using defaults for missing bulletinfo fields from an error to a warning. +- UI: + - Updated Chinese localisation thanks to user Marv1n-M. + - Propagate new localisation strings to non-English localisation files. Seach for commented out entries with value "???". + - Fix the alignment indicator not showing for dual-mode APS turrets. + - Waypoints / Waypoint Course builder can now set gate models on a per-gate basis, using models stored in /BDArmory/Models/WayPoint/ + - BDA Craft Utilities Tool Lift to Weight readout split into Dry and Wet mass LTW scores. + - Can set what resources are considered part of drymass for Dry LtW calculations via new Drymass Resources button. + - Fix Kinetic Resist readout in the Armor Stats subsection of the BDA Craft utilities Tool being inverted. +- AI / WM: + - Improvements to the Orbital AI + - PID control option available, by default is active only during weapon firing, can be toggled to be active always or never active. + - If a fixed weapon is selected, AI will orient itself to fire the weapon, even if the weapon is not aligned with the forward direction of the craft. + - Improved maneuvering behavior, it is now better at intercepting targets, killing velocity, and prioritizes combat over minor orbital corrections when within combat range. + - Add option to engage head-on or broadside (for use with turreted weapons). + - Add option to select broadside direction (only active when PID is enabled). + - AI is now able to use reverse engines for maneuvers provided the "Reverse Engines" option is enabled (enabled by default). + - Add minimum firing speed option. If below this speed and within weapons range, AI will increase speed. + - Add option for AI using RCS to manage velocity relative to the target when firing (enabled by default). + - Add options to enable/disable evading gunfire using RCS and/or engine thrust. + - Add option to allow ramming (enabled by default). If enabled, AI will try to ram the target when it loses weapons, if disabled the AI will withdraw from combat. + - AI will not engage vessels if doing so will put the AI in risk of de-orbiting. + - Fixed AI repeatedly jumping back and forth between orbit correction and other modes. + - AI is now capable of avoiding collisions with other vessels, debris, and asteroids. + - AI is now properly aims and fires unguided missiles in orbit. + - AI will automatically move to closer intercept ranges if it is unable to lock missiles after 20s. + - Improvements to VTOL AI: + - VTOL AI given secondary speed controller; VTOL AI will now throttle engines set to 'independent thrust' for horizontal speed control independently of primary engines being used for lift/powered hover. + - AI will no longer ignore max missiles per target when using multimissile launchers for missile interception. + - AIs/WM no longer subject to GForce failures if KSP part G-Tolerances enabled. + - Don't set the global infinite fuel option when maintaining fuel levels for individual vessels. + - Don't raycast for terrain normals when taking off from water, just use the up direction. +- Armor / Hull: + - Revert invalid HullTypeNum values to that of Aluminium and log a warning. + - Fix errors in armor damage calcs causing HE rounds to do decreasing amounts of armor damage over subsequent hits. + - Armor damage/integrity loss now adjustable via the Armor penetration Mult slider in the BDA Settigns menu, General Sliders subsection. + - Fix shrapnel damage from explosions phasing through armor. +- Competition: + - Fix GM culling based on HP remaining to use percentage remaining instead of the raw part count. + - Add an option to average duplicates in the tournament parser. + - Fix incorrect parsing of unicode chars in the tournament parser for "wins". + - Add 'Stop Waypoints' button to stop a Waypoints comp started via the 'Run Waypoints' button. +- Detectors: + - Radars/IRSTs now have a 'resourceName' field to set what resource they use. + - Fixes datalink from allied craft with sonar not displaying sonar contacts on non-splashed receiving craft. + - FLIR ball/Camera pod now have different niches; Camera pod is now longer ranged, FLIR ball has faster camera traverse. + - Targeting cameras now list traverse rate and max view range in the part menu infocard. + - Added radarChaffClutterFactor (default 1) field to radars, to permit setting how effective chaff is against the radar. Used when guiding SARH missiles/radar-linked gun turrets, see TWS radar .cfg for more details. +- Game Modes: + - Added g-force limits game mode for part failure and Kerbal GLOC (note: the pilot AI's g-limiter still doesn't work well). + - Subsystem Battle Daamge to turrets/generators above 50% HP now reduces performance instead of disabling them. + - Waypoints mode now works with ground spawned caft. + - Fix Infinite Fuel until First Gate option for Waypoints Mode. + - Asteroids Field: + - Adjust the asteroid field game mode to also work in orbit. + - Adjust forces on asteroids to keep them reasonably contained within the requested field volume and to avoid pinballing. + - Adjust the asteroid anomalous attraction distance profile to peak at 707m and drop to 0.25 at 0m and 0 at just over 1km. + - Improved competition messages related to asteroid fields. +- Weapons: + - 'ecPerShot' deprecated, weapons now use 'secondaryAmmoPerShot' for controlling EC/secondary resource usage for things like railguns, etc. + - Adds new 'secondaryAmmoName' field (default ElectricCharge), weapons that used ammo + ElectricCharge (railguns, etc) no longer required to use EC as the secondary ammo. + - Electrolaser EMP damage per shot/sec now set by 'laserDamage' instead of ecPerShot amount. + - Fix Burst Length Override softlocking the weapon after the first burst. + - Weapons can now use Hull Materials with a massMod >= 1. + - Missiles: + - Improvements + - Missile parts can be configured without a warhead and will be considered kinetic energy weapons. Upon impact, they will apply their kinetic energy as damage to the target. + - Change missile drop time increment from 0.5s to 0.1s. + - Missiles are now capable of intercepting each other at orbital speeds and will not phase through each other, even without the ContinousCollisions mod. + - Modular Missiles can be selected to intercept missiles if they have Engage Missiles enabled. + - Modular Missiles with Orbital Guidance now perform vacuum clearance maneuver (similar to HEKVs in space). + - Improvements to regular missile orbital guidance, missiles can now use homingType = orbital without RCS. + - Missiles with RCS will use RCS to offset gravity. + - Add targetCoM configuration option for IR missiles. If set to true, the missile will target the center of mass instead of the hottest part. + - Firing conditions for IR missiles tightened up, AI should no longer select rear-Aspect IR missiles against head-on targets. + - Missiles with RCS can now have multiple forwardRCS transforms. + - Adds new ModuleMissileMagazine, MultiMissileLauncher reloadable rails and missile pods can now resupply from external missile magazine parts. + - Fixes + - Fix Modular Missiles with Orbital Guidance not hitting vessel and missile targets. + - Fix point defense missile interceptors not firing. + - Missile turrets now track targets properly in orbit. + - Fix missiles with no targeting type (i.e. AIR-2) not firing within engage range. + - Fix IR missiles occasionally suddenly switching lock to allied craft behind them. + - Fix edge-case issue where firing radar missiles would get soft-locked if trying to switch to a radar lock that didn't exist. + - Fix missile exhaust plume FX scaling. + - Fix missile softlock issue if AI tries to fire a laser/GPS missile at a target further than the targeting camera's max range. + - Fix nukes in vacuum erroneously using the wrong mass value for calculating impulse from the detonation. + - Parts / Balancing + - Add new Kinetic Kill Vehicle (KKV) missile. The KKV is an orbital missile with 6 km/s delta-V and no warhead. It relies on kinetic energy from hitting a target directly to deal damage. + - Re-balanced the HE-KV-1 missile. The HE-KV-1 now has 3 km/s delta-V with a 60 km range and is best at shorter ranges (under 30 km). The HE-KV-1 retains the same mass. + - Orbital guidance missiles now have drag applied to them in atmosphere. + - Guns maxDeviation reduced following discovery they were miscalibrated last accuracy refactor; gun accuracy will see up to ~50% improvement across the board. + - Fix delay-fuzed rounds occasionally not detonating. + - Add support for guided bullets (needs radar/laser lock on target) for specialty ammo, new bulletDef field 'guidanceDPS', sets degrees/sec ammo can turn. + - The part menu infocard for Guns now lists weapon accuracy. + - The bullet stats section of Guns part menu infocards now lists fuze type. + - EMP weapons will no longer shutdown asteroids. + - Rockets: + - Rework the rocket aiming to be consistent with how rockets fly, both in atmosphere and in orbit. + - Add a "detonate at minimum distance" option to flak/proximity rockets (enabled by default). + - Aero-stabilization is now stateless (doesn't depend on the time since launch, only on current aero-forces). + - Hydra70 and S-8KOM rocket types updated and variants added. + - Fixes S-8KOM rocket mass. + - Optimizations to Incindiary rockets, should now be a bit better at settings things on fire. + - Don't apply lead and weapon offset corrections to the pilot AI for turrets. + - Proximity detonation now uses the target's "average radius" instead of its "max radius". + - Shell ejection direction, variability (deviation), delay and lifetime are now customisable in the weapon config. + - Mostly fix the bomb-aimer - bombs without significant lift work fairly well, but JDAMs can be fairly imprecise. + - Avoid double-counting HP+armour of intervening parts in explosions and scale the damage reduction by the appropriate modifier. +- Spawning: + - Allow specifying the reference heading for the first craft for circular spawning (loses precision within 1° of the poles). + - Spawned Kerbs now have uniform G-tolerance. +- RWP: + - Add S6R6, S6R7, S6R8, S6R9 and S6R10 defaults and custom logic. + - Refresh various UI text fields from the settings values when updating from RWP settings. +- Evolution: + - Various fixes for the evolution engine from user themonthlydaily: + - Applies mutations to the correct craft. + - Fixes subsequent mutations to be applied to the base craft instead of the most recently modified version. + - Correctly save seed craft. + - Adds a display of which parameters are currently being mutated/tested. + - Fix various other internal bugs. + +1.6.12.0 +IMPROVEMENTS / FIXES +- General: + - Fix Pwing HP scaling for large pwings being used as fuselages. + - Fix PWing Control Surface Offset angle resetting on one side. + - Add a time-sync logging option (with auto-enable for competitions) for logging in-game vs real-time timestamps for post-recording resynchronisation. + - Toggle and logging interval options are available in the "Other Settings" section of the settings. + - The timestamps are logged as compressed CSV files in the BDArmory/Logs/TimeSync folder. + - Add Waypoint Mode Course Builder GUI tool (accessible from the Waypoints Section of the vessel Spawner GUI) for easy construction of custom waypoint courses. + - Can either manually construct course from scratch or record a course as you manually pilot a craft along the course path. Courses/gates on loaded courses can be deleted via right-click. + - List of things that cannot be set to Wood hull material expanded to other functional parts (servos/radiators/ISRU converters/RTGs) that couldn't logically be made out of wood. + - Restore the 'ControlledActions' on the KAL too. + - Restructuring of the localisation files (both layout and many tags) for maintainability and ease-of-editing in IDEs. + - Adds a script for maintaining structure between localisation files. + - Some entries in languages other than en-us will require updating — these are currently commented out with "???" as a placeholder. + - Adds option to toggle LightFX for explosions on/off for performance/parallax compatibility. + - Adds option to toggle off bullet water hit effects for performance. + - Skip the 'ProgressTracking' scenario when creating a clean save to avoid triggering tutorials again. + - Expand the debug info for missing elements when loading materials to be more helpful. +- UI: + - Vessel Switcher GUI and competition Marquee now display Waypoint times to hundredths of a second. + - Gravity Gun moved to the Lasers tab. + - VTOL and Orbital AIs added to the AI GUI and adjustments made to the layout for the surface AI for consistency. + - Make ammo bars show amounts respecting fuel flow priorities. +- AI / WM: + - Fix Kerbal G-Loss of Consciousness implementation; AI now goes deadstick when pilot is knocked out. + - Can now toggle AI radar shut-off behavior when targeted by HARMs either on a per-radar basis, or globally for all radars on a craft via the Weapon Manager. + - AI now understands how to aim weapons not aligned with prograde for Schrage Musik-style weapon arrangements. + - Note: depending on the airframe, angles outside of roughly -5° — 35° pitch and -10° — 10° yaw are dynamically unstable. + - AI now understands how to aim fixed guns laterally offset from CoM for B Wing-style weapon arrangements. + - Remove legacy noise added to fly-to-vessel that would throw off a craft's ability to snipe at long-range. + - AI now takes evasive maneuvers when under attack from incoming INS/GPS missiles. + - Fixes AutoTune infinite fuel not applying to Firespitter helicopter engines. + - Remove the requirement of having a WM for the AI to work. + - Add the post-terrain avoidance cool-down slider to the AI GUI. + - AI now more intelligent about turning on Radars/Sonars; will not automatically turn on Active Sonar if present on a craft. +- Competition: + - Save/load tournament score weights to/from the settings.cfg. + - Waypoint Mode max laps can now be set to a user-specified value (WAYPOINT_MAX_LAPS, default 5) in the BDA settings menu. + - Only show non-zero fields in the results.csv and when parsing multiple files separately. + - Don't break spawning if a craft loses parts while waiting for the WM if "Start Comp. Despite Failures" is enabled. + - Disable all FX and projectiles when clearing the field for spawning. +- Game Modes: + - Waypoint Gates can now set AI speed limits on a per-gate basis to permit setting up braking zones before a turn/Rally courses with target speeds/etc. + - 'Activate Guard After Gate n' setting now ends the course when FFA is complete, not when last alive craft completes the course. + - Vengeance Mutator blast radius and delay can now be set in the BDA settings menu. + - Don't check for fuel for afterburners if infinite fuel is enabled. + - Fix issue with Subsystem Battle Damage option occasionally setting non-flammable parts/parts without subsystem modules on fire. +- Weapons: + - Fix lasers being able to fire as long as > 0 EC is present. + - Add bulletLuminance field for setting non-tracer brightness. + - Fix weapons in salvo mode with symmetry twins firing when only enough ammo for one weapon to fire. + - Fix heatRays not adding heat to hit parts. + - Fix kinetic damage having damage reduction from armor being applied when hitting unarmored parts. + - Fix sabot rounds reporting incorrect penetration values in the right click menu. + - Missiles: + - Fix missile exhausts not turning off. + - Can now set cluster missile trigger distance when mounting cluster missiles on reloadable rails. + - Nuke LightFX radius now set by effective blast range, not thermalRadius value. + - Fix NRE when AI tries to fire Modular Missiles. + - Fix heatseeking Missiles with very narrow sensorFOVs losing lock right before impact with a target. + - Fix manually fired INS missiles using GPS coords if set. + - Fix issue with AI fired radar missiles getting stuck and not firing if the first missile fired could not get a lock. + - Fix an issue with unguided INS bomb guidance. + - Improved inertial guidance performance when handing off to terminal radar guidance. + - Added adapted rangeFac behavior to Kappa guidance to improve guidance performance, particularly for close ranged targets. Now an altitude limit is prescribed for the missile based on 10 * rangeFac * range^(vertVelComp). + - Fixed maddog/dumbfire behavior of ARH missiles, missile should now be able to lock onto targets in front of it when launched. + - Throttle now determines fuel burn, missile burnout for missiles with fuel burn now depends on fuel expenditure rather than just preset burn times. + - Fix some typos and inconsistencies in the wording of the right click menu. + - Improve logic for when and how AI attempts dumbfire launches when missing requisite sensors. + - Improve logic for sonar-guided weapons, will no longer try to satisfy sonar need with radars if present. +- Detectors: + - Visual improvements and variants added for the AN/APG-63 Radome (from Spartwo). + - Fixed rare bug where, when spawning in vehicles while the on-screen debug elements are turned on the vessel radar cross-section would update too early, causing the radar cross-section to default to the vessel's mass. +- RWP: + - Add S6R5 dynamic recoil gamemode; recoil scales with vessel thrust. + - Improved handling of auto-adjusted BDA settings for RWP. + - Update the RWP specific settings for recent and earlier rounds. +- ModIntegration: + - Fix the type to unbox MidChordSweep to. + +v1.6.11.0 +IMPROVEMENTS / FIXES +- General: + - Recompile the shaders on an older version of Unity so that they're compatible with KSP v1.9.1 and later. + - Fix false positives of Krakensbane being active on vessel switches. + - Add a part Tree Hierarchy visualizer to the BDA Craft Utilities Tool for checking part offset. +- UI: + - Make the time-scaling slider visible without needing to activate time override. + - Extend the camera-switch slider to up to 15s. +- AI / WM: + - Clamp the vertical adjustment of the fly-to position to at most half the distance to the target (should fix the issue of being unable to attack targets directly below). + - Add a slider for biasing the Immelmann pitch direction when not close to terrain/min altitude. + - Add a slider for the initial roll-relevance and preserve the best roll-relevance between auto-tuning runs. +- Competition: + - Add "Parts lost to asteroids" to the continuous spawn tournament score logs and parser. + - Fix world switching for auto-resuming continuous spawn tournaments. + - Make the "Auto-Quit On Tournament End" option apply whenever it's visible. + - Suppress the "Dumping scores..." messages unless competition debugging is enabled. +- Game Modes: + - Add Gun Game game option to the Mutator GameMode - craft progress through a list of mutators (weapon changes or otherwise) every time they score a kill. Ideally used with Continuous Spawn. + - Asteroid Field: + - Adjust the anomalous attraction strengths to scale better with typical asteroid radii. + - Add an inter-asteroid repulsion at close ranges to prevent asteroids clumping too much. + - Make the asteroids nigh indestructible so that they don't thin out over time. +- Weapons: + - Reduce ECPerShot cost of electrolasers. + - Heatrays now check for part occlusion. + - Fix non-damaging lasers (gravity guns, etc.) from not scoring in competition. + - Fix the luminance of tracers on pooled bullets not being reset (not every bullet is a tracer now). + - Lasers now deal damage to buildings. + - Missiles: + - Fix Missile Exhaust FX issue. +- RWP: + - Add RWP override settings (in RWP_settings.cfg) for automatically enabling/disabling RWP options when the RWP toggle is toggled or the RWP slider is adjusted. + - Settings adjusted when RWP is enabled aren't saved, but can be manually added to the RWP_settings.cfg file. + - Settings adjusted when RWP is disabled are saved to settings.cfg as usual and serve as the base that the RWP settings override. + - Overrides for round 0 are global overrides and are applied first before round specific overrides are applied. + - Toggling the RWP toggle or moving the slider first reverts settings to the non-RWP values, then applies the global and round-specific RWP overrides. + - For convenience, a few settings are excluded from being reverted: settings section toggles and most spawn settings. Also, inf fuel / inf EC currently aren't affected. + - The defaults for the RWP_settings.cfg file will be saved when settings are first saved and changes made to the file override the defaults. + - Default RWP settings for previous rounds and the upcoming S6R2 have been added. + - Restrict the RWP slider to only include rounds that have overrides and move it to the top of the section to avoid issues with layout changes while adjusting the slider. + +v1.6.10.1 +IMPROVEMENTS / FIXES +- UI: + - Updated German localisation from EzBro. + - Fix some localisation tags and duplicates. +- AI / WM: + - Don't reset PID values to stored auto-tuning ones when switching the clamped/unclamped toggle. + - Add an auto-tuning summary line to the AI GUI when auto-tuning is not running (when there's something to show). +- Weapons: + - Fix rockets detonating too early. + - Missiles: + - Fix missiles not having their exhaust and boost FX. + +v1.6.10.0 +IMPROVEMENTS / FIXES +- General: + - Fix Krakensbane adjustments when the active vessel dies. + - Make sure various pooled objects are disabled when leaving the flight scene. + - Battle damage fuel leak rates now scale by (bullet caliber)^2, instead of (bullet caliber). Fuel leak rates for 20mm caliber remain the same, calibers higher and lower than 20mm will now have higher and lower leak rates, respectively. + - Fixes for Kerbal Safety not always detecting EVA kerbals. +- UI: + - Updated Chinese localization thanks to user Marv1n-M. + - Fix part filtering by search terms in the editors. + - Adjust timing and conditions for auto-camera switching to and away from missiles. + - Fix tracer visuals when the active vessel dies. + - New Opacity option in BDA Team Icons to set icon opacity. + - Adds CASE ammo explosion radii visualizer in the editors, enable via F3. + - Continuous Spawn now shows lives remaining in the Vessel Switcher GUI. + - Add a UI scaling option for BDA's windows. + - Improve the logarithmic scaling in the radar windows. + - Add a 'withZero' parameter to semi-log sliders that allows them to include 0 and uses lower precision for the smallest range. + - Add a 'GUIUtils.HorizontalSemiLogSlider' for simplifying horizontal sliders for semi-log float ranges outside of PAWs. + - Add a fractional component to the precision of semi-log sliders for rounding, e.g., sigFig = 1.5f gives '..., 8.5, 9, 9.5, 10, 15, 20, ...'. Switch the spawn distance slider to use this. + - Add a power-law float range type and 'GUIUtils.HorizontalPowerSlider' for simplifying horizontal sliders for power-law float ranges outside of PAWs. +- AI / WM: + - Add new "Kinematic Msl Evasion" toggle to Pilot AI under the evasion settings. When this is enabled the AI will evaluate the best maneuver to defeat a beyond-visual-range (BVR) missile using only kinematic maneuvers. The Time to Impact Before Evade setting on the weapons manager should be set to a high value when this evasion option is enabled. Maneuvers the AI will use are: + - Crank - Fly at an angle to the target, while still maintaining radar lock. + - Beam/Notch - Fly perpendicular to the missile. + - Turn away/Turn cold - Fly away from the missile. + - Notch & dive - This is the standard AI behavior if this toggle is disabled. It is also used if the toggle is enabled and the AI is within lethal range of the missile. + - Invert the roll target when the fly-to direction is below and behind the craft within the Immelmann turn angle (fixes death spirals). + - Fix automatic gun range adjustments in the WM again. + - Prevent Orbital AI from changing attitude while launched missile is still near spacecraft. + - Add Smoke CM timing/interval/repetition settings. + - Fix the missile evasion direction using the wrong reference frame. + - Automatically disable precision input mode when auto-tuning (KSP's UI element doesn't switch though). + - Add a slider for prioritizing using brakes to slow down when braking is allowed. +- Armor / Hull: + - Radar stealth coatings - can now apply radar absorbent/reflective materials on a per-part basis. + - Armor coatings less than 10mm thick will provide lessened benefits. + - Adds new radarReflectivity field to Armor materials to specify degree of radar absorbtion/reflectivity of the armor. + - Adds new radarMod field to Hull materials to set degree of radar return off unarmored parts. + - Adds new Radar Absorbent Coating armor type. + - Adds new Radar Absorbent Foam hull material. +- Detectors / Countermeasures: + - Fix missing resource for the Bubble CM. + - Fix targeting cameras not being able to track targets. +- Weapons: + - Corrections to penetration and ricochet velocity changes and ricochet positions. + - Stop APS firing at dead missiles. + - 'subProjectileCount' has been deprecated and replaced with 'projectileCount'; this specifies the number of pellets per shot (for shotguns, etc.) to make it more intuitive. Existing shotgun-type weapons will need to update their bulletDefs. + - The subMunitionType field for beehive ammo now uses the syntax of "ammotype; quantity" (e.g. subMunitionType = 35x228AHEADPellet; 30) with number of projectiles released on detonation set here instead of the subMunition's bulletDef. + - BDA will try to automatically detect and adjust old beehive ammo types, but mods should update their bulletDefs accordingly. + - Fix the aiming reticles of guns (fixed and turrets) and auto-proxy-tracking when using MouseAimFlight (again). + - Add new projectileTTL field to bulletDefs, use to set time in seconds beehive round subprojectiles will persist for. Optional, defaults to 0.5. + - Switch the gun range sliders to use a power-law slider instead of a semi-log slider to keep typical ranges closer to the centre of the slider. + - Missiles: + - Fix issue with Rotary Rails trying to use missiles that have previously been blown off from combat damage. + - Fix issue with MultiMissileLauncher Turrets throwing an NRE when AI firing. + - Fix mass changes of missiles that decouple boosters and add missile mass debug telemetry. + - Fix radar and heat targeting leading targets excessively (most apparent in orbit). + - Fix missile drag curve having negative drag at ~25 deg AoA. + - Fix AI occasionally not firing radar missiles; Git Issue #573. + - Change target offset on missiles with continous rod warheads from Detonation Distance / 3 to Min(Blast Radius / 3, Detonation Distance / 3). + - Fix missile RCS FX constantly displaying. + - Missiles with RCS/Orbital homing types (i.e. HEKV) in vacuum now perform turn maneuver after launch to face target and only throttle up once facing target. + - Fixes to anti-missile missile point-defense logic to support missile turret launched interceptors. + - Fix radar missiles not locking and firing until well within their max engagement range. + - Add new dragArea configuration variable for missiles to set reference area for drag independently from the lift area. If not set, this defaults to the liftArea setting in the missile configuration file. + - Fix missiles ignoring guns with priority. + - Added missile g-limiting. Now you can set a missile maximum g-limit using the "gLimit" field. Additionally a "gMargin" field exists which allows you to specify an allowable margin the missile can miss by to optimize drag. I.e., the missile is allowed to pull only gLimit - gMargin if pulling gLimit will result in too much drag. + - Corrected pronav and augpronav normal acceleration using the g-limiting feature. + - Improved missile turning behavior, turns should be slightly smoother overall. + - Added Kappa guidance, an optimal type of midcourse guidance for in-atmosphere missiles. This re-uses the LoftAngle, LoftMaxAltitude and LoftRangeOverride fields, though LoftRangeOverride is now the range where the missile will switch from lofting to the midcourse guidance. Introduces a new field, "kappaAngle" which is a trajectory shaping angle, higher values mean a steeper terminal trajectory. + - Fixed missile contact detonation logic, a bug was present which lead to inaccurate detection of missile contact. + - Improved AAMLoft performance against maneuvering targets. + - EMP warheads only detonate when armed and no longer do additional damage. + - Add dumbTerminalGuidance bool (default true) to missiles; setting dumbTerminalGuidance = false will have missiles continue to use primary guidance until terminal targeting mode has a lock, vs immediately switching to terminal guidance the moment the distance trigger is satisfied. +- Competition: + - Add an option for the continuous spawn central point to follow the centroid of surviving craft (biased back towards the original spawn point). + - Add an option to automatically resume continuous spawn and to automatically shutdown KSP when continuous spawn ends due to having fewer than 2 craft left. + - Update the continuous spawn log parser to handle multiple files, either separately or combined, and to only show non-zero columns. + - Bias the continuous spawn's "bubble shuffle" to prioritise craft with fewer spawns. + - Hide the ALIVE / DEAD string in continuous spawn when the UI is hidden. + - Delay spawning craft in continuous spawning while the death cam is active to avoid lag during that shot. +- RWP: + - Add RWP S6R1 specific overrides. + +v1.6.9.0 +IMPROVEMENTS / FIXES +- UI: + - Fix the alternate (hidden UI) window locations (for scores and vessel switcher) not being saved correctly. +- General: + - New texture for 1.25m radomes. + - Add a max distance threshold setting to Team Icons. + - Fix "Camera Switch: Incl. Missiles" not switching to missiles about to hit their target. +- AI / WM: + - Add new Orbital AI part for spacecraft in orbit: + - The Orbital AI will use orbital maneuvers to close within gun range of the target to fire weapons. It will also automatically fire missiles when able. + - The AI will maneuver at the Maneuver Speed setting and once within gun range try to fire guns while at the Strafe Speed setting. + - It will evade missiles based on the countermeasure settings on the Weapons Manager and evade gunfire using AI settings similar to those on the Pilot AI. + - When spawned at orbital altitudes using the BDA Vessel Spawner, craft with the Orbital AI will be automatically spawned into orbit (other options for orbital spawning are cheat menu and HyperEdit mod). + - Orbital AI is adapted from the StockCombatAI (KCS) mod, check it out here https://github.com/Halbann/StockCombatAI/releases! + - Base the Immelmann turn pitch direction on the local pitch angular velocity (i.e., pitch down if already rotating that way — fixes the pitch oscillation behaviour when targeting an opponent behind the craft while inverted near terrain). + - Don't summon the NaN-Kraken when the post-terrain avoidance cool-down is 0. + - Tightened up 'on target' threshold for dumbfiring missiles. + - Perform banked turns when the target is behind us at long range and extending is allowed instead of Immelmann turns or turning directly to target. Threshold is clamp(8 * max speed, 1000, 4000). + - Pilot AI turns will now turn harder (using Max AoA setting) towards the notch direction the closer the missile is to impact. + - Factor the user-defined steer limit into the turn radius calculations. + - Use a more gentle take-off slope for the initial take-off. + - Add an Evasion Min Range Threshold option to only trigger evasion from an attacker using guns when they're beyond that distance. + - If enemies are detected nearby but beyond gun range and we're not aiming, perform nonlinear (evasive) weaving to avoid long-range sniping. + - Make the gun range slider more tolerant to following increases in the max gun range. +- Detectors: + - Can now set targeting camera slew speed using "traverseRate" field in ModuleTargetingCamera. + - Improve Targeting Camera ability to track moving/flying targets. +- Weapons: + - Fixes for initial bullet placement and simulated hit position when Krakensbane is not active. + - Account for the initial bullet placement in the bullet traveled distance. + - Fixes for aiming reticles of guns (both fixed and turrets) when using MouseAimFlight. + - Show where aimer lines in the SPH are blocked and by what. + - Add Deploy toggle to see where weapons with deploy animations will point when deployed vs retracted (e.g. M230 chaingun). + - Fix lasers using wrong armor thickness value for damage reduction calcs. + - Fixes point defense/APS turrets not engaging incoming missiles. + - Fix proximity/flak/beehive ammo detonation timing when used in orbit. + - Missiles: + - Add new Orbital guidance option for Modular Missiles. + - Missile will attempt to intercept orbital target at the user-set Max Speed value. + - homingType = orbital replaces homingType = RCS in missile configuration files (BDA is still backwards-compatible with homingType = RCS). + - Improvements to modular missile AAM guidance types so that they work in orbit. + - Fix for HEKV launching unguided in non-ideal situations. + - Fix clustermissiles launching copies of themselves instead of submunitions. + - Fix clustermissiles not respecting submunition trigger distance. + - Fix clustermissiles acting like reloadable missiles when not set up for reloading. + - Fix Guidance Type None missiles not holding course. + - GPS missiles now require a target cam or radar lock for GPS coords. + - INS missiles can now use either radar or IRST tracking. + - INS missiles now have datalink capability for mid-flight target coordinate updates (uses gpsUpdates field), update rate clamped to Radar/IRST scan speed. + - ECM can now jam GPS/INS datalink if missile within hostile ECM AoE. + - Fix error with Inertial guidance target prediction for non-updating missiles. + - Fix Missiles having 10 armor instead of 2. + - Fix SARH missiles that relock to a new target not updating their target vessel. + - Changed missile missed check, missiles now check if they have come within 400m, are post-thrust, or are more than 1km behind target, and have exceeded a grace period of maxTurnRateDPS/15. +- Countermeasures: + - Add vessel's velocity to chaff ejection particle effect. + - Use vessel acceleration instead of velocity for chaff decoy factor calculation when in space. + - Add option in WM's CM settings for auto-deploying CMs when not in guard mode (e.g., for manual combat). +- Vessel Mover / Spawning: + - Check for outdated loadMeta files when refreshing the craft list in the vessel selection window and update them since KSP doesn't update them when you save your craft! + - Fix the spawn point selection sometimes not registering. + - Vessels with an orbital AI that are spawned at safe orbital altitudes are spawned into circular orbits. Vessels are spawned pointing away from each other. + - Increase maximum spawn distance to 200km. + - Add a "bubble shuffle" to the waiting craft in continuous spawning to increase the randomness of the spawn order. +- ModIntegration: + - Fixes for inhibiting CameraTools when initial spawning during continuous spawning and disable auto-switching to newly spawned craft joining the competition. + - Provide helpers for CameraTools so that the camera can aim at missiles' targets (requires CameraTools v1.31.0). + +v1.6.8.0 +IMPROVEMENTS / FIXES +- General: + - Add tracking of 'named modules' (from non-dependency DLLs) to the VesselModuleRegistry. + - Optimise "Vector3.Distance(from, to) < dist" type checks. +- UI: + - rcsOverride enabled ECMJammers now properly affect RCS in the SPH/VAB RCS GUI. + - Fix Equivalent Armor Thickness readout in the Armor GUI. + - Fix custom ammobelt UI overwriting belts on heterogeneous weapon groups. +- AI / WM: + - AI GUI fixes for surface AI. + - Fix AI disengagement from launched nuclear/EMP ordnance if within projected blast radius. + - Expose the weave strength factor as a tunable parameter for the surface and VTOL AI. + - AI will now toggle/disable ECM Jammers if under fire from GPS/antiradiation ordnance. + - Adjust the off-boresight angles for DLZ checks for extending to prevent super long-range extends. + - Fix FSM not being started when initially deploying landing gear. +- Weapons: + - Fix BurstFirelength and FireAngleOverride toggles not working in flight. + - Fix APS parts getting mixed in with global weapon shortnaming and overriding barrage settings. + - Anti-Ballistic APS no longer assigns multiple turrets to a single incoming shell, now respects reloading/overheated state of the weapon during target assignment. + - Guns now properly respect crossfeed. + - Don't apply the unity integrator correction to the unsupported gravity component of the aiming (improves on-orbit aiming). + - Improve the estimate of the required OverlapSphere for Vessel-Relative Bullet Checks for hypervelocity craft. + - Abrams Turret maxDeviation decreased to 0.1; Abrams bullet spread should reliably hit a tank-sized target from 2km again. + - Missiles: + - Add new Inertial guidance mode. + - Add new 'inertialDrift' missileLauncher field for INS drift, in meters/sec. + - Have torpedoes head to last known contact point and start circling it, instead of current sink to bottom behavior. + - MultiMissileLauncher deploy anim toggle now affects symmetry twins. + - Fix various NullReferenceExceptions when trying to use Modular Missiles. + - Antirad missiles using antiRadTypes = 9 now properly home in on active ECM jammers. + - Craft without Radar/radarlock will now wait to fire radar LOAL missiles until target distance is close enough that radarTimeout < (tgtDist - activeRadarRange) / optimalAirspeed. + - Fix NRE when trying to fire laser-guided missiles from a turret with no targeting cam. + - Single stage Modular Missiles now activate their engine on launch without needing an AG setup. + - Continuously-updating GPS-guided ordnance can now be jammed by a ECM jammer if within the jammer's garble radius. + - Missile Interception reworked, tied to PD logic. Missiles set to engage missiles will automatically launch to intercept incoming missiles (assuming viable launch geometry). + - Hopefully fix the Missile Turret phantom force issue. + - Fix GPS missiles fired from a cargo/custom bay losing lock immediately on firing. + - Fix typo in Dynamic Launch Zone calculation that was preventing some missiles from launching. + - Remove global toggle to use Dynamic or Static launch range for missiles and replace it with a per-missile toggle. When toggled on, the missile will launch missiles at their max engagement range without taking into account craft velocities. + - Incorporate missile frontAspectHeatModifier value into missile selection logic so rear-aspect missiles are not selected if they cannot be fired. +- Competition: + - Include last-damaged-by tracking for bullet explosions that damage vessels, not just hits. + - GM kill when weaponless now looks for no weapons with ammo instead of no weapons. + +v1.6.7.0 +IMPROVEMENTS / FIXES +- General: + - Kerbal Foundries wheel and track HP now properly scales with wheel size. + - ProcArmor now has symmetry attach. + - Fix Armor panels < 10mm thick resetting to 25mm. + - Toggling Battle Damage will disable Paintball Mode, if enabled. +- UI: + - Windows that show despite the UI being disabled (i.e., Vessel Switcher and Score windows) store a separate size and location for that mode. + - Use a resize handle instead of buttons for horizontally resizing the Vessel Switcher window. +- AI / WM: + - When ramming, if the target is also trying to ram us, aim more directly at them (avoids unstable dynamics). + - Fix some depth-keeping logic with Submarine AI. + - Guard Mode toggles off if enabled when loading into a battle site from the KSC that had active combatants at the time player previously exited to Space Center. + - Fix secondary target assignment logic assigning targets irrespective of target priority settings. + - Add UNDERWATER_VISION view distance option so ships require sonar to detect submerged subs, and submerged subs require sonar to detect other vessels vs being able to see them as long as they are within guard range. + - Add attack logic for gun-armed submarines when BULLET_WATER_DRAG is disabled. + - AI now respect target priority settings when looking at landed or splashed targets. + - Fix issue with ground vehicle AIs not selecting missiles against ground targets in some circumstances. + - AI now less jittery when doing bombing runs. + - Add new 'Maintain Min Range' toggle for Land AI; if enabled Vees will stop/reverse to maintain minimum engage distance to target. + - Change Surface AI cruise behavior; if MaintainMinRange, craft will slow as before until minRange reached; if disabled, Vees will maintain flank speed regardless of target distance. + - Add missing combat Depth slider to AI GUI for Subs. + - AI now has longer object permanence for stationary/slow land targets it has lost sight of. + - VTOL AI makes sure Landed status is disabled during take-off. +- Countermeasures: + - Add missing Active Sonar countermeasure. +- Detectors: + - Fix Radars utilizing a max of 3 locks, regardless of radar-lock capable radars present on a vessel. + - Enemy passive sonars no longer show up on the Radar Warning Reciever. + - IRSTs now return the hottest enemy target they detect, rather than whatever is first on the targets list when using it for manually firing heatseeking missiles. + - Fix multiple radars locking the same targets; radars will now properly distribute targets to lock amongst themselves. + - Fix Radar GUI not properly displaying radars using custom radarTransforms. +- Weapons: + - Bullets now take relative velocity into account when calculating damage. + - Fix Dual-mode APS turrets getting slaved to radar when firing on standard targets and not subsequently engaging incoming missile targets. + - Fix APS occasionally getting stuck on phantom targets. + - Rework aiming logic for improved long-range aiming for vessels moving at orbital speeds (still not entirely correct for low Kerbin orbit at longer ranges, but significantly better). + - Missiles: + - Fix missiles not firing on surface ships. + - Missiles now properly receive explosive damage from point defense HE rounds/interceptor missiles in paintball mode. + - Fix missiles occasionally getting stuck when fired from VLS. + - Fix Missiles failing LOS check due to kerbins curvature for ship vs ship engagements. + - Fix for GPS missiles fired from multiMissileLaunchers losing lock when fired. + - Fix guidance type = none missiles detonating 5km from launcher. + - Fix NRE from manually firing GPS missiles from a multimissilelauncher. + - AIs attempting to dumbfire multimissileLaunchers will now take missile launch vector into account instead of vessel prograde. + - Adds new Settings option 'LIMITED_ORDINANCE' to clamp multimissilelaunchers and reloadable rails to a single salvo's worth of ammo. + - Fixed GPS coords not updating for bombs. + - Fixed occasional NRE when using laser-guided missiles in a surface to surface role. + - AI can now aim unguided/dumbfired missiles with the missile turret. + - AI can now use JDAMs/other guided bombs as unguided bombs when lacking targeting cam instead of aborting. + - Improve Bomb lead calcs for better bombing accuracy against moving targets. + - Fix turrets spinning wildly about when AI loses track of target and target position goes stale. + +v1.6.6.0 +IMPROVEMENTS / FIXES +- General: + - Procedural Wing max armor thickness now based on mean thickness of the wing. + - Adds new Acoustic Decoy CM dropper. +- UI: + - Add Japanese localization files thanks to user Taka005. +- AI / WM: + - Fix the cubic solver giving invalid results for nearly degenerate cubics (used in aiming). + - Fix the debug line trajectories not showing the final part of the trajectory. + - Add experimental Submarine AI option to Surface Driver AI. + - Various weapon selection logic tweaks to support sensible weapon selection/use when submerged and firing on non-submerged targets. + - Add torpedo bombing logic to pilot AI. + - AI will now lead moving ground targets when bombing. + - AI will start dropping bombs from reloadable rails/bomblet dispensers early for proper carpet bombing instead of dropping them behind a target. + - Fix AI dropping bombs several hundred meters behind target. +- Detectors: + - Add new radarTransformName field to ModuleRadar/new irstTransformName field to ModuleIRST to allow setting custom radar direction/setting multiple radars to a part using custom radar models. + - Add new sonarType field to Radar for setting up Active or Passive sonars. +- Countermeasures: + - Add Acoustic Decoy CM support via 'countermeasureType = decoy'. + - APS point defense target acquisition and assignment logic rewritten, APS will now semi-intelligently identify and distribute targets among guns in the PD network. + - APS weapons now respect Fire Interval and Burst Length settings. +- Weapons: + - Missiles: + - Fix some NREs when manually firing missiles. + - Add new guidanceDelay field to allow setting a delay between missile launch and missile beginning to maneuver towards a target. + - Dynamic Launch Range now uses missile EngageRange max instead of static launch range. + - Improved launch behavior for missiles set to be fired at very short range. + - Add LineOfSight check when firing missiles. + - Fix torpedoes not working. + - Missile/Bomb fratricide prevention now based on team instead of sourcevessel. + - Add Salvo size slider to MultiMissile Launchers, enabled via the new MultiMissileLauncher .cfg 'setSalvoSize' bool. + - Improve launch of missiles from reloadable rails. + - Add new MultiMissileLauncher 'deploySpeed' field to set deploy animation duration. + - Add intrinsic scaling option to MultiMissileLauncher for missile pods and the like, use new fields 'scaleTransformName' and 'lengthTransformName' + - Fix issue with air-dropped torpedoes exploding on impact with the water despite being within their waterImpactTolearance. + - Fix torpedo behavior if trying to target something with a CoM above water level + - Tweaked weapon lead aim calcs for guns firing beehive shells, Oerlikon should hit more often now. + - MultiMissileLaunchers can now support GPS guided missiles launching at multiple simultaneous targets. +- Competition: + - Add new GM Kill conditions for planes that have lost engines/weapons/set percentage of their parts and LandedKillTimer conditions for surface AIs that have found themselves in water/water AIs that have beached themselves/sunk. + - Don't auto-resume tournaments when custom spawn templates are selected. +- Vessel Spawning: + - Allow using custom spawn templates for airborne spawning too (with altitude >10m). + - Add option to immediately set airborne-spawned vessels (with a pilot AI) to their idle speeds. + - Tweaks to initial focussed vessel after circular spawning. + - Always perform vessel validity checks and wait for WMs to appear in the Vessel Switcher during post-spawning even when not immediately starting a competition. +- Vessel Mover: + - Make the vessel selection more resilient to corrupted vessel files/loadmeta. + - Default the vessel selection window to using the VAB if the initial vessel is spawned through the launchpad. + +v1.6.5.0 +IMPROVEMENTS / FIXES +- General: + - Fix Pwing HP error with Pwing edges not contributing as much HP as they should. +- UI: + - Fix numeric input fields not updating when they should. + - New Team Icons toolbar icon for easier identification. + - Add colour coding to the number of parts in the vessel selection windows. +- AI: + - Add a toggle to the pilot AI to disable ramming ground targets. + - Add a toggle to the pilot AI for treating the min altitude as a hard limit. + - Include the TWR of a vessel when determining the gain altitude slope, allowing much higher slopes. + - Include a climb rate at half the take-off climb rate while evading below min altitude. + - Prevent inverted loops when below min altitude and improve the inverted loop threshold calculation. + - Improve vessel collision avoidance by prioritising non-debris and by prioritising potential collisions by size. + - Add a "post-avoidance cool-down" slider to the pilot AI to increase pitch away from terrain for a short period after avoiding terrain. + - Add a Manoeuvering steer mode similar to NormalFlight steer mode, but with Aiming mode's roll behaviour. + - Switch a number of inappropriate uses of Aiming steer mode to Manoeuvering mode. + - Add a green debug line for showing the pitch target direction the AI is aiming for (which is not necessarily the same as the position the AI is told to go to). +- Armor: + - Allow changing the unclamped min/max dimensions for proc armor in the settings.cfg (PROC_ARMOR_ALT_LIMITS). +- Weapons: + - Fix APS point defenses not respecting Infinite Ammo cheat. + - Fix APS weapons not activating when Guardmode enabled, if they aren't already active. + - Anti-Missile weapons can now shoot down missiles in Paintball mode. + - Missiles: + - Add option (in Show Missile and Countermeasure Settings) to allow missiles to be fired from their full dynamic launch range instead of static launch range. + - Add trigger range setting for cluster missile submunition launch. + - Fix MultiMissileLaunchers resetting detonation Distance. + - Fix MML "TNT Mass Equivalent = -0.0018726" PAW label. + - Fixed Missiles with ModuleMissileRearm improperly launching. + - Fixed MMLs resetting detonation distance. + - Multi Missile launchers now check clearance based on tube orientation regardless of overrideReferenceTransform setting. + - Missile/bomb fratricide prevention now also checks the source vessel name to allow explosives to be destroyed by explosives of the same type from another vessel. + - Fix NRE from SARH missiles fired without a lock. + - Missiles now properly calculate offboresight angle from missile orientation, not vessel prograde. +- Competition: + - Adjust round progress strings in tournaments: ranked => 0→N, shuffled => 1→N. + - Report total number of rounds for ranked tournaments properly. + +v1.6.4.1 +IMPROVEMENTS / FIXES +- UI: + - Add an option for disabling the self-updating of the text in numeric input fields. + - When the value fails to parse (after the delay) the text will turn red. + - Toggling the numeric fields or closing the windows will reset the field to the most recently successfully parsed value. + - The text will still be updated if the parsed value becomes clamped within the limits of the field. + - Make the delay for the "read and interpret" logic of numeric input fields customisable (default 0.5s). +- AI: + - Some QoL improvements for auto-tuning: automatically enable AI and engines and move the craft to the autotuning altitude. +- Detectors: + - Fix Conformal Decals throwing off RCS calculation in editor (SPH/VAB). + - Hide scenery during RCS calculation in editor (SPH/VAB) to prevent scenery from affecting calculation. + - Add for/aft/side/top/bot RCS readout to the On-Screen Telem debug option when ASPECTED_RCS is enabled. + - Add slider for ASPECTED_RCS_OVERALL_RCS_WEIGHT to the BDA Settings menu. +- Countermeasures: + - Fix exceptions caused by ECM jammers. +- Competition: + - Fix a bug in the score window for teams competitions. + - Automatically enable the teams score toggle in the score window if the "Teams" slider in the vessel spawner window is set to teams mode on start-up. + - Dump a "team scores.log" file to the Tournament folder for teams tournaments after each round. + +v1.6.4.0 +IMPROVEMENTS / FIXES +- General: + - Make ammo consumption respect staging and priority order unless "externalAmmo = false" in the weapon config. + - Don't auto-switch vessels when MouseAimFlight is active. + - Fixed an issue where modules on a vessel re-entering the PRE range weren't being updated in the VesselModuleRegistry. + - Fix a number of issues with Pwing+FAR HP calc. + - PWing Thickness-based Mass+HP now takes PWing Edges into account if PWing Edge Lift enabled. + - Fix armored cockpit mass resetting on load. + - Add search tags to all BDA parts. +- AI: + - Make the autotuning re-centering distance customisable. + - Add unclamped max values for auto-tuning speed and altitude. + - Prevent weapon selection from choosing weapons when target is closer than the weapon's minSafeDistance. + - Fix AI potentially seeing beyond viewrange. + - Increase preemptive lead distance AI will begin leading target before entering engagement range. + - AI will now switch off of Afterburner if AB is out of fuel (for Rapiers/other LF/non-air AB-mode engines). +- Vessel Spawning: + - Disable gravity easing when using 'Warp Here' above 10m altitude. +- Vessel Mover: + - Add support for browsing craft in subfolders of the SPH and VAB (also in the custom spawn templates). + - Allow resizing the vessel selection window and remember its position. +- Weapons: + - Fix various NREs related to missiles and multi-missile launchers. + - Adds optional tracer smoke. + - new 'smokeTexturePath' setting for ModuleWeapon to set custom smoke trail FX, leave blank for default bullet FX + - Properly support weapons with multiple ModuleWeapon modules. + - Turret integration with ModuleMultiLauncher for MML style Hydra Turrets, etc. + - Fix missiles fired from same source vessel causing each other to explode when they cross within proximity detonation distance. + - Add flareEffectivity = 1 in missile configs. Modifies how the missile targeting is affected by flares, 1 is fully affected (normal behavior), lower values mean less affected (0 is ignores flares), higher values means more affected. +- Countermeasures: + - Add rcsOverride field to ModuleECMJammer to enable setting craft RCS to a set value. + - Split CM time into missile evade time and CM deploy time to allow setting a craft to begin evading, then deploy CM, etc. + - Fix an issue with chaff vessel modules summoning the Kraken on undocking. + - Fix an NRE related to jammers enabling. +- Competition: + - Make the saving of backup tournaments optional (hidden setting TOURNAMENT_BACKUPS in settings.cfg). + - Added a ranked tournament mode. + - Ranked tournaments begin with a single shuffled round (craft assigned to heats randomly), then continues to generate ranked rounds (craft assigned to heats based on their current rank) until the number of ranked rounds run matches or exceeds the rounds slider (which can be adjusted during a tournament). + - Ranked tournaments only work for FFA style tournaments. nCr and gauntlet style tournaments aren't supported. + - For teams tournaments, the combined score for the teams is used for the ranking. + - Enable the score window via the "Sc" button on the Vessel Switcher window. + - Use the "UI" button to keep the window visible even when the rest of the UI is disabled (F2). + - Use the "±" buttons to adjust the font size. + - Use the "W" button to adjust the score weights. + - Use the "T" button to switch between individual and team scores for teams competitions. + - Left click and drag the drag handle to resize the window. + - Right click the drag handle to switch to auto-adjusting height. + - Scores update after each round (not heat) in a ranked tournament. + - The tournament.state file uses a new format (recursively encoded JSON due to Unity's limited JSON functionality), contains a lot more information and isn't compatible with older tournament.state files. + - A script (parse_tournament_state.py) can convert the tournament.state file to proper JSON (tournament.json) and back again for ease of editing. + - Fix a minor bug in the tournament log parsing script where wins weren't always being counted for the score. + - Make the saving of backup tournaments optional (hidden setting in settings.cfg). + - Add NPC support for FFA-RNG tournaments (both ranked and non-ranked). + - To use NPCs, create a "NPCs" folder in the folder for the tournament (e.g., "AutoSpawn/MyTournament/NPCs"), add the NPC files to this folder, then set the "NPCs Per Heat" slider to the desired number of NPCs. + - NPC files will be reused as necessary to provide the appropriate number of randomly selected NPCs each heat. + - When generating heats, the number of NPCs is taken into account for the total number of vessels per heat (e.g., 8 vessels per heat with 2 NPCs per heat would give 6 players + 2 NPCs in each heat). + - The "Tournament Scores" window will only show the scores for non-NPCs. + - Add an "auto" setting for the "Vessels Per Team Per Heat" in teams tournaments where the teams will simply be all the craft files in each team's folder. +- Detectors + - Add settings toggle ASPECTED_RCS that enables real-time RCS evaluation based on the target aircraft's azimuth and elevation relative to the radar evaluating it. + - Enable from the BDA settings menu under Missile & CM settings: "Real-Time Aspected RCS." + - The real-time aspected RCS is dependent on a weighted average against overall craft RCS, so lower overall craft RCS will lead to better aspected RCS. + - Overall craft RCS weighting in this average is dependent on ASPECTED_RCS_OVERALL_RCS_WEIGHT = 0.25 in the settings.cfg file. + - Final aspected RCS will be = (1-ASPECTED_RCS_OVERALL_RCS_WEIGHT) * [Aspected RCS] + ASPECTED_RCS_OVERALL_RCS_WEIGHT * [Overall RCS]. + - RWRs now activate when they first detect a missile instead of when missile enters visual range. + - RWRs now track active Radar missiles regardless of missile visibility if VARIABLE_MISSILE_VISIBILITY is enabled. +- RWP: + - S5R10 special settings. + +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 ordnance 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 ordnance 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 Ordnance 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 Ordnance 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. +- Add slider in the pilot AI for waypoint yaw authority time to switch from normal steering mode control of yaw to aiming steering mode of yaw when approaching waypoints. +- Add relevant AI GUI entries for these too. +- Also check for WM when checking that a waypoint competition is running. + +v1.4.18.3 +IMPROVEMENTS +- Waypoint visualization added. + - 3 rings with radii 500m, 100m and 10m, with a central crosshair and animated arrows. + - Togglable in the Waypoints Options section of the Vessel Spawner window. + - Waypoint model can be customized to whatever, provided the model URL is "BDArmory/Models/WayPoint/model". + +v1.4.18.2 +IMPROVEMENTS +- Predict the minimum deviation on the frame before passing a waypoint to avoid the bad fly-to direction for the frame after passing that waypoint. +- Roll into waypoints 0.5s ahead of the waypoint in preparation for the next one. +- Add initial support for using splines. + +v1.4.18.1 +FIXES +- Check waypoint completion when in waypoints mode even if a different action is being taken (fixes extra large deviations and missed waypoints). +IMPROVEMENTS +- Interpolate the waypoint deviation to give a better estimate of the minimum deviation. + +v1.4.18.0 +Note: Due to some internal refactoring, CameraTools also needs to be updated to version 1.21.0 to work with this version of BDArmory. +FIXES +- Add checks to only adjust scores when a competition is active. +- Fix interpretation of demo vessel optimisation script. +- Fix CASE-2 ammo reduction when cloning part. +- Fix laser battledamage. +- Fix DebugLines not disabling when toggled off. +- Reorder custom bay toggle branch so it's not overwritten by airstream check (fixes custom cargo bay not being deployed). +- Fixed Anti-Radiation missiles not working properly. +- Fix barrage mode fire rate decrease/guns never overheating. +- Fix armor panels being too protective against HE. +- Fix selection logic for missiles when targeting missiles. +- 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). + - The VTOL AI behaves like the Surface AI in combat. + - The AI flies your craft by using throttle to control altitude and pitch to control speed. + - The AI will try to stay above the minimum altitude at all times and if it can, at the default altitude outside of combat and the combat altitude during combat. + - The AI will obey the max pitch and max bank settings and fly no faster than the set maximum and combat speed settings. + - Don't treat passive (non-radar-guided) missiles outside visual range as incoming missile threats until they enter visual range. + - Lower the altitude at which gear is toggled to the minimum of 50m or minAltitude/2. + - Lower the minimum of the terrain avoidance sliders to allow values below 1. +- WM: + - Extend MMG smartpick selection logic. + - Targets in guard mode no longer receive RWR warnings for missile launches outside their visual range. + - Craft in guard mode will turn off their radars if they have an incoming anti-radiation missile within visual range. +- Armor: + - Explosive Reactive Armor HP is now customizable. +- Weapons: + - Remove the 0.04s delay on firing a rocket. + - Add explModelPath and explSoundPath to Sidam. + - Clean up rocket aiming, switch it to use the more accurate leap-frog numerical integrator and use ballistic aiming once the thrust period is over. + - Set laser weapons to use standard FX pipeline. + - Create new antiradTargetTypes variable for missiles that allows you to specify the RWR Threat Type that the anti-radiation missile should target. Default is 0,5 (SAM,Detection). See the HARM config file for more details. +- GameModes: + - Paintball Mode now affects Nukes. + - Waypoint Mode added for racing/time trials instead of combat. +- Tournamements: + - Add checks to only adjust scores when a competition is active (fixes differences in the API scores due to timing issues). + - Use base64 encoded vessel names internally in the parser to avoid issues with craft naming (e.g., craft with ':' in the name). + - Adds NPC swapper functionality to remote orchestration tournaments. +- RWP: + - Add Hall of Shame code. +- UI: + - Add a Waypoints Mode section to the Vessel Spawner window when the Waypoints game mode is enabled. +- Internal: + - Replace hard-coded RWR threat types with RWRThreatType reference. + - Remove reference to TargetingModeTerminal since TargetingMode is updated to be equal to TargetingModeTerminal once terminal guidance activates. + - New spawn and orchestration strategies are partially implemented. + - Some refactoring of the spawning code is in progress. + +v1.4.17.0 +FIXES +- Fix auto-resume evolution not finding the evolution.state file. +- Fix localization for target priority on weapons manager. +- Configure the paths when loading the statefile if they're not configured. +- Avoid NRE in ModuleResourceDrain due to detonating when the parent part is being destroyed. +- Fixes Ammo belt Configuration, can no longer be used to stick ammo from one gun into the belt of another. +- Fixes Weapon Manager GUI weapon selection glitch. +- Fixes gun reticles floating a few meters in front of the weapon. +- Fix subMunitionCount = 0 in custom bullet/rocket defs breaking weapons. +- Fix Protect VIP(s) target priority for when there are more than 2 teams. +- Give default gpsTargetName as Unknown instead of empty to avoid KSP's flightstate autosave from complaining. +IMPROVEMENTS +- General: + - Add in the remaining localisations for parts. + - Add hotkey option for time scaling. + - Add options (in Radar Settings section) to invert the mouse in the targeting window. + - Add an Auto-Load-To-KSC option (in the GamePlay settings section). +- AI: + - Use maxAllowedAoA in post-stall test in FlyToPosition instead of hard-coded 30°. + - Tweak the clamped altitude and speed min/max values to give more usable sliders. + - Scale steer limiter based on atmospheric density in order to account how dynamic pressure will vary based on this value. + - Add the target's average radius to the vessel-avoidance detection. + - Adjust the default vessel-avoidance values and increase the upper limit of the avoidance strength slider. + - Use an average of the recent speed instead of the max speed in determining the turn radius. + - Adjust the extending to update the extend-from position when the extend-from target is a vessel. + - Replace the extend multiplier with explicit extend distances for air-to-air, air-to-ground (guns) and air-to-ground. +- WM: + - Add a Protect Teammates target priority. +- GameModes: + - Add options for respecting the flow state of stealing (in)/stolen (out) resources. +- UI: + - Add a "Slider Resolution" slider to the pilot AI (currently just PID, Altitudes and Speeds) to allow finer or coarser adjustments to those sliders in the PAW (not the AI GUI for now). +- Weapons: + - Add in option to set an action group toggled for when weapons are selected/deselected (can be used for deploying weapons or for other actions). + - Ammo belt tool now loads in existing belts, if present. + - Abrams can now properly select ammo types in flight. +- Armor: + - Configurable Armor panels should cause less lag on loading in. +- Tournaments: + - Add option to save the summary plot to a PNG file. Use '-s' to save to a random tempfile, use '-s fig.png' to specify the file. + - Tweak the auto-vessels-per-heat optimisation for when we're just over the limit. +- Internal: + - Add option to use average radius in Vessel.GetRadius instead of max. + - Rename Misc.Misc to Misc.Utils to fix naming conflict between namespace and class. + - Some debug message cleanup to reduce log spam. + - Replace loading a clean.sfs save file with generating a new clean sandbox game. + +v1.4.16.1 +FIXES +- Fix for tournament.state files with craft filenames containing commas (don't use filenames containing '],['!). +- Fixes issue breaking some guns in v1.4.16.0. +IMPROVEMENTS +- Adds localization for various parts. +- Adds Deployable Missile Rail demo part. + +v1.4.16.0 +FIXES +- Don't clamp values when switching from text input to sliders and the unclamped option has been toggled. +- Fix wrong component type in BD check. +- Fix rocketpod RoF slider. +- Fix barrage-firing burst-fire weapons; these will now properly ripple again. +- Remove the restriction on the target being ahead of the weapon for opening/closing cargo bays as it messes with missile bays while planes are evading. +- Fix ammo selector issue. +- Fix various NREs. +IMPROVEMENTS +- General: + - Add Waterfall config for the Saturn engine. + - Increase the HP of the combat seat to the same as a kerbal (500). + - Add a time scaling option to the GamePlay section of the settings for speeding up/slowing down the game rate while competitions are active. + - This does not affect the physics time-step, which remains at 0.02, but simply tries to make the in-game clock go faster with respect to a wall clock, so it should not affect the physics (if they're implemented correctly). + - WARNING: This is likely to mess with any other mod that adjusts the time rate, e.g., TimeControl, if they're active at the same time. + - WARNING: Consistency of the results (compared to running at normal speed) is not yet verified, but initial results look promising. +- Internal: + - Add the script for evaluating the score weights and the old dmg_analysis (for .50cal, 20mm and 30mm ammo) to '_Other Stuff'. + - Remove executable status of the vessel-trace plotting script. + - Replace hard-coded layer mask numbers in raycasts and similar with bitwise combinations of the LayerMasks enum. Many of these raycast include questionable/unknown layers. + - Move the main competition update loop (death checks, etc.) to be run consistently from the physics updates instead of from the rendering updates to remove results bias due to computer performance when PHYSICS_FRAME_DT_LIMIT is large. +- AI: + - Tweaks to VacuumAI terrain avoidance. + - Add the off-by-one frame fix to ramming and missile guidance. + - Add logic for planes in Follow mode for the period between taking off and reaching their min altitude. + - Add unclamped values for the low- and high-speed steer limiters. +- Weapons: + - Move finalFire checks into a single function that runs in AimAndFire during the FashionablyLate timing phase instead of being spread between Update and FixedUpdate. + - Add check to AI when using a weapons group to grab the lead offset from a weapon that can fire (has ammo/not reloading/overheated/pointing at self), not merely the first weapon found. + - Reduce spin up times for Vulcan/Gaus. + - Fix shrapnel radius of missiles/bombs/shells using standard warheads. + - Add support for multiple laserbeamFX textures. + - Add DeployableRail module for missilebays. + - Explosions from impact-fuzed HE bullets now properly bypass armor when calculating damage to the hit part. + - Add weapon priority slider allowing setting custom weapon selection order for Missiles/Bombs. +- Armor: + - Add additional check to ArmorTool, can no longer make engines out of wood. + - Proc Armor panels no longer clamped to 0.5m increments. + - Fixes proc armor panels slightly shifting attached parts on load. +- Tournamements: + - Add some timestamp and duration output to the tournament summary. + - Include damage taken in the console output of the parsing script. + - Adjust spacing in output from parsing script. + - Add an optional title to the summary plots. + - Add debug statement about which savegame is loaded with auto-resume functionality. + - Simplify the score calculation slightly and make it slightly more easily customisable. + - Update the score weights in the tournament parsing script to be more balanced. + - Remove version strings from the python script filenames and add a --version option to the scripts instead. + - Add a new (default) 'Fill Seats' option to fill all cockpits, or fill the first combat seat if no cockpits are found. + +v1.4.15.0 +FIXES +- Adds some checks for inerted tanks catching fire in some edge cases. +- Fix firebottle gui not updating when they're used. +- Make nukes detonate when fired. +- Fixes for continuous spawning. +- Only report scores to the remote API and stop the heat if the competition actually starts (avoids the empty result from a competition failing to start). +- Register the collision detection time for the case where the other vessel has disappeared (fixes some invalid death times). +- Move evolution path configuration from constructor to Awake to satisfy MonoBehaviour conditions. +- Fix cost of aluminum armor panels. +- Fueltanks will no longer randomly explode when shot. +- Fix ModulePartVariants giving negative money with wooden parts. +- Fix LeakFX not appearing. +- Fix fuel leak duration. +- Fix InertTanks/SelfSealing Tanks/Armored Cockpits not persisting through reloading. +- Fix various NREs. +IMPROVEMENTS +- General: + - Option for forcing gliders. Specify 'NoEngines' in the RWP cheat-code box to toggle it for now, or just edit the settings.cfg file. + - An updated continuous spawning log parser. + - Adjust the ballistic trajectory simulation time-step multiplier based on the distance to the target to give improved debug lines/targeting line visuals. + - Armor Panels now use Armor Integrity instead of HP. + - Nukes now support custom FX. +- AI: + - Also avoid 0 altitude (terrain avoidance) for bodies without solid surfaces or oceans (i.e., Jool). + - Reworked the integral component of the PID to work as an actual PID again: + - The integral is no longer reset to 0 when the pitch error changes sign, nor decays with time. + - The integral is now vector valued to account for coupling between the yaw and pitch axes due to roll and the individual pitch and yaw components are projected out from this integral. + - The maximum contribution to the steering from the integral component is 1 (similar to the magnitude of the individual components before this change, but larger than the original 0.2 prior to the previous change). + - The internal yaw damping factor is increased from 0.2 to 0.33 to keep the internal ratios between the axes consistent for each of the P, I and D components (1:0.33:0.1 for pitch:yaw:roll). + - Use missile velocity vector for evasion instead of missile relative position to prevent turning into missiles when they are at close range. + - Use soonest time to closest approach instead of closest distance to select highest threat missile. + - Change closest approach point to ignore missile threats to 3 Blast Radius from 1.5 Blast Radius. + +Message #tea + +- Weapons: + - Adds Chaingun functionality; chainguns have user configurable RoF in the SPH/VAB. + - Adds Weapon Spool up time functionality (for Gatling guns, etc). + - Lasers get damage/FX growth option for increasing damage/changing beam width/color the longer they've been firing. + - Adds support for EMP, Beehive, Impulse, Gravitic, and Nuclear bullets. + - Expands GUI ammo labels to include above types. +- Battle Damage: + - Restore ability to add firebottles to engines. + - Adds a toggle for whether burning fueltanks will explode. + - Toggling paintball mode now toggles BattleDamage off. +- Tournaments: + - Add the savegame field to tournament.state on tournament generation. + - Add Auto-Quit at end of tournament hidden option (for automation: AUTO_QUIT_AT_END_OF_TOURNAMENT). + - Add custom max number of rounds to tournaments (edit the settings.cfg file to set it: TOURNAMENT_ROUNDS_CUSTOM). + - Make the auto-generated vessels-per-heat setting customisable (by editing the settings.cfg). + - Rename the summary plotting script and allow specifying the tournament folder. + +v1.4.14.1 +FIXES +- Fix typo in 2x1 legacy armour panel. +- Fixes missile turret Cost. +- Fixes missile RMB warhead info. +- Fixes NRE with proc armor parts on loading a craft. +- Nukes no longer explode when destroyed (if engineCore = false, so S3R1 Chernobyl functionality still present, just not all the time). +- Account for starting resource cost modifier to part cost, fix discount to reduce up to 500 from cost. +IMPROVEMENTS +- Restores ability to set custom tracer stats in weapon.cfg. Use "tracerOverrideWidth = true" in the cfg to use override values instead of dynamically determined tracer settings. +- BattleDamage fire chance on hit now editable in the settings config. +- Teams: + - Adds ability to set teams to neutral in team selector/VesselSwitcher/WM. + - Right clicking on the team button in the Team Selector/WM will toggle non-default teams (A, B, Neutral) neutral status. + - Right clicking on the [t] button to the right of a vessel button in the VS will open the Team Selector. + - Left click will switch to next combat team, mouse-3 will switch to next neutral team. + +v1.4.14.0 +FIXES +- Switch worlds to the spawn point before spawning if the spawn point is on a different world (fixes broken tournament resuming on other worlds). +- Use GetRadarAltitudeAtPos instead of radarAltitude as the latter reads wrong for our purposes when underwater. +- Fix the bad check preventing competition start if a vessel doesn't take off. +- Fix NRE in CheckExtend. +- Remove extraneous trajectory simulation when firing a bullet. +- Fix infinite recursion bug in the evolution engine. +- Compensate for the delay in calculating the effects of collisions and properly attribute deaths (including death order and time) due to ramming correctly. +- Fix bug in part counting that was occassionally causing negative part counts in rams. +- Handle EVA kerbal disappearing during KerbalSafety configuring. +- Add null check to IsKerbalEVA extension. +- Fixes .50 turret explosion model. +- Fixes rocket aim reticle hovering a meter in front of muzzle. +- Fixes some localization typos. +- Fix Nuke FX scaling issue. +- Fixes Fuel Inerting mass discrepancy. +- Fix Inerted tanks still being able to catch fire. +- Fix Firebottles not extinguishing fires. +- Fix CASE II bug setting non-integer ammo amounts. +- Fix Armor of pretty much any thickness completely stopping explosion damage. +IMPROVEMENTS +- AI/WM: + - Use CM threshold for determining missile closest approach instead of a fixed 2s value. + - Add alternate max for strafingSpeed. + - Switch turrets to use the Firing Angle instead of needing to be directly on-target. + - Added a nonlinear oscillation ("Evasion/Extension Nonlinearity" in the pilot AI) when extending or evading to avoid going in a straight line. + - Lasers now selectable by the AI for attacking ground targets. + - Adds some catches to AI MMG selection logic. +- Armour: + - Abrams Turret can now support more than 10 armor. + - Armor type 'None' no longer takes damage/spalls. + - Armor Plates can now be set to armorType 'None', used as structural panels. + - Adds new BDAdjustableArmor module. + - Armor panels replaced with new procedural armor panels. Old panels moved to legacy to prevent breaking saved craft. +- Weapons: + - Adds underwater rocket trajectory calcs to permit AI to aim underwater rocket weapons. + - Nuke damage falloff calcs corrected, blast radii no longer far larger than they should be. + - Nukes will no longer hit craft that have managed to get out of the blast zone before the blast wave arrives. + - Adds Clusterbomb rmb Info. + - Adds shaped charge and continuous rod warhead options to Rockets/Missiles/Bombs, use "warheadType = "shapedcharge" or "continuousrod" in the BDexplosivePart Module of the part.cfg, or setting "shaped = true" in the rocket config. + - BDA stock missiles updated with new warheads where appropriate - check RMB info in the editor. +- Tournaments: + - Add option to auto-generate a tournament if auto-resuming and there isn't an incomplete tournament to run. +- Parser: + - Use a more natural sorting order for rounds in the parser so the score-by-round is consistent. + - Use more precision in the csv file for the cumulative scores. +- Evolution: + - Maintain evolution state and properly add in support for auto-resuming evolutions. + - Add current heat to the evolution window. + - Include rockets fired and rocket strikes in the evolution scoring metric. + - Update the set of parameters the evolution engine can modify for the latest pilot AI and remove invalid ones. + - Limit evolved control surface authority to 150% (i.e., in-game limits). + - Add auto-quit memory threshold between evolution groups. +- Spawning: + - Add some checks for missing vessels during post-spawning. + +v1.4.13.2 +FIXES +- Fix comboboxes triggering force close on themselves when they should be opening. +- Use the correct spawn distance based on the spawn distance toggle for tournaments. +- Don't extend from ground targets when using a weapon that doesn't target ground targets, immediately look for another target instead. +IMPROVEMENTS +- Set the default evasionTimeThreshold to 0.1s. +- Make all custom paths relative to the KSP install directory to remove dependence on running location (PRE needs fixing for this to be usable though). +- Count rockets fired separately from shots fired and calculate rocket accuracy from the number of rocket strikes. +- Disable guard mode on spawned vessels during spawning and clear their current target (prevents using AG10 to activate guard mode earlier than should be allowed). +- Extend vessel easing during spawning until a vessel's cockpit is in the water (or it has stopped sinking) to allow it to settle a bit. + +v1.4.13.1 +IMPROVEMENTS +- Apply/de-apply the intake hack when toggling the option in the settings. +- Hack intakes (if enabled) on newly spawned vessels spawned by VesselMover or SPH→runway. +- Count rockets fired separately from shots fired and calculate rocket accuracy from the number of rocket strikes. + +v1.4.13.0 +FIXES +- Tournaments: + - Fix some typos and bugs in assist counting in the local tournament parser. + - Clear rocket dictionaries between heats. Compute assists under the same conditions for local and remote tournaments. + - Cause a spawn failure instead of throwing an exception so that tournaments can retry spawning. +- Craft/Battle Damage: + - Fix SelfSealingTanks being addable to engines. + - Fix SelfSealingTanks not showing on/off status on load. + - Fix SelfSealingTanks not respecting symmetry. + - Re-add ModuleSelfSealingTank to Cockpits (for armored cockpits). + - Fix fires not extinguishing if fuel < 0. + - Fix engine fire behavior, now capped to max one fire at a time. + - Fix Steel parts gaining more than the 1.75x HP they should be getting. + - Fix fire generation on subsystem damage. + - Fixes non-fueltank fires not burning out and extinguishing after a period of time. + - Fix inert tanks not catching all possible sources of fire generation. + - Fix invalid cast and NRE for modular missiles. +- AI: + - Fix AI aim offset being offset. + - Only extend when a bomb is selected once extending is requested. + - Activate only the current mode of multi-modal engines. + - Release pilot AI from attack command if they have a target within gun range (fixes bug with very long-range guns not engaging targets at competition start), regardless of whether RWP toggle is on. +- General: + - Fix Paintball decals not appearing in paintball Mode. + - Fix AIR-2 Genie cost. + - Fix Explosions/Nukes not damaging buildings. + - Fix RESET_ARMOR resetting hull material. + - Fix Audio cutting out when firing lasers using looping SFX. + - Fix for KerbalSafety sometimes being given the KerbalSeat instead of the KerbalEVA as the part. +IMPROVEMENTS +- Internal: + - Allow BDGUIComboBox to be relocated/resized. + - Rework Nuke EMP radius calc; EMP radius at lower alt/lower yield is now much smaller. + - Bullet penetration depth capped to hypervelocity penetration depth. + - Add option for laser mutators to use Team Color for beam color (set bulletType in the mutator config to "def"). + - Add part and vessel to debug info for AddDamage and AddHealth. + - Add debug info for when fires get added if debug labels are enabled. + - Slight buff to chaff factor. + - Add some missing localisations. + - Remove expensive Find call during spawning, since we already know which objects it needs to find. + - Only run more expensive debug checks on the debug version, not the release version. +- BattleDamage: + - Fix SelfSealingTanks maxing tank capacity when switching instead of simply adjusting maximum. + - Increases CASE cost at higher levels (mass increase, ammo capacity reduction). + - Reduce firebottle extinguish delay time to 4 sec. + - Fire chance from HE rounds reduced from 33% to 20%. + - Hits to Wings/ControlSurfaces/Subsystems/Cockpits with overpenetrating rounds will now only proc a single BattleDamage event. + - Fires will no longer consume a firebottle per fire; on trigger, single firebottle will now extinguish all internal fires present on the part. + - Surface fires will now extinguish if vessel moving > 120m/s. + - Adds Fuel Inerting option to fuel tanks to prevent fires. + - Adds Armored Cockpit option to cockpits to reduce/prevent pilot Kill battledamage. + - Adds ProcWings support - can now add Firebottles/SelfSealing/Inerted tanks to Pwing fueltanks; Pwings with fuel can now catch on fire when shot. + - Adds missing BattleDamage 'Subsystem Damage' toggle. + - Makes BD to control surfaces less immediately debilitating. +- UI: + - Add check for disabled PRE when competitions are active. + - Add a check for the number of AIs and WMs during spawning (when RWP is enabled) and give an in-game message if there's not one of each. + - Armor GUI submenu functionality based on current HP/Armor/Hull Reset toggle settings. + - Adds Hull Material visualizer to SPH/VAB Armor GUI tool. + - Improves Weapon Manager GUI infolink descriptions of target priority settings. + - Add off-world spawning to Spawn menu (spawn locations and tournaments get automatically upgraded). + - 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. +- Competitions: + - Adds Orbital Deployment round start mode - craft are spawned at 80km, and gravity hacked to 10 for 30 sec to accelerate craft downwards (request from SirJohn). + - Distinguish between deliberate and accidental rams. Accidental rams don't contribute to the score. +- Tournaments: + - Add a '-z' option to the tournament parser to shift the scores so that the lowest is 0. + - Add taken counts to the summaries. + - Add a '-z' option to the tournament parser to shift the scores so that the lowest is 0. + - Adjust rocket parts hit and damage fields to match ruby schema in the remote orchestration. + - 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: + - Add additional control "Afterburner Priority" for afterburner toggling under Speed category of AI. Default value of 50. 0 and AI will not use AB, 100 and AI will always use AB. + - Vary AB hysteresis based on "Afterburner Priority" setting using an exponential average to smooth the vessel's TWR (lag factor of 0.5s) to judge whether AI should toggle AB vs. using instantaneous acceleration. + - Default Guard Range increased to 20km. + - Allow custom/modded missile and bomb bays. Set which action group controls the bay toggling on the missile/bomb itself and the AI will automatically open and close the bay when dropping the ordnance. + - New default AI and WM settings. +- Spawning: + - Respawn the spawn probe when showing the spawn point if the active vessel has been destroyed. + - Use the backup location 'KSP/Ships/SPH' as a fall-back for the spawn probe location as CKAN puts it there. +- RWP: + - Enabling the Runway Project toggle will clamp guard range to a minimum of 20km. +ADDED/REMOVED/CHANGED SETTINGS VALUES +- Adds Intake Hack setting to autohack intakes to work in non-Oxygen atmospheres. +- Add a toggle in the Competition Settings section for closing the settings when clicking the start competition button. + +v1.4.12.6 +FIXES +- Add a check for tournamentState.rounds being null when checking whether to resume a tournament. (Fixes failure to generate tournaments under some conditions.) + +v1.4.12.5 +Re-release due to not having recompiled the release version before releasing it. + +v1.4.12.4 +FIXES +- Internal: + - Don't update the VesselModuleRegistry for a vessel when it's not yet loaded. (Fixes the missing AI/WM issue when reverting/quickloading.) + - Use a different function to reset the crew's inventory as the other one wasn't working properly. + - Register survivors with the remote orchestration in case they're didn't get registered for anything else. + - Also catch the LES as a SRB in ModuleSelfSealingTank. +- AI: + - Fix off-by-one-physics-frame error in FlyToTarget that was affecting aiming (especially noticeable for lasers). + - Remove extra Deg2Rad factor from low velocity correction to terrain avoidance so that it actually has an effect (post-stall behaviour near terrain). + - Revert the 0.5s afterburner change as it caused issues with multiple engines not toggling simultaneously. + - Sync the input fields the correct way on vessel switches (and loads). (Fixes the AI settings getting reset in flight mode when switching vessels.) + - Fix NRE in targeting info caused by moving spawning point with a vessel that has a current target. +- UI: + - Fix paintball mode not applying decals due to impulse weapons check. + - Fix the length of the substring in debug message for vessels that fail to spawn. + - Fix check in ExplosionFX for in-game messages about explosions (removes the rocket strike spam). +- RWP: + - Remove the previous conditions for the RWP S4R2 condition as they aren't being used for S4R2. +IMPROVEMENTS +- UI: + - Add "Show Self" option to Team Icons (icon + name only). +- Internal: + - Use warning level log entries for issues with the BDAScoreClient. Expand debug message to include stage number. + - Register and de-register game events in Kerbal Safety so as not to interfere with kerbals until the first competition is started. +- RWP: + - Apply the S4R2 'fill seats' condition to vessel spawning. +- Tournaments: + - Store unfinished tournament state files in 'PluginData/Unfinished Tournaments/' and remove them when they're finished. (Copy them to the usual tournament.state file when in the KSC screen to resume them.) + - Add a --show-weights option to the tournament parsing script to show the current weights of the various components of the score. + - Add an option to the parsing script to not show the cumulative scores by round. + +v1.4.12.3 +FIXES +- Sanitise null strings in paths. +- Fix armor panels reverting to 2 thickness. +- Fix some errors in the MM patch for parts. +IMPROVEMENTS +- Better in-game and log messages for when a competition fails to start due to a vessel disappearing. +- (RWP only) Add in-game reminder for AI and WM not attached to the root part. + +v1.4.12.2 +FIXES +- Fix negative HP issue with Reactive Armor. +- Fix CalcShrapnelDamage potentially reporting a negative number for damage. +- Add some recursion checks to ModuleCase. +- Add check to prevent setting Weapon material to Wood. +- Can no longer change Armor Panel hull material. +- SRBs no longer explode when empty if BattleDamage enabled. +- Fixes Fuel Tank explosion yield increasing when associated resource(s) is empty. +- Empty Ammo boxes will no longer explode when destroyed. +IMPROVEMENTS +- Re-add Firebottles, SelfSealing Tank option to Making History Soyuz tank. + +v1.4.12.1 +FIXES +- Fix stack overflow in CASE explosions from detonating SRBs. +- Fix NRE in PooledRockets. +- Fix ordering of Gameplay settings. +- Fix localisation for new Reset Material toggle. +- Take a copy of the collider list from overlapSphereColliders to avoid potential issues since it's static and ProcessingBlastSphere can be called recursively. +IMPROVEMENTS +- General: + - Reduce the weighting of rocket hits in the parsing script. + - Add some more debugging to inter-competition debug statements. + +v1.4.12.0 +FIXES +- Sort the protoVessel part list during spawning to fix the badly rooted vessel spawning bug. +- Fix BDA part category placement to be usable on low resolution screens. +- Don't add explosive forces when paintball mode is enabled. +- Armor will no longer take damage when using Paintball Mode. +- Subsection targeting GUI label no longer reverts to CoM. +- Paintballs will no longer overpenetrate if base ammo type is armor piercing. +- Shatterable armor materials now properly take full armor loss amount each shattering hit, instead of a reducing amount every subsequent hit. +- Armor panel armor values no longer vary based on part orientation. +- Armor Panel buoyancy fixed. +- Rockets no longer calculate shrapnel hits twice. +- Fixes unit conversion error in armor spalling calcs. +- Bullet Penetration calc now properly takes angle of impact into account. +- Minimal seat filling should fill the first non-weapon crewed command part, not the first non-weapon crewed part. +- Iteratively refine the aiming point (measured to take 2-3 iterations typically) to avoid the turret premature firing issue. +- Fix issue of NaN appearing in bullet damage. +- Add a check for failing to load the tournament state. +- Some fixes for continuous spawning, but it's still misbehaving. +- Apply hull modifier to proc wings also. +- Use the OS-dependent directory separator char for player-visible paths (fixes team names containing the 'AutoSpawn' folder in Windows). +- Replace ', ' with ',' in the summary.csv file for teams for consistent team member naming. +- Rework the extending code to remove the spaghetti and fix the bug where the plane would extend in a straight line from ground targets without AI control. + +IMPROVEMENTS +- General: + - Update the tournament parser to sort teams by wins, have the same column order in the csv as in the console and include cumulative score per round results. + - Use RemoveVessel in VesselSpawner.cs for removing spectators for more consistent removal. + - Disable guard mode on aerial spawn to be consistent with competition start sequence (in case there's a delay between spawning and starting where someone activated guard mode on AG10). +- UI: + - Use a slightly more compact spacing in the vessel switcher when teams only have 1 vessel and short team names are used. + - Remove unused "fire in range" toggle from weapon UI. + - Add a UTC timestamp to the clock. + - Armor Tool GUI now lists comparative strength of material vs steel, and provides a general estimate of material effectiveness against various forms of damage. + - In Flight Armor GUI slider changed to Armor Integrity showing percent remaining armor. + - Clarifies armor debugging statements. + - Armor Reset toggle split into separate Armor Reset and Hull Reset toggles. +- AI: + - Add a 0.5s cool-down to the AI toggling the afterburner for when the hysteresis is insufficient. +- Countermeasures: + - Add separate countermeasure settings to the weapons manager for chaff. + - Update default values for flares and chaff to be more effective. + - Lower default chaff factor to slightly nerf chaff performance. + - Add Vessel-Relative Bullet Checks for more accurate bullet checks that work in high-velocity environments (enabled by default). + - Replace smoothing of target velocity and acceleration with Brown's double exponential smoothing for improved aiming, particularly at longer ranges. +- Armor: + -Armor panel thickness now starts at 25, can be increased up to old maximums. + -Armor panel area (in m^2) can be set in .cfg via "armorVolume =" field. + -Bullets will now only hit (and deal damage to) armor panels once. + -Armor Panel HP now directly proportional to armor integrity. + -ModuleReactiveArmor has been rebuilt and now works. + -Adds Reactive Armor example part. + +ADDED/REMOVED/CHANGED SETTINGS VALUES + - Vessel-Relative Bullet Checks in advanced Gameplay settings. + - Armor Reset toggle split into separate Armor Reset and Hull Reset toggles. + +v1.4.11.2 +FIXES + - Fix Intake Battle Damage issue. + - Intake Battle Damage now clamped to a minimum of 1/4 original intake area. + - AP rounds that penetrate multiple parts now only register a single 'hit' for scoring. + - Fixes Nuke FX scale and scaling. +IMPROVEMENTS + - Adds Armour Reset toggle to BDA settings menu to reset craft armor/hull material to defaults. + - Missiles can now use in-flight animations with the 'flightAnimationName =' field. + - Adds config fields to BDModuleNuke for setting custom FX/sound. + - Nukes now have Editor info cards. +ADDED/REMOVED/CHANGED SETTINGS VALUES + - Added Armor Reset toggle. + +v1.4.11.1 +FIXES +IMPROVEMENTS + - Adds AIR-2 Genie example weapon. +ADDED/REMOVED/CHANGED SETTINGS VALUES + +v1.4.11.0 +FIXES + - Fix some exceptions related to flames. + - Fix for spawning where VESSELNAMING tag is different from the vessel's name (the name gets reverted so that the VESSELNAMING tag is effectively ignored). + - Fix bug where interrupted tournaments could lose team information. + - Fix exception in Team Icons. + - Allow extra attempt at spawning when no terrain was found as it sometimes works the second time. + - Fixes laser damage calc incorrectly using laser part armor thickness instead of target. + - Fixes AI not using AGMS against ground targets. + - Fixes sabot deformation. + - Fixes missiles not respecting max missiles per target setting. + - Rockets will now properly score only a single hit if HE. + - Scoring now tracks rockets fired. + - EMPs from nukes will no longer repeatedly fry a hit vessel. +IMPROVEMENTS + - General: + - Add optional persistent FX for smoke/debris from explosions. + - Explosions will now damage a part if detonating inside it. + - Adds generic FX spawner. + - Renames RWPS3R2NukeModule to BDModuleNuke, and expands it into a missile-usable module. + - Adds some FX to nuke detonations. + - Nukes now act like explosions and delay damage/heat/impulse imparted to target based on distance from detonation. + - UI: + - Reworks the BDA Settings Menu to be less cluttered/more organized/intuitive. + - Less commonly used toggles/settings now enabled via Advanced User Settings toggle. + - Disable Kill Timer setting removed, this is now handled by the Kill Timer slider. + - Vessel tracing manual toggle moved from the Vessel Switcher Window to the Settings window (in the Other section when Advanced User Settings is enabled). + - Settings window doesn't automatically close when starting a competition. + - Settings are saved when switching scenes if the UI is open. + - Tournaments: + - Add an 'Auto-Resume Tournaments' option to automatically load the savegame associated with an unfinished tournament, enter flight mode and resume the tournament on KSP launch. + - Add an 'Auto-Quit Memory Threshold' option (when auto-resuming tournaments is enabled) to automatically quit KSP between heats if the managed memory is above a threshold. Note: native (unmanaged) memory is not measurable from within Unity, so actual memory usage may be higher. In particular, the GfxDriver can use around 5GB of memory, but isn't measurable in non-debug versions of the Unity player. See the "DEBUG Memory Usage" lines in the KSP.log. + - Add results.json parser for N-choose-K style tournaments to produce a table of who-beat-who." + - Expand the 'Folders' teams mode for spawning and tournaments to 'Per Folder / Per Craft File' where, if no subfolders are found, then each craft file in the folder is treated as if it were in a subfolder with the name of the craft file (minus the '.craft'). + - Competitions: + - Landed Kill Timer no longer affects vessels using Surface AIs. + - AI: + - Apply a correction to the roll target when a low-altitude inverted-roll is requested to avoid looping down and immediately triggering terrain avoidance. + - Armor: + - Adds numeric field to Armor GUI thickness slider. + - Allow armor thickness adjustment in non-"integer of 5" increments. + - Remove lowered max temp of aluminum/steel hulls. + - Bullets: + - Sabot rounds now identified via mass, caliber, and length instead of caliber and velocity. + - Bullet deformation now controlled by caliber and HE ratio. + - Rework bullet collision, bullets now do both entry and exit damage to parts they (over)penetrate. + - Adds three new fuze types - Impact, Delay, and Penetrating. + - Proximity/Timed fuze rounds will not detonate if they hit something within their arming dist. + - Battle Damage: + - Fires Add Heat no longer prevents damage from occurring. + - Add Battle Damage option for fire damage to scale to fire intensity. + - Engines and intakes now properly take Battle Damage relative to damage taken. + - Engines now properly clamp thrust to thrust floor value when Battle Damage. + - Default Battle Damage settings are mostly true by default. + - Evolution: + - Add a hidden AUTO_RESUME_EVOLUTION setting to resume evolution on restarting KSP (similar to and overrides the 'Auto-Resume Tournaments' setting). +ADDED/REMOVED/CHANGED SETTINGS VALUES + - Performance Logging option removed as it did nothing. + - InstaKill option removed (only affects lasers, use the InstaGib mutator instead). + - Auto-resume tournaments and Auto-quit KSP advanced options added to the Gameplay section, plus a hidden Auto-resume evolution setting. + - Persistent FX setting added. + +v1.4.10.0 +FIXES + - General: + - Consider 'splashed' vessels as 'landed' when spawning. + - Symmetric parts are updated when armour type or hull material is adjusted in the editor. + - Fix exception in updating radar locked targets. + - Remove duplicate 'Detonate At Minimum Distance' toggle from non-modular missiles. + - AI GUI: + - Get the AI on the initial load when reverting to the editor. + - Fix the AI GUI updating the values when switching vessels when using numeric fields. + - Fix some surface AI values being initialised from the wrong variables in the AI GUI. + - Mutators: + - Fix speed mutators from setting engine throttle to 0. + - Fix mutator mode interaction with weapon arenas and impulse weapons (gravity gun). + - Weapons: + - Fix some weapon selection criteria. + - Fix lasers doing negative damage. + - Fixes issue where AI would sometimes select rockets over guns for AtA when gun was superior option. + - Fixes some typos in Stingray.cfg. + - Torpedoes are no longer affected by Chaff. + - Beam-guided munitions can now properly target splashed targets. + - Battle Damage: + - Heartbleed/Armor melting no longer occurs when game is paused + - Adds check so empty fuel tanks will no longer catch fire if part temp exceeds fuel ignition point. + - Autoigniting fuel tanks will only add a single fire. + - Removes ability to add firebottles to SRBs. + - Fuel Tanks/Ammo Boxes now require 'Fires' and 'Fires Add Heat' enabled in Battle Damage settings to catch fire if too hot. + - Burning Fuel tanks now properly destroy the part if they explode. + - SPH/VAB + - The Armor Tool GUI will no longer reset armor thickness of all parts to 10 when opened. +IMPROVEMENTS + - Weapons: + - Adds clusterbombs to weapon selection logic. + - Adds support for beam-guided AtA missiles. + - AI should now select best, rather than first, bomb available when attacking ground targets. + - Selection logic now weights cluster ammo for missile interception. + - AI will can now select rockets/lasers to use at close range against underwater targets. + - AI can now target flying targets with laser-guided munitions. + - AI will not use EMP missiles against targets that are currently EMP'ed. + - Adds water splash FX for bullets hitting water. + - Bullets can now ricochet off water. + - Added an "Underwater Bullet Drag" option for bullets and rockets to have considerable drag when in water. When enabled, the following applies: + - Bullets with caliber < 75 and rockets fired from above water stop when entering water and explode if they're explosive. + - Rockets with sufficient thrust duration can be fired from underwater at splashed/submerged targets that are close enough. + - Weapons targeting nearly fully submerged targets will target the water surface immediately above the target. + - Weapon selection updated to reflect the above changes. + - AI will no longer select guns when underwater. + - UI: + - Add a toggle for showing the competition status messages with the UI hidden. + - Add option to TimeWarp between tournament rounds (not heats) on planets with a solid surface (due to a technical constraint). + - Add Underwater vessel status to LoadedVesselSwitcher window and put the status after the vessel name. + - Expand the low altitude GM kill limit to depths of -10km. + - A clearer message when a tournament stops due to spawning/competition start failures. + - Small Warhead 'Detonate At Minimum Distance' toggle renamed to Warhead Trigger Distance, to avoid confusion when building modular missiles. + - RWP: + - Add S4R1 RWP option for globally adjusting the firing rate of weapons in-game. + - Add S4R2 RWP option Zombie Mode where only root part takes damage. + - Add non-headshot dmg multiplier slider to adjust how much damage non-root parts take. + - Add toggle to allow battledamage to occur on non-root parts. + - Internal: + - Adjust camera weights for picking a better vessel to switch to. + - Add a IsUnderwater vessel extension. + - Adds hidden legacy armor toggle to disable armor overhaul changes (armor types/hull material) accessible via editing settings.cfg. + - General: + - Adjust armor panel buoyancy, armor panels will float again when at max thickness. + - Automatically create a GameData/Custom/Flags directory for standardising the location of flags for BDArmory competitions. + - Price rebalance. + - Some RWP specific options (to be decalred once the rounds have been run). +ADDED/REMOVED/CHANGED SETTINGS VALUES + - Low altitude kill limit expanded and "Off" value changed. + - Legacy armour option added (config file only). + - Competition status messages with hidden UI option added. + - Underwater bullet drag option added. + +v1.4.9.1 +FIXES + - Fix various fields in the AI GUI for surface AIs. + - Fix broadside direction to not be reversed. + - Set default mutator mode (for new installs) to false. + - Add missing checks for empty mutator list (was causing competitions to fail to start). + - Fix laser damage calculation. + +v1.4.9.0 +FIXES + - Fixes an NRE in PartExtension.GetSize that was causing KSP to crash when deploying a flag. + - Properly reset the AIGUI input fields when adding and removing multiple AIs. + - Add missing field to surface AI input fields. + - Fix Evolution UI bug and allow 0 adversaries. + - Make the CASE-I explosion directed in the up direction of the part. + - Check for null vessels in ForceUpdateRadarCrossSections (fixes competitions not starting due to this exception). + - When setting HP of parts, use an alternate approach to getting volume based on partSize when GetAverageBoundSize gives ridiculous results. Failing that, just base it on mass. (Fixes HP on TweakScale and AP+ parts.) + - Actually run the armour and hull setup functions in flight mode. + - Add conformal decal exception to HP/Armor visualizer. + - Make rendering of RCS snapshots and HP/Armor visualisers in the editor wait for ship changes to finish being triggered before rendering to reduce unnecessary renders causing flickering. + - Various fixes for shrapnel damage. + - Fix penetration velocity reduction. + - Fix barrage mode not cycling through singlebarrel weapons. + - Fix RPM for multibarrel weapons, actual RPM is now listed RPM, not listed RPM * num of barrels. + - Fix occasional NRE in AnalyseCollision. +IMPROVEMENTS + - Internal: + - Add angle parameter for creating directed explosions (defaults to 100°). + - Log 'StopCompetition' events to the log as well as the screen. + - Armor: + - Add attach node sizes to armor panels. + - Add Editor RMB context info for armor panels to inform players of panel armor mass. + - AI: + - Allow collision avoidance detection from vessels approaching from behind. + - Don't apply premature dive fix when evading. + - Add an option to ignore gunfire from the current target for evasion purposes. (I.e., don't trigger evasion when jousting, only from other sources.) + - Game modes: + - Add Asteroid Field game mode, with anomalous attraction option. + - Add Mutator mode: + - Select one or more mutators to change up how weapons and craft behave for the round. + - Mutators can be applied globally, per vessel, or on kill. +KNOWN ISSUES + - Occasional graphical glitch for proc wings in the SPH/VAB due to HP/Armor visualizer. Doesn't affect flight mode and the color resets if the craft is reloaded or the proc wings color settings are nudged. + +v1.4.8.2 +FIXES + - Fixes armor panel settings not persisting from Editor to Flight. + - Fixes steel hull material massively increasing weight. + - Triangular armor panels/wings now properly have 1/2 armor mass of rectangular panels. + - Put the Gau-22 in the correct category in the SPH. + - Fix minor mesh errors on the Sidam. + - Don't detonate CASE ammo on vessel packing or unloading. + - More fixes for proc-wings: + - HP is updated properly on vessel load. + - Mostly fixed interaction between the HP/Armor visualizer and proc wings shaders. +IMPROVEMENTS + - Add a slider for controlling the distance threshold of showing team icons. + +KNOWN ISSUES + - Occasional graphical glitch for proc wings in the SPH/VAB due to HP/Armor visualizer. Doesn't affect flight mode and the color resets if the craft is reloaded or the proc wings color settings are nudged. + - Flickering of the RCS window, particularly when the Armor window is open. + - Tweakscale HP is misbehaving beyond certain size changes. + +v1.4.8.1 +FIXES + - Fix (mostly) Pwing - HPVisualizer issue + - Fix burstfire + - Fix procwing HP bug +IMPROVEMENTS + - Internal stuff: + - Implement the AddPlayerToRammingInformation and RemovePlayerFromRammingInformation functions (not used yet). + +v1.4.8.0 +FIXES + - Fix logic of which vessels to ignore in RCS snapshots. + - onDisable -> OnDisable. Deactivate FireFX properly such that the parentPart always gets cleared (also in FuelLeakFX.cs). + - Fix line-of-sight checks in ExplosionFX for exploding parts on vessels. + - Don't detonate for surface fires, excess fuel explosions cause surface fires. + - Don't trigger CASE ammo explosions on scene changes. + - Fix rippleRPM. + - Fixes to burstfire. + - Fix checks for uncontrolled vessels. + - Fix LangVersion in .csproj to build with later than C# 7.3. + - Fix missing team in summary when the team is only dead. + - Fix bug with 'dead teams' in tournament parser script. + - A variety of minor bug fixes and optimisations. +IMPROVEMENTS + - General: + - Adds support for weapons with multiple fire animations or barrels (battleship guns/wirbelwinds/etc). + - Adds Sidam quad 25mm to demonstrate multi-fireanim/multi-barrel functionality, and the GAU-22 to provide a fixed 25mm weapon. + - Fix fuel fires/ammo explosions occasionally yielding larger than expected damage. + - Ammo explosion max damage now capped. + - Weapons with integral CASE will no longer add an additional 2000 funds to part cost on load. + - Add N-choose-K style tournament option. + - Variant engine for automated craft evolution. + - AI/WM: + - Added stand-off distance to try to avoid getting closer than this distance when tailing another craft. + - Improved vessel-vessel collision avoidance based on look-ahead and avoidance strength and only avoiding as long as necessary (note: this uses different settings in the pilot AI, so old craft may need re-tuning for this). + - Expand targeting logic for rockets. + - Adds AI GUI, available in both editors and in flight, with configurable hotkey (default 'Numpad /') and optional toolbar button. + - Expands Weapon Manager GUI, adds target priorities tab. + - Adds togglable context labels and infolinks to AI and WM GUIs for on-hand info about what each setting does. + - Ki improvements with 0.995 decay factor and fixed +1/-1 clamps. + - Update target priority to work for ground targets. + - Armor Overhaul: + - Armor now adds mass to craft, relative to thickness and part size. + - Can now select from multiple armor materials, each with their own strengths and weaknesses. + - Adds spalling damage from near-penetrating hits. + - Adds shrapnel damage from explosive detonations. + - Adds Armor tool to SHP/VAB editors for easy armor manipulation across a craft. + - Adds HP visualizer, to see HP of all parts across craft. + - GameModes: + - Adds Space Tools BDA setting. + - Adds Space Friction toggle for starwars-style space dogfights. + - Adds contragrav setting for atmospheric use for things that reasonably shouldn't fly - sky battleships/stock zeppelins/etc - or for null-atmo fights where wings can't generate lift. + - Adds repulsor setting for hover vehicles. + - Rework Tag mode to use the new scoring datastructure. + - Scoring: + - Rework the code interally for a more consistent approach. + - Sliders for controlling the time limits for head-shots and kill-steals. + - Head-shots are now separated into clean-kills (within the time limit and was the only plane to damage the target), head-shot (within the time limit, but not the only plane to damage the target) and kill-steals (within both time limits). + - Separate scoring categories for rockets and battle damage, giving: bullets, rockets, missiles, ramming and battle damage (including self-inflicted). + - Improve score-related UI messages. + - Updated parsing script to handle these new categories. + - Remote orchestration is not yet updated, so report rocket damage as missile damage to the remote orchestration for now. + - Fix bugs in continuous spawning and integrate it into the new scoring datastructure. + - UI: + - Always show a clock during competitions and competition start for time synchronisation of video post-processing. + - Add the AI/WM GUI toggle hotkeys into the hotkey settings so they can be set. + - Increase the default width of the vessel spawner window a bit to fit the tournament status for long tournaments. Automatically adjust existing window width if running a tournament with >99 heats. + - Extra damage sliders for rocket and battle damage multipliers. + - Add an Advanced Tweakables toggle to the BDArmory settings for easier access. + - Add a toggle for the BDA AI toolbar button. + - Internal stuff: + - Balance for 25mm round (tested in FJRT2). + - Adjustment of battle damage damage. + - Automatically create the AutoSpawn folder on entering flight mode if it doesn't already exist. + - Move toolbar button logic to BDArmorySetup.cs and enable toolbar button in SPH and VAB too for accessing BDA settings window. + - Move SpawnField class to BDArmory.UI.NumericInputField. + - Track the current HP ignoring ignored parts as needed. + - Deprecate BD_AMMO_DMG_MULT. + - CASE now uses EST.BattleDamage. + - Change competition update tick to 1s to match best precision of vessel state checks. + - Detection of crew being killed in 'uncontrolled' checks. + +v1.4.7.1 +FIXES + - Quick fix for craft names using invalid filename chars in the remote orchestration. + - Hide the Asteroid Rain radius slider when Follow Spread is enabled. + - Fix some NREs. + - Bias testing: + - Add a random spawn order toggle to the vessel spawner window, which applies in single spawn mode (i.e., everything except continuous spawning). + - Add option to parsing script to only parse the first N logs for use in analysing trends in results. +IMPROVEMENTS + - Compiled with KSP 1.12 assemblies. + - Improve MIA (survived/not-survived) definition in results logs. + - Add telemetry readout to crafts TeamIcons HUD. + - Ammo belts: + - Add API ammo option to Vulcans, .50 cals, GAU, and M230. + - Add 35x228AHEADBullet ammo option to Oerlikon Millennium turret + +v1.4.7.0 +FIXES + - Fixes weapons without ammo not firing if Infinite Ammo enabled + - EMPs will no longer shutdown SRBs + - Rounds with the 'Proximity' Fuze type now properly proximity detonate + - Adjust Krakensbane adjustments in 'full' Kerbal Safety to hopefully reduce amount of Krakenation. + - Make the default value for kerbal safety be 'partial' instead of 'full' due to stability issues with 'full'. + - Fix various NREs and memory leaks. +IMPROVEMENTS + - General + - Make the WM, MMG, pilot AI and surface AI modules get ignored by explosions, bullets, rockets, lasers and missiles so that they can be placed externally without being easily destroyed. + - Disable Kerbal Safety "R.I.P." in-game messages as they were too spammy. + - Add option for tournament parser to parse multiple tournaments. + - Allow turrets other than the current one to track the main target so they're ready to fire if the current one is unable to do so. + - Optimizes RocketFx, reduces rocket performance impact + - Adds score reporting to proximity-fuzed rockets + - Rocket soundFX is now customizable in weapon .cfg using the 'rocketSoundPath' field + - Expands Scoring metrics; Adds HP remaining (how much of the vessel is left at end of heat) and MIA (is the vessel a wreck falling out of the sky, or an intact plane?) fields + - Minor tweaks to Ammo Type PAW descriptions + - Pulse laser beam duration now clamped to fire rate + - Adds Incendiary ammo, plus associated BattleDamage support + - Adds Custom Ammo Loadouts; weapons with multiple ammo types can be configured to fire non-homogeneous loadouts (e.g. AP, AP, AP, HE, AP...) + - Adds Custom Ammo Belt config GUI + - Weapons can be configured to use custom ammo belts in .cfg with useCustomBelt = true and ammoBelt = foo; bar; + - New VesselModuleRegistry class for much faster access to vessel modules which gives a nice overall performance boost. + - New Asteroid Rain game mode added. + - Resource stealing: + - Change resource steal to drain and fill containers evenly w.r.t. fraction of the containers, still respecting priority settings. + - Add option for counter-measure stealing. + - Add a few fuel and ammo resources that were missed. + - Steal integer amounts of ammo and counter-measures. + - Altitudes: + - Add Max Altitude (AGL) setting to pilot AI with a toggle to enable/disable it. + - Add action group options for 'Toggle Max Altitude', 'Enable Max Altitude' and 'Disable Max Altitude'. + - Change killer GM altitude limits to AGL. + - Team Icons + - Adds reminder message to toggle stock vessel labels if they are enabled + - Adds check if legacy Team Icons is installed. Remove prior installations of Team Icons + - Integrates Team Icons into BDA + - Adds User-selectable colors + - Team colors no longer change if a team is completely destroyed + - Adds Team Color indicators to Vessel Switcher GUI + - Paintball mode now uses Team-colored paintballs for easy hit tracking + - Adds Rocket alert icons + - Threat Indicators now show all vessels attacking a vessel + 200f + - Adds Telemetry HUD to Craft Icons + - Battle Damage + - Adds Damage Multiplier slider for Battle Damage Ammo Explosion damage + - Ammo Explosion now properly scales damage based on distance, ammo remaining in the box + - Ammo Explosions now properly report damage from explosions to attacker vessel responsible for triggering the explosion + - Fuel Tank explosions now less damaging, now use Ammo Explosion Damage Mult setting + - Adds ability to set lower limit on Engine thrust reduction from battle Damage + - Adds adjustable threshold for engine kills from Battle Damage + - Crew Fatalities now report name of vessel pilot was in + - Crew Fatalities now knock out AI if all pilots killed + - Self-Sealing tanks now prevent leaks unless below 50% HP + - Leak rate now dependent on tank pressure + - Fire burn rate now dependent on tank volume and available fuel + - Firebottles temporarily shut down burning engines to extinguish them + - Fires can now add heat to burning part + - If Fire Damage and Fire heat enabled, fires will begin do damage once part temp exceeds damage threshold + - Fueltanks/ammo boxes that are too hot will cook off + - Fires now properly recognize MonoPropellant tanks + - Fires now distinguish between surface and internal. Internal fires require firebottles to extinguish, surface fires will burn for a short duration, but not consume resources + +v1.4.6.2 +FIXES + - Add in steer limiter localization text (is used on Modular Missile Guidance) + - Always score damage from lasers, but only score hits with pulse lasers or when the accumulator is sufficient. + - Fix bug in advanced targeting for fixed guns. + - Adjust intake damage amounts. +IMPROVEMENTS + - Add SRB battle damage. + - Filled command seats can extinguish fires. + +v1.4.6.1 +FIXES + - Fix various occasional NREs. + - Fix bug in parser for 'dead teams'. + - When a missile has lost its MissileBase set its isMissile tag to false so it no longer counts as a missile. + - Don't detonate CASE ammo when the vessel is null, packed or not loaded. + - Fixed overheated guns sometimes not being detected as overheated. + - Fix AI having issues avoiding terrain when in an inverted dive (would happen most frequently during missile evasion). +IMPROVEMENTS + - Updated HP patch for AoA Technologies. + - Add check for whether guns with low RPM can fire soon, switch to a different weapon if the next time a low RPM gun can fire is greater than the Weapon Manager fire interval setting. + - Re-add multiple subsystem targeting to the advanced targeting. + +v1.4.6.0 +FIXES + - Store the armed/IFF state in the craft files instead of just the GUI strings. + - Use the correct ShipConstruct in the editor for setting the max gun range and set a minimum of 10 for the max gun range to avoid breaking the part window sliders. + - Add another check for vessels disappearing during spawning. + - Add a 0.1s time-out to the ballistic trajectory closest approach simulation to prevent freezes. + - Pause fire effects when the game is paused. + - Store the armed and IFF states of explosive parts, not their GUI strings. + - Fix missiles not being targeted when Engage Missiles option was selected. + - Fix for the AI stopping extending prematurely. + - Fix extending to get enough distance to fire missiles not working. +IMPROVEMENTS + - Migrate non-static configs to the PluginData folder to avoid invalidating the ModuleManager cache (should give slightly faster start-up times). + - Add a Game Modes section to the settings and rearrange the various toggles to match. + - Add Heart-Bleed and Resource-Steal game modes. + - Fuel resources that can be stolen are 'LiquidFuel', 'SolidFuel', 'Oxidizer', 'IntakeAir' and 'ElectricCharge'. + - Ammo resources that can be stolen are those ending in 'Ammo', 'Shells' and 'Rocket'. + - Remove Target Missiles toggle from the weapons manager, this is now automatically set based on if weapons onboard the craft have the engage missiles option enabled. + - Expose AI extending settings to be editable by user. The AI extends when all of the below conditions are true: + - The target is outside of an angle in front of the craft (default of 78 degrees, now selected using Extend Target Angle). + - The target's speed is less than the craft's speed (default of target's speed is less than 80% of craft speed, now selected using Extend Target Velocity Factor). + - The target is close (default of within 400 meters, now selected using Extend Target Distance). + - Advanced targeting setting deprecated - targeting options are now per vessel. + - Max turret target setting deprecated - is now set per vessel. + - Adds Max Turret Targets setting to Weapon manager. + - Adds Targeting Options to Weapon manager. + +v1.4.5.0 +FIXES + - Fix the floating origin corrections to the vessel traces and apply the corrective rotations to the coordinates such that the local coordinates are (east, up, north) and the identity rotation is a craft pointing nose up with the underside pointing north. Include a simple python script to plot the traces. + - Re-enable the extending debugging (when debug labels are on). + - Fix the smoothing of the target acceleration and velocity for long-range targets to be applied dynamically so as to not affect short range targets and to reset when switching targets. +IMPROVEMENTS + - Update the in-flight Guard Menu to match the settings in the weapon manager in the SPH. + - Dynamically adjust (max) gun range based on the guns that are actually on a vessel. + - Add log and competition status messages about disabling a vessel due to EMP damage. + - Upgrade the numerical integrator for bullets and ballistic trajectory simulations to use the 2nd order symplectic leapfrog method (this vastly improves the accuracy and speed of the trajectory simulations). Allows for ballistic aiming out to at least 50km. + - Include dead teams information in the results. + - Re-worked steer limiter. Steer limits are now based on two control points, defined by four variables + - Low Speed Control Point: Steer limit and Speed + - High Speed Control Point: Steer limit and Speed + - Below the low speed control point speed, the limit will be equal to the low speed steer limit. + - Above the high speed control point speed, the speed will be equal to the high speed control point limit. + - Between the two control points, the steer limit will vary linearly based on the craft's current speed. + - Add toggle for whether competition status messages are displayed. + - Change clearance check minimum drop time to greater than or equal to 0.1s instead of greater than 0.3s + - Add 1.5 safety factor to blast radius determination for missile evasion + - Clamp the minimum radar missile FOV used for missile evasion to 40 degrees, results in fewer instances of craft turning back towards a missile at inopportune times. + +v1.4.4.4 +FIXES + - Fix missiles firing without exhaust and heat-seeking missiles firing without a target. + - Regenerate exhaust prefab object pools when they're detected to contain nulls due to scene changes or reloads. + - Trigger missile exhaust prefab detachment on part.OnJustAboutToDie and OnDestroy as well. + - Add KeyNotFound and NRE checks in CheckForMissingParts. + - Add in SetupAudio fix in some other places for audioSource. Add check for broken animations and give error instead of exception. + - Remove ramming debugging toggle and just use the regular debug labels toggle instead. Adjust the debug statements in DebrisDelayedCleanUp. + - Don't update RCS if we've just fired a missile or are trying to fire a missile. + - Correctly calculate turret gimbal limits check, taking into account bullet drop. + - Add generic rocket resource for rocket ammo boxes (fixes rocket launchers not being able to use rocket ammo boxes) +IMPROVEMENTS + - Add option to trace vessel paths and orientations. Traces are logged in GameData/BDArmory/Logs/VesselTraces/-.json + +v1.4.4.3 +FIXES + - The improved aiming code is now fixed (properly accounts for movement of the aiming vessel). Planet curvature is not yet taken into account. + - Re-initialise the missile audio sources if they're null (fixes failure-to-fire issue). + +v1.4.4.2 +FIXES + - Revert the improved gun aiming code as it's not working properly for non-stationary targets. + - Fix potential NRE in DetachExhaustPrefab. + +v1.4.4.1 +FIXES + - Fix AI trying to evade missiles after they were no longer threats or did not exist. + - Use object pooling for missile exhaust prefabs to avoid memory leakage. + +v1.4.4.0 +FIXES + - Add safety check around parsing BDA settings values. + - Re-add laser min range check. + - Multitargeting: + - Multitarget now multitargets missiles. + - Fix part targeting not resetting properly. + - Update the target parts lists correctly. Only triger target part list updates when toggling options instead of every frame when the settings are open. + - Add check for the list of targetable parts being empty. + - Remove arc length term from DLZ calculation for ground launched missiles (this was breaking VLS parts). + +IMPROVEMENTS + - Missile evasion: + - Behavior changes to notch and dive against missiles, pull away before impact for heat seekers only (continuing to notch is more effective against radar missiles). Fix spinning in place behavior that had occurred occasionally. + - Simplify entry conditions for missile evasion behavior (only occurs within threshold of when your craft is set to dispense chaff/flares regardless of whether it has chaff or flares onboard). + - Proper throttle settings for chaffing vs flaring. + - Add check if radar missiles are still boresighted towards craft as a condition to treat missiles as a threat. + - Shift 'AimAndFireAtEndOfFrame' to occur during the FashionablyLate timing phase of FixedUpdate and rename it 'AimAndFire'. This synchronises firing with the physics instead of waiting until the scene is rendered. It also occurs before Krakensbane adjustments have been made. + - Simplify target threat to either be 0 (not under attack) or 1 (under attack). + - Make Kerbal Safety a slider instead of a toggle to allow disabling ejection from cockpits, but still have existing kerbalEVAs deploy parachutes. + - Add 'Start Comp. NOW Delay' slider for automatically triggering 'Start Competition NOW' after a given delay. + - Tournament results parser: + - Add missile hits taken to the parser ('HitByMis' in console output, 'missileHitsTaken' in CSV file). + - Hide empty columns in the parser console output. + +v1.4.3.3 +FIXES + - Fix various NREs. + - Add stacktrace to exception warnings in ModuleCASE. + - Detonate bullets and rockets when they hit kerbals. +IMPROVEMENTS + - Improve the gun aiming code. + - Include missile hits separately from count of parts hit by missiles in scoring logic and update tournament parser. + +v1.4.3.2 +FIXES + - Add missing Weapon condition checks. + - Yet another attempt at avoiding the CometVessel bug. + - Escape double quotes in vessel names when writing the logs to avoid breaking JSON parsing. + +v1.4.3.1 +FIXES + - Add missing values for Hydra70 and 8KOMS rockets. + - Fix smartpick not finding priority 0 lasers. +IMPROVEMENTS + - Add gravity gun (RWP S3R4), F-86 launcher, generic rocket ammo and FFAR70 rocket definition. + +v1.4.3.0 +FIXES + - Fix rockets overriding the weapon name and breaking the editor. + - Maintain incoming missile warning while missiles are still incoming. + - Limit 'firing at me' detection (from guns) to the range of the weapon being fired. + +IMPROVEMENTS + - Make altitude limits independent of RWP and change localisation. + - Make missile targeting check depend on blast radius of incoming missile instead of fixed at 60m and include check for passing in close proximity. + - Return a full list of incoming missiles sorted by distance in UpdateGuardViewScan instead of just the first found. + - Convert 'under attack' and 'under fire' routines to maintain their state until 1s after last notification instead of simply resetting after a fixed period. + - Adds Impulse/Gravitc functionality for ballistics/rockets/lasers + - Adds some new configuration fields + - impulseWeapon = foo //gun/laser will deal impulse instead of damage + - Impulse = n //magnitude of impulse delivered per bullet/rocket/pulse laser shot, and impulse/sec delivered by beam lasers. setting this negative will make attractor lasers instead of repulsor lasers + - graviticWeapon = foo //gun/laser will temporarily modify mass of hit part + - massAdjustment = n //mass, in tons, added per sec by beams or per hit by bullets/rockets/pulselasers + - Adds some new rocketdef config options + - gravitic = foo //rocket will temporarily modify weight of parts hit by rocket and its explosion + - impulse = foo //rocket applies non-damaging impulse to parts hit by rocket/explosion + - choker = foo //rocket will temporarily disable air intakes caught in its blast radius + - EMP = foo //rocket will apply EMP buildup to targets within blast radius + +v1.4.2.10 +FIXES + - Prevent log spam from ModuleWeapon.CheckAIAutoFire. + - Fix width of drag window in Loaded Vessel Switcher. + +IMPROVEMENTS + - Add a min altitude limit for the killer GM (once a competition is active). + - Make the sequenced competition command sequence dependent on the BDA round. + +v1.4.2.9 +FIXES + - Fixes for Killer GM scoring attribution and ramming detection. + - Fix for if a rootpart is null during spawning. + - Set the kraken eating things active for the correct RWP rounds. + - Fix local tournament sequencing when using sequenced competition (S3R3). + +v1.4.2.8 +FIXES + - Make the Kraken eating things code specific to certain RWP rounds and only active during a competition. + - Fixes to sequenced competition for RWP S3R3 to work with the remote orchestration. + - Update vessel switcher teams list as necessary. + - Remove direction from explosions of impacting bullets as that makes no sense. + - Add debug log warnings for previously silently ignored exceptions and fix a couple of NREs. + - Fix exception in explosion handling when part.rb is null (but part.Rigidbody isn't). + - Reloading/MultiTarget fix/weaponPriority + - Multitarget fix + - Chernobyl bugfixes + - Various NRE fixes + +IMPROVEMENTS + - Separate bullet movement and collision detection into separate functions. Detect collisions in the initial frame of a bullet so point blank shots work. + - Detect exit collision with objects when the barrel of the gun is within the object's collider, i.e., sub-point-blank shots. + - Added a slider for configuration of specific RWP rounds. + - Adjust parsing scripts for updated logging tags format. Add slider for setting the length of time of the death camera. + - Standardise the format of debug log messages to '[BDArmory.ModuleName']: ... + - Reload anim support + +v1.4.2.7 +FIXES + - Add a "Kerbal Inventory" slider to configure how kerbals' inventories are adjusted when "Kerbal Safety" is enabled (default is "Parachute Only"). + +v1.4.2.6 +FIXES + - Fix bug in loading spawn locations on fresh installs. + - Give vessel spawning more than 5 frames to wait for the reference transforms. + - Add checks in various parts of the code to avoid potentially applying forces to physicsless parts. + +IMPROVEMENTS + - Tweak explosions from the nuclear engine code to include triggering on detachment (i.e., no WM) and give a 0.5s delay after detachment or running out of fuel before exploding. + +v1.4.2.5 +FIXES + - Wait for reference transforms to be assigned during spawning to avoid NREs. + - Tweak auto number of vessels for FFA with only a few craft. + - Fix a variety of NREs in the nuclear engine code. + - Avoid applying forces to physicsless parts (no rigid body), which removes the need for try..catch block. Clean up code. + - Track last valid atmDensity of nuclear engine to ensure correct value while vessel being destroyed. + - Use the approximate GetArea function instead of the radiative area if the radiative area is not available. + - Clean up formulas for blast impulse. Only apply damage/impulse to each part once per explosion. + +v1.4.2.4 +FIXES + - Fix various NREs. + - Remove null-coalescing operators from Monobehaviours as they don't work in Unity. + - Multi-Target barrage fix: weapons targeting a target outside their fire arc will now be skipped instead of bringing the barrage to a halt until the offending weapon can be brought to bear. + +IMPROVEMENTS + - Added a few more interesting spawning points (currently requires removing the old spawn_locations.cfg file in order to update, this will be changed soon). + - Re-work rocket damage to do kinetic damage and penetrate armour and correctly report scoring info. + - Shared bullet/rocket code moved to Misc.ProjectileUtils.cs. + - Weapon priority adjustments: + - Guns targeting ground will now prioritize biggest impact (bullet mass * velocity) instead of tntmass. + - Guns targeting air will now prioritize RPM for fighter-sized targets, and prioritize larger caliber guns when targeting larger targets (bombers/zeppelins/Ace Combat superweapons). + - Add weapon priority slider allowing setting custom weapon selection order for guns/rockets. + - Added Chernobyl Nuke module for RWP S3R2. + - Update the list of maintainers and contributors in the README. + +v1.4.2.3 +FIXES + - Fix the issue of player names in the remote orchestration containing the separator character. + +v1.4.2.2 +FIXES + - Correct logic for checking if a weapon is powering down/disabled. + - Actually use the modified team starting distance for teams. +IMPROVEMENTS + General: + - Allow configurable muzzleTransformName. + - Trigger RemoveCometVessel on onCometSpawned instead of onNewVesselCreated. + - Allow middle click individual 'T' icons to set neutral team. + - Small optimisation in explosion book-keeping. + - Increase maximum firing angle to 4 in both the weapon and the weapon manager. + + Kerbal Safety: + - Ejection from cockpits and cabins is now working. Note: ejecting lots (>100) of kerbals simultaneously can cause KSP to freeze for several minutes. + - Set random rotation and torque to ejected kerbals. Disable collisions while manually moving the EVA kerbal. + - Let the Kraken eat kerbals that get invalid positions (NaN). Note: this doesn't appear to be an issue now due to the fix above. + - Self-destruct action group for WM (helps with testing things). + - Remove the kerbals' jetpacks to save 45kg. + + Parsing script: + - Add default interpreter (for Linux) to parsing script. + - Default to looking for the latest tournament folder. + - Rearrange display order so that the 'Score' column is first after the 'Name' column + - Fix check for default team names in summary + - Only print teams summary if it's not 'A', 'B', ... as those aren't consistent. + - Update formatting of tournament parser output to console for consistent column spacing. + - Add various options to tournament parsing script (run with -h flag for help). + -q Don't print results summary to console. (default: False) + -n Don't create summary files. (default: False) + -s Compute scores. (default: True) + -so Only display the scores in the summary on the console. (default: False) + -w Score weights (in order of main columns from 'Wins' to 'Ram'). (default: 1,0,-1.5,1,2e-3,3,1,5e-3,1e-5,0.01,1e-7,5e-2) + -c Parse the logs in the current directory as if it was a tournament without the folder structure. (default: False) + +v1.4.2.1 +FIXES + - Fix a stack overflow due to recursion in IsValidVessel check. + - Swap labels of 'high' and 'low' speed steer limiters. + +v1.4.2 +FIXES + - Further adjustments to remove annoying space objects that get spawned during competitions. + - Add warning about missing SpawnProbe.craft. + - Bugfix for Scores when starting competition without using spawning. + - Fix occassional NRE in SmartFindTarget + - Adjust 'inhibiting gain alt' debug messages. + - Fix for damage reduction formula, armor damage reduction occurs for armor thickness > 20. Max armor thickness of 1500 results in ~90% damage reduction. + - Disable (missing) legacy code analysis tools in .csproj. +IMPROVEMENTS + - Explosive damage penetration: + - Basic explosive damage penetration handling. Parts closer to the explosion are ablated first with a small amount of bleed through for relatively weak parts. + - Add comment on original BDA methodology source. Calculate positive phase time based on source method equations. Correct CalculateIncidentImpulse not bounding scaledDistance + - Remote orchestration: + - Add slider for inter-heat delay for remote orchestration. Also, doesn't rely on RWP option being enabled now. + - Make remote orchestration baseURL configurable, defaults to FC. + - Pilot AI: + - Enable storing and restoring settings (keyed by vessel name) for tuning a vessel in-flight and setting those settings in the SPH. + - Weapon manager / weapon configuration: + - Rename Target Acceleration to Target TWR since that's what it is + - Change firing angle to scale with distance and size of target. + - Include maxDeviation/2 from the weapon in the firing angle calculation to allow more firing at longer range for spray-and-pray guns. Increase max firing angle to 3. + - Prioritize weapons that are within their firing angle. + - Adjust weapon prioritisation of being within firing angle for firing angle scaling with target size and distance. + - Added VIPs, which allow you to set up behavior such as guarding your VIP, or attacking enemy VIPs and ignoring craft defending them. + - Add new "Is VIP" toggle to the weapons manager. Allows you to set a craft as a "VIP" which opens up new target priority options. + - Added new target priority options "Protect My VIPs" and "Attack Enemy VIPs". Setting positive values on these sliders will result in the craft targeting enemies that are targeting VIPs on its own team, or attacking enemy VIPs, respectively. + - Don't try to engage torpedoes with guns/missiles/lasers. + - Use other weapons for ground targets if missiles fired is greater than or equal to max missiles on target setting. + - Don't use arc length calculation for torpedo DLZ since they can turn much more quickly in water than airborne missiles. + - Advanced Targeting option in BDA Settings: + - Enables turrets to target multiple simultaneous targets + - Enables selecting CoM or non-CoM targeting options + - General: + - Updated tournament parsing script that puts subfields in their own columns in the CSV file. + - Move GetRadius to VesselExtension.cs. + - Localisation changes for steer limiter. + - Update Typhoon engine (caesar_hone_60.cfg) + +v1.4.1 +FIXES + - Fix missile hit attribution from missiles without BDExplosivePart modules. + - Don't include negative thrust percentage drives (Danny2462 drives) in max acceleration calculation as they don't contribute to the thrust. + - Fix BDRotaryRail stack overflow and give warning if the conditions that gave rise to it occur again. + - Put chute into right state before deploying it with WM AG. + - Don't assign gender to spawned kerbals, let KSP do it. + - Fix CheckSafeToFireGuns for debris preventing firing due to being too close to the target. + - Reduced (but not eliminated) exceptions due to comet vessels. + - Change the minimum damage amount of 0.01 to 0 as this was the cause of the fire damage over time applying despite the game being paused or slowed with time control. + - Fix NREs in explosions from ClusterBomb.cs + - Add KSP version dependent checks for part.IsKerbalEVA and crew.ResetInventory. + - Updates Rocket targeting, fixes aimer alignment + - Updates laser Infos with proper resource usage +IMPROVEMENTS + UI and quality-of-life tweaks: + - Add fault tolerance for missing bullet defs in GetInfo. + - Add similar handing of missing or broken rocket info as for bullet info. + - Move ConfigNode functions to their own translation unit. + - Add 'Clear Debris Now' and 'Clear Bystanders Now' buttons to the Vessel Spawner window (in Spawn Options). + - Option for using the old format of the Vessel Switcher window ('t'). Option for reassigning teams on spawn or retaining the original team name defined in the SPH. Right clicking 'T' in the Vessel Switcher window resets vessels to their original teams. Spawning from 'Folders' assigns the folder as the team name if team reassignment is enabled. + - Remember position of RWR, Radar and TargetingCamera windows. + - Add optional extra parameter to CreateExplosion to specify the weapon name (to be used for in-game messages for missile strikes). + AI: + - Remove 150m constant from terrain avoidance and base the terrain alert detection radius on the vessel diameter to improve ground strafing. + - New slider to control strafing speed of ground targets. + - Lasers and missiles now respect minimum engagement range settings + - Fixed issues that were preventing AI from finishing the extending routine + - Speed Adjusted Steer Limiter + Spawning and Tournaments: + - Teams options in the spawning and local tournament settings, including custom serialisation (doesn't support commas in craft file names) and UI adjustments. + - Configure how seats are filled in the Vessel Spawner Window: minimal, all control points, also cabins. Crewed weapons always get filled with 1 crew. + - Option to reset maxHitPoints on vessels loaded into flight mode (e.g., during spawning). Uses the MM patched value if available. + - More consistent addition of _1, _2, etc., to vessel names which should make teams of the same vessel have valid results summaries in many cases without having to rename each craft (which is still a better solution!), not just the files. + - Add RESULT line to competition log to indicate overall result of the competition (needed for teams). + - Updated format of log files for teams. Updated tournament parsing script. + - Add break-down of deaths (clean Bullets, clean Missiles, clean Rams, Assists, Suicides) and kills (Bullets, Missiles, Rams), and normalised death order and death time to the results summary. + - Set KSP version of SpawnProbe to 1.9.0 to avoid issues with spawning on older installations. + Kerbal Safety: + - Move kerbal safety code from autonomous combat seat check in BDACompetition.cs to KerbalSafety.cs with its own toggle. + - Free-falling kerbals deploy parachutes. + - Kerbals in falling command seats leave the seat. + - Vessels (and kerbals therein) are recovered during spawning instead of getting destroyed. + - Surviving kerbals are recovered and immediately available for flying in the next competition instead of being MIA. + - Ejection from cockpits is still a WIP. + Battle Damage: + - Battle damage is now configurable with multiple sub-categories, with the following toggles. Per-hit proc chance is also adjustable. + - Fuel Tanks + - FuelTanks will now leak when hit + - Tanks may be configured to be self-sealing, trading some fuel capacity for leak resistance + - Tank leak rate and leak duration can be adjusted + - Ammo Boxes + - Ammo boxes will potentially explode if hit + - Ammo Boxes can be set to explode when destroyed + - Adds ModuleCASE to ammo parts. Caselevel can be set from 0-2, higher levels reduce ammo explosion damage, at the cost of increased mass and cost. + - Engines + - Engine thrust reduced when hit, will start leaking fuel/catch fire/shut down when badly damaged + - Intakes performance reduced when hit + - Engine Gimbals can fail when hit + - Wings + - Wing damage per hit reduced + - Wing lift loss capped at 50% total lift + - Wing damage per hit is now configurable + - Ailerons can fail when hit + - Subsystems + - Subsystem components + - SAS/Radar/etc can be disabled when hit + - Command + - Control damage when hit + - Pilots can be killed from cockpit damage + - Fires - if a tank is leaking fuel, chance for leak to ignite + - Fire damage over time + - Fires in cockpits or other crewed or crew adjacent parts will be automatically extinguished. + - Adds ModuleSelfSealingTank, can be toggled to prevent tanks leaks until tank receives substantial damage. it also adds the ability to configure fueltanks and engines to carry firebottles to extinguish fires + RCS: + - Improved RCS calculation accuracy and speed, your craft's RCS may be slightly different from 1.4.0.7 + - Recalculate RCS occasionally when parts are lost (to account for damage or missiles firing) + - Fix engine afterburner textures affecting RCS calculation (was an issue for Tiger and Cheetah engines from AirplanePlus) + Missiles: + - New variables that affect heat seeking missile performance: lockedSeekerFOVBias, and lockedSeekerVelocityBias. See Sidewinder config file for more details. + - Fixed allAspect and maxOffBoresight to work as intended. Vertical launch systems (VLS) and high off-boresight missiles should work as intended. See AIM-120 config file for details. + - Add ability for Air-to-Ground missiles to lead targets based on their velocity. + - Fixed Dynamic Launch Zone calculation, should result in more close-range missile fires and high off-boresight missiles only firing when they are likely to hit their target. + - Better explanation of what missile parameters do added to Sidewinder and AIM-120 config files (new heat seeker value description in Sidewinder config file). + - Updated torpedo tuning from Kurgan. + Target Priority: + - Target acceleration target priority now returns TWR/10 instead of instantaneous acceleration/10. Is set to -1 when target craft loses engines. + - Less teammates engaging target priority now returns -1 when all teammates are engaging the same target instead of 0 (allows for better ganging up on the same target). + Parts: + - New armor panels, rescaled armor panel part mass, cost, HP to scale properly based on size. + - Remove AIM-4 missiles, are moved to the Flying Circus mod for now, planned to be re-released in a future BDA expansion mod. + - Added new Typhoon engine to fill the gap between Juno and Panther. + Effects: + - New FireFX effect + +v1.4.0.7 +FIXES + - Fix rendered elevations being negative of the intended elevation. + - Fix logic for NRE check in TargetPriMass. +IMPROVEMENTS + - Check that falling kerbals haven't regained their seat before deploying their parachute. + - Force RCS update on competition start to allow landing gear and other robotics to have move into flight positions. + - Adjust RCS normalization from 4.0 to 3.8 so sphere with 1m^2 cross section has 1m^2 RCS. + - Change lockbreaking strength to 300, was agreed upon value after some discussion with BDAc communities. + - Change minimum lockbreak to 0. + +v1.4.0.6 +FIXES + - Fix spawning over water to use water height instead of terrain below. Allow negative spawn altitudes. + - Fix stack overflow if collision avoidance period is 0. + - Check for various NREs in target priority functions. +IMPROVEMENTS + - Rename 'Steer Factor' to 'Steer Power', 'Steer Ki' to 'Steer Correction' and add '(P)', '(I)' and '(D)' to the three fields. + +v1.4.0.5 +FIXES + - Fix craft being able to fire radar missiles at a target they could not lock when another target they could lock was nearby. This usually happened when the target launched missiles, since missiles have a 999 m^2 RCS by default. + +v1.4.0.4 +FIXES + - Bugfixes for rocket turrets. + - Bugfix for Oerlikon bulletType. +IMPROVEMENTS + - Tweaks to fire angle: + - Default of 3° + - Adjustable for each weapon from 0° to 6° + +v1.4.0.3 +FIXES + - Fix broken rotation caused by the radar snapshot rendering process. + - Give the terrain renderer more time to spawn the terrain when spawning craft at a new location. +IMPROVEMENTS + - Speed up the radar snapshot rendering. + +v1.4.0.2 +IMPROVEMENTS + - Use values from BDArmory's default bullet definition for bullet definitions that are missing fields or have invalid values. + - Warn about these in the logs. + +v1.4.0.1 +IMPROVEMENTS + - Improved parsing of the bullet and rocket defs with feedback on what went wrong. Use grep "ERR.*\[BDArmory\]" KSP.log (or similar search functionality) to find the failures. + +v1.4.0 +FIXES + - A whole bunch. +IMPROVEMENTS + Note: Mod creators should look at the new BulletDef configuration options in order to update custom BulletDefs. + - Major overhaul of ModuleWeapon thanks to SuicidalInsanity. + - Now supports magazine-fed weapons that must be occasionally reloaded + - Now supports burst-fire weapons (multiple shots per fire command) + - Now supports crewed weapons that require a kerbal present to fire + - Now supports shotgun-type weapons + - RocketLaucher functionality has been folded into ModuleWeapon, Rocketlaunchers will need to be reconfigured. + - RocketLaunchers can now be supplied from external ammo boxes. + - Gyrojet guns now supported + - Rocket class weapons can now use FlaK settings (airDetonation, proximityDetonation, maxAirDetonationRange) + - Can set custom fire sounds for Rockets + - Pulse Lasers now supported + - Lasers can now use accuracy setting (MaxDeviation) + - Lasers can do explosive damage on hit + - Lasers no longer Aim-bot + - Laser and rocket scoring implementation + - Weapons now support multiple ammo types + - Adds per-ammo tracer settings + - Adds per-ammo fuze settings + - Airdetonation/proximity detonation now per-ammotype, set in bulletdef via fuze setting + - Changes to bulletdefs - added a fuze setting: none, timed,proxi,flak + - Tracer stats moved to bulletdefs following ammoswap implementation + - Tracer width now dynamically determined based on caliber; can be overridded in weapon cfg (laser support, mainly) + - Tightened up Ai accuracy - missilefire; changed permitted firing tolerance from 2-4 deg to user-customizable 0-2 deg + - Added cluster ammo weighting to bool smartweaponselect + - Adds debilitated bool to TargetInfo/MissileFire for EMP weapons + - Rockets use object pooling and are configured in BulletDefs/BD_Rockets.cfg. + - New ModuleWeapon.cfg settings + - BeltFed = False // enables mag-fed weapons + - RoundsPerMag = n // shots per magazine or shots per burst if BurstFire = true + - ReloadTime = n // time(sec) needed to reload + - BurstFire = true // enables burst fire; use with RoundsPerMag + - Crewserved = true // weapon part must have CrewCapacity > 0 and Kerbal occupant + - ExternalAmmo = true // rocket class weapons use external ammo boxes + - RocketPod = true // default true, used if rocketpod w/ rocket submodels (e.g. Hydra70) + - Setting rocketPod = true will default RoundsPerMag to the # of rocket submodels + - Setting rocketpod = false will default externalAmmo to true + - RocketPod = true and externalAmmo = true will set BeltFed = false + - PulseLaser = true // enables pulse laser functionality instead of beam, uses roundsPerMinute for firerate, Maxdeviation for accuracy, same as guns + - HEpulses = false // enables laser impacts to act like HE rounds + - 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 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 deprecated + - New Rocket_defs settings + - rocketMass = n //weight in tons (10kg rocket is 0.01, etc) + - caliber = n // rocket diameter + - thrust = n // thrust + - thrustTime = n // rocket fires for n seconds + - shaped = false //does rocket have shaped charge warhead + - flak = false //air + proximity detonation + - explosive = true //does it explode + - tntMass = n //mass of tnt in kg + - subProjectileCount = 1 //cluster rocket quantity, default 1 + - thrustDeviation = 0.1 //accuracy, higher = wider divergence from boresight + - rocketModelpath = foo //file location of rocket model, e.g. "BDArmory/Parts/h70Launcher/h70Rocket/model" + - Improvements to RCS/stealth/ECM: + - Craft with a lower modified radar signature (from ECM Modules that reduce signature) no longer have increased lock range. The range at which craft are locked will be accurate based on the RCS Window in the SPH/VAB if no jammers are used. + - Re-worked RCS system. Calculate 95 azimuth/elevation pairs across 0-180 deg Az, 23 to -23 deg El, take the 3rd quartile of the distribution to use as the total RCS. + - Scale RCS up by 4 to better match reality and provide better balancing against stock radars. + - Display worst three Az/El pairs in the BDA editor window so that users can improve their design. + - Fix to turn on all ECM jammers when an incoming radar-missile is detected. + - Improvements to missiles: + - Allow firing multiple missiles at the same radar target without losing radar lock. Firing missiles at multiple locked targets needs considerable work to be feasible. + - Fix known issues with terminal guidance. + - Add option for configuring FireFOV of a turret (fires when pointing within this angle of target). Reduce step size of fire interval to 0.5s. + - Improvements to pilot AI: + - Allow users to set "Firing Angle," the angle the weapons are within of being on target before firing. Default is 1 deg. Setting is on Weapons Manager. Setting only affects weapons with a yaw and pitch range of less than 5 degrees for both axes. + - Make wing commander 'spread' and 'lag' values persistent. + - Allow pilots in the "Attack" command to evade if they are being fired at by another craft + - Sliders for controlling threshold and period of collision avoidance between vessels. + - Allow craft to evade if they are under attack while being commanded to "Attack." + - UI improvements: + - Improved behaviour for entering spawn coordinates and altitude. + - Add a slider in tournament settings to control delay between heats. + - Other improvements: + - Further balancing of the 30mm tnt mass to 0.077 (from 0.081). + - B9 Proc Wings HP patch + +v1.3.8.2 +IMPROVEMENTS + - Rebalance 20mm and 30mm ammo tnt amount. + - Weapons using electric charge count as always having ammo (not just those ending in 'Laser'). + - Remove surface attachment option from combat seat. + - Add option to automatically kill off WMs after 5s when they become uncontrolled. + - Kerbals in falling combat seats eject to safety. + - Deploy halo chute for falling kerbals. Add AG to WM to deploy chutes on external kerbals. + +v1.3.8.1 +FIXES + - Report death time to web orchestration in seconds instead of minutes +IMPROVEMENTS + - Autonomous combat seats: when disabled (default) during competitions, combat/command seats disable the AI when the kerbal leaves the seat and vessels that are comprised of only a single combat/command seat explode. + - Reduce max fireBurstLength to 10 and stepIncrement to 0.05 to allow more control of gun burst length. + - Updated HP patch from CeruleanEyes + +v1.3.8 +FIXES + Main fixes: + - The remaining main memory leaks have been eliminated resulting in a noticeable performance boost. (There are still some leaks, but they amount to only a few GB if left running overnight.) + - Explosions are no longer limited to 5 at a time. + - Fixed decals not being returned to the object pool so that they work efficiently now. + Other fixes: + - Use the correct spawn distance value in the remote orchestration. + - Unity's fake nulls strike again: properly reset explosion object pools on scene changes. + - KSP is labelling some missiles as debris. Detect these and don't remove them. + - Bugfixes, memory leak fixes and optimisations. + - Don't use Parallel.ForEach to update ramming information as Unity doesn't like get_transform being called from threads. + - Assign font attributes in Awake instead of Constructor. + - Fix potential NRE in ApplyDamage in PooledBullet.cs + - Fix a potential NRE in continuous spawning where a spawned vessel gets destroyed before it's activated. + - Fix special case of vessels with combat seats getting renamed. + - Reset spawn position and rotation after spawning as sometimes KSP packs and unpacks craft and resets things! + - Wait an extra update during spawning to allow reference transforms to be assigned. + - Use altitudes from FlightGlobals when raycasting fails during spawning. +IMPROVEMENTS + Main improvements: + - Rebalance of explosion damage from bullets to approximate the previous DPS of the browning, vulcan and gau-8. + - AI/WM now cost 0. + Other improvements: + - Remove the '5' from the options for heat size in the automatic vessels per heat optimisation to ensure a minimum heat size of 5. + - Add configurable keybindings for tournament setup and running. Improve layout of 'Edit Inputs' window. + - Added an 'auto-enable vessel-switching' option to general settings. Added 'auto' mode for tournament generation that chooses an appropriate number of vessels per heat and distributes the deficit over multiple heats. + - Set up paintball decal pools when the paintball toggle is toggled. + - Extra damage sliders in-game + - Small optimisation to avoid reallocating Ray instance in PooledBullet.cs + - Add the explModelPath to the browninganm2.cfg + - Use a split slider for tournament rounds setting to allow up to 100 rounds. + - Reuse deathOrder variable instead of fetching it from BDACompetitionMode.Instance again. + - Use minSpeed instead of idleSpeed in steer limiter. Scale possibleAccel by 1/15 as before (but without G) for now until we find a better solution. + - Additional regular check for vessels renamed as planes due to the combat seat. + - Automatically end a continuous spawning competition when there's nothing left to spawn and <2 active craft. + - Add a max altitude GM killer. + - Updated Python scripts for parsing competition results logs. + - Also trigger competition end when there's only 1 team left. + - Include engine acceleration but not gravity in GetSteerLimiterForSpeedAndPower as it makes gliders and RegainEnergy work better. + - Use afterburners when regaining energy. + - Trigger the target scan after all the teams have been assigned. + - Enable target priority by default. + - Stop extending when changing targets. + - Include death time and normalised death order in web orchestration reporting with surviving player death order/time set to max + +v1.3.7 +FIXES + - Fix bug in spawning due to setting a non-default control point orientation. + - Merge two flags that represent the same status. + - Don't reload settings when opening the main settings window as it is already loaded on start and messes with other settings windows. + - Fixes AI not using Torpedos +IMPROVEMENTS + - Local tournament mode (in the Vessel Spawner window). + - Re-worked debris/bystander removal. + - Sliders (in general settings) for many competition timers (such as the killer GM and various grace periods). + - Cargo bays only automatically close if they've been opened to drop bombs/missiles. + - Move vessels directly to sea-level if spawning in water. + - Removed RWP season 2 round 3 specific code. + +v1.3.6.5 +FIXES + - Fix resetting missile on miss. Add a bindable action group for resetting the missile if it has a pilot AI. +IMPROVEMENTS + - Use aiming mode for steering when within 8s of ramming target. + - Add in easing factors for ramming, correct missing negative term, use ramming vessel acceleration instead of relative acceleration. + - Improved lead-time calculations for missile guidance with ease-in factors. + - Enable target lock for missiles with weapon managers to prevent re-targeting issues. + - Add 1f to sum of priorities to help with target bias. + +v1.3.6.4 +FIXES + - Various +IMPROVEMENTS + - In-game configuration of Round 3 targets spawn point. + - Preserve target spawn settings when leaving/entering flight mode. + - Save settings when closing the Vessel Spawner window. + - Set target spawn location on using 'interesting' buttons. + - Add a countdown slider for the Round 3 GUI and set the default target geoCoords to 1°N (equivalent to around 11 km). + - Re-enable detonation of armed explosive parts when they are destroyed. + - Use partHit position instead of vessel.CoM in proximity calculations. Estimate distance on next fixed update and detonate if expected to collide next fixed update. + - Use time to closest point of approach for AAM guidance. + - Use new Saturn texture created by Stardust + +v1.3.6.3 +FIXES + - Don't autodestruct missiles if the explosive parts are using the override. + - Show message about why the vessel spawning timed out. + - Prevent windows from being competely off-screen (the drag region may still be off-screen though). Toggling "Strict Window Boundaries" will bring all the windows onto the screen if you can't find them. +IMPROVEMENTS + - Absolute Distance vs Distance Factor toggle and expanded Spawn Distance slider for ranges up to 20km. (Note: for distances over 10km, it struggles due to the PRE and terrain. Using an airborne spawn first can help fix this.) + - Add 'Detonate At Minimum Distance' to MMG. (Note: MMG doesn't have an 'ignore friendlies' yet.) + - Add 'Detonate At Minimum Distance' for explosive parts too. + - Note: the effect of 'Detonate At Minimum Distance' is to delay the detonation/triggering once it's within the detonation radius until the distance is minimised. + +v1.3.6.2 +FIXES + - Increase the ground finding raycast distance for spawning at ground level for large radii. - Give the craft an extra 5s to land and set the spawnFailureReason if it times out. + +v1.3.6.1 +FIXES + - Various +IMPROVEMENTS + - Allow the missile to get out of NotSafe mode when it is its own parent, which enables proximity detonation on such missiles. + - Update release command conditions + - Reuse pilot/missile combos if miss occurs + +v1.3.6 +FIXES + - Various +IMPROVEMENTS + - Separate Vessel Spawner window. + - Proximity fuze added to warhead, with IFF check. + - Team spawning function. + - Add death times to death order. + - Better default values for dynamic damping to decent values. + - Enable APBulletMod + - Add death order to records. + - Relax min < max condition for damping sliders. Relabel min/max as off-target/on-target for clarity. + - Use a time-based check for recentlyDamaged condition in camera switch weighting. + - Add pyramids location to default interesting spawn locations. + - Paintball, now with 100% more paint + - Special (temporary) button for spawning Round 3 of Season 2 of Runway Project. + +v1.3.5.6 +FIXES + - Various +IMPROVEMENTS + - Use secret token for reporting of scores in remote orchestration. + +v1.3.5.5 +FIXES + - Bugfix (don't adjust gravity after the competition stops). + +v1.3.5.4 +FIXES + - Bugfix for near-terrain spawning. +IMPROVEMENTS + - Continuous single spawning mode (when enabled, the single spawn will respawn vessels 10s after the competition ends; it also automatically starts competitions). + - Relocation of competition duration slider to general slider settings. + - Competition duration slider now affects all competitions instead of just remotely orchestrated ones and has an 'unlimited' setting. + +v1.3.5.3 +IMPROVEMENTS + - Change some variable names to reduce bandwidth due to JSON payloads. + +v1.3.5.2 +FIXES + - 10s grace period instead of 60s for Gravtity Hacks (Increase Gravity on Death). +IMPROVEMENTS + - Improvements to the remote orchestration window. + - Ramming and parts lost and missile damage tracked for web service. + +v1.3.5.1 +FIXES + - Fix the status in the Remote Orchestration Window + +v1.3.5 +FIXES + - Fixed various different issues that made flares ineffective. + - Synchronise continuous spawning to competition update ticks when competitions are active. + - Adjust location of camera reversion for continuous spawning and make sure that the camera distance gets set. +IMPROVEMENTS + - Change section toggles in settings window to full-width buttons to make them more obvious. + - Added a toggle for keeping BDA windows within the screen. + - Added a toggle for dumping continuous spawning logs every spawn toggle. + - Improvements for the Remote Orchestration Window + - Improvement to location switching with the 'interesting spawn locations' buttons and camera handling during spawning. + - Latest HP part fixes from CeruleanEyes for some Mk2/H parts. + - Included SuicidalInsanity's B9 HP patch + Gravity hacks: + - Gravity value displayed on vessel switcher + - Kill time of 1s when landed with gravity hacks enabled + - Adjustments to gravity hacks to scale with the number of craft in the competition and for continuous spawning. + +v1.3.4.2 +FIXES + - Fix a bug with competitions failing to start when using the web orchestration due to the mass team switch not happening immediately. + +v1.3.4.1 +FIXES + - Fix the VESSELNAMING issue that prevents some players from having their scores registered. + +v1.3.4 +FIXES + - Fix craft not responding to multiple missile threats. + - Missing condition for regular kills with missiles. + - Clear the pilotActions dictionary to avoid ' is Dead' when starting a new competition with previously used pilots. + - Adjust DeathOrder for continuous spawn to avoid ' killed by ' messages on spawning. + - Fix death counts in logged scores for vessels that have run out of lives. + - Don't clean up SpaceObjects in RemoveDebris as it is a source of null refs in the base game. +IMPROVEMENTS + - Adjust vessel switcher window's display of dead vessels to reflect clean kills vs regular ones. + - Only count clean kills as kill, give assists to all vessels with hits for regular kills. + - Support scoring for hits and damage in/out. + - Interesting Vessel Spawn locations with separate cfg file (spawn_locations.cfg). + - Trigger result logging on scene change from BDACompetitionMode. + - Store the competition distance. + - Automatically dump scores when leaving flight mode. + - Extra setting REMOTE_LOGGING_VISIBLE in the settings.cfg file only to hide the remote orchestration from the settings window. Defaults to False. + - Start tilted 10° outwards when spawning in the air. + - In-game messages when dumping competition logs. + - Rolling spawning (sliders for controlling the maximum number of craft and the number of lives they have for continuous spawning). + - Use user-set altitude and other settings for remote orchestration spawning. + +v1.3.3.1 +IMPROVEMENTS + - Set default value of 'Default FFA Targeting' to false. + - Proper targeting logic for surface vessels when target priority is enabled. + +v1.3.3 +FIXES + - Several bugfixes ('packing for orbit' for continuous spawning of command seats, revert debris clean-up coroutine). +IMPROVEMENTS + - Disable camera switch on continuous spawning. + - Hide DEAD vessels in continuous spawning mode. + - Allow negative weightings for target priority sliders. + - Lower min combat speed speed to match take-off speed. + - Damage multipler slider. + +v1.3.2.2 +IMPROVEMENTS + - Updated model for the Mk1 Open Cockpit. + +v1.3.2.1 +FIXES + - Don't allow minimum altitude of 0 as it breaks taking off. +IMPROVEMENTS + - Updated collision model for Mk1 Open Cockpit. + +v1.3.2 +IMPROVEMENTS + - Mk1 Open Cockpit and EAS-2 Combat Seat (with built-in AI & WM). + - Adjustments to spawning for planets without surfaces and over oceans. + - Extra setting for controlling ease-in speed during spawning. + - Settings toggle for Runway Project vs official. + - UI tidy up for AI settings. + - Bank and pitch angle limiters. + - Improved input for spawning coordinates (0.5s delay). + - Spawn distance factor slider. + - More tolerance (time) on the invalid vessel checks. + +v1.3.1.1 +FIXES + - Update the ramming information datastructures for the new vessel instances during continuous spawning. + +v1.3.1 +IMPROVEMENTS + - Added new M1 Abrams HE and M102 HE turrets and textures. + - Added extra target priority controls. + - Default target priority controls approximate "FFA Combat Targeting". + +v1.3.0 +FIXES + - Major memory leak fix, giving better stability and performance for longer competitions (aubranium, DocNappers). +IMPROVEMENTS + - Remote orchestration (aubranium). + - HP changes for flags (Josue). + - HP changes for parts (CeruleanEyes). + - Improved spawning routines with ground, airborne and continuous spawning modes (DocNappers). + - Fire ALL engines on planes that don't set AG10 (DocNappers). + - Improved competition status messaging system (DocNappers). + - Tag mode (Josue). + - Out-of-ammo kill timer for continuous spawning mode to prevent craft wasting too much time trying to ram (DocNappers). + - Vessel switcher UI improvements (DocNappers). + - Missile scoring: parts destroyed (DocNappers). + - Damage tracking: ballistic + explosive damage from bullet hits and explosive damage from missile strikes (DocNappers). + - Score dumps to separate files (in GameData/BDArmory/Logs) (DocNappers). + - Handling of badly built craft, mostly (i.e., those with AI/WM not on the cockpit or command seats) (DocNappers). + - Partial support for command seats: they work, but frequently mess up the vessel switcher window when they spawn; this doesn't affect the scoring though (Josue, DocNappers). + - Control of automatic camera switching frequency (Josue). + - Target prioritisation (Josue). + +v1.2.1.3 +FIXES + - Check for both vessel type not being debris and vessel name not ending in "Debris" when checking if a vessel is really alive. + +v1.2.1.2 +FIXES + - Don't count missiles as debris in clean-up routine. + +v1.2.1.1 +FIXES + - Fixed spacing in some displayed messages. + +v1.2.1 +FIXES + - Don't overwrite settings.cfg, just generate one if it doesn't exist. + - FFA Combat Targeting is enabled by default. + +v1.2.0 + +v1.1.1 + +v1.1.0 + +v1.0.0 + +**** TAKEN OVER BY RWP DEV TEAM AND VERSION NUMBERS ADJUSTED **** + +v1.3.4 * NEW FEATURES: * Recompiled for KSP 1.9.1 * FIXES @@ -87,18 +4208,18 @@ v1.2.4 v1.2.3 * NEW FEATURES: - * Recompiled for KSP 1.5.1 + * Recompiled for KSP 1.5.1 * EC per shot for energy weapons like Rail guns. #486 * EMP Weapons logic. - * Modular Missiles: Min speed before guidance trigger, time between stages. - * New High Explosive resource for missiles. + * Modular Missiles: Min speed before guidance trigger, time between stages. + * New High Explosive resource for missiles. * New smoke model. * Autopilot: Orbiting direction can be set. * ENHANCEMENTS: * Explosive blast forces increased. * Hitpoints rounding reduced to 100. #432 - * Missiles can be jettisoned using action group #539 + * Missiles can be jettisoned using action group #539 * Max detonation range for air explosive bullets increased from 3500m to 8000m. * Autopilot improvements: pitchKi has saner values, also steering added. * Guard mode: Better calculation of missiles away. @@ -118,7 +4239,7 @@ v1.2.3 * Some exceptions controlled * Heat missiles will not lock friendly vessels. #586 * Fixed issue where modular missiles were detecting themselves as enemy vessels and detonating. - * Modular Missiles: roll correction fixed. + * Modular Missiles: roll correction fixed. * BALANCE * Bullet mass rebalance for lower calibers #515 @@ -130,18 +4251,14 @@ v1.2.2.2a * FIXES * added in missing high speed missle fix. somehow got lost in the merge migration. -v1.2.2.2a -* FIXES -* added in missing high speed missle fix. somehow got lost in the merge migration. - v1.2.2.2 * Recompiled for KSP 1.4.5 * FIXES - *Error spam in Log when a radar is destroyed. An orphaned index was not properly being updated. - Reported in BDAc Forum post: https://forum.kerbalspaceprogram.com/index.php?/topic/155014-14x-bdarmory-continued-v1221-7102018-vessel-mover-camera-tools-bdmk22-destruction-effects-burn-together/&do=findComment&comment=3425233 - Thanks to greydragon70 for his thoughtful evaluation of BDac and issue reporting. - * Corrected an error where high speed missiles would sometimes not detonate in the correct location. + *Error spam in Log when a radar is destroyed. An orphaned index was not properly being updated. + Reported in BDAc Forum post: https://forum.kerbalspaceprogram.com/index.php?/topic/155014-14x-bdarmory-continued-v1221-7102018-vessel-mover-camera-tools-bdmk22-destruction-effects-burn-together/&do=findComment&comment=3425233 + Thanks to greydragon70 for his thoughtful evaluation of BDac and issue reporting. + * Corrected an error where high speed missiles would sometimes not detonate in the correct location. * ENHANCEMENTS: * Increased maximum allowable missles per target to 18, up from 6. @@ -149,15 +4266,15 @@ v1.2.2.2 * Added 2 new EMP equipped missiles HellFire EMP, and AIM-120 EMP. Small pulse radius but demonstrates the feature. * Added 3 different sized EMP pulse FX, allows for additional sized pulse weapons. * Added Reloadable Missile Rail (ModuleMissileRearm. Now missile launchers can be reloadable! - This module can be added to missile launcher designs to allow reloading. (code used with permission courtesy of @flywyx). - Requires unity based modifications, normal rails will not function as reloadable unless additional transforms are added. + This module can be added to missile launcher designs to allow reloading. (code used with permission courtesy of @flywyx). + Requires unity based modifications, normal rails will not function as reloadable unless additional transforms are added. * Added new standard resource High Explosive. Provides better matching of tntMass for balance. * Added an Ammo switcher feature (no more Firespitter required) * Added new Universal Ammo Box part that uses the new Ammo switcher feature. Old part remains for backawards compatability. - Use the new part going forward. To remove the need for FireSpitter, replace existing Ammo Box parts with the new part on existing craft. + Use the new part going forward. To remove the need for FireSpitter, replace existing Ammo Box parts with the new part on existing craft. * Improved sub categories for BDA parts. This helps with the clutter in the Editor under the BDA category. * Removed BDACategoryModule as a result of the BDA categories refactor. - Existing craft may see a module not found warnings in the log. This will have no ill effects and can be safely ignored. + Existing craft may see a module not found warnings in the log. This will have no ill effects and can be safely ignored. * Improved smoke effects for smoke canister launchers * Added new Jet engine based on the J-404. Licensed from KTech. (Thanks @TheKurgan, @SpannerMonkey(SMCE) & @XOC2008!) * Added new BDAc Test Drone MKIII craft, utilizing the new engine. @@ -199,7 +4316,7 @@ v1.2.1.4 * Added mouse scrollwheel zooming in TargetCam window. Now you can zoom in and zoom out using the mousewheel. * Added reactive armor part module. Modders can now take advantage of a new armor type in BDAc. * Added Wet Weapons Check module (ModuleWWC). This module will pevent submerged weapons from firing (like when a ship sinks). - The depth is user selectable. + The depth is user selectable. v1.2.1.3 @@ -222,12 +4339,12 @@ v1.2.1.2 * Correct automatic resizing of WingCommander Window * Reintroduce BDAcUniversalAmmoBox Module Manager config file. Somehow was dropped during a merge somewhere back. * Separate RWR operation from RWR display. RWR should be enabled at all times when a Radar is installed on craft. - - Opening and closing the RWR window now leaves RWR operation active. Git Issues 410, #411, #461 + - Opening and closing the RWR window now leaves RWR operation active. Git Issues 410, #411, #461 * removed part configs for the AGM86-cruise an the RBS-15 They have been broken, and are superceded by improved weapons. * ENHANCEMENTS: * Increase the limits of the Radar and RWR window scaling to allow for larger windows. - - Add min and max settings to the settings.cfg to allow user defined limits. Git Issue #521 + - Add min and max settings to the settings.cfg to allow user defined limits. Git Issue #521 * Add mouse driven RWR and Radar windows resizing. Now you can intuitivly resize by mouse. Click an drag the lower right corner. * Redesigned Targeting Cam window to incorporate all features added to the Radar window - resizing, boundary checks, mouse resize, relocate buttons * Retune BDAc missiles to improve characteristics, performance, and balance. Tuning performed by Kergan @@ -310,25 +4427,25 @@ v1.0.0 * NEW FEATURES: * Radar & Stealth, re-design of how the radar system works: - - New radar reflection shader (credit: original great idea by JR!), calculating cross section in m^2: - + allows building for stealth using angled surfaces (“scatter those pesky radar waves away!”) - + calculation is normalized so that a 1x1 structural plate directly frontally facing the radar yields a return of exactly 1 m^2 - - Redesign of the radar system, radars all have different ranges & detection performance: - + radar part configs changed, new fields (floatcurves) to define detection and lock/track performance, new field to specify ground clutter effectiveness - - New in-editor analysis window to help evaluate a craft’s cross section and performance against selectable radars - - New options for configuration of radar guided missiles in the part config: - + can now explicitly specify active seeker performance (via a FloatCurve, similar to radars) - + can now explicitly specify the static radar cross section of a missile for detection purpose - - Adjustment of the Chaff countermeasure system: more gradual effect of decoying missiles away (instead of previous “all-or-nothing”) - - Adjustment of IR missiles and flare countermeasures: simulating now more of a “imaging infrared” seeker than an old dumb seeker – to be successfully decoyed, flares have to more closely match the thermal spectrum of the part the seeker has locked on to + - New radar reflection shader (credit: original great idea by JR!), calculating cross section in m^2: + + allows building for stealth using angled surfaces ("scatter those pesky radar waves away!") + + calculation is normalized so that a 1x1 structural plate directly frontally facing the radar yields a return of exactly 1 m^2 + - Redesign of the radar system, radars all have different ranges & detection performance: + + radar part configs changed, new fields (floatcurves) to define detection and lock/track performance, new field to specify ground clutter effectiveness + - New in-editor analysis window to help evaluate a craft's cross section and performance against selectable radars + - New options for configuration of radar guided missiles in the part config: + + can now explicitly specify active seeker performance (via a FloatCurve, similar to radars) + + can now explicitly specify the static radar cross section of a missile for detection purpose + - Adjustment of the Chaff countermeasure system: more gradual effect of decoying missiles away (instead of previous "all-or-nothing") + - Adjustment of IR missiles and flare countermeasures: simulating now more of a "imaging infrared" seeker than an old dumb seeker — to be successfully decoyed, flares have to more closely match the thermal spectrum of the part the seeker has locked on to * Massive improvements to ballistic missile guidance and modular missiles, ability to simulate ICBM flight paths: - - Ballistic guidance has been enhanced to allow firing missiles accurately even at distances greater than 500 km - - A new field called BallisticOverShootFactor has been added to missiles, the greater the value the further the missile will go before the terminal maneuver begins - - By default all missiles now can steer in vacuum. New field called "vacuumSteerable" can be set to false to disable this behaviour - - Auto SAS activation when the missile is fired. Before it was only activated if the parent vessel had it switched on. - - Improved the Cruise and Ballistic Guidance. Ballistic Guidance will switch to AGM guidance as soon as it reached the 50% of the distance - - Guard mode in space will consider orbiting vessels as "flying" (air targets), hence engagement of space targets is possible now + - Ballistic guidance has been enhanced to allow firing missiles accurately even at distances greater than 500 km + - A new field called BallisticOverShootFactor has been added to missiles, the greater the value the further the missile will go before the terminal maneuver begins + - By default all missiles now can steer in vacuum. New field called "vacuumSteerable" can be set to false to disable this behaviour + - Auto SAS activation when the missile is fired. Before it was only activated if the parent vessel had it switched on. + - Improved the Cruise and Ballistic Guidance. Ballistic Guidance will switch to AGM guidance as soon as it reached the 50% of the distance + - Guard mode in space will consider orbiting vessels as "flying" (air targets), hence engagement of space targets is possible now * New dedicated sonar ping and torpedo ping sounds, courtesy of PapaJoe! @@ -881,7 +4998,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) @@ -1403,4 +5520,4 @@ v0.1.1 - added bullet drop toggle v0.1 -- initial release of .50cal turret & ammo \ No newline at end of file +- initial release of .50cal turret & ammo diff --git a/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/flameD.png b/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/flameD.png new file mode 100644 index 000000000..1e6cb2172 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/flameD.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/model.mu b/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/model.mu new file mode 100644 index 000000000..a544d1df0 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/srbsmoke.png b/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/srbsmoke.png new file mode 100644 index 000000000..85af65b2e Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/FX/FireFX/srbsmoke.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/FX/FuelLeakFX/DustParticle.png b/BDArmory/Distribution/GameData/BDArmory/FX/FuelLeakFX/DustParticle.png new file mode 100644 index 000000000..0524f5b87 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/FX/FuelLeakFX/DustParticle.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/FX/FuelLeakFX/model.mu b/BDArmory/Distribution/GameData/BDArmory/FX/FuelLeakFX/model.mu new file mode 100644 index 000000000..e1008fd53 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/FX/FuelLeakFX/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg index 6b7634b7c..e0c26d9b8 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg @@ -24,7 +24,7 @@ Localization #autoLOC_bda_1000019 = omnidirectional #autoLOC_bda_1000020 = boresight #autoLOC_bda_1000021 = EC/sec: <<1>> - #autoLOC_bda_1000022 = Field of view: <<1>>° + #autoLOC_bda_1000022 = Az. FoV: <<1>>° - <<2>>° #autoLOC_bda_1000023 = RWR Threat Type: <<1>> #autoLOC_bda_1000024 = Capabilities: #autoLOC_bda_1000025 = - Scanning: <<1>> @@ -38,6 +38,54 @@ 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 + #autoLOC_bda_1000039 = Active Sonar + #autoLOC_bda_1000040 = Passive Sonar + #autoLOC_bda_1000041 = El. FoV: <<1>>° - <<2>>° + } + 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 { @@ -61,23 +109,45 @@ Localization } ja { - #autoLOC_bda_1000000 = Disable Radar - #autoLOC_bda_1000001 = Enable Radar - #autoLOC_bda_1000002 = SAM - #autoLOC_bda_1000003 = FIGHTER + #autoLOC_bda_1000000 = レーダーを無効 + #autoLOC_bda_1000001 = レーダーを有効 + #autoLOC_bda_1000002 = 同じ + #autoLOC_bda_1000003 = 搭乗員 #autoLOC_bda_1000004 = AWACS - #autoLOC_bda_1000005 = MISSILE - #autoLOC_bda_1000006 = DETECTION - #autoLOC_bda_1000007 = UNKNOWN - #autoLOC_bda_1000008 = Radar Type: <<1>> - #autoLOC_bda_1000009 = Range: <<1>> meters - #autoLOC_bda_1000010 = RWR Threat Type: <<1>> - #autoLOC_bda_1000011 = Can Scan: <<1>> - #autoLOC_bda_1000012 = Track-While-Scan: <<1>> - #autoLOC_bda_1000013 = Can Lock: <<1>> - #autoLOC_bda_1000014 = Can Receive Data: <<1>> - #autoLOC_bda_1000015 = Simultaneous Locks: <<1>> - #autoLOC_bda_1000016 = Radar Requires EC + #autoLOC_bda_1000005 = ミサイル + #autoLOC_bda_1000006 = 検出 + #autoLOC_bda_1000007 = 不明 + #autoLOC_bda_1000008 = レーダの種類: <<1>> + #autoLOC_bda_1000009 = 幅: <<1>> m + #autoLOC_bda_1000010 = RWR脅威の種類: <<1>> + #autoLOC_bda_1000011 = スキャン可能: <<1>> + #autoLOC_bda_1000012 = スキャンしながら追跡: <<1>> + #autoLOC_bda_1000013 = ロックが可能: <<1>> + #autoLOC_bda_1000014 = データを受信可能: <<1>> + #autoLOC_bda_1000015 = 同時にロック: <<1>> + #autoLOC_bda_1000016 = レーダーにはECが必要 + #autoLOC_bda_1000017 = ソナー + #autoLOC_bda_1000018 = データリンクのみ + #autoLOC_bda_1000019 = 全方向性 + #autoLOC_bda_1000020 = ボアサイト + #autoLOC_bda_1000021 = EC/秒: <<1>> + #autoLOC_bda_1000022 = 視野: <<1>>° + #autoLOC_bda_1000023 = RWR脅威の種類: <<1>> + #autoLOC_bda_1000024 = 能力: + #autoLOC_bda_1000025 = - スキャン中: <<1>> + #autoLOC_bda_1000026 = - スキャンしながら追跡: <<1>> + #autoLOC_bda_1000027 = - ロック中: <<1>> + #autoLOC_bda_1000028 = - 最大ロック数: <<1>> + #autoLOC_bda_1000029 = - データの受信: <<1>> + #autoLOC_bda_1000030 = 性能: + #autoLOC_bda_1000031 = - 検出: <<1>> m^2 @ <<2>> km + #autoLOC_bda_1000032 = - 検出: (なし) + #autoLOC_bda_1000033 = - ロック/トラック: <<1>> m^2 @ <<2>> km + #autoLOC_bda_1000034 = - ロック/トラック: (なし) + #autoLOC_bda_1000035 = - グラウンドクラッター係数: <<1>> + #autoLOC_bda_1000036 = IRSTを無効 + #autoLOC_bda_1000037 = IRSTを有効 + #autoLOC_bda_1000038 = - 検出: <<1>> ºC @ <<2>> km } ru { @@ -118,5 +188,29 @@ Localization #autoLOC_bda_1000014 = 允许接受下行数据: <<1>> #autoLOC_bda_1000015 = 允许锁定多个目标: <<1>> #autoLOC_bda_1000016 = 雷达需要消耗电量 + #autoLOC_bda_1000017 = 声纳 + #autoLOC_bda_1000018 = 仅数据链 + #autoLOC_bda_1000019 = 全向 + #autoLOC_bda_1000020 = 视轴 + #autoLOC_bda_1000021 = 电量/秒: <<1>> + #autoLOC_bda_1000022 = 视场: <<1>>° + #autoLOC_bda_1000023 = 威胁告警类型: <<1>> + #autoLOC_bda_1000024 = 能力: + #autoLOC_bda_1000025 = - 扫描: <<1>> + #autoLOC_bda_1000026 = - 跟踪时扫描: <<1>> + #autoLOC_bda_1000027 = - 锁定: <<1>> + #autoLOC_bda_1000028 = - 最大锁定数量: <<1>> + #autoLOC_bda_1000029 = - 数据链: <<1>> + #autoLOC_bda_1000030 = 性能: + #autoLOC_bda_1000031 = - 探测: <<1>> m^2 @ <<2>> km + #autoLOC_bda_1000032 = - 探测: (无) + #autoLOC_bda_1000033 = - 锁定/跟踪: <<1>> m^2 @ <<2>> km + #autoLOC_bda_1000034 = - 锁定/跟踪: (none) + #autoLOC_bda_1000035 = - 地面杂波影响: <<1>> + #autoLOC_bda_1000036 = 关闭 IRST + #autoLOC_bda_1000037 = 启用 IRST + #autoLOC_bda_1000038 = - 探测: <<1>> ºC @ <<2>> km + #autoLOC_bda_1000039 = 主动声纳 + #autoLOC_bda_1000040 = 被动声纳 } } 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..a0b084437 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/de-de.cfg @@ -0,0 +1,1578 @@ +// Notes: +// - The indentation provides fold region info for IDEs. +// - #LOC_A = #LOC_B is valid. +// - Check for duplicates with: grep -o '^\s*#LOC[^ ]\+' en-us.cfg |tr -d ' '|sort|uniq -c|grep -v '\s1' +// - Propagate changes in en-us.cfg to the other localisation files by running 'python3 ../_Other\ Stuff/localisation_organisation_sync.py' in the BDArmory/BDArmory folder. + +Localization +{ + de-de + { + // Generic + #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_On = An + #LOC_BDArmory_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_Reload = Neu laden + #LOC_BDArmory_Generic_Help = Hilfe + #LOC_BDArmory_Generic_Select = Auswählen + #LOC_BDArmory_Generic_SaveandClose = Speichern und schließen + #LOC_BDArmory_VesselStatus_Landed = (Gelandet) + #LOC_BDArmory_VesselStatus_Splashed = (Im Wasser) + #LOC_BDArmory_VesselStatus_Underwater = (Unter Wasser) + #LOC_BDArmory_false = Falsch + #LOC_BDArmory_true = Wahr + #LOC_BDArmory_Enabled = Aktiviert + #LOC_BDArmory_Disabled = Deaktiviert + #LOC_BDArmory_Enable = aktivieren + #LOC_BDArmory_Disable = deaktivieren + + // WM Window + #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_rippleText1 = Sperrfeuer: <<1>> s/min + #LOC_BDArmory_WMWindow_rippleText2 = Salve + #LOC_BDArmory_WMWindow_barrageStagger = Gestaffelt + #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_NoWeaponManager = Kein Waffen-Kontrollsystem gefunden! + + // WM Guard Menu + #LOC_BDArmory_WMWindow_NoneWeapon = Kein(e) + #LOC_BDArmory_WMWindow_GuardMode = Wächtermodus <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = Kadenz + #LOC_BDArmory_WMWindow_BurstLength = Feuerstoß-Dauer + #LOC_BDArmory_WMWindow_FiringTolerance = Winkel-Toleranz + #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 + // Advanced Targeting + #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 + + // WM Target Priority + #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 + + // WM Modules + #LOC_BDArmory_WMWindow_RadarWarning = Radar-Warnung + #LOC_BDArmory_WMWindow_GPSCoordinator = GPS Koordinator + #LOC_BDArmory_WMWindow_WingCommand = Flügelmann + // WM GPS Module + #LOC_BDArmory_WMWindow_GPSTarget = GPS Ziel + #LOC_BDArmory_WMWindow_NoTarget = Kein Ziel + + // WM infolink + // WM infolink Weapons + #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. + + // WM infolink Guard Menu + #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. + + // WM infolink Target Priority + #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. + + // Settings Window + #LOC_BDArmory_Settings_Title = BDArmory Einstellungen + #LOC_BDArmory_Settings_AdvancedUserSettings = Einstellungen für Fortgeschrittene + // Section Toggles + #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 + + // Graphics / UI + #LOC_BDArmory_Settings_DebugSettingsToggle = Debug-Einstellungen + #LOC_BDArmory_Settings_AIToolbarButton = KI Symbolleisten-Schaltfläche + #LOC_BDArmory_Settings_VMToolbarButton = VM in Knopfleise + #LOC_BDArmory_Settings_UIScale = Anzeigeskalierung + #LOC_BDArmory_Settings_UIScaleFollowsStock = Scalierung folgt UI + #LOC_BDArmory_Settings_Instakill = Sofortige Zerstörung + #LOC_BDArmory_Settings_InfiniteAmmo = Unendliche Munition + #LOC_BDArmory_Settings_InfiniteMissiles = Unendliche Raketen + //#LOC_BDArmory_Settings_InfiniteCountermeasures = ??? Infinite Countermeasures + #LOC_BDArmory_Settings_BulletFX = Schuss-Grafikeffekte + #LOC_BDArmory_Settings_BulletHits = Einschusslöcher + #LOC_BDArmory_Settings_WaterHitFX = Wasser-Effekte + #LOC_BDArmory_Settings_LightFX = Lighteffekte + #LOC_BDArmory_Settings_PerfOptions = Grafikeffekte an + #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_AimAssistMode_Target = Zielhilfe (Ziel) + #LOC_BDArmory_Settings_AimAssistMode_Aimer = Zielhilfe (Vorhalte) + #LOC_BDArmory_Settings_GUIBackgroundOpacity = Fenster-Transparenz + #LOC_BDArmory_Settings_DrawAimers = Ziellinien anzeigen + + // Debugging + #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_ResetScrollZoom = Zoom zurücksetzen + + // Gameplay FIXME These need more sorting + #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_AutoLogTimeSync = Zeitsynchrone Aufzeichnung automatisch starten + #LOC_BDArmory_Settings_LogTimeSyncInterval = Zeitsynchrone Aufzeichnung Intervall + #LOC_BDArmory_Settings_LogTimeSyncStart = Aufzeichnung starten + #LOC_BDArmory_Settings_LogTimeSyncStop = Aufzeichnung beenden + #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_AutoResumeContinuousSpawn = Kontinuierlich Starten 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_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_CompChecks = ??? Use AI/WM Overrides + #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_CameraSwitchIncludeMissiles = Auch auf Raketen umschalten + #LOC_BDArmory_Settings_ScrollZoomPrevention = Mausrad-Zoom verhindern + #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_RestoreKAL = KAL Kontroller zurücksetzen + //#LOC_BDArmory_Settings_DisableGuardModeOnSpawn = ??? Disable Guard Mode on Spawn + #LOC_BDArmory_Settings_IntakeHack = Lufteinlässe funktionieren ohne Sauerstoff + #LOC_BDArmory_Settings_PWingsHack = Prozedurale Flügel-Kanten (Edges) erzeugen Auftrieb + #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_gungame_progression = Gewechselte Waffe bei Neustart beibehalten + #LOC_BDArmory_settings_gungame_cycle = Waffenliste zyklisch durchlaufen + // General Sliders + #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_ExplosiveBattleDamageMultiplier = Kampfschaden-Explosionen + #LOC_BDArmory_Settings_ArmorExplosivePenetrationResistanceMultiplier = Resistenz von Panzerung gegen Explosivschaden (Multiplikator) + #LOC_BDArmory_Settings_BuildingDamageMultiplier = Gebäude-Schaden + #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = Implosionsschaden + #LOC_BDArmory_Settings_SecondaryEffectDuration = Spezialwaffen Schaden-Dauer + #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = Schaden durch ballistische Flugbahn + #LOC_BDArmory_Settings_ArmorMassMultiplier = Panzerung-Masse Multiplikator + #LOC_BDArmory_Settings_DebrisCleanUpDelay = Trümmer entfernen nach + #LOC_BDArmory_Settings_NumericInputSelfUpdate = Zahleneingabe automatisch registrieren nach x Sekunden + #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 limitieren + #LOC_BDArmory_Settings_Max_Armor_Limit = Maximale Panzerung limitieren + + // Game Modes + // Heart-Bleed + #LOC_BDArmory_Settings_HeartBleed = Verschleiß + #LOC_BDArmory_Settings_HeartBleedRate = Verschleiß Rate + #LOC_BDArmory_Settings_HeartBleedInterval = Verschleiß Intervall + #LOC_BDArmory_Settings_HeartBleedThreshold = Verschleiß Grenze (% Leben) + + // Resource Steal + #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 + + // Asteroids + #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 + + // 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_MutatorGungame = Waffenspiel (nach Abschuss austauschen) + #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 + + #LOC_BDArmory_Settings_WaypointsMode = Wegpunkt-Modus + #LOC_BDArmory_Settings_GLimitsMode = Blackout durch G-Kräfte + + // Battle Damage + #LOC_BDArmory_Settings_BDSettingsToggle = Kampfschaden-Einstellungen + #LOC_BDArmory_Settings_BD_Proc = Häufigkeit + //#LOC_BDArmory_Settings_BD_Proc_Pen = ??? Proc Min Penetration + #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_JointStrength = Strukturell + #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 + + // Radar / Other Settings + #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_IGNORE_TERRAIN_CHECK = Radar ignoriert Grund + #LOC_BDArmory_Settings_CHECK_WATER_TERRAIN = Erkennung prüft Wasser + #LOC_BDArmory_Settings_RADAR_NOTCHING = Radar Notching + #LOC_BDArmory_Settings_Notching_Factor = Notch Effektivitäts-Faktor + #LOC_BDArmory_Settings_Notching_SCR_Factor = Notch SCR Faktor + + // Competition / Tournament + #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_CompetitionGMWeaponKill = Fahrz. ohne Waffen entf. + #LOC_BDArmory_Settings_CompetitionGMEngineKill = Fahrz. ohne Antrieb entf. + #LOC_BDArmory_Settings_CompetitionGMDisableKill = Zerstörte Fahrz. entf. + #LOC_BDArmory_Settings_CompetitionGMHPKill = Beschädigte Fahrz. entf. + #LOC_BDArmory_Settings_CompetitionGMKillDelay = Entefernen verzögern + #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_Settings_CompetitionWaypointTimeThreshold = Wegpunkt Gnadenfrist + + // BDA Remote (defunct) + #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 + + // Input Settings + #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_TemporarilyShowMouse = Mausbewegung verlangsamen + #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. + + // Weapon Config + #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_saveClose = Speichern & Schließen + #LOC_BDArmory_reset = Zurücksetzen + #LOC_BDArmory_applyTo = Anwenden an + #LOC_BDArmory_WeaponGroup = Waffengruppen + #LOC_BDArmory_AddToWpnGroup = Zu Waffengruppe hinzufügen: + #LOC_BDArmory_thisWeapon = Diese Waffe + #LOC_BDArmory_SymmetricWeapons = Symmetrische Waffe + + #LOC_BDArmory_CustomFireKey = Benutzerdefinierte Feuer-Taste + #LOC_BDArmory_SetCustomFireKey = Benutzerdefinierte Feuer-Taste einstellen + + #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_MissileCMRange = Gegenmassnahmen Reichweite + #LOC_BDArmory_MissileCMInterval = Gegenmassnahmen Intervall + + // Adjustable Rails + #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 -- + + // Vessel Spawner + #LOC_BDArmory_BDAVesselSpawner_Title = BDA Turnier-Starter + // Spawn Options + #LOC_BDArmory_Settings_SpawnOptions = Start-Einstellungen + #LOC_BDArmory_Settings_SpawnDistanceFactor = Start-Abstand Faktor + #LOC_BDArmory_Settings_SpawnRefHeading = Referenz-Himmelsrichtung + #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_CSFollowsCentroid = Startpunkt folgt Zentroid (KS) + #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = Einzelnes Fahrzeug kontinuierlich starten (S) + #LOC_BDArmory_Settings_SpawnRandomOrder = Zufällige Startreihenfolge (S) + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = Wettkampf automatisch starten + #LOC_BDArmory_Settings_SpawnInitialVelocity = Starten mit Reisegeschwindigkeit + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = Start-Teil hier Starten + #LOC_BDArmory_Settings_OutOfAmmoKillTime = Entfernen wenn Munition leer nach (KS) + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Turnier Start hier + #LOC_BDArmory_Settings_SaveSpawnLoc = Koordinaten Speichern + #LOC_BDArmory_Settings_ClearDebrisNow = Jetzt Trümmer beseitigen + #LOC_BDArmory_Settings_ClearBystandersNow = Jetzt Zuschauer entfernen + // Fill Seats + #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 + + // Teams + #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_Custom_Template = Benutzerdefinierte Vorlage + #LOC_BDArmory_Settings_Teams_SplitEvenly = In Teams aufteilen: + + #LOC_BDArmory_Settings_SpawnFilesLocation = Fahrzeug-Dateien Unterordner + // 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_TemplateSelection = Vorlage auswählen + #LOC_BDArmory_Settings_CustomSpawnTemplate_SaveCraftToTemplate = Flugzeug-URL abspeichern + + // 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 + + // Interesting Spawn Locations + #LOC_BDArmory_Settings_SpawnLocations = Interessante Orte + #LOC_BDArmory_Settings_WarpHere = Zu Startkoordinaten wechseln + #LOC_BDArmory_Settings_Planet = Planeten/Mond auswählen + + // Tournament Options + #LOC_BDArmory_Settings_TournamentOptions = Turnier-Einstellungen + #LOC_BDArmory_Settings_TournamentStyle = Turnier-Stil + #LOC_BDArmory_Settings_TournamentRoundType = Runden-Typ + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = Pause zwischen den Läufen + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = Zeit vorspulen zw. Läufen + //#LOC_BDArmory_Settings_TournamentTimeWarpDaylight = ??? Daylight + #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_TournamentNPCsPerHeat = NSCs Pro Lauf + #LOC_BDArmory_Settings_TournamentSetup = Turnier aufsetzen + #LOC_BDArmory_Settings_TournamentRun = Turnier starten + #LOC_BDArmory_Settings_TournamentStop = Turnier beenden + + // Waypoints + #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 + + #LOC_BDArmory_Settings_SingleSpawn = Einzlne Runde starten + #LOC_BDArmory_Settings_ContinuousSpawning = Kontionuierlich Starten (KS) + #LOC_BDArmory_Settings_CancelSpawning = KS abbrechen + + // Waypoint GUI + #LOC_BDArmory_BDAWaypointBuilder_Title = Wegpunkt-Kurs + #LOC_BDArmory_WP_LoadCourse = Kurs laden + #LOC_BDArmory_WP_NewCourse = Neuer Kurs + #LOC_BDArmory_WP_ChooseCourse = Kurs auswählen + #LOC_BDArmory_WP_Create = Erstellen + #LOC_BDArmory_WP_Record = Aufzeichnen + #LOC_BDArmory_WP_TimeStep = Zeit-Schritt (s) + #LOC_BDArmory_WP_Recording = Nehme Kurs auf.. + #LOC_BDArmory_WP_FinishRecording = Aufzewichnung beenden + #LOC_BDArmory_WP_Spawnpoint = Flugzeug-NamenStartpunkt + #LOC_BDArmory_WP_AddGate = Tor hinzufügen + #LOC_BDArmory_WP_Waypoint = Wegpunkt + #LOC_BDArmory_WP_SpeedLimit = Höchstgeschwindigkeit + #LOC_BDArmory_WP_Increment = Steigerung + #LOC_BDArmory_WP_MaxLaps = Maximae Rundenzahl + #LOC_BDArmory_WP_GuardActivate = Kampfbereitschaft nach + #LOC_BDArmory_WP_CourseDefaults = Standard Kurs-Einstellungen + #LOC_BDArmory_WP_SelectModel = Tor-Grafik auswählen + + // Vessel Mover + #LOC_BDArmory_VesselMover_Title = BDA Vessel Mover + #LOC_BDArmory_VesselMover_VesselSelection = Fahrzeug Auswahl + #LOC_BDArmory_VesselMover_CrewSelection = Besatzung Auswahl + #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_PlaceAfterSpawn = Nach Starten Platzieren + //#LOC_BDArmory_VesselMover_DeconflictVesselName = ??? Deconflict Vessel Name + #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 + #LOC_BDArmory_VesselMover_CloseOnCompetitionStart = Bei Kampfbeginn schließen + + // Craft Browser + #LOC_BDArmory_CraftBrowser_InvalidParts = UNGÜLTIGE TEILE + #LOC_BDArmory_CraftBrowser_UnknownModules = Module + #LOC_BDArmory_CraftBrowser_Clear = Entfernen + #LOC_BDArmory_CraftBrowser_ClearAll = Alle entfernen + #LOC_BDArmory_CraftBrowser_Refresh = Aktualisieren + #LOC_BDArmory_CraftBrowser_Parts = Teile + #LOC_BDArmory_CraftBrowser_Mass = Gewicht + #LOC_BDArmory_CraftBrowser_Version = Version + #LOC_BDArmory_CraftBrowser_Craft = Fahrzeug + #LOC_BDArmory_CraftBrowser_Folder = Speicherort + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnails = Fehlende Vorschauen generieren + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsRecurse = Unterordner einbeziehen + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingFor = Generiere Vorschau für... + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingForCraftIn = Generiere Vorschauen für Fahrzeuge in + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFinished = Vorschauen generiert! + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFailure = Kann Vorschau nicht generieren für + + // Scores + #LOC_BDArmory_BDAScores_Title = Turnier Punkte + #LOC_BDArmory_BDAScores_Weights = Punktgewichtung + #LOC_BDArmory_BDAScores_Round = Runde + #LOC_BDArmory_BDAScores_Heat = Lauf + #LOC_BDArmory_BDAScores_Unlimited = Unlimitiert + #LOC_BDArmory_BDAScores_Score = Punkte + #LOC_BDArmory_BDAScores_Lives = Leben + + // Staging Icons + #LOC_BDArmory_ProtoStageIconInfo_Reloading = Nachladen + #LOC_BDArmory_ProtoStageIconInfo_Overheat = Überhitzung + #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = Keine Munition + //#LOC_BDArmory_ProtoStageIconInfo_CMsOut = ??? CMs Depleted + + // Wing Commander + #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 + + // Vessel Switcher + #LOC_BDArmory_BDAVesselSwitcher_Title = BDA Fahrzeug-Übersicht + + // 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 + + // 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_MissileIFF = Suchrakete erkennt Freund/Feind + #LOC_BDArmory_MissileIFF_enabledText = Freund/Feind-Erkennung an + #LOC_BDArmory_MissileIFF_disabledText = Freund/Feind-Erkennung aus + #LOC_BDArmory_TimeBetweenStages = Zeit zwischen Stufen + #LOC_BDArmory_AI_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 + + // WM (PAW) + // Target Priority + #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 + + // Countermeasures + #LOC_BDArmory_Countermeasure_Settings = Einstellungen Gegenmaßnahmen + #LOC_BDArmory_EvadeThreshold = Ausweichen x Sekunden vor Einschlag + #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_SmokeRepetition = Rauch pro Abwehrsequenzt + #LOC_BDArmory_SmokeInterval = Rauch Intervall + #LOC_BDArmory_SmokeWaitTime = Rauch Abwehrsequenz Pause + #LOC_BDArmory_ChaffFactor = Düppel Effektivität + #LOC_BDArmory_NonGuardModeCMs = Gegenmassnahmen außerhalb Wächter-Modus + + #LOC_BDArmory_IsVIP = Ist VIP? + #LOC_BDArmory_IsVIP_enabledText = Ja + #LOC_BDArmory_IsVIP_disabledText = Nein + //#LOC_BDArmory_WM_IsPrimaryWM = ??? Is Primary + + // AI (PAW) + // Pilot AI + // PID + #LOC_BDArmory_AI_PID = PID Regler + #LOC_BDArmory_AI_SteerPower = Stärke (P) + #LOC_BDArmory_AI_SteerKi = Korrektur (I) + #LOC_BDArmory_AI_SteerDamping = Dämpfung (D) + #LOC_BDArmory_AI_SteerMaxError = Maximaler Steuer-Fehler + + // Dynamic damping + #LOC_BDArmory_AI_DynamicSteerDamping = Dynamische Steuerungs-Dämpfung + #LOC_BDArmory_AI_DynamicDamping = Dynamische Dämpfung + #LOC_BDArmory_AI_DynamicDampingMin = Dämpfung + #LOC_BDArmory_AI_DynamicDampingMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_AI_DynamicDampingFactor = Umschaltfunktion + + // 3-axis damping + //#LOC_BDArmory_AI_3AxisSteerDamping = ??? 3-Axis Steer Damping + + // 3-axis static damping + //#LOC_BDArmory_AI_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + //#LOC_BDArmory_AI_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + //#LOC_BDArmory_AI_SteerDampingRoll = ??? Steer Damping Roll (Dr) + + // 3-axis dynamic damping + #LOC_BDArmory_AI_DynamicDampingPitch = Dynamische Dämpfung Nicken + #LOC_BDArmory_AI_DynamicDampingPitchMin = Dämpfung Nicken + #LOC_BDArmory_AI_DynamicDampingPitchMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_AI_DynamicDampingPitchFactor = N. Umschaltfunktion + #LOC_BDArmory_AI_DynamicDampingYaw = Dynamische Dämpfung Gieren + #LOC_BDArmory_AI_DynamicDampingYawMin = Dämpfung Gieren + #LOC_BDArmory_AI_DynamicDampingYawMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_AI_DynamicDampingYawFactor = G. Umschaltfunktion + #LOC_BDArmory_AI_DynamicDampingRoll = Dynamische Dämpfung Rollen + #LOC_BDArmory_AI_DynamicDampingRollMin = Dämpfung Rollen + #LOC_BDArmory_AI_DynamicDampingRollMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_AI_DynamicDampingRollFactor = R. Umschaltfunktion + + // Auto-tuning + #LOC_BDArmory_AI_PID_AutoTune = PID Automatisch einstellen + #LOC_BDArmory_AI_PID_AutoTuning_Loss = Verlustfunktion + #LOC_BDArmory_AI_PID_AutoTuning_NumSamples = Auto-Einst. Zahl der Messwerte + #LOC_BDArmory_AI_PID_AutoTuning_FastResponseRelevance = Auto-Einst. bevorzugt agiles Flugverhalten + #LOC_BDArmory_AI_PID_AutoTuning_InitialLearningRate = Auto-Einst. anfängliche Lernrate + #LOC_BDArmory_AI_PID_AutoTuning_InitialRollRelevance = Auto-Einst. anfängliche Roll-Relevanz + #LOC_BDArmory_AI_PID_AutoTuning_Speed = Auto-Einst. Geschwindigkeit + #LOC_BDArmory_AI_PID_AutoTuning_Altitude = Auto-Einst. Flughöhe + #LOC_BDArmory_AI_PID_AutoTuning_RecenteringDistance = Auto-Einst. neu zentrieren bei Abstand (km) + #LOC_BDArmory_AI_PID_AutoTuning_FixedP = Auto-Einst. mit fixiertem P + #LOC_BDArmory_AI_PID_AutoTuning_ClampMaximums = Auto-Einst. berücksichtigen Maxima + #LOC_BDArmory_AI_PID_AutoTuning_Summary = Auto-Einst. Report + + // Altitudes + #LOC_BDArmory_AI_Altitudes = Flughöhe (ü.G.) + #LOC_BDArmory_AI_DefaultAltitude = Standardhöhe + #LOC_BDArmory_AI_MinAltitude = Mindesthöhe + #LOC_BDArmory_AI_MaxAltitude = Maximale Höhe + #LOC_BDArmory_AI_HardMinAltitude = Harte Mindesthöhe + #LOC_BDArmory_AI_BombingAltitude = Bombenabwurfhöhe + #LOC_BDArmory_AI_DiveBombing = Sturzbombadierung aktivieren + + // Speeds + #LOC_BDArmory_AI_Speeds = Geschwindigkeit + #LOC_BDArmory_AI_MaxSpeed = Höchstgeschwindigkeit + #LOC_BDArmory_AI_TakeOffSpeed = Abhebegeschwindigkeit + #LOC_BDArmory_AI_MinSpeed = Min. Geschw. im Kampf + #LOC_BDArmory_AI_StrafingSpeed = Bodenangriff-Geschw. + #LOC_BDArmory_AI_IdleSpeed = Reisegeschwindigkeit + #LOC_BDArmory_AI_ABPriority = Nachbrenner-Priorität + #LOC_BDArmory_AI_ABOverrideThreshold = Nachbrenner an wenn langsamer als + #LOC_BDArmory_AI_BrakingPriority = Brems-Priorität + + // Control + #LOC_BDArmory_AI_ControlLimits = Kontroll-Limits + #LOC_BDArmory_AI_SteerLimiter = Steuerung einschränken + #LOC_BDArmory_AI_LowSpeedSteerLimiter = Faktor bei niedriger Geschw. + #LOC_BDArmory_AI_LowSpeedLimiterSpeed = Niedrige Geschw. = + #LOC_BDArmory_AI_HighSpeedSteerLimiter = Faktor bei hoher Geschw. + #LOC_BDArmory_AI_HighSpeedLimiterSpeed = Hohe Geschw. = + #LOC_BDArmory_AI_AltitudeSteerLimiterFactor = Faktor bei Großer Höhe + #LOC_BDArmory_AI_AltitudeSteerLimiterAltitude = Große Höhe = + #LOC_BDArmory_AI_AttitudeLimiter = Lagekontrolle Limit + #LOC_BDArmory_AI_BankLimiter = Neigungswinkel Limit + #LOC_BDArmory_AI_WaypointPreRollTime = Neigung vor Wegpunkt (s) + #LOC_BDArmory_AI_WaypointYawAuthorityTime = Gieren am Wegpunkt (s) + #LOC_BDArmory_AI_MaxAllowedGForce = Maximale G-Kraft + #LOC_BDArmory_AI_MaxAllowedAoA = Maximaler Anstellwinkel + #LOC_BDArmory_AI_PostStallAoA = Post-Stall (Überziehen) Anstellwinkel + #LOC_BDArmory_AI_ImmelmannTurnAngle = Winkel f. Immelmann-Wende + #LOC_BDArmory_AI_ImmelmannPitchUpBias = Immelmann Hochzieh-Tendenz + + // Evade / Extend + #LOC_BDArmory_AI_EvadeExtend = Ausweichen / Abstand Gewinnen + #LOC_BDArmory_AI_ExtendMultiplier = Abstand gewinnen (Faktor *300m) + #LOC_BDArmory_AI_ExtendDistanceAirToAir = Abstand im Luftkampf (m) + #LOC_BDArmory_AI_ExtendAngleAirToAir = Ziel-Winkel im Luftkampf + #LOC_BDArmory_AI_ExtendDistanceAirToGroundGuns = Abstand bei Bodenangriff mit Kanonen (m) + #LOC_BDArmory_AI_ExtendDistanceAirToGround = Abstand bei Bodenangriff (m) + #LOC_BDArmory_AI_ExtendTargetVel = Ziel-Geschwindigkeits-Faktor + #LOC_BDArmory_AI_ExtendTargetAngle = Ziel-Winkel + #LOC_BDArmory_AI_ExtendTargetDist = Angestrebter Ziel-Abstand + #LOC_BDArmory_AI_ExtendAbortTime = Abstand Gewinnen abbrechen nach x Sekunden + #LOC_BDArmory_AI_ExtendMinGainRate = Abst. Gew. min. rel. Geschw. + #LOC_BDArmory_AI_ExtendToggle = Abstand Gewinnen an/aus + #LOC_BDArmory_AI_MinEvasionTime = Ausweichen min. Zeit + #LOC_BDArmory_AI_EvasionNonlinearity = Haken Schlagen (Faktor) + #LOC_BDArmory_AI_EvasionThreshold = Gegner zielt auf mich (Radius) + #LOC_BDArmory_AI_EvasionErraticness = Unberechenbarkeit bei Ausw. m. RKS + #LOC_BDArmory_AI_EvasionTimeThreshold = Gegner zielt auf mich (Zeit) + #LOC_BDArmory_AI_EvasionMinRangeThreshold = Ausweichen minimale Entfernung + #LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe = Meinem Ziel nicht ausweichen + #LOC_BDArmory_AI_EvasionMissileKinematic = Kinematisches Ausweichen bei Raketen + #LOC_BDArmory_AI_CollisionAvoidanceThreshold = Bei Passage unter X m ausweichen + #LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod = X Sekunden vorausberechnen + #LOC_BDArmory_AI_CollisionAvoidanceStrength = Ausweich-Stärke (*50°) + #LOC_BDArmory_AI_StandoffDistance = Sicherheitsabstand + + // Terrain + #LOC_BDArmory_AI_Terrain = Terrainvermeidung + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMin = Flugzeug Wendekreis Faktor (min) + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMax = Flugzeug Wendekreis Faktor (max) + #LOC_BDArmory_AI_TerrainAvoidanceCriticalAngle = Rückenflug-Terrainvermeidung kritischer Winkel + #LOC_BDArmory_AI_TerrainAvoidanceVesselReactionTime = Flugzeug Reaktionszeit + #LOC_BDArmory_AI_TerrainAvoidancePostAvoidanceCoolDown = Pause nach Terrainvermeidung + #LOC_BDArmory_AI_WaypointTerrainAvoidance = Terrainvermeidung am Wegpunkt + + // Ramming + #LOC_BDArmory_AI_Ramming = Rammen + #LOC_BDArmory_AI_ControlSurfaceLag = Steuerverzögerung beim Rammen + #LOC_BDArmory_AI_AllowRamming = Rammen aktivieren + #LOC_BDArmory_AI_AllowRammingGroundTargets = Auch Bodenziele + + // Ejection (unused) + #LOC_BDArmory_AI_Ejection = Schleudersitz + #LOC_BDArmory_AI_EjectOnImpendingDoom = Auswerfen, wenn dem Untergang geweiht + + #LOC_BDArmory_AI_SliderResolution = Auflösung der Schieberegler + // Idle / Orbit Behavior + #LOC_BDArmory_AI_Orbit = Richtung beim Kreisen\u0020 + #LOC_BDArmory_AI_Orbit_Starboard = Uhrzeigersinn + #LOC_BDArmory_AI_Orbit_Port = Gegen den Uhrzeigersinn + #LOC_BDArmory_AI_Orbit_Random = Zufall + #LOC_BDArmory_AI_Standby = Bereitschaftsmodus + + // Up-to-eleven + #LOC_BDArmory_AI_UnclampTuning = Limits entfernen + #LOC_BDArmory_AI_UnclampTuning_enabledText = Unlimitiert + #LOC_BDArmory_AI_UnclampTuning_disabledText = Limitiert + + // Surface / VTOL / Orbital AI + #LOC_BDArmory_AI_VehicleType = Art der Kampfeinheit + #LOC_BDArmory_AI_MaxSlopeAngle = Maximaler Steigungswinkel + #LOC_BDArmory_AI_CruiseSpeed = Reisegeschwindigkeit + #LOC_BDArmory_AI_CombatSpeed = Kampfgeschwindigkeit + #LOC_BDArmory_AI_CombatAltitude = Kampf-Höhe + #LOC_BDArmory_AI_TargetPitch = Rückwärtsneigung + #LOC_BDArmory_AI_MaxDrift = Maximale Drift + #LOC_BDArmory_AI_MaxPitchAngle = Max. Rückwärtsneigung + #LOC_BDArmory_AI_BankAngle = Seitlich Neigung + #LOC_BDArmory_AI_WeaveFactor = Haken Schlagen (Faktor) + #LOC_BDArmory_AI_MaxBankAngle = Max. seitliche Neigung + #LOC_BDArmory_AI_BroadsideAttack = Angriffsvektor + #LOC_BDArmory_AI_BroadsideAttack_enabledText = Breitseite + #LOC_BDArmory_AI_BroadsideAttack_disabledText = Bug + #LOC_BDArmory_AI_MinEngagementRange = Min. Angriffsabstand + #LOC_BDArmory_AI_MaxEngagementRange = Max. Angriffsabstand + #LOC_BDArmory_AI_ForceFiringRange = Abstandsbereich für Anfgiff ohne Schub + #LOC_BDArmory_AI_MaintainEngagementRange = Min. Angriffsabstand beibehalten + #LOC_BDArmory_AI_ManeuverRCS = RKS Aktiv + #LOC_BDArmory_AI_ManeuverRCS_enabledText = Manöver + #LOC_BDArmory_AI_ManeuverRCS_disabledText = Kampf + #LOC_BDArmory_AI_FiringRCS = RKS während Feuern benutzen + #LOC_BDArmory_AI_FiringRCS_enabledText = Geschwindigkeit regeln + #LOC_BDArmory_AI_FiringRCS_disabledText = Nur Maneuver + #LOC_BDArmory_AI_ReverseEngines = Antrieb umkehren + #LOC_BDArmory_AI_EngineRCSRotation = Antrieb für Rotation verwenden + #LOC_BDArmory_AI_EngineRCSTranslation = Antrieb für Translation verwenden + #LOC_BDArmory_AI_OrbitalPIDActive = PID Regler aktiv + #LOC_BDArmory_AI_RollMode = Breitseite Roll-Modus + #LOC_BDArmory_AI_MinObstacleMass = Min. Hindernis-Masse + #LOC_BDArmory_AI_PreferredBroadsideDirection = Bevorzugte Breitseite + #LOC_BDArmory_AI_GoesUp = Skala geht bis + #LOC_BDArmory_AI_GoesUp_enabledText = Elf + #LOC_BDArmory_AI_GoesUp_disabledText = Zehn + #LOC_BDArmory_AI_ManeuverSpeed = Manöver-Geschwindigkeit + #LOC_BDArmory_AI_FiringSpeedMin = Min. Geschwind. zum Feuern + #LOC_BDArmory_AI_FiringSpeedLimit = Max. Geschwind. zum Feuern + #LOC_BDArmory_AI_AngularSpeedLimit = Winkelgeschwindigkeit Limit + #LOC_BDArmory_AI_EvasionRCS = Ausweichen mit RKS + #LOC_BDArmory_AI_EvasionEngines = Ausweichen mit Antrieb + + // AI GUI + #LOC_BDArmory_AIWindow_title = KI Autopilot + #LOC_BDArmory_AIWindow_infoLink = Infolink + #LOC_BDArmory_AIWindow_NoAI = Keine KI am Fahrzeug! + // Sections + #LOC_BDArmory_AIWindow_PID = PID Regler + #LOC_BDArmory_AIWindow_Altitudes = Höhe + #LOC_BDArmory_AIWindow_Speeds = Geschwindigkeit + #LOC_BDArmory_AIWindow_Control = Kontrolle + #LOC_BDArmory_AIWindow_EvadeExtend = Ausweichen/Abstand-Gewinnen + #LOC_BDArmory_AIWindow_Terrain = Terrain + #LOC_BDArmory_AIWindow_Ramming = Rammen + #LOC_BDArmory_AIWindow_Combat = Kampf + #LOC_BDArmory_AIWindow_Misc = Diverses + + // Panel + // Pilot + // PID + #LOC_BDArmory_AIWindow_SteerPower = Stärke der Kontrolleingabe (Faktor) + #LOC_BDArmory_AIWindow_SteerPower_ContextLow = <-- Träge + #LOC_BDArmory_AIWindow_SteerPower_ContextHigh = Nervös --> + #LOC_BDArmory_AIWindow_SteerKi = Korrektur (I) + #LOC_BDArmory_AIWindow_SteerKi_ContextLow = <-- Zu wenig Vorhalte + #LOC_BDArmory_AIWindow_SteerKi_ContextHigh = Zu viel Vorhalte --> + #LOC_BDArmory_AIWindow_SteerDamping = Dämpfung (D) + #LOC_BDArmory_AIWindow_SteerDamping_ContextLow = <-- Wackelig + #LOC_BDArmory_AIWindow_SteerDamping_ContextHigh = Steif --> + //#LOC_BDArmory_AIWindow_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextLow = <-- Wackelig + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextHigh = Steif --> + //#LOC_BDArmory_AIWindow_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextLow = <-- Wackelig + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextHigh = Steif --> + //#LOC_BDArmory_AIWindow_SteerDampingRoll = ??? Steer Damping Roll (Dr) + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextLow = <-- Wackelig + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextHigh = Steif --> + #LOC_BDArmory_AIWindow_SteerMaxError = Maximale Abweichung + #LOC_BDArmory_AIWindow_SteerMaxError_ContextLow = Träge und einfach + #LOC_BDArmory_AIWindow_SteerMaxError_ContextHigh = Schnell und hart + #LOC_BDArmory_AIWindow_DynDampMin = Generlle Dämpfung + #LOC_BDArmory_AIWindow_DynDampMin_Context = Dampfung Minumum + #LOC_BDArmory_AIWindow_DynDampMax = Auf-Ziel Dämpfung + #LOC_BDArmory_AIWindow_DynDampMax_Context = Dämpfung Maximum + #LOC_BDArmory_AIWindow_DynDampMult = Generell/Auf-Ziel Transition + #LOC_BDArmory_AIWindow_DynDampMult_Context = Nichtlinearität der Umschaltfunktion + + // Auto-tuning + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = Auto Einst. (A-E) #Messwerte + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextLow = <-- Ungenauer + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextHigh = Genauer -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = A-E Agiles Flugverhalten + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextLow = <- Bessere Dämpfung + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextHigh = Agiler -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = A-E Initiale Lernrate + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate_Context = Diesen Wert verringern, wenn die PID-Änderungen zu groß sind + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance = Auto-Einstel. Initiale Rollrate + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance_Context = Wie viel Roll-Abweichung zum gemessenen Fehler beiträgt + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = A-E Geschwindigkeit + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed_Context = Ziel-Geschwindigkeit + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = A-E Flughöhe + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude_Context = Versuchen ±Mindestflughöhe um diesen Wert zu bleiben + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance = A-E Zentrieren bei Entfernung + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance_Context = Entfernung vom Startpunkt, bei der das Flugzeug neu zentriert wird + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = A-E Variablen fiexieren... + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = A-E Maxima berücksichtigen + // PID fixed fields + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P = ??? P + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I = ??? I + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D = ??? D + // 3-axis damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch = ??? Dp + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw = ??? Dy + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll = ??? Dr + // Dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OffTarget = ??? DOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OnTarget = ??? DOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_Factor = ??? DF + // 3-axis dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OffTarget = ??? DpOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OnTarget = ??? DpOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_Factor = ??? DpF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OffTarget = ??? DyOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OnTarget = ??? DyOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_Factor = ??? DyF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OffTarget = ??? DrOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OnTarget = ??? DrOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_Factor = ??? DrF + + // Altitudes + #LOC_BDArmory_AIWindow_DefaultAltitude = Standard-Flughöhe + #LOC_BDArmory_AIWindow_DefaultAltitude_Context = Flughöhe in Ruhe + #LOC_BDArmory_AIWindow_MinAltitude = Minimale Flughöhe + #LOC_BDArmory_AIWindow_MinAltitude_Context = KI versucht, über dieser Höhe zu bleiben + #LOC_BDArmory_AIWindow_MaxAltitude = Max. Höhe (ü. Grund) + #LOC_BDArmory_AIWindow_MaxAltitude_Context = KI versucht, unter dieser Höhe zu bleiben + #LOC_BDArmory_AIWindow_BombingAltitude = Bombenabwurfhöhe + #LOC_BDArmory_AIWindow_BombingAltitude_Context = KI versucht, Bomben bei dieser Flughöhe abzuwerfen + #LOC_BDArmory_AIWindow_DiveBomb = Surzbombadierung aktivieren + + // Speeds + #LOC_BDArmory_AIWindow_MaxSpeed = #LOC_BDArmory_AI_MaxSpeed + #LOC_BDArmory_AIWindow_MaxSpeed_Context = Maximale Kampfgeschwindigkeit + #LOC_BDArmory_AIWindow_TakeOffSpeed = #LOC_BDArmory_AI_TakeOffSpeed + #LOC_BDArmory_AIWindow_TakeOffSpeed_Context = KI zieht bei dieser Geschwindigkeit die Nase hoch + #LOC_BDArmory_AIWindow_MinSpeed = #LOC_BDArmory_AI_MinSpeed + #LOC_BDArmory_AIWindow_MinSpeed_Context = KI versucht über dieser Geschwindigkeit zu bleiben + #LOC_BDArmory_AIWindow_StrafingSpeed = #LOC_BDArmory_AI_StrafingSpeed + #LOC_BDArmory_AIWindow_StrafingSpeed_Context = + #LOC_BDArmory_AIWindow_IdleSpeed = #LOC_BDArmory_AI_IdleSpeed + #LOC_BDArmory_AIWindow_IdleSpeed_Context = Reisegeschwindigkeit (nicht im Kampf) + #LOC_BDArmory_AIWindow_ABPriority = #LOC_BDArmory_AI_ABPriority + #LOC_BDArmory_AIWindow_ABPriority_Context = Häufigkeit der Nachbrenner-Nutzung verändern + #LOC_BDArmory_AIWindow_ABOverrideThreshold = Nachbrenner an wenn langsamer als + #LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = Nachbrenner aktivieren wenn Flugzeug langsamer als + #LOC_BDArmory_AIWindow_BrakingPriority = #LOC_BDArmory_AI_BrakingPriority + #LOC_BDArmory_AIWindow_BrakingPriority_Context = Bevorzugt Bremen zum Verlangsamen benutzen falls erlaubt + + // Control + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = Faktor bei niedriger Geschw. + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter_Context = Kontroll-Limit wenn langsamer als "Niedrige Geschwindigkeit" + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = Niedrige Geschw. = + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed_Context = Niedrige Geschwindigkeit: + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = Faktor bei hoher Geschw. + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter_Context = Kontroll-Limit wenn schneller als "Hohe Geschwindigkeit" + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = Hohe Geschw. = + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed_Context = Hohe Geschwindigkeit: + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Große Höhe Steuerfaktor + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor_Context = Kontroll-Limit abhängig von der Flughöhe + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Große Höhe + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude_Context = Höhe, ab der die Kontrolle verringert wird + #LOC_BDArmory_AIWindow_BankLimiter = Neigungswinkel Limit + #LOC_BDArmory_AIWindow_BankLimiter_Context = <- weniger Neigungswinkel Limit mehr -> + #LOC_BDArmory_AIWindow_MaxAllowedGForce = Maximale G-Kräfte + #LOC_BDArmory_AIWindow_MaxAllowedGForce_Context = KI versucht, dieses G-Limit nicht zu überschreiten + #LOC_BDArmory_AIWindow_MaxAllowedAoA = Maximaler Anstellwinkel + #LOC_BDArmory_AIWindow_MaxAllowedAoA_Context = KI versucht, diesen Anstellwinkel nicht zu überschreiten + #LOC_BDArmory_AIWindow_WaypointPreRollTime = Neigung vor Wegpunkt (s) + #LOC_BDArmory_AIWindow_WaypointPreRollTime_Context = Rollen vor Erreichen eines Wegpunktes + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = Stärker gieren am Wegpunkt (s) + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime_Context = Stärkeres Gieren beim Erreichen eines Wegpunktes + #LOC_BDArmory_AIWindow_PostStallAoA = Post-Stall Anstellwinkel + #LOC_BDArmory_AIWindow_PostStallAoA_Context = Post-Stall Flugmodus wenn Anstellwinkel höher als + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle = Immelmann-Wende Winkel + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle_Context = Fahrzeug benutzt nur Nicken (Pitch) um auf das Ziel zu schwenken innerhalb dieses Konus + #LOC_BDArmory_AIWindow_ImmelmannPitchUpBias = Immelmann Hochzieh-Tendenz + #LOC_BDArmory_AIWindow_ImmelmannPitchUpBias_Context = < weniger — Hochzieh-Tendenz — mehr > + + // Evade / Extend + #LOC_BDArmory_AIWindow_Evade = Ausweichen + #LOC_BDArmory_AIWindow_MinEvasionTime = #LOC_BDArmory_AI_MinEvasionTime + #LOC_BDArmory_AIWindow_MinEvasionTime_Context = Minimale Zeit, die die KI einem Schuss ausweicht + #LOC_BDArmory_AIWindow_EvasionThreshold = Entfernung + #LOC_BDArmory_AIWindow_EvasionThreshold_Context = Ausweichen beginnt, wenn Angreifer innerhalb dieses Radius zielt + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Zeit + #LOC_BDArmory_AIWindow_EvasionTimeThreshold_Context = Ausweichen beginnt, wenn länger als diese Zeit im Schussfeld + #LOC_BDArmory_AIWindow_EvasionMinRangeThreshold = Minimaler Abstand + #LOC_BDArmory_AIWindow_EvasionMinRangeThreshold_Context = Nur ausweichen, wenn Abstand zum Angreifer weiter ist + #LOC_BDArmory_AIWindow_EvasionNonlinearity = Nichtlinearität + #LOC_BDArmory_AIWindow_EvasionNonlinearity_Context = Stärke des Haken-Schlagens beim Ausweichen + + #LOC_BDArmory_AIWindow_Avoidance = Kollision vermeiden + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Zusammenstoß-Abstand + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold_Context = Anderen Flugzeugen ausweichen, wenn sie näher kommen als dies + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Zusammenstoß-Vorausberechnung + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod_Context = KI berechnet Flugbahnen voraus für diesen Zeitraum + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Ausweich-Stärke + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength_Context = Stärke des Ausweichmanövers + #LOC_BDArmory_AIWindow_StandoffDistance = Sicherheitsabstand + #LOC_BDArmory_AIWindow_StandoffDistance_Context = KI versucht Gegner ins Schussfeld zu bringen wenn Abstand größer also dies + + #LOC_BDArmory_AIWindow_Extend = Abstand-Gewinnen + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Abst. gew. Luft-Luft + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = KI versucht, diesen Abstand zu erreichen + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = Abst. gew. Winkel Luft-Luft + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = Steigflug-Winkel beim Abstand-Gewinnen + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Abst. gew. Luft-Boden (Kanone) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = Abstand gewinnen bei Bodenangriff mit Kanonen + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Abst. gew. Luft-Boden (Bomben) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = Abstand gewinnen bei Bodenangriff mit Bomben + #LOC_BDArmory_AIWindow_ExtendTargetVel = Abst. gew. Geschw. Faktor + #LOC_BDArmory_AIWindow_ExtendTargetVel_Context = Gegner zu langsam, um ihn aus akt. Position ins Schussfeld zu bringen + #LOC_BDArmory_AIWindow_ExtendTargetAngle = Winkel zum Ziel + #LOC_BDArmory_AIWindow_ExtendTargetAngle_Context = Winkel, ab dem der Gegner nicht ins Schussfeld genommen werden kann + #LOC_BDArmory_AIWindow_ExtendTargetDist = Entfernung zum Ziel + #LOC_BDArmory_AIWindow_ExtendTargetDist_Context = Abstand, bei dem der Gegner zu nah ist, um ihn ins Schussfeld zu bringen + #LOC_BDArmory_AIWindow_ExtendAbortTime = Abst. gew. Abbr. nach x Sek. + #LOC_BDArmory_AIWindow_ExtendAbortTime_Context = Abbrechen nach x Sekunden wenn Abstand nicht hergestellt werden kann + #LOC_BDArmory_AIWindow_ExtendMinGainRate = Abst. Gew. min. rel. Geschw. + #LOC_BDArmory_AIWindow_ExtendMinGainRate_Context = Minimale relative Geschwindigkeit, mit der Abstand gewonnen wird + + // Terrain + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin = Flugzeug Wendekreis Faktor (min) + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin_Context = Wendekreis-Faktor, wenn Flugzeug dem Boden ausweichen kann, ohne zu rollen + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax = Flugzeug Wendekreis Faktor (max) + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax_Context = Wendekreis-Faktor, wenn Flugzeug beim Ausweichen auch rollen muss + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle = Kritischer Winkel f. Ausweichen während invertiertem Flug + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle_Context = Krit. W. f. Terrainverm.: Erst rollen oder invertiert ausw. + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime = Flugzeug Reaktionszeit + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime_Context = Geschätze Verzögerung bis Terrainvermeidung ausgefgührt wird + #LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown = Verzögerung für erneutes Ausweichen + #LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown_Context = Zeit, nach der Ausweichen wieder aktiviert werden kann + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = Wegpunkt-Terrainvermeidung + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance_Context = Bereich und Stärke der Wegpunkt-Terrainvermeidung + + // Ramming + #LOC_BDArmory_AIWindow_ControlSurfaceLag = Steuerverzögerung + #LOC_BDArmory_AIWindow_ControlSurfaceLag_Context = Ramm-Trajektorie optimieren mit Steuerverzögerung + // Combat + // Up-to-eleven + // Idle / Orbit Behavior + #LOC_BDArmory_AIWindow_Orbit_Context = Richtung beim Kreisen + #LOC_BDArmory_AIWindow_Standby_Context = KI übernimmt, wenn Ziel in den Überwachungskreis einfliegt + + // Surface / VTOL / Orbital + #LOC_BDArmory_AIWindow_VehicleType = #LOC_BDArmory_AI_VehicleType + #LOC_BDArmory_AIWindow_VehicleType_Context = Die Kampfeinheit operiert auf diesen Terrains + #LOC_BDArmory_AIWindow_MaxSlopeAngle = #LOC_BDArmory_AI_MaxSlopeAngle + #LOC_BDArmory_AIWindow_MaxSlopeAngle_Context = Maximaler Steigungswinkel des Fahrzeugs + #LOC_BDArmory_AIWindow_CruiseSpeed = #LOC_BDArmory_AI_CruiseSpeed + #LOC_BDArmory_AIWindow_CruiseSpeed_Context = #LOC_BDArmory_AIWindow_IdleSpeed_Context + #LOC_BDArmory_AIWindow_CombatSpeed = #LOC_BDArmory_AI_CombatSpeed + #LOC_BDArmory_AIWindow_CombatSpeed_Context = <- weniger - Zielgeschw. beim Kampf - mehr -> + #LOC_BDArmory_AIWindow_CombatAltitude = #LOC_BDArmory_AI_CombatAltitude + #LOC_BDArmory_AIWindow_CombatAltitude_Context = Kampf-Höhe/Tiefe + #LOC_BDArmory_AIWindow_TargetPitch = #LOC_BDArmory_AI_TargetPitch + #LOC_BDArmory_AIWindow_TargetPitch_Context = Bevorzugte Rückwärtsneigung + #LOC_BDArmory_AIWindow_MaxDrift = #LOC_BDArmory_AI_MaxDrift + #LOC_BDArmory_AIWindow_MaxDrift_Context = Maximaler Drift-Winkel bei Kurvenfahrt + #LOC_BDArmory_AIWindow_MaxPitchAngle = #LOC_BDArmory_AI_MaxPitchAngle + #LOC_BDArmory_AIWindow_MaxPitchAngle_Context = <- weniger - Maximale Rückwärtsneigung - mehr -> + #LOC_BDArmory_AIWindow_BankAngle = #LOC_BDArmory_AI_BankAngle + #LOC_BDArmory_AIWindow_BankAngle_Context = #LOC_BDArmory_AIWindow_BankLimiter_Context + #LOC_BDArmory_AIWindow_WeaveFactor = #LOC_BDArmory_AI_WeaveFactor + #LOC_BDArmory_AIWindow_WeaveFactor_Context = Stärke d. Haken-Schlagens unter Feuer + #LOC_BDArmory_AIWindow_MaxBankAngle = #LOC_BDArmory_AI_MaxBankAngle + #LOC_BDArmory_AIWindow_MaxBankAngle_Context = <- weniger - max Rollwinkel beim Auto-Einst. - mehr -> + #LOC_BDArmory_AIWindow_BroadsideAttack = #LOC_BDArmory_AI_BroadsideAttack + #LOC_BDArmory_AIWindow_BroadsideAttack_Context = Orientierung der Kampfeinheit zum Ziel beim Angriff + #LOC_BDArmory_AIWindow_MinEngagementRange = #LOC_BDArmory_AI_MinEngagementRange + #LOC_BDArmory_AIWindow_MinEngagementRange_Context = Minimaler Abstand, unter dem die KI nicht angreifen wird + #LOC_BDArmory_AIWindow_MaxEngagementRange = #LOC_BDArmory_AI_MaxEngagementRange + #LOC_BDArmory_AIWindow_MaxEngagementRange_Context = Maximaler Abstand, oberhalb dessen die KI nicht angreifen wird + #LOC_BDArmory_AIWindow_ForceFiringRange = #LOC_BDArmory_AI_ForceFiringRange + #LOC_BDArmory_AIWindow_ForceFiringRange_Context = Innerhalb dieses Abstandbereiches Feuert to KI ohne Schub + #LOC_BDArmory_AIWindow_MaintainEngagementRange = #LOC_BDArmory_AI_MaintainEngagementRange + #LOC_BDArmory_AIWindow_MaintainEngagementRange_Context = Das Fahrzeug stoppt / setzt zurück wenn das Ziel näher ist + #LOC_BDArmory_AIWindow_ManeuverRCS = #LOC_BDArmory_AI_ManeuverRCS + #LOC_BDArmory_AIWindow_ManeuverRCS_Context = RKS benutzen + #LOC_BDArmory_AIWindow_FiringRCS = #LOC_BDArmory_AI_FiringRCS + #LOC_BDArmory_AIWindow_FiringRCS_Context = RKS benutzen, um die Geschwindigkeit beim Feuern zu regeln + #LOC_BDArmory_AIWindow_ReverseEngines = #LOC_BDArmory_AI_ReverseEngines + #LOC_BDArmory_AIWindow_ReverseEngines_Context = Nach vorn zeigende Raketenmotoren zum Bremsen verwenden + #LOC_BDArmory_AIWindow_EngineRCSRotation = #LOC_BDArmory_AI_EngineRCSRotation + #LOC_BDArmory_AIWindow_EngineRCSRotation_Context = Raketenmotoren, die nicht auf Achse montiert sind, zur Lagesteuerung verwenden + #LOC_BDArmory_AIWindow_EngineRCSTranslation = #LOC_BDArmory_AI_EngineRCSTranslation + #LOC_BDArmory_AIWindow_EngineRCSTranslation_Context = Raketenmotoren, die Senkrecht auf der longitudinalen Achse stehen, für Translation verwenden + #LOC_BDArmory_AIWindow_OrbitalPIDActive = #LOC_BDArmory_AI_OrbitalPIDActive + #LOC_BDArmory_AIWindow_OrbitalPIDActive_Context = Konditionen für das Aktivieren des PID Reglers + #LOC_BDArmory_AIWindow_RollMode = #LOC_BDArmory_AI_RollMode + #LOC_BDArmory_AIWindow_RollMode_Context = Wenn PID Regler aktiv ist, soll die KI rollen, um diese Seite des Schiffes auf das Ziel zu richten + #LOC_BDArmory_AIWindow_MinObstacleMass = #LOC_BDArmory_AI_MinObstacleMass + #LOC_BDArmory_AIWindow_MinObstacleMass_Context = Minimale Masse, ab der ein Hindernis umfahren wird + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection = #LOC_BDArmory_AI_PreferredBroadsideDirection + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context = Bevorzugte Seite, die die Kampfeinheit zum Gegner richten soll + #LOC_BDArmory_AIWindow_ManeuverSpeed = #LOC_BDArmory_AI_ManeuverSpeed + #LOC_BDArmory_AIWindow_ManeuverSpeed_Context = Relative Geschwindigkeit zum Ziel + #LOC_BDArmory_AIWindow_minFiringSpeed = #LOC_BDArmory_AI_FiringSpeedMin + #LOC_BDArmory_AIWindow_minFiringSpeed_Context = Relative Geschwindigkeit zum Ziel beim Feuern + #LOC_BDArmory_AIWindow_FiringSpeed = #LOC_BDArmory_AI_FiringSpeedLimit + #LOC_BDArmory_AIWindow_FiringSpeed_Context = Maximale relative Geschwindigkeit zum Ziel beim Feuern + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit = #LOC_BDArmory_AI_AngularSpeedLimit + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit_Context = Maximale Winkelgeschwindigkeit relativ zum Ziel beim Feuern + #LOC_BDArmory_AIWindow_EvasionErraticness = #LOC_BDArmory_AI_EvasionErraticness + #LOC_BDArmory_AIWindow_EvasionErraticness_Context = Unberechenbarkeit bei Ausweichen (Nichtlinearität) + #LOC_BDArmory_AIWindow_EvasionRCS = #LOC_BDArmory_AI_EvasionRCS + #LOC_BDArmory_AIWindow_EvasionRCS_Context = RKS zum Ausweichen von gegnerischem Feuer verwenden + #LOC_BDArmory_AIWindow_EvasionEngines = #LOC_BDArmory_AI_EvasionEngines + #LOC_BDArmory_AIWindow_EvasionEngines_Context = Antrieb zum Ausweichen von gegnerischem Feuer verwenden + + + // AI infolink + // Pilot AI + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_PidHelp_SteerPower = 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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_AltHelp = Diese Einstellungen bestimmen die Flughöhe, die die KI in verschiedenen Situationen anstrebt. + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_Def = Standardhöhe - Dies ist die Höhe über Grund, die die KI anstrebt, wenn kein Luftkampf stattfindet. + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_min = Mindesthöhe - Bei unterschreiten diese Höhe über Grund geht das Flugzeug in den Steigflug über. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_AltHelp_bombing = Bombenabwurf-Flughöhe - Die KI wird beim Bombenabwurf versuchen, Horizontalflug auf dieser Höhe beizubehalten + + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp = Diese Einstellungen bestimmen die Flug-Geschwindigkeit, die die KI in verschiedenen Situationen anstrebt. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_SpeedHelp_idle = Reisegeschwindigkeit - Definiert die Geschwindigkeit, die die KI vor dem Luftkampf beim Kreisen oder beim Fliegen zur Startposition anstrebt. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_SpeedHelp_ABpriority = Nachbrenner-Priorität - Bestimmt, ab welcher benötigten Beschleunigung die KI den Nachbrenner aktiviert. + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABOverrideThreshold = Geschwindigkeit, bei deren Unterschreiten der Nachbrenner aktiviert wird, wenn bereits maximaler Schub anliegt. + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_BrakingPriority = Brems-Priorität - Bestimmt, wie stark ti KI die Bremsen verwenden wird, um die Geschwindigkeit zu verringern, falls erlaubt. + + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp = Kontroll-Limits bestimmen die Stärke von Steuereingaben unter verschiedenen Bedingungen und Höchstwerte für Neigungswinkel und G-Kraft. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_ControlHelp_modeSwitches = Manöver nach Strömungsabriss - Anstellwinkel, ab dem der Steuermodus umgeschaltet wird. Ist das Ziel innerhalb des Immelmann-Konus und hinnter dem Flugzeug, dann wird das Flugzeug nicken (pitch) und nicht rollen. + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_Immelmann = Immelmann-Wende Winkel - Beim Flug zu einem Ziel, dass sich innerhalb der Immelmann-Wende Winkels befindet, wird das Flugzeug hochziehen/abfallen, um das Ziel anzupeilen, ohne erst zu rollen. In der Nähe der Minimalen Flughöhe bestimmt die Immelmann-Hochzieh-Neigung, dass das Flugzeug hochzieht (oder bei umgekehrtem Flug abfällt), um Höhe zu gewinnen, ohne vorher zu rollen. + + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_EvadeHelp_standoff = Sicherheitsabstand - Bestimmt den Mindestabstand, unterhalb dessen die KI den Angriff abbrechen und Abstand gewinnen wird. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_EvadeHelp_ExtendAngle = Winkel zum Ziel - Bestimmt den Winkel zum Ziel, bei dessen Überschreiten die KI Abstand gewinnen wird. + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendDist = Ziel-Abstand - Bestimmt den Abstand, bei dessen Unterschreiten die KI versuchen wird, Abstand zu gewinnen. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_miscHelp = Misc Settings - These control non-combat behaviors that don't fit elsewhere. + #LOC_BDArmory_AIWindow_infolink_Pilot_orbitHelp = Orbit Direction - This is the direction the AI will orbit when idling, either Clockwise or Counterclockwise. + #LOC_BDArmory_AIWindow_infolink_Pilot_standbyHelp = Standby Toggle - If enabled, the AI will automatically turn on when targets enter its Guard Range. + + // Surface AI + #LOC_BDArmory_AIWindow_infolink_Surface_Type = 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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_Speeds = Reisegeschwindigkeit und (Kampf-) Höchstgeschwindigkeit bestimmen die bevorzugte Geschwindigkeit, die die KI un Ruhe bzw, beim Kampf anstreben soll. + #LOC_BDArmory_AIWindow_infolink_Surface_Drift = Maximaler Driftwinkel - Bestimmt den maximalen Driftwinkel bei Kurvenfahrt. + #LOC_BDArmory_AIWindow_infolink_Surface_Bank = Neigungswinkel - Bestimmt den maximalen seitlichen Neigungswinkel, den die KI erlauben soll. + #LOC_BDArmory_AIWindow_infolink_Surface_Weave = Haken Schlagen (Faktor) - Wenn das Fahrzeug unter Feuer steht, schlägt es Haken, um nicht getroffen zu werden. Dieser Faktor kontrolliert die Stärke des Haken-Schlagens. + #LOC_BDArmory_AIWindow_infolink_Surface_SteerPower = 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_AIWindow_infolink_Surface_SteerDamping = 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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_Engagement = Min./Max. Angriffsabstand - Bestimmt den minimalen und maximalen Abstand zum Ziel. Die KI versucht, die Kampfeinheit innerhalb dieses Abstandfensters zu halten. + #LOC_BDArmory_AIWindow_infolink_Surface_RCS = RKS - Bestimmt, ob die KI das RKS System beim Manövrieren benutzen soll. + #LOC_BDArmory_AIWindow_infolink_Surface_AvoidMass = Kollisionsvermeidung abh. von Gewicht des Ziels - Legt das Mindestgewicht fest, unterhalb dessen ein Ziel nichtr umfahren, sondern gerammt wird. + #LOC_BDArmory_AIWindow_infolink_Surface_MaintainMinRange = Minimalen Abstand beibehalten - Einschlten, damit ein Landfahzeug stehenbleibt/umkehrt um den Mindestanbstand beizubehalten, oder ob es abdreht und anderweitig Abstand gewinnt. + #LOC_BDArmory_AIWindow_infolink_Surface_Altitude = Cruise-Tiefe und Kampf-Tiefe (bei U-Booten) + + // VTOL AI + #LOC_BDArmory_AIWindow_infolink_VTOL_PID = 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_infolink_VTOL_Altitudes = Standard-Flughöhe - Die Höhe über Grund, die das Flugzeug außerhalb des Kampes einnimmt \nMin./max. Höhe - Die KI versucht, zwischen der minimalen und maximalen Höhe zu bleiben. Ist die Flughöhe kleiner oder größer, wird das Flugzeug in Steig- oder Sinkflug gehen. + #LOC_BDArmory_AIWindow_infolink_VTOL_Speeds = Höstgeschwindigkeit beim Manövrieren und Kampf-Geschwindigkeit. + #LOC_BDArmory_AIWindow_infolink_VTOL_Control = Maximale Hochzieh- und Neigungs-Winkel - Bestimmt die maximalen Winkel, die die KI anstreben wird. \nBreitseiten-Richtung - Teilt der KI mit, ob die Steuerboard- oder Backbordseite bevorzugt werden soll.\nRKS - Bestimmt, ob RKS auch zum Manövrieren verwendet wird (sonst nur im Kampf). + #LOC_BDArmory_AIWindow_infolink_VTOL_Combat = Ausweichen-Nichtlinearität - Unter Beschuss versucht das Flugzeug durch chaotisch oszillierende Flugbahn, den Angreifer abzuschütteln. Diese Einstellung kontrolliert die Stärke der Oszillation (und damit auch den Verlust an Geschwindigkeit, der ggfs. vermieden werden sollte!).\nMinimale und maximale Angriffs-Abstand - Bestimmt den Abstand, den die KI für einen ANgriff herzustellen versucht. + + // Orbital AI + #LOC_BDArmory_AIWindow_infolink_Orbital_PID = 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 Raumfahrzeug 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 Raumfahrzeug 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 Raumfahrzeug so schnell wie möglich, jedoch ohne Überschwinger agiert. + #LOC_BDArmory_AIWindow_infolink_Orbital_Combat = Angriffsvektor und Breitseite - Bestimmt, wie die KI zu Zielen fliegt und sie anzielt. Das Raumfahrzeug kann Ziele mit dem Bug oder einer der beiden lateralten Seiten anpeilen. Die Einstellungen werden nur bei aktivem PID Regler aktiv. \n\nMinimale Entfernung für Angriff - Die KI wird versuchen, diesen Abstand herzustellen, wenn das Ziel näher sein sollte. \n\nAngriff ohne Schub - Innerhalb dieser Distanz vom Ziel (aber außerhalb des Minimalen Angriffs-Abstandes) feuert die KI, ohne den Schub zu verändern. + #LOC_BDArmory_AIWindow_infolink_Orbital_Speeds = Maneuver-, Angriffs- und Winkel-Geschwindigkeit - Bestimmen die Geschwindigkeiten, die die KI anstreben wird. + #LOC_BDArmory_AIWindow_infolink_Orbital_Control = KRS - Bestimmt, ob RKS für die Kontrolle der relativen Geschwindigkeit zum Ziel verwendet wird, und ob RKS auch sonst ür normale Maneuver verwendet wird. \n\nNach vorn gerichtete Raketenmotoren - Bestimmt, ob nach vorn gerichtete Raketenmotoren zum Bremsen verwendet werden. + #LOC_BDArmory_AIWindow_infolink_Orbital_Evasion = 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.\nRKS-Ausweichen und Raketenmotor-Ausweichen bestimmt, ob RKS oder Raketenmotoren zum Ausweichen verwendet werden. + + // Missile Config + #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_UseStaticMaxLaunchRange = Maximale Abschussentfernung dynamisch oder statisch + #LOC_BDArmory_ProximityTriggerDistance = Gefechtskopf Explosionsdistanz + #LOC_BDArmory_clustermissileTriggerDistance = Sub-Munition auswerfen bei Entfernung + #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_Missile_CruiseSpeed = Marschflugkörper-Geschwindigkeit + #LOC_BDArmory_CruiseAltitude = Reisehöhe + #LOC_BDArmory_CruisePredictionTime = Verhergesagte Reisedauer + //#LOC_BDArmory_CruisePopup = ??? Cruise Popup Attack + #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_LoftMaxAltitude = Loft maximale Höhe + #LOC_BDArmory_LoftRangeOverride = Loft Reicheweite überschreiben + #LOC_BDArmory_LoftAltitudeAdvMax = Loft maximale Höhe Vorteil + #LOC_BDArmory_LoftMinAltitude = Loft minimale Höhe + #LOC_BDArmory_LoftAngle = Loft Steigrate + #LOC_BDArmory_LoftTermAngle = Loft Terminations-Winkel + #LOC_BDArmory_LoftRangeFac = Loft Reichweite-Faktor + #LOC_BDArmory_LoftVelComp = Loft horizontale Geschwindigkeits-Kompensation + #LOC_BDArmory_LoftVertVelComp = Loft vertikale Geschwindigkeits-Kompensation + #LOC_BDArmory_LoftAltComp = Loft Höhenkompensation + #LOC_BDArmory_terminalHomingRange = Abstand f. finalen Zielanflug + + #LOC_BDArmory_EMPBlastRadius = EMP Wirkungsradius + #LOC_BDArmory_OrdnanceAvailable = Kampfmittel Vorhanden + #LOC_BDArmory_MissileAssign = Rakete Zuweisen + #LOC_BDArmory_CurrentLocks = Aktuelle Aufschaltungen + #LOC_BDArmory_Offset = Bewaffnung Abstand + #LOC_BDArmory_Deploy_Time = Waffenbereitschaft-Verzögerung + + // Safety Systems + #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_CASE_Sim = Explosion simulieren + #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_AddedCost = Zusätzliche Kosten + #LOC_BDArmory_AddedMass = Gewicht des Sicherheitssystems + #LOC_BDArmory_DryMass = Leergewicht + + // Turret Config + #LOC_BDArmory_MaxPitch = Max Schwenkber. (vert.) + #LOC_BDArmory_MinPitch = Min Schwenkber. (vert.) + #LOC_BDArmory_YawRange = Horiz. Schwenkbereich + //#LOC_BDArmory_YawStandbyAngle = ??? Yaw Standby Angle + #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_AimOverrideFalse = ??? Aim With This Weapon + //#LOC_BDArmory_AimOverrideTrue = ??? Revert Default Aim + #LOC_BDArmory_ReturnTurret = Geschützturm zurücksetzen + #LOC_BDArmory_ToggleAnimation = Animation an/aus + + // Missile UI + #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_TurretLoft = ??? Lofted Aimpoint + //#LOC_BDArmory_TurretLoftFac = ??? Loft Velocity Factor + #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_DynamicRadar = Bei aufschaltung durch ARM Radar ausschalten + + // WM Config + #LOC_BDArmory_GuardMode = Wächter-Modus:\u0020 + #LOC_BDArmory_Team = Team + #LOC_BDArmory_Allies = Verbündete + #LOC_BDArmory_Weapon = Waffe + #LOC_BDArmory_FiringPriority = Auswahlpriorität + //#LOC_BDArmory_weaponChannel = ??? Weapon Channel + #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_MissilesRange = Use Dynamic Launch Zone + #LOC_BDArmory_MissilesOnTarget = 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_dynamic = Dynamisch + #LOC_BDArmory_static = Statisch + + #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_Multiple = Mehrere + #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 + #LOC_BDArmory_NextTankSetup = Nächstes Tank-Setup + #LOC_BDArmory_PreviousTankSetup = Vorheriges Tank-Setup + + // 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_missile_text = Raketen-Bezeichnung + #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_opacity = Opacity: + #LOC_BDArmory_Icon_distance_threshold = Abstand-Schwellenwert: + #LOC_BDArmory_Icon_max_distance_threshold = Max. Entfernung: + #LOC_BDArmory_Icon_telemetry = Telemetrie aktivieren; + //#LOC_BDArmory_Icon_StoreTeamColors = ??? Store Team Colors + #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_RAMFoam = Radar-Absorbierender Schaum + #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_ArmorLiftStacking = Zu starke Überlappung (Lift Stacking) + #LOC_BDArmory_ArmorStats = Panzerung Eigenschaften + #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_partTreeVisualizer = Verbindungsbaum aller Teile + //#LOC_BDArmory_checkVessel = ??? Check Vessel Legality + #LOC_BDArmory_ArmorSelect = Hüllenmaterial auswählen + #LOC_BDArmory_DryMassWhitelist = Verbrauchsmitten, die zur Trockenmasse gezählt werden + #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 + //#LOC_BDArmory_ArmorToolPartCount = ??? Part Count Exceeded! + //#LOC_BDArmory_ArmorToolEngineCount = ??? Too Many Engines: + //#LOC_BDArmory_ArmorToolEngineCountFloor = ??? Too Few Engines: + //#LOC_BDArmory_ArmorToolTWR = ??? TWR Exceeded: + //#LOC_BDArmory_ArmorToolLTW = ??? LTW Exceeded: + //#LOC_BDArmory_ArmorToolMaxMass = ??? Mass Limit Exceeded: + //#LOC_BDArmory_ArmorToolMaxPoints = ??? Point Limit Exceeded: + //#LOC_BDArmory_ArmorToolIllegalParts = ??? Illegal Parts: + //#LOC_BDArmory_ArmorToolNonCockpit = ??? not attached to cockpit + //#LOC_BDArmory_ArmorToolOversizedPWings = ??? pWings exceeding max Lift - check Lift Visualizer + //#LOC_BDArmory_ArmorToolVesselLegal = ??? Vessel legal! + + + // Missile & CM Settings + #LOC_BDArmory_Settings_MissileCMToggle = Einstellungen für Raketen und Gegenmassnahmen anzeigen + #LOC_BDArmory_Settings_AspectedRCS = Echtzeit-Radar-Querschnitt (RCS) + #LOC_BDArmory_Settings_AspectedRCSOverallRCSWeight = RCS Faktor + #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 + + // Texture switching + #LOC_BDArmory_NextTexture = Nächste Textur + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg index 4c8e25646..9132e2720 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg @@ -1,14 +1,40 @@ +// Notes: +// - The indentation provides fold region info for IDEs. +// - #LOC_A = #LOC_B is valid. +// - Check for duplicates with: grep -o '^\s*#LOC[^ ]\+' en-us.cfg |tr -d ' '|sort|uniq -c|grep -v '\s1' +// - Propagate changes in en-us.cfg to the other localisation files by running 'python3 ../_Other\ Stuff/localisation_organisation_sync.py' in the BDArmory/BDArmory folder. + Localization { en-us { + // Generic + #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_On = On + #LOC_BDArmory_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_Reload = Reload + #LOC_BDArmory_Generic_Help = Help + #LOC_BDArmory_Generic_Select = Select + #LOC_BDArmory_Generic_SaveandClose = Save and Close #LOC_BDArmory_VesselStatus_Landed = (Landed) #LOC_BDArmory_VesselStatus_Splashed = (Splashed) + #LOC_BDArmory_VesselStatus_Underwater = (Underwater) + #LOC_BDArmory_false = False + #LOC_BDArmory_true = True + #LOC_BDArmory_Enabled = Enabled + #LOC_BDArmory_Disabled = Disabled + #LOC_BDArmory_Enable = Enable + #LOC_BDArmory_Disable = Disable + // WM Window #LOC_BDArmory_WMWindow_title = BDA Weapon Manager #LOC_BDArmory_WMWindow_GuardModebtn = Guard Mode #LOC_BDArmory_WMWindow_ArmedText = Trigger is\u0020 @@ -18,147 +44,612 @@ 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 #LOC_BDArmory_WMWindow_GuardMenu = Guard Menu #LOC_BDArmory_WMWindow_ModulesToggle = Modules - #LOC_BDArmory_WMWindow_NoneWeapon = None - #LOC_BDArmory_WMWindow_NoneWeapon = Guard Mode <<1>> - #LOC_BDArmory_WMWindow_FiringInterval = Firing Interval - #LOC_BDArmory_WMWindow_BurstLength = Burst Length - #LOC_BDArmory_WMWindow_FiringTolerance = Firing Angle - #LOC_BDArmory_WMWindow_FieldofView = Field of View - #LOC_BDArmory_WMWindow_VisualRange = Visual Range - #LOC_BDArmory_WMWindow_GunsRange = Guns Range - - #LOC_BDArmory_WMWindow_MissilesTgt = Missiles/Tgt - #LOC_BDArmory_WMWindow_TargetType = Target Type: - #LOC_BDArmory_WMWindow_TargetType_Missiles = Missiles - #LOC_BDArmory_WMWindow_TargetType_All = All Targets - #LOC_BDArmory_WMWindow_RadarWarning = Radar Warning Receiver - #LOC_BDArmory_WMWindow_GPSCoordinator = GPS Coordinator - #LOC_BDArmory_WMWindow_WingCommand = Wing Command #LOC_BDArmory_WMWindow_NoWeaponManager = No Weapon Manager found. - #LOC_BDArmory_WMWindow_GPSTarget = GPS Target - #LOC_BDArmory_WMWindow_NoTarget = No Target + // WM Guard Menu + #LOC_BDArmory_WMWindow_NoneWeapon = None + #LOC_BDArmory_WMWindow_GuardMode = Guard Mode <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = Firing Interval + #LOC_BDArmory_WMWindow_BurstLength = Burst Length + #LOC_BDArmory_WMWindow_FiringTolerance = Firing Angle + #LOC_BDArmory_WMWindow_FieldofView = Field of View + #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: + #LOC_BDArmory_WMWindow_TargetType_Missiles = Missiles + #LOC_BDArmory_WMWindow_TargetType_All = All Targets + // Advanced Targeting + #LOC_BDArmory_Settings_Adv_Targeting = Advanced Targeting + #LOC_BDArmory_Selecttargeting = Select Targeting Option + #LOC_BDArmory_targetSetting = Targeting + #LOC_BDArmory_TargetCOM = CoM + #LOC_BDArmory_Weapons = Weapons + #LOC_BDArmory_Engines = Engines + #LOC_BDArmory_Command = Cockpits + #LOC_BDArmory_Mass = Heaviest Parts + #LOC_BDArmory_Random = Random Parts + + // WM Target Priority + #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. + #LOC_BDArmory_WMWindow_targetAccel = Target TWR + #LOC_BDArmory_WMWindow_targetClosingTime = Closing Time + #LOC_BDArmory_WMWindow_targetgunNumber = Weapon Num. + #LOC_BDArmory_WMWindow_targetMass = Target Mass + #LOC_BDArmory_WMWindow_targetAllies = Less Allies Engaging + #LOC_BDArmory_WMWindow_targetThreat = Target Threat + #LOC_BDArmory_WMWindow_defendTeammate = Defend Teammate + #LOC_BDArmory_WMWindow_targetVIP = Attack VIP + #LOC_BDArmory_WMWindow_defendVIP = Defend VIP + + // WM Modules + #LOC_BDArmory_WMWindow_RadarWarning = Radar Warning Receiver + #LOC_BDArmory_WMWindow_GPSCoordinator = GPS Coordinator + #LOC_BDArmory_WMWindow_WingCommand = Wing Command + // WM GPS Module + #LOC_BDArmory_WMWindow_GPSTarget = GPS Target + #LOC_BDArmory_WMWindow_NoTarget = No Target + + // WM infolink + // WM infolink Weapons + #LOC_BDArmory_WMWindow_Weapons_Desc = Weapons - This tab displays all weapons / weapon groups on the vessel. Clicking on a weapon(group) name will select and activate that weapon, allowing it to be manually fired if a gun, rocket, or laser. If it is a missile weapon, the 'Trigger is Disarmed' toggle must be switched to ARMED before missiles can be fired. + #LOC_BDArmory_WMWindow_Ripple_Salvo_Desc = Missiles, when selected, will have a Ripple RPM option. This sets how fast missiles should be sequentially fired if the trigger is held down. Guns, rockets, or lasers with a Rate of Fire below 1500 rounds per minute, will have a Barrage/Salvo toggle instead. If multiple weapons of the same type/weapon group are present, Barrage mode will cause each weapon to fire in sequence. Salvo mode will have every weapon fire simultaneously. + + // WM infolink Guard Menu + #LOC_BDArmory_WMWindow_GuardTab_Desc = Guard - this settings group controls controls how and when the AI will use the weapons on the craft. + #LOC_BDArmory_WMWindow_FiringInterval_Desc = Firing Interval - This sets how frequently, in seconds, the AI will scan for targets, which translates to how frequently it will fire the selected weapon. + #LOC_BDArmory_WMWindow_BurstLength_desc = Burst Length - This controls how long, in seconds, the AI will continue to fire the selected weapon. If 0, the AI will fire for (1/2 * Firing Interval) seconds. + #LOC_BDArmory_WMWindow_FiringTolerance_desc = Firing Angle - This controls when the AI considers itself on target, and will fire the weapon. An angle of 1 means that the target must be within a target cone that is equal in width to the Target Radius, plus the spread of the selected weapon. More accurate weapons will have a narrower target cone, and vice-versa. An angle of 2 is a target cone 2x the width of the Target Radius, etc. When a target is within this target cone, the AI will fire the currently selected weapon. + #LOC_BDArmory_WMWindow_FieldofView_desc = Field of View - This controls the field of view of the AI. A setting of 360 means it can see everything in all directions; a value less than 360 means the AI can only see targets within a cone this many degrees wide. + #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. + + // WM infolink Target Priority + #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. + #LOC_BDArmory_WMWindow_targetAccel_desc = Target Acceleration - Higher values weight targeting preference towards targets with faster acceleration, lower values towards slower targets. + #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. + + // Settings Window #LOC_BDArmory_Settings_Title = BDArmory Settings - #LOC_BDArmory_Settings_GeneralSettingsToggle = General Toggle Settings - #LOC_BDArmory_Settings_SliderSettingsToggle = General Slider Settings - #LOC_BDArmory_Settings_RadarSettingsToggle = Radar Settings - #LOC_BDArmory_Settings_OtherSettingsToggle = Other Settings - #LOC_BDArmory_Settings_Instakill = Instakill - #LOC_BDArmory_Settings_InfiniteAmmo = Infinite Ammo - #LOC_BDArmory_Settings_BulletHits = Bullet Hits - #LOC_BDArmory_Settings_EjectShells = Eject Shells - #LOC_BDArmory_Settings_AimAssist = Aim Assist - #LOC_BDArmory_Settings_DrawAimers = Draw Aimers - #LOC_BDArmory_Settings_DebugLines = Debug Lines - #LOC_BDArmory_Settings_DebugLabels = Debug Labels - #LOC_BDArmory_Settings_RemoteFiring = Remote Firing - #LOC_BDArmory_Settings_ClearanceCheck = Clearance Check - #LOC_BDArmory_Settings_AmmoGauges = Ammo Gauges - #LOC_BDArmory_Settings_ShellCollisions = Shell Collisions - #LOC_BDArmory_Settings_BulletHoleDecals = Bullet Hole Decals - #LOC_BDArmory_Settings_PerformanceLogging = Performance Logging - #LOC_BDArmory_Settings_StrictWindowBoundaries = Strict Window Boundaries - #LOC_BDArmory_Settings_DisableKillTimer = Disable Kill Timer - #LOC_BDArmory_Settings_ShowEditorSubcategories = Show Editor Subcategories - #LOC_BDArmory_Settings_AutocategorizeParts = Autocategorize Parts - - #LOC_BDArmory_Settings_DamageMultiplier = Damage Multiplier - #LOC_BDArmory_Settings_ExtraDamageSliders = Extra Damage Sliders - #LOC_BDArmory_Settings_BallisticDamageMultiplier = Ballistic Damage Multiplier - #LOC_BDArmory_Settings_ExplosiveDamageMultiplier = Explosive Damage Multiplier - #LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier = Missile Explosive Multiplier - #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = Implosive Damage Multiplier - #LOC_BDArmory_Settings_DebrisCleanUpDelay = Debris Removal Delay - #LOC_BDArmory_Settings_MaxBulletHoles = Max Bullet Holes - #LOC_BDArmory_Settings_TerrainAlertFrequency = Terrain Check Frequency - #LOC_BDArmory_Settings_CameraSwitchFrequency = Camera Switch Frequency - #LOC_BDArmory_Settings_DisableRamming = Disable Ramming - #LOC_BDArmory_Settings_DebugRammingLogging = Debug Ramming Logging - #LOC_BDArmory_Settings_DefaultFFATargeting = Default FFA Targeting - #LOC_BDArmory_Settings_TagMode = Tag Mode - #LOC_BDArmory_Settings_PaintballMode = Paintball Mode - #LOC_BDArmory_Settings_DumbIRSeekers = Disable Flare Rejection - #LOC_BDArmory_Settings_RunwayProject = Runway Project - #LOC_BDArmory_Settings_BattleDamage = Battle Damage - #LOC_BDArmory_Settings_GravityHacks = Increase Gravity on Death - #LOC_BDArmory_Settings_AutoEnableVesselSwitching = Auto-Enable Vessel-Switching - #LOC_BDArmory_Settings_AutonomousCombatSeats = Autonomous Combat Seats - #LOC_BDArmory_Settings_DestroyWMWhenNotControlled = Destroy Uncontrolled WMs - #LOC_BDArmory_Settings_PeaceMode = Peace Mode + #LOC_BDArmory_Settings_AdvancedUserSettings = Advanced User Settings + // Section Toggles + #LOC_BDArmory_Settings_GeneralSettingsToggle = Gameplay Settings + #LOC_BDArmory_Settings_GraphicsSettingsToggle = Graphics/UI Settings + #LOC_BDArmory_Settings_SliderSettingsToggle = General Slider Settings + #LOC_BDArmory_Settings_RadarSettingsToggle = Radar Settings + #LOC_BDArmory_Settings_GameModesSettingsToggle = Game Modes + #LOC_BDArmory_Settings_OtherSettingsToggle = Other Settings + #LOC_BDArmory_Settings_CompSettingsToggle = Competition Settings + #LOC_BDArmory_Settings_GMSettingsToggle = GM Settings + + // Graphics / UI + #LOC_BDArmory_Settings_DebugSettingsToggle = Debugging + #LOC_BDArmory_Settings_AIToolbarButton = AI Toolbar Button + #LOC_BDArmory_Settings_VMToolbarButton = VM Toolbar Button + #LOC_BDArmory_Settings_UIScale = UI Scale + #LOC_BDArmory_Settings_UIScaleFollowsStock = Follow Stock + #LOC_BDArmory_Settings_Instakill = Instakill + #LOC_BDArmory_Settings_InfiniteAmmo = Infinite Ammo + #LOC_BDArmory_Settings_InfiniteMissiles = Infinite Ordnance + #LOC_BDArmory_Settings_InfiniteCountermeasures = Infinite Countermeasures + #LOC_BDArmory_Settings_BulletFX = Bullet FX + #LOC_BDArmory_Settings_BulletHits = Bullet Hits + #LOC_BDArmory_Settings_WaterHitFX = Water Hit FX + #LOC_BDArmory_Settings_LightFX = Light FX + #LOC_BDArmory_Settings_PerfOptions = Enable FX + #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 + + // Debugging + #LOC_BDArmory_Settings_DebugTelemetry = On-Screen Telemetry + #LOC_BDArmory_Settings_DebugLines = Debug Lines + #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 + + // Gameplay FIXME These need more sorting + #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 + #LOC_BDArmory_Settings_StrictWindowBoundaries = Strict Window Boundaries + #LOC_BDArmory_Settings_PersistentFX = Persistent FX + #LOC_BDArmory_Settings_DisableKillTimer = Disable Kill Timer + #LOC_BDArmory_Settings_TraceVessels = Auto-Enable Trace Vessel Paths + #LOC_BDArmory_Settings_TraceVesselsManualStart = Start Tracing + #LOC_BDArmory_Settings_TraceVesselsManualStop = Stop Tracing + #LOC_BDArmory_Settings_AutoLogTimeSync = Auto-Enable Time-Sync Logging + #LOC_BDArmory_Settings_LogTimeSyncInterval = Time-Sync Log Interval + #LOC_BDArmory_Settings_LogTimeSyncStart = Start Logging + #LOC_BDArmory_Settings_LogTimeSyncStop = Stop Logging + #LOC_BDArmory_Settings_ShowEditorSubcategories = Show Editor Subcategories + #LOC_BDArmory_Settings_AutocategorizeParts = Autocategorize Parts + #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_AutoResumeContinuousSpawn = Auto-Resume Cts Spawn + #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 + #LOC_BDArmory_Settings_TimeScale = Time Scale + #LOC_BDArmory_Settings_legacyArmor = Enable Legacy Armor + #LOC_BDArmory_Settings_DisableRamming = Disable Ramming + #LOC_BDArmory_Settings_DefaultFFATargeting = Default FFA Targeting + #LOC_BDArmory_Settings_TagMode = Tag Mode + #LOC_BDArmory_Settings_PaintballMode = Paintball Mode + #LOC_BDArmory_Settings_DumbIRSeekers = Disable Flare Rejection + #LOC_BDArmory_Settings_RunwayProject = Runway Project + #LOC_BDArmory_Settings_CompChecks = Use AI/WM Overrides + #LOC_BDArmory_Settings_RunwayProjectRound = Runway Project Round + #LOC_BDArmory_Settings_BattleDamage = Battle Damage + #LOC_BDArmory_Settings_GravityHacks = Increase Gravity on Death + #LOC_BDArmory_Settings_AutoEnableVesselSwitching = Auto-Enable Vessel-Switching + #LOC_BDArmory_Settings_AutonomousCombatSeats = Autonomous Combat Seats + #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_DisableGuardModeOnSpawn = Disable Guard Mode on Spawn + #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 + #LOC_BDArmory_Settings_KerbalSafetyInventory_ResetDefault = Reset Default + #LOC_BDArmory_Settings_KerbalSafetyInventory_ChuteOnly = Parachute Only + #LOC_BDArmory_Settings_PeaceMode = Peace Mode + #LOC_BDArmory_settings_FireRate = Fire Rate Override + #LOC_BDArmory_settings_FireRateCenter = Fire Rate Override Center + #LOC_BDArmory_settings_FireRateSpread = Fire Rate Override Spread + #LOC_BDArmory_settings_FireRateBias = Fire Rate Override Bias + #LOC_BDArmory_settings_FireRateHitMultiplier = Fire Rate Hit Multiplier + #LOC_BDArmory_settings_ZombieMode = Zombie Mode + #LOC_BDArmory_settings_zombieDmgMod = Zombie Non-headshot Dmg Mult + #LOC_BDArmory_settings_gungame_progression = Keep progresson on respawn + #LOC_BDArmory_settings_gungame_cycle = Cycle List + // General Sliders + #LOC_BDArmory_Settings_DamageMultiplier = Damage Multiplier + #LOC_BDArmory_Settings_ExtraDamageSliders = Extra Damage Sliders + #LOC_BDArmory_Settings_BallisticDamageMultiplier = Ballistic Damage Multiplier + #LOC_BDArmory_Settings_ExplosiveDamageMultiplier = Explosive Damage Multiplier + #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_NumericInputSelfUpdate = Numeric Input Self Update + #LOC_BDArmory_Settings_Scoring_HeadShot = Head-Shot Time Limit + #LOC_BDArmory_Settings_Scoring_KillSteal = Kill-Steal Time Limit + #LOC_BDArmory_Settings_MaxBulletHoles = Max Bullet Holes + #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_Max_Armor_Limit = Max Armor Limit + + // Game Modes + // Heart-Bleed + #LOC_BDArmory_Settings_HeartBleed = Heart Bleed + #LOC_BDArmory_Settings_HeartBleedRate = Heart Bleed Rate + #LOC_BDArmory_Settings_HeartBleedInterval = Heart Bleed Interval + #LOC_BDArmory_Settings_HeartBleedThreshold = Heart Bleed Threshold + + // Resource Steal + #LOC_BDArmory_Settings_ResourceSteal = Resource Steal + #LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateIn = Respect Flow State In + #LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateOut = Respect Flow State Out + #LOC_BDArmory_Settings_FuelStealRation = Fuel Steal Ration + #LOC_BDArmory_Settings_AmmoStealRation = Ammo Steal Ration + #LOC_BDArmory_Settings_CMStealRation = CM Steal Ration + + // Asteroids + #LOC_BDArmory_Settings_AsteroidField = Asteroid Field + #LOC_BDArmory_Settings_AsteroidFieldNumber = Number Of Asteroids + #LOC_BDArmory_Settings_AsteroidFieldAltitude = Asteroid Field Altitude + #LOC_BDArmory_Settings_AsteroidFieldRadius = Asteroid Field Radius + #LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction = Anomalous Attraction + #LOC_BDArmory_Settings_AsteroidRain = Asteroid Rain + #LOC_BDArmory_Settings_AsteroidRainNumber = Number Of Asteroids + #LOC_BDArmory_Settings_AsteroidRainAltitude = Asteroid Rain Altitude + #LOC_BDArmory_Settings_AsteroidRainRadius = Asteroid Rain Radius + #LOC_BDArmory_Settings_AsteroidRainFollowsCentroid = Follows Vessels' Location + #LOC_BDArmory_Settings_AsteroidRainFollowsSpread = Follows Vessels' Spread + + // Space hack stuff + #LOC_BDArmory_Settings_SpaceHacks = Space Combat Tools + #LOC_BDArmory_Settings_SpaceFriction = Space Friction + #LOC_BDArmory_Settings_IgnoreGravity = Ignore gravity + #LOC_BDArmory_Settings_Repulsor = Enable Repulsor Effect + #LOC_BDArmory_Settings_SpaceFrictionMult = Cornering Multiplier + + // Mutator Gamemode stuff + #LOC_BDArmory_Settings_Mutators = Mutators + #LOC_BDArmory_MutatorSelect = Select Mutators + #LOC_BDArmory_Settings_MutatorGlobal = Apply Globally + #LOC_BDArmory_Settings_MutatorKill = Apply On Kill + #LOC_BDArmory_Settings_MutatorGungame = GunGame Progression + #LOC_BDArmory_Settings_MutatorTimed = Apply On Timer + #LOC_BDArmory_Settings_MutatorDuration = Duration + #LOC_BDArmory_UI_MutatorStart = Active Global Mutator + #LOC_BDArmory_UI_MutatorShuffle = Mutators Shuffled! + #LOC_BDArmory_Settings_MutatorNum = Number of Active Mutators + #LOC_BDArmory_Settings_MutatorIcons = Show Mutator Icons + + #LOC_BDArmory_Settings_WaypointsMode = Waypoints Mode + #LOC_BDArmory_Settings_GLimitsMode = Override G-Force Limits + #LOC_BDArmory_Settings_AimingVisualMalus = Aiming Visual Malus + + // Battle Damage + #LOC_BDArmory_Settings_BDSettingsToggle = Battle Damage settings + #LOC_BDArmory_Settings_BD_Proc = Proc Frequency + #LOC_BDArmory_Settings_BD_Proc_Pen = Proc Min Penetration + #LOC_BDArmory_Settings_BD_Engines = Propulsion Systems Damage + #LOC_BDArmory_Settings_BD_Prop_Dmg_Mult = Propulsion Damage Amount + #LOC_BDArmory_Settings_BD_Prop_floor = Engine Min Thrust + #LOC_BDArmory_Settings_BD_Prop_flameout = Engine flameout + #LOC_BDArmory_Settings_BD_Intakes = Intake Damage + #LOC_BDArmory_Settings_BD_Gimbals = Gimbal Damage + #LOC_BDArmory_Settings_BD_Aero = Flight Systems Damage + #LOC_BDArmory_Settings_BD_Aero_Dmg_Mult = Wing Damage Amount + #LOC_BDArmory_Settings_BD_CtrlSrf = Control Surface Damage + #LOC_BDArmory_Settings_BD_Command = Command & Control Damage + #LOC_BDArmory_Settings_BD_PilotKill = Crew Fatalities + #LOC_BDArmory_Settings_BD_Tanks = Fuel Tank Damage + #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 + #LOC_BDArmory_Settings_BD_Fires = Fires + #LOC_BDArmory_Settings_BD_DoT = Fire Damage + #LOC_BDArmory_Settings_BD_Fire_Dmg = Fire Damage/s + #LOC_BDArmory_Settings_BD_FireHeat = Fires Add Heat + #LOC_BDArmory_Settings_BD_FuelFireEX = Fuel Explosions + #LOC_BDArmory_Settings_BD_ZombieMode = Allow Battle Damage + + // Radar / Other Settings + #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 + #LOC_BDArmory_Settings_UIVolume = UI Volume + #LOC_BDArmory_Settings_WeaponVolume = Weapon Volume + #LOC_BDArmory_Settings_IGNORE_TERRAIN_CHECK = Detection Ignores Terrain + #LOC_BDArmory_Settings_CHECK_WATER_TERRAIN = Detection Checks Water + #LOC_BDArmory_Settings_RADAR_NOTCHING = Radar Notching + #LOC_BDArmory_Settings_Notching_Factor = Notch Effectiveness Factor + #LOC_BDArmory_Settings_Notching_SCR_Factor = Notch SCR Factor + + // Competition / Tournament + #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 + #LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay = Bystander Removal Delay + #LOC_BDArmory_Settings_CompetitionKillerGMFrequency = Killer GM Frequency + #LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod = Killer GM Grace Period + #LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh = Altitude Limit High + #LOC_BDArmory_Settings_CompetitionAltitudeLimitLow = Altitude Limit Low + #LOC_BDArmory_Settings_CompetitionGMWeaponKill = Kill Weaponless Craft + #LOC_BDArmory_Settings_CompetitionGMEngineKill = Kill Engineless Craft + #LOC_BDArmory_Settings_CompetitionGMDisableKill = Kill Disabled Craft + #LOC_BDArmory_Settings_CompetitionGMHPKill = Kill Damaged Craft + #LOC_BDArmory_Settings_CompetitionGMKillDelay = GM Kill Delay + #LOC_BDArmory_Settings_CompetitionStarting = Starting Competition... + #LOC_BDArmory_Settings_DogfightCompetition = Dogfight Competition + #LOC_BDArmory_Settings_StartCompetition = Start Competition + #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 + + // BDA Remote (defunct) + #LOC_BDArmory_BDARemoteOrchestration_Title = BDA Remote Orchestration + #LOC_BDArmory_Settings_RemoteLogging = Remote Orchestration + #LOC_BDArmory_Settings_RemoteInterheatDelay = Inter-heat Delay + #LOC_BDArmory_Settings_RemoteSync = Run Via Remote Orchestration + #LOC_BDArmory_Settings_CompetitionID = Competition ID + + // Input Settings + #LOC_BDArmory_InputSettings_Weapons = Weapons + #LOC_BDArmory_InputSettings_TargetingPod = Targeting Pod + #LOC_BDArmory_InputSettings_Radar = Radar + #LOC_BDArmory_InputSettings_VesselSwitcher = Vessel Switcher + #LOC_BDArmory_InputSettings_Tournament = Tournament + #LOC_BDArmory_InputSettings_TimeScaling = Time Scaling + #LOC_BDArmory_InputSettings_TemporarilyShowMouse = Temporarily Show Mouse + #LOC_BDArmory_InputSettings_GUI = GUI + #LOC_BDArmory_InputSettings_BackBtn = Back + #LOC_BDArmory_InputSettings_recordedInput = Press a key or button. + #LOC_BDArmory_InputSettings_SetKey = Set Key + #LOC_BDArmory_InputSettings_Clear = Clear + + // Weapon Config + #LOC_BDArmory_Ammo_Setup = Ammo Loadout Configuration + #LOC_BDArmory_Ammo_Weapon = Selected Weapon: + #LOC_BDArmory_Ammo_Belt = Current Belt: + #LOC_BDArmory_advanced = Ammo Config: Advanced + #LOC_BDArmory_simple = Ammo Config: Simple + #LOC_BDArmory_useBelt = Using Custom Loadout: + #LOC_BDArmory_save = Save + #LOC_BDArmory_saveClose = Save & Close + #LOC_BDArmory_reset = Reset + #LOC_BDArmory_applyTo = Apply To + #LOC_BDArmory_WeaponGroup = Weapon Group GUI + #LOC_BDArmory_AddToWpnGroup = Add to Weapon Group: + #LOC_BDArmory_thisWeapon = this weapon + #LOC_BDArmory_SymmetricWeapons = symmetric weapons + + #LOC_BDArmory_CustomFireKey = Custom Fire Key + #LOC_BDArmory_SetCustomFireKey = Set Custom Fire Key + + #LOC_BDArmory_EjectVelocity = Eject Velocity + #LOC_BDArmory_TNTMass = TNT mass equivalent + #LOC_BDArmory_BlastRadius = Blast Radius + #LOC_BDArmory_WeaponName = Weapon Name\u0020 + #LOC_BDArmory_GuidanceType = Guidance Type\u0020 + #LOC_BDArmory_TargetingMode = Targeting Mode\u0020 + #LOC_BDArmory_ActiveRadarRange = Active Radar Range + #LOC_BDArmory_MissileCMRange = Countermeasure Range + #LOC_BDArmory_MissileCMInterval = Countermeasure Interval + + // Adjustable Rails + #LOC_BDArmory_Rails = Rails + #LOC_BDArmory_IncreaseHeight = Height ++ + #LOC_BDArmory_DecreaseHeight = Height -- + #LOC_BDArmory_IncreaseLength = Length ++ + #LOC_BDArmory_DecreaseLength = Length -- + #LOC_BDArmory_RailsPlus = Rails++ + #LOC_BDArmory_RailsMinus = Rails-- + // Vessel Spawner #LOC_BDArmory_BDAVesselSpawner_Title = BDA Vessel Spawner - #LOC_BDArmory_Settings_SpawnOptions = Spawn Options - #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Set Vessel Spawn Point Here - #LOC_BDArmory_Settings_SpawnDistanceFactor = Spawning Distance Factor - #LOC_BDArmory_Settings_SpawnDistance = Spawning Distance - #LOC_BDArmory_Settings_SpawnDistanceToggle = Absolute Distance vs Factor - #LOC_BDArmory_Settings_SpawnEaseInSpeed = Spawning Ease-In Speed - #LOC_BDArmory_Settings_SpawnConcurrentVessels = Concurrent Vessels (CS) - #LOC_BDArmory_Settings_SpawnLivesPerVessel = Lives Per Vessel (CS) - #LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn = Dump Logs Every Spawn (CS) - #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = Continuous Single Spawn (S) - #LOC_BDArmory_Settings_OutOfAmmoKillTime = Out-of-ammo Kill Time (CS) - #LOC_BDArmory_Settings_SpawnLocations = Interesting Spawn Locations + // Spawn Options + #LOC_BDArmory_Settings_SpawnOptions = Spawn Options + #LOC_BDArmory_Settings_SpawnDistanceFactor = Spawning Distance Factor + #LOC_BDArmory_Settings_SpawnRefHeading = Reference Heading + #LOC_BDArmory_Settings_SpawnDistance = Spawning Distance + #LOC_BDArmory_Settings_SpawnDistanceToggle = Absolute Distance vs Factor + #LOC_BDArmory_Settings_SpawnReassignTeams = Reassign Teams + #LOC_BDArmory_Settings_SpawnEaseInSpeed = Spawning Ease-In Speed + #LOC_BDArmory_Settings_SpawnConcurrentVessels = Concurrent Vessels (CS) + #LOC_BDArmory_Settings_SpawnLivesPerVessel = Lives Per Vessel (CS) + #LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn = Dump Logs Every Spawn (CS) + #LOC_BDArmory_Settings_CSFollowsCentroid = Spawn Point Follows Centroid (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_SpawnInitialVelocity = Air-Spawn With Idle Speed + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = Spawn Spawn-Probe Here + #LOC_BDArmory_Settings_OutOfAmmoKillTime = Out-of-ammo Kill Time (CS) + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Set Vessel Spawn Point Here + #LOC_BDArmory_Settings_SaveSpawnLoc = Save Location + #LOC_BDArmory_Settings_ClearDebrisNow = Clear Debris Now + #LOC_BDArmory_Settings_ClearBystandersNow = Clear Bystanders Now + // Fill Seats + #LOC_BDArmory_Settings_SpawnFillSeats = Fill Seats + #LOC_BDArmory_Settings_SpawnFillSeats_Minimal = Minimal + #LOC_BDArmory_Settings_SpawnFillSeats_Default = Cockpits or Combat Seat + #LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints = All Control Points + #LOC_BDArmory_Settings_SpawnFillSeats_Cabins = Also Cabins + + // Teams + #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 + // 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_TemplateSelection = Template Selection + #LOC_BDArmory_Settings_CustomSpawnTemplate_SaveCraftToTemplate = Save Craft URLs + + // Observers + #LOC_BDArmory_Settings_Observers = Observers + #LOC_BDArmory_ObserverSelection_Title = Observer Selection + #LOC_BDArmory_ObserverSelection_SelectAll = Select All + #LOC_BDArmory_ObserverSelection_SelectNone = Select None + + // Interesting Spawn Locations + #LOC_BDArmory_Settings_SpawnLocations = Interesting Spawn Locations + #LOC_BDArmory_Settings_WarpHere = Warp Here + #LOC_BDArmory_Settings_Planet = Select Planet + + // Tournament Options + #LOC_BDArmory_Settings_TournamentOptions = Tournament Options + #LOC_BDArmory_Settings_TournamentStyle = Tournament Style + #LOC_BDArmory_Settings_TournamentRoundType = Round Type + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = Delay Between Heats + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = TimeWarp Between Rounds + #LOC_BDArmory_Settings_TournamentTimeWarpDaylight = Daylight + #LOC_BDArmory_Settings_TournamentRounds = Rounds + #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_TournamentNPCsPerHeat = NPCs Per Heat + #LOC_BDArmory_Settings_TournamentSetup = Set Up Tournament + #LOC_BDArmory_Settings_TournamentRun = Run Tournament + #LOC_BDArmory_Settings_TournamentStop = Stop Tournament + + // Waypoints + #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 + #LOC_BDArmory_Settings_SingleSpawn = Single Spawn #LOC_BDArmory_Settings_ContinuousSpawning = Continuous Spawning #LOC_BDArmory_Settings_CancelSpawning = Cancel Spawning - #LOC_BDArmory_Settings_TournamentOptions = Tournament Options - #LOC_BDArmory_Settings_TournamentFilesLocation = Craft Files Location - #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = Delay Between Heats - #LOC_BDArmory_Settings_TournamentRounds = Rounds - #LOC_BDArmory_Settings_TournamentVesselsPerHeat = Vessels Per Heat - #LOC_BDArmory_Settings_TournamentSetup = Set Up Tournament - #LOC_BDArmory_Settings_TournamentRun = Run Tournament - #LOC_BDArmory_Settings_TournamentStop = Stop Tournament - - #LOC_BDArmory_Settings_RWRWindowScale = RWR Window Scale - #LOC_BDArmory_Settings_RadarWindowScale = Radar Window Scale - #LOC_BDArmory_Settings_TargetWindowScale = Target Window Scale - #LOC_BDArmory_Settings_TriggerHold = Trigger Hold - #LOC_BDArmory_Settings_UIVolume = UI Volume - #LOC_BDArmory_Settings_WeaponVolume = Weapon Volume - - #LOC_BDArmory_Settings_CompetitionDistance = Competition Distance - #LOC_BDArmory_Settings_CompetitionDuration = Competition Duration - #LOC_BDArmory_Settings_CompetitionFinalGracePeriod = Final Grace Period - #LOC_BDArmory_Settings_CompetitionInitialGracePeriod = Initial Grace Period - #LOC_BDArmory_Settings_CompetitionKillTimer = Landed Kill Timer - #LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay = Bystander Removal Delay - #LOC_BDArmory_Settings_CompetitionKillerGMFrequency = Killer GM Frequency - #LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod = Killer GM Grace Period - #LOC_BDArmory_Settings_CompetitionKillerGMMaxAltitude = Killer GM Max Altitude - #LOC_BDArmory_Settings_CompetitionStarting = Starting Competition... - #LOC_BDArmory_Settings_DogfightCompetition = Dogfight Competition - #LOC_BDArmory_Settings_StartCompetition = Start Competition - #LOC_BDArmory_Settings_StartCompetitionNow = Start Competition NOW. - #LOC_BDArmory_Settings_EditInputs = Edit Inputs - - #LOC_BDArmory_BDARemoteOrchestration_Title = BDA Remote Orchestration - #LOC_BDArmory_Settings_RemoteLogging = Remote Orchestration - #LOC_BDArmory_Settings_CompetitionID = Competition ID + // Waypoint GUI + #LOC_BDArmory_BDAWaypointBuilder_Title = Waypoint Course Tool + #LOC_BDArmory_WP_LoadCourse = Load Course + #LOC_BDArmory_WP_NewCourse = New Course + #LOC_BDArmory_WP_ChooseCourse = Select Course + #LOC_BDArmory_WP_Create = Create + #LOC_BDArmory_WP_Record = Record + #LOC_BDArmory_WP_TimeStep = Timestep (s) + #LOC_BDArmory_WP_Recording = Recording Course.... + #LOC_BDArmory_WP_FinishRecording = Finish Recording + #LOC_BDArmory_WP_Spawnpoint = Spawnpoint + #LOC_BDArmory_WP_AddGate = Add Gate + #LOC_BDArmory_WP_Waypoint = Waypoint + #LOC_BDArmory_WP_SpeedLimit = Speed limit + #LOC_BDArmory_WP_Increment = Increment + #LOC_BDArmory_WP_MaxLaps = Max Laps + #LOC_BDArmory_WP_GuardActivate = Activate Guard After + #LOC_BDArmory_WP_CourseDefaults = Use Course Settings + #LOC_BDArmory_WP_SelectModel = Waypoint Type - #LOC_BDArmory_Generic_SaveandClose = Save and Close - - #LOC_BDArmory_InputSettings_Weapons = Weapons - #LOC_BDArmory_InputSettings_TargetingPod = Targeting Pod - #LOC_BDArmory_InputSettings_Radar = Radar - #LOC_BDArmory_InputSettings_VesselSwitcher = Vessel Switcher - #LOC_BDArmory_InputSettings_Tournament = Tournament - #LOC_BDArmory_InputSettings_BackBtn = Back - #LOC_BDArmory_InputSettings_recordedInput = Press a key or button. - #LOC_BDArmory_InputSettings_SetKey = Set Key - #LOC_BDArmory_InputSettings_Clear = Clear + // Vessel Mover + #LOC_BDArmory_VesselMover_Title = BDA Vessel Mover + #LOC_BDArmory_VesselMover_VesselSelection = Vessel Selection + #LOC_BDArmory_VesselMover_CrewSelection = Crew Selection + #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_DeconflictVesselName = Deconflict Vessel Name + #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 + // Craft Browser + #LOC_BDArmory_CraftBrowser_InvalidParts = INVALID PARTS + #LOC_BDArmory_CraftBrowser_UnknownModules = Modules + #LOC_BDArmory_CraftBrowser_Clear = Clear + #LOC_BDArmory_CraftBrowser_ClearAll = Clear All + #LOC_BDArmory_CraftBrowser_Refresh = Refresh + #LOC_BDArmory_CraftBrowser_Parts = Parts + #LOC_BDArmory_CraftBrowser_Mass = Mass + #LOC_BDArmory_CraftBrowser_Version = Version + #LOC_BDArmory_CraftBrowser_Craft = Craft + #LOC_BDArmory_CraftBrowser_Folder = Folder + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnails = Generate Missing Thumbnails + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsRecurse = Recurse subfolders + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingFor = Generating thumbnail for + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingForCraftIn = Generating thumbnail for craft in + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFinished = Finished generating thumbnails. + #LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFailure = Unable to capture thumbnail of + + // Scores + #LOC_BDArmory_BDAScores_Title = Tournament Scores + #LOC_BDArmory_BDAScores_Weights = Score Weights + #LOC_BDArmory_BDAScores_Round = Round + #LOC_BDArmory_BDAScores_Heat = Heat + #LOC_BDArmory_BDAScores_Unlimited = Unlimited + #LOC_BDArmory_BDAScores_Score = Score + #LOC_BDArmory_BDAScores_Lives = Lives + + // Staging Icons #LOC_BDArmory_ProtoStageIconInfo_Reloading = Reloading #LOC_BDArmory_ProtoStageIconInfo_Overheat = Overheat #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = Ammo Depleted + #LOC_BDArmory_ProtoStageIconInfo_CMsOut = CMs Depleted + // Wing Commander #LOC_BDArmory_WingCommander_Title = WingCommander #LOC_BDArmory_WingCommander_Guiname1 = Formation Spread #LOC_BDArmory_WingCommander_Guiname2 = Formation Lag @@ -177,152 +668,667 @@ Localization #LOC_BDArmory_WingCommander_Lag = Lag #LOC_BDArmory_WingCommander_ScreenMessage = Select target coordinates.\nRight-click to cancel. + // Vessel Switcher #LOC_BDArmory_BDAVesselSwitcher_Title = BDA Vessel Switcher - //GUI Names - #LOC_BDArmory_EjectVelocity = Eject Velocity - #LOC_BDArmory_TNTMass = TNT mass equivalent - #LOC_BDArmory_BlastRadius = Blast Radius - #LOC_BDArmory_WeaponName = Weapon Name\u0020 - #LOC_BDArmory_GuidanceType = Guidance Type\u0020 - #LOC_BDArmory_TargetingMode = Targeting Mode\u0020 - #LOC_BDArmory_ActiveRadarRange = Active Radar Range + // Evolution + #LOC_BDArmory_Evolution_Title = BDA Evolution + #LOC_BDArmory_Evolution_Options = Evolution Options + #LOC_BDArmory_Evolution_HeatsPerGroup = Heats per Group + #LOC_BDArmory_Evolution_MutationsPerHeat = Mutations per Heat + #LOC_BDArmory_Evolution_AdversariesPerHeat = Adversaries per Heat + #LOC_BDArmory_Evolution_ID = Evolution + #LOC_BDArmory_Evolution_Status = Status + #LOC_BDArmory_Evolution_Group = Group + #LOC_BDArmory_Evolution_Heat = Heat - #LOC_BDArmory_Rails = Rails - #LOC_BDArmory_IncreaseHeight = Height ++ - #LOC_BDArmory_DecreaseHeight = Height -- - #LOC_BDArmory_IncreaseLength = Length ++ - #LOC_BDArmory_DecreaseLength = Length -- - #LOC_BDArmory_RailsPlus = Rails++ - #LOC_BDArmory_RailsMinus = Rails-- - - #LOC_BDArmory_PilotAI_PID = PID Controller - #LOC_BDArmory_SteerFactor = Steer Factor - #LOC_BDArmory_SteerKi = Steer Ki - #LOC_BDArmory_SteerDamping = Steer Damping - - #LOC_BDArmory_DynamicSteerDamping = Dynamic Steer Damping - #LOC_BDArmory_DynamicDamping = Dynamic Damping - #LOC_BDArmory_DynamicDampingMin = Off-target Damping - #LOC_BDArmory_DynamicDampingMax = On-target Damping - #LOC_BDArmory_DynamicDampingFactor = Dynamic Damping Factor - #LOC_BDArmory_3AxisDynamicSteerDamping = 3-Axis Dynamic Damping - #LOC_BDArmory_DynamicDampingPitch = Dynamic Damping Pitch - #LOC_BDArmory_DynamicDampingPitchMin = Off-target Pitch Damping - #LOC_BDArmory_DynamicDampingPitchMax = On-target Pitch Damping - #LOC_BDArmory_DynamicDampingPitchFactor = Dyn. Pitch Damping Factor - #LOC_BDArmory_DynamicDampingYaw = Dynamic Damping Yaw - #LOC_BDArmory_DynamicDampingYawMin = Off-target Yaw Damping - #LOC_BDArmory_DynamicDampingYawMax = On-target Yaw Damping - #LOC_BDArmory_DynamicDampingYawFactor = Dyn. Yaw Damping Factor - #LOC_BDArmory_DynamicDampingRoll = Dynamic Damping Roll - #LOC_BDArmory_DynamicDampingRollMin = Off-target Roll Damping - #LOC_BDArmory_DynamicDampingRollMax = On-target Roll Damping - #LOC_BDArmory_DynamicDampingRollFactor = Dyn. Roll Damping Factor - - #LOC_BDArmory_TargetPriority = Target Priority - #LOC_BDArmory_TargetPriority_CurrentTarget = Current Target - #LOC_BDArmory_TargetPriority_TargetScore = Target Score - #LOC_BDArmory_TargetPriority_Settings = Target Priority Settings - #LOC_BDArmory_TargetPriority_CurrentTargetBias = Current Target Bias - #LOC_BDArmory_TargetPriority_TargetProximity = Target Distance - #LOC_BDArmory_TargetPriority_CloserAngleToTarget = Closer Angle to Target - #LOC_BDArmory_TargetPriority_TargetAcceleration = Target Acceleration - #LOC_BDArmory_TargetPriority_ShorterClosingTime = Shorter Closing Time - #LOC_BDArmory_TargetPriority_TargetWeaponNumber = Target Weapon Number - #LOC_BDArmory_TargetPriority_TargetMass = Target Mass - #LOC_BDArmory_TargetPriority_FewerTeammatesEngaging = Fewer Teammates Engaging - #LOC_BDArmory_TargetPriority_TargetThreat = Target Threat - #LOC_BDArmory_TargetPriority_AngleOverDistance = Angle / Distance - - #LOC_BDArmory_Countermeasure_Settings = Countermeasure Settings - #LOC_BDArmory_CMThreshold = Time to Impact Before CM - #LOC_BDArmory_CMRepetition = CM Dump per Sequence - #LOC_BDArmory_CMInterval = CM Dump Interval Time - #LOC_BDArmory_CMWaitTime = Sequence Restart Delay - - // Modular Missile, Custom Weapons - #LOC_BDArmory_SteerLimiter = Steer Limiter + // Modular Missile, Custom Weapons #LOC_BDArmory_StagesNumber = Stages Number #LOC_BDArmory_StageToTriggerOnProximity = Stage to Trigger On Proximity #LOC_BDArmory_RollCorrection = Roll Correction #LOC_BDArmory_RollCorrection_enabledText = Roll enabled #LOC_BDArmory_RollCorrection_disabledText = Roll disabled + #LOC_BDArmory_MissileIFF = Seeker IFF + #LOC_BDArmory_MissileIFF_enabledText = IFF enabled + #LOC_BDArmory_MissileIFF_disabledText = IFF disabled #LOC_BDArmory_TimeBetweenStages = Time Between Stages - #LOC_BDArmory_MinSpeedGuidance = Min Speed before guidance + #LOC_BDArmory_AI_MinSpeedGuidance = Min Speed before guidance #LOC_BDArmory_ClearanceRadius = Clearance radius #LOC_BDArmory_ClearanceLength = Clearance length #LOC_BDArmory_showRFGUI = Show Weapon Name Editor #LOC_BDArmory_showRFGUI_enabledText = Weapon Name GUI #LOC_BDArmory_showRFGUI_disabledText = GUI - #LOC_BDArmory_PilotAI_Altitudes = Altitudes - #LOC_BDArmory_DefaultAltitude = Default Alt. - #LOC_BDArmory_MinAltitude = Min Altitude + // WM (PAW) + // Target Priority + #LOC_BDArmory_TargetPriority = Target Priority + #LOC_BDArmory_TargetPriority_CurrentTarget = Current Target + #LOC_BDArmory_TargetPriority_TargetScore = Target Score + #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 + #LOC_BDArmory_TargetPriority_TargetProtectTeammate = Protect Teammates + #LOC_BDArmory_TargetPriority_TargetProtectVIP = Protect My VIPs + #LOC_BDArmory_TargetPriority_TargetAttackVIP = Attack Enemy VIPs - #LOC_BDArmory_PilotAI_Speeds = Speeds - #LOC_BDArmory_MaxSpeed = Max Speed - #LOC_BDArmory_TakeOffSpeed = TakeOff Speed - #LOC_BDArmory_MinSpeed = MinCombatSpeed - #LOC_BDArmory_IdleSpeed = Idle Speed - - #LOC_BDArmory_MaxDrift = Max drift - - #LOC_BDArmory_PilotAI_ControlLimits = Control Authority Limits - #LOC_BDArmory_AttitudeLimiter = Attitude Limit - #LOC_BDArmory_BankLimiter = Bank Angle Limit - #LOC_BDArmory_maxAllowedGForce = Max G - #LOC_BDArmory_maxAllowedAoA = Max AoA - - #LOC_BDArmory_PilotAI_EvadeExtend = Evasion/Extension - #LOC_BDArmory_ExtendMultiplier = Extend Multiplier - #LOC_BDArmory_ExtendToggle = Extend Toggle - #LOC_BDArmory_MinEvasionTime = Min Evasion Time - #LOC_BDArmory_EvasionThreshold = Evasion Distance Threshold - #LOC_BDArmory_EvasionTimeThreshold = Evasion Time Threshold - #LOC_BDArmory_CollisionAvoidanceThreshold = Vessel Avoidance Threshold - #LOC_BDArmory_CollisionAvoidancePeriod = Vessel Avoidance Period - - #LOC_BDArmory_PilotAI_Terrain = Terrain Avoidance - #LOC_BDArmory_TurnRadiusTwiddleFactorMin = Terrain Avoidance Tuning Min - #LOC_BDArmory_TurnRadiusTwiddleFactorMax = Terrain Avoidance Tuning Max + // Countermeasures + #LOC_BDArmory_Countermeasure_Settings = Countermeasure Settings + #LOC_BDArmory_EvadeThreshold = Time to Impact Before Evade + #LOC_BDArmory_CMThreshold = Time to Impact Before CM + #LOC_BDArmory_CMRepetition = Flare Dump per Sequence + #LOC_BDArmory_CMInterval = Flare Dump Interval Time + #LOC_BDArmory_CMWaitTime = Flare Sequence Restart Delay + #LOC_BDArmory_ChaffRepetition = Chaff Dump per Sequence + #LOC_BDArmory_ChaffInterval = Chaff Dump Interval Time + #LOC_BDArmory_ChaffWaitTime = Chaff Sequence Restart Delay + #LOC_BDArmory_SmokeRepetition = Smoke Launch per Sequence + #LOC_BDArmory_SmokeInterval = Smoke Launch Interval Time + #LOC_BDArmory_SmokeWaitTime = Smoke Sequence Restart Delay + #LOC_BDArmory_ChaffFactor = Chaff Susceptibility + #LOC_BDArmory_NonGuardModeCMs = Non-GuardMode CMs + + #LOC_BDArmory_IsVIP = Is VIP? + #LOC_BDArmory_IsVIP_enabledText = Yes + #LOC_BDArmory_IsVIP_disabledText = No + #LOC_BDArmory_WM_IsPrimaryWM = Is Primary + #LOC_BDArmory_UnderAttackAG = Under Attack AG + + // AI (PAW) + // Pilot AI + // PID + #LOC_BDArmory_AI_PID = PID Controller + #LOC_BDArmory_AI_SteerPower = Steer Power (P) + #LOC_BDArmory_AI_SteerKi = Steer Correction (I) + #LOC_BDArmory_AI_SteerDamping = Steer Damping (D) + #LOC_BDArmory_AI_SteerMaxError = Steer Max Error + + // Dynamic damping + #LOC_BDArmory_AI_DynamicSteerDamping = Dynamic Steer Damping + #LOC_BDArmory_AI_DynamicDamping = Dynamic Damping + #LOC_BDArmory_AI_DynamicDampingMin = Off-target Damping + #LOC_BDArmory_AI_DynamicDampingMax = On-target Damping + #LOC_BDArmory_AI_DynamicDampingFactor = Dynamic Damping Factor + + // 3-axis damping + #LOC_BDArmory_AI_3AxisSteerDamping = 3-Axis Steer Damping + + // 3-axis static damping + #LOC_BDArmory_AI_SteerDampingPitch = Steer Damping Pitch (Dp) + #LOC_BDArmory_AI_SteerDampingYaw = Steer Damping Yaw (Dy) + #LOC_BDArmory_AI_SteerDampingRoll = Steer Damping Roll (Dr) + + // 3-axis dynamic damping + #LOC_BDArmory_AI_DynamicDampingPitch = Dynamic Damping Pitch + #LOC_BDArmory_AI_DynamicDampingPitchMin = Off-target Pitch Damping + #LOC_BDArmory_AI_DynamicDampingPitchMax = On-target Pitch Damping + #LOC_BDArmory_AI_DynamicDampingPitchFactor = Dyn. Pitch Damping Factor + #LOC_BDArmory_AI_DynamicDampingYaw = Dynamic Damping Yaw + #LOC_BDArmory_AI_DynamicDampingYawMin = Off-target Yaw Damping + #LOC_BDArmory_AI_DynamicDampingYawMax = On-target Yaw Damping + #LOC_BDArmory_AI_DynamicDampingYawFactor = Dyn. Yaw Damping Factor + #LOC_BDArmory_AI_DynamicDampingRoll = Dynamic Damping Roll + #LOC_BDArmory_AI_DynamicDampingRollMin = Off-target Roll Damping + #LOC_BDArmory_AI_DynamicDampingRollMax = On-target Roll Damping + #LOC_BDArmory_AI_DynamicDampingRollFactor = Dyn. Roll Damping Factor + + // Full 3-axis PID + #LOC_BDArmory_AI_3AxisPID = Full 3-Axis PID + #LOC_BDArmory_AI_3AxisPIDPitchMult = Pitch Power (Pp) + #LOC_BDArmory_AI_3AxisPIDPitchKi = Pitch Correction (Ip) + #LOC_BDArmory_AI_3AxisPIDPitchDamping = Pitch Damping (Dp) + #LOC_BDArmory_AI_3AxisPIDYawMult = Yaw Power (Py) + #LOC_BDArmory_AI_3AxisPIDYawKi = Yaw Correction (Iy) + #LOC_BDArmory_AI_3AxisPIDYawDamping = Yaw Damping (Dy) + #LOC_BDArmory_AI_3AxisPIDRollMult = Roll Power (Pr) + #LOC_BDArmory_AI_3AxisPIDRollKi = Roll Correction (Ir) + #LOC_BDArmory_AI_3AxisPIDRollDamping = Roll Damping (Dr) + + // Auto-tuning + #LOC_BDArmory_AI_PID_AutoTune = PID Auto-Tune + #LOC_BDArmory_AI_PID_AutoTuning_Loss = Auto-Tuning Loss + #LOC_BDArmory_AI_PID_AutoTuning_NumSamples = Auto-Tuning Number Of Samples + #LOC_BDArmory_AI_PID_AutoTuning_FastResponseRelevance = Auto-Tuning Fast Response Relevance + #LOC_BDArmory_AI_PID_AutoTuning_InitialLearningRate = Auto-Tuning Initial Learning Rate + #LOC_BDArmory_AI_PID_AutoTuning_InitialRollRelevance = Auto-Tuning Initial Roll Relevance + #LOC_BDArmory_AI_PID_AutoTuning_Speed = Auto-Tuning Speed + #LOC_BDArmory_AI_PID_AutoTuning_Altitude = Auto-Tuning Altitude + #LOC_BDArmory_AI_PID_AutoTuning_RecenteringDistance = Auto-Tuning Recentering Distance (km) + #LOC_BDArmory_AI_PID_AutoTuning_FixedP = Auto-Tuning Fixed P + #LOC_BDArmory_AI_PID_AutoTuning_ClampMaximums = Auto-Tuning Clamp Max + #LOC_BDArmory_AI_PID_AutoTuning_Summary = Auto-Tuning Summary + + // Altitudes + #LOC_BDArmory_AI_Altitudes = Altitudes + #LOC_BDArmory_AI_DefaultAltitude = Default Alt. + #LOC_BDArmory_AI_MinAltitude = Min Altitude + #LOC_BDArmory_AI_MaxAltitude = Max Altitude (AGL) + #LOC_BDArmory_AI_HardMinAltitude = Hard Min Altitude + #LOC_BDArmory_AI_BombingAltitude = Bombing Altitude + #LOC_BDArmory_AI_DiveBombing = Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AI_Speeds = Speeds + #LOC_BDArmory_AI_MaxSpeed = Max Speed + #LOC_BDArmory_AI_TakeOffSpeed = Take-Off Speed + #LOC_BDArmory_AI_MinSpeed = Min Combat Speed + #LOC_BDArmory_AI_StrafingSpeed = Strafing Speed + #LOC_BDArmory_AI_IdleSpeed = Idle Speed + #LOC_BDArmory_AI_ABPriority = Afterburner Priority + #LOC_BDArmory_AI_ABOverrideThreshold = Afterburner Override Threshold + #LOC_BDArmory_AI_BrakingPriority = Braking Priority + + // Control + #LOC_BDArmory_AI_ControlLimits = Control Authority Limits + #LOC_BDArmory_AI_SteerLimiter = Steer Limiter + #LOC_BDArmory_AI_LowSpeedSteerLimiter = Low-Speed Steer Limiter + #LOC_BDArmory_AI_LowSpeedLimiterSpeed = Low-Speed Limiter Speed + #LOC_BDArmory_AI_HighSpeedSteerLimiter = High-Speed Steer Limiter + #LOC_BDArmory_AI_HighSpeedLimiterSpeed = High-Speed Limiter Speed + #LOC_BDArmory_AI_AltitudeSteerLimiterFactor = Altitude Steer Limiter Factor + #LOC_BDArmory_AI_AltitudeSteerLimiterAltitude = Altitude Steer Limiter Altitude + #LOC_BDArmory_AI_AttitudeLimiter = Attitude Limit + #LOC_BDArmory_AI_BankLimiter = Bank Angle Limit + #LOC_BDArmory_AI_WaypointPreRollTime = Waypoint Pre-Roll Time + #LOC_BDArmory_AI_WaypointYawAuthorityTime = Waypoint Yaw Authority Time + #LOC_BDArmory_AI_MaxAllowedGForce = Max G + #LOC_BDArmory_AI_MaxAllowedAoA = Max AoA + #LOC_BDArmory_AI_PostStallAoA = Post-Stall AoA Mode-Switch + #LOC_BDArmory_AI_ImmelmannTurnAngle = Immelmann Turn Angle + #LOC_BDArmory_AI_ImmelmannPitchUpBias = Immelmann Pitch-Up Bias + + // Evade / Extend + #LOC_BDArmory_AI_EvadeExtend = Evasion/Extension + #LOC_BDArmory_AI_ExtendMultiplier = Extend Multiplier + #LOC_BDArmory_AI_ExtendDistanceAirToAir = Extend Distance Air-To-Air + #LOC_BDArmory_AI_ExtendAngleAirToAir = Extend Angle Air-To-Air + #LOC_BDArmory_AI_ExtendDistanceAirToGroundGuns = Extend Distance Air-To-Ground (Guns) + #LOC_BDArmory_AI_ExtendDistanceAirToGround = Extend Distance Air-To-Ground + #LOC_BDArmory_AI_ExtendTargetVel = Extend Target Velocity Factor + #LOC_BDArmory_AI_ExtendTargetAngle = Extend Target Angle + #LOC_BDArmory_AI_ExtendTargetDist = Extend Target Distance + #LOC_BDArmory_AI_ExtendAbortTime = Extend Abort Time + #LOC_BDArmory_AI_ExtendMinGainRate = Extend Min Gain Rate + #LOC_BDArmory_AI_ExtendToggle = Extend Toggle (Air-To-Air) + #LOC_BDArmory_AI_MinEvasionTime = Min Evasion Time + #LOC_BDArmory_AI_EvasionNonlinearity = Evasion/Extension Nonlinearity + #LOC_BDArmory_AI_EvasionThreshold = Evasion Distance Threshold + #LOC_BDArmory_AI_EvasionErraticness = Evasion Erraticness + #LOC_BDArmory_AI_EvasionTimeThreshold = Evasion Time Threshold + #LOC_BDArmory_AI_EvasionMinRangeThreshold = Evasion Min Range Threshold + #LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe = Don't Evade My Target + #LOC_BDArmory_AI_EvasionMissileKinematic = Kinematic Msl Evasion + #LOC_BDArmory_AI_CollisionAvoidanceThreshold = Vessel Avoidance Threshold + #LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod = Vessel Avoidance Look-Ahead + #LOC_BDArmory_AI_CollisionAvoidanceStrength = Vessel Avoidance Strength + #LOC_BDArmory_AI_StandoffDistance = Stand-off Distance + + // Terrain + #LOC_BDArmory_AI_Terrain = Terrain Avoidance + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMin = Terrain Avoidance Tuning Min + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMax = Terrain Avoidance Tuning Max + #LOC_BDArmory_AI_TerrainAvoidanceCriticalAngle = Inverted Terrain Avoidance Critical Angle + #LOC_BDArmory_AI_TerrainAvoidanceVesselReactionTime = Vessel Reaction Time + #LOC_BDArmory_AI_TerrainAvoidancePostAvoidanceCoolDown = Post-Avoidance Cool-Down + #LOC_BDArmory_AI_WaypointTerrainAvoidance = Waypoint Terrain Avoidance + + // Ramming + #LOC_BDArmory_AI_Ramming = Ramming + #LOC_BDArmory_AI_ControlSurfaceLag = Ramming Control Surface Lag + #LOC_BDArmory_AI_AllowRamming = Allow Ramming + #LOC_BDArmory_AI_AllowRammingGroundTargets = Include Ground Targets + + // Ejection (unused) + #LOC_BDArmory_AI_Ejection = Ejection + #LOC_BDArmory_AI_EjectOnImpendingDoom = Eject If Doomed + + #LOC_BDArmory_AI_SliderResolution = Slider Resolution + // Idle / Orbit Behavior + #LOC_BDArmory_AI_Orbit = Orbit Direction\u0020 + #LOC_BDArmory_AI_Orbit_Starboard = Starboard (CW) + #LOC_BDArmory_AI_Orbit_Port = Port (CCW) + #LOC_BDArmory_AI_Orbit_Random = Either (CW/CCW) + #LOC_BDArmory_AI_Standby = Standby Mode + + // Up-to-eleven + #LOC_BDArmory_AI_UnclampTuning = Unclamp tuning\u0020 + #LOC_BDArmory_AI_UnclampTuning_enabledText = Unclamped + #LOC_BDArmory_AI_UnclampTuning_disabledText = Clamped + + // Surface / VTOL / Orbital AI + #LOC_BDArmory_AI_VehicleType = Vehicle Type + #LOC_BDArmory_AI_MaxSlopeAngle = Max Slope Angle + #LOC_BDArmory_AI_CruiseSpeed = Cruise Speed + #LOC_BDArmory_AI_CombatSpeed = Combat Speed + #LOC_BDArmory_AI_CombatAltitude = Combat Altitude + #LOC_BDArmory_AI_TargetPitch = Moving Pitch + #LOC_BDArmory_AI_MaxDrift = Max drift + #LOC_BDArmory_AI_MaxPitchAngle = Max Pitch Angle + #LOC_BDArmory_AI_BankAngle = Bank Angle + #LOC_BDArmory_AI_WeaveFactor = Weave Factor + #LOC_BDArmory_AI_MaxBankAngle = Max Bank Angle + #LOC_BDArmory_AI_BroadsideAttack = Attack Vector + #LOC_BDArmory_AI_BroadsideAttack_enabledText = Broadside + #LOC_BDArmory_AI_BroadsideAttack_disabledText = Bow + #LOC_BDArmory_AI_MinEngagementRange = Min Engagement Range + #LOC_BDArmory_AI_MaxEngagementRange = Max Engagement Range + #LOC_BDArmory_AI_ForceFiringRange = Zero Throttle Firing Range + #LOC_BDArmory_AI_MaintainEngagementRange = Maintain Min Range + #LOC_BDArmory_AI_ManeuverRCS = RCS Active + #LOC_BDArmory_AI_ManeuverRCS_enabledText = Always + #LOC_BDArmory_AI_ManeuverRCS_disabledText = Combat Only + #LOC_BDArmory_AI_FiringRCS = RCS While Firing + #LOC_BDArmory_AI_FiringRCS_enabledText = Manage Velocity + #LOC_BDArmory_AI_FiringRCS_disabledText = Maneuvers Only + #LOC_BDArmory_AI_ReverseEngines = Reverse Engines + #LOC_BDArmory_AI_EngineRCSRotation = Engine RCS (Rotation) + #LOC_BDArmory_AI_EngineRCSTranslation = Engine RCS (Translation) + #LOC_BDArmory_AI_OrbitalPIDActive = PID Active For + #LOC_BDArmory_AI_RollMode = Broadside Dir + #LOC_BDArmory_AI_MinObstacleMass = Min Obstacle Mass + #LOC_BDArmory_AI_PreferredBroadsideDirection = Preferred Broadside Dir + #LOC_BDArmory_AI_GoesUp = Goes up to + #LOC_BDArmory_AI_GoesUp_enabledText = Eleven + #LOC_BDArmory_AI_GoesUp_disabledText = Ten + #LOC_BDArmory_AI_ManeuverSpeed = Maneuver Speed + #LOC_BDArmory_AI_FiringSpeedMin = Min Firing Speed + #LOC_BDArmory_AI_FiringSpeedLimit = Max Firing Speed + #LOC_BDArmory_AI_AngularSpeedLimit = Angular Speed Limit + #LOC_BDArmory_AI_EvasionRCS = RCS Evasion + #LOC_BDArmory_AI_EvasionEngines = Thrust Evasion + + // AI GUI + #LOC_BDArmory_AIWindow_title = AI Manager + #LOC_BDArmory_AIWindow_infoLink = Infolink + #LOC_BDArmory_AIWindow_NoAI = No AI found on vessel. + // Sections + #LOC_BDArmory_AIWindow_PID = PID + #LOC_BDArmory_AIWindow_Altitudes = Altitudes + #LOC_BDArmory_AIWindow_Speeds = Speeds + #LOC_BDArmory_AIWindow_Control = Control + #LOC_BDArmory_AIWindow_EvadeExtend = Evade/Extend + #LOC_BDArmory_AIWindow_Terrain = Terrain + #LOC_BDArmory_AIWindow_Ramming = Ramming + #LOC_BDArmory_AIWindow_Combat = Combat + #LOC_BDArmory_AIWindow_Misc = Misc - #LOC_BDArmory_PilotAI_Ramming = Ramming - #LOC_BDArmory_ControlSurfaceLag = Ramming Control Surface Lag - #LOC_BDArmory_AllowRamming = Allow Ramming + // Panel + // Pilot + // PID + #LOC_BDArmory_AIWindow_SteerPower = Steer Power (P) + #LOC_BDArmory_AIWindow_SteerPower_ContextLow = <- Sluggish + #LOC_BDArmory_AIWindow_SteerPower_ContextHigh = Twitchy -> + #LOC_BDArmory_AIWindow_SteerKi = Steer Correction (I) + #LOC_BDArmory_AIWindow_SteerKi_ContextLow = <- Undershoot + #LOC_BDArmory_AIWindow_SteerKi_ContextHigh = Overshoot -> + #LOC_BDArmory_AIWindow_SteerDamping = Steer Damping (D) + #LOC_BDArmory_AIWindow_SteerDamping_ContextLow = <- Wobbly + #LOC_BDArmory_AIWindow_SteerDamping_ContextHigh = Stiff -> + #LOC_BDArmory_AIWindow_SteerDampingPitch = Steer Damping Pitch (Dp) + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextLow = <- Wobbly + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextHigh = Stiff -> + #LOC_BDArmory_AIWindow_SteerDampingYaw = Steer Damping Yaw (Dy) + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextLow = <- Wobbly + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextHigh = Stiff -> + #LOC_BDArmory_AIWindow_SteerDampingRoll = Steer Damping Roll (Dr) + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextLow = <- Wobbly + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextHigh = Stiff -> + #LOC_BDArmory_AIWindow_SteerMaxError = Max Error + #LOC_BDArmory_AIWindow_SteerMaxError_ContextLow = <- Slow & Easy Tuning + #LOC_BDArmory_AIWindow_SteerMaxError_ContextHigh = Fast & Harder Tuning -> + #LOC_BDArmory_AIWindow_DynDampMin = Off-target Damping + #LOC_BDArmory_AIWindow_DynDampMin_Context = Minimum Damping + #LOC_BDArmory_AIWindow_DynDampMax = On-target Damping + #LOC_BDArmory_AIWindow_DynDampMax_Context = Maximum Damping + #LOC_BDArmory_AIWindow_DynDampMult = Dynamic Damping Factor + #LOC_BDArmory_AIWindow_DynDampMult_Context = Damping Magnitude + #LOC_BDArmory_AIWindow_3AxisPIDPitchMult = Pitch Power (Pp) + #LOC_BDArmory_AIWindow_3AxisPIDPitchKi = Pitch Correction (Ip) + #LOC_BDArmory_AIWindow_3AxisPIDPitchDamping = Pitch Damping (Dp) + #LOC_BDArmory_AIWindow_3AxisPIDYawMult = Yaw Power (Py) + #LOC_BDArmory_AIWindow_3AxisPIDYawKi = Yaw Correction (Iy) + #LOC_BDArmory_AIWindow_3AxisPIDYawDamping = Yaw Damping (Dy) + #LOC_BDArmory_AIWindow_3AxisPIDRollMult = Roll Power (Pr) + #LOC_BDArmory_AIWindow_3AxisPIDRollKi = Roll Correction (Ir) + #LOC_BDArmory_AIWindow_3AxisPIDRollDamping = Roll Damping (Dr) - #LOC_BDArmory_UnclampTuning = Unclamp tuning\u0020 - #LOC_BDArmory_UnclampTuning_enabledText = Unclamped - #LOC_BDArmory_UnclampTuning_disabledText = Clamped + // Auto-tuning + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = A-T Num Samples + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextLow = <- Less accurate + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextHigh = More accurate -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = A-T Fast Response + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextLow = <- Better damping + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextHigh = Faster response -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = A-T Initial LR + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate_Context = Lower this if the changes in the PID values are too large + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance = A-T Initial RR + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance_Context = How much the roll errors contribute to the loss + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = A-T Speed + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed_Context = Target speed for auto-tuning at + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = A-T Altitude + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude_Context = Try to stay within ±min altitude of this + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance = A-T Recentering Distance + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance_Context = Distance from start at which re-centering is triggered + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = A-T Fixed Fields + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = A-T Clamp Max + // PID fixed fields + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P = P + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I = I + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D = D + // 3-axis damping + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch = Dp + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw = Dy + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll = Dr + // Dynamic damping + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OffTarget = DOff + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OnTarget = DOn + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_Factor = DF + // 3-axis dynamic damping + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OffTarget = DpOff + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OnTarget = DpOn + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_Factor = DpF + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OffTarget = DyOff + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OnTarget = DyOn + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_Factor = DyF + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OffTarget = DrOff + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OnTarget = DrOn + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_Factor = DrF + // Full 3-axis PID + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Pp = Pp + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Ip = Ip + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Dp = Dp + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Py = Py + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Iy = Iy + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Dy = Dy + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Pr = Pr + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Ir = Ir + #LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Dr = Dr - // Idle Behavior - #LOC_BDArmory_Orbit = Orbit\u0020 - #LOC_BDArmory_Orbit_enabledText = Starboard (CW) - #LOC_BDArmory_Orbit_disabledText = Port (CCW) - #LOC_BDArmory_StandbyMode = Standby Mode + // Altitudes + #LOC_BDArmory_AIWindow_DefaultAltitude = Default Alt. + #LOC_BDArmory_AIWindow_DefaultAltitude_Context = AI returns to this when idle + #LOC_BDArmory_AIWindow_MinAltitude = Min Altitude + #LOC_BDArmory_AIWindow_MinAltitude_Context = AI tries to remain above this altitude + #LOC_BDArmory_AIWindow_MaxAltitude = Max Altitude (AGL) + #LOC_BDArmory_AIWindow_MaxAltitude_Context = AI tries to remain below this altitude + #LOC_BDArmory_AIWindow_BombingAltitude = Bombing Altitude + #LOC_BDArmory_AIWindow_BombingAltitude_Context = AI tries to bomb at this altitude + #LOC_BDArmory_AIWindow_DiveBomb = Enable Dive Bombing - #LOC_BDArmory_On = On - #LOC_BDArmory_Off = Off + // Speeds + #LOC_BDArmory_AIWindow_MaxSpeed = #LOC_BDArmory_AI_MaxSpeed + #LOC_BDArmory_AIWindow_MaxSpeed_Context = AI will remain below this speed + #LOC_BDArmory_AIWindow_TakeOffSpeed = #LOC_BDArmory_AI_TakeOffSpeed + #LOC_BDArmory_AIWindow_TakeOffSpeed_Context = Speed AI will begin pitch input on take-off + #LOC_BDArmory_AIWindow_MinSpeed = #LOC_BDArmory_AI_MinSpeed + #LOC_BDArmory_AIWindow_MinSpeed_Context = AI will try to regain energy if below this speed + #LOC_BDArmory_AIWindow_StrafingSpeed = #LOC_BDArmory_AI_StrafingSpeed + #LOC_BDArmory_AIWindow_StrafingSpeed_Context = Ground attack speed + #LOC_BDArmory_AIWindow_IdleSpeed = #LOC_BDArmory_AI_IdleSpeed + #LOC_BDArmory_AIWindow_IdleSpeed_Context = Non-combat cruise speed + #LOC_BDArmory_AIWindow_ABPriority = #LOC_BDArmory_AI_ABPriority + #LOC_BDArmory_AIWindow_ABPriority_Context = Modifies the threshold for enabling the afterburner + #LOC_BDArmory_AIWindow_ABOverrideThreshold = Afterburner Override + #LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = Force use of afterburner if below this speed and full throttle + #LOC_BDArmory_AIWindow_BrakingPriority = #LOC_BDArmory_AI_BrakingPriority + #LOC_BDArmory_AIWindow_BrakingPriority_Context = Prioritize using brakes to slow down when allowed + + // Control + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = Low-Speed Limiter + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter_Context = Limits control below low speed limit + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = Low Limiter Speed + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed_Context = AI uses Low-Speed Limiter below this speed + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = High-Speed Limiter + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter_Context = Limits control above high speed limit + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = High Limiter Speed + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed_Context = AI fully limited to high limit above this speed + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Alt Steer Factor + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor_Context = Factor to decrease/increase steer limit based on altitude + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Alt Steer Altitude + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude_Context = Altitude to begin decreasing/increasing the steer limit + #LOC_BDArmory_AIWindow_BankLimiter = Bank Angle Limit + #LOC_BDArmory_AIWindow_BankLimiter_Context = Max roll angle + #LOC_BDArmory_AIWindow_MaxAllowedGForce = Max G + #LOC_BDArmory_AIWindow_MaxAllowedGForce_Context = Maneuvers will try not to exceed this G limit + #LOC_BDArmory_AIWindow_MaxAllowedAoA = Max AoA + #LOC_BDArmory_AIWindow_MaxAllowedAoA_Context = Maneuver AoA will try not to exceed this AoA + #LOC_BDArmory_AIWindow_WaypointPreRollTime = WP Pre-Roll Time + #LOC_BDArmory_AIWindow_WaypointPreRollTime_Context = Start rolling ahead of reaching the waypoint + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = WP Yaw Auth Time + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime_Context = Increase yaw response when approaching waypoint + #LOC_BDArmory_AIWindow_PostStallAoA = Post-Stall AoA + #LOC_BDArmory_AIWindow_PostStallAoA_Context = Switch flight modes for post-stall beyond this AoA + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle = Immelmann Turn Angle + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle_Context = Craft will just pitch up to aim at a target in this cone + #LOC_BDArmory_AIWindow_ImmelmannPitchUpBias = Immelmann Pitch-Up Bias + #LOC_BDArmory_AIWindow_ImmelmannPitchUpBias_Context = < Down — Bias direction on current pitch rate — Up > + + // Evade / Extend + #LOC_BDArmory_AIWindow_Evade = Evasion + #LOC_BDArmory_AIWindow_MinEvasionTime = #LOC_BDArmory_AI_MinEvasionTime + #LOC_BDArmory_AIWindow_MinEvasionTime_Context = Min time AI evades an attack + #LOC_BDArmory_AIWindow_EvasionThreshold = Distance Threshold + #LOC_BDArmory_AIWindow_EvasionThreshold_Context = Evades if incoming fire within this distance + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Time Threshold + #LOC_BDArmory_AIWindow_EvasionTimeThreshold_Context = Min time under fire to trigger evasion + #LOC_BDArmory_AIWindow_EvasionMinRangeThreshold = Min Range Threshold + #LOC_BDArmory_AIWindow_EvasionMinRangeThreshold_Context = Only evade if the attacker is beyond this range. + #LOC_BDArmory_AIWindow_EvasionNonlinearity = Evade/Extend Nonlinearity + #LOC_BDArmory_AIWindow_EvasionNonlinearity_Context = Strength of oscillations when evading/extending + + #LOC_BDArmory_AIWindow_Avoidance = Vessel Avoidance + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Distance Threshold + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold_Context = Dodge incoming craft within this dist + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Look-Ahead Time + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod_Context = How many seconds ahead AI looks for collisions + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Response Strength + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength_Context = How hard the AI will break away from incoming craft + #LOC_BDArmory_AIWindow_StandoffDistance = Stand-off Dist. + #LOC_BDArmory_AIWindow_StandoffDistance_Context = Distance the AI will try to close to on a target + + #LOC_BDArmory_AIWindow_Extend = Extension + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Distance A2A + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = Extend Distance Air-To-Air + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = Angle A2A + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = Desired angle of climb when extending + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Distance A2G Guns + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = Extend Distance Air-To-Ground (Guns) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Distance A2G + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = Extend Distance Air-To-Ground + #LOC_BDArmory_AIWindow_ExtendTargetVel = Target Vel. Factor + #LOC_BDArmory_AIWindow_ExtendTargetVel_Context = Sets when target is too slow to turn towards + #LOC_BDArmory_AIWindow_ExtendTargetAngle = Target Angle + #LOC_BDArmory_AIWindow_ExtendTargetAngle_Context = Sets when target is outside turn radius + #LOC_BDArmory_AIWindow_ExtendTargetDist = Target Distance + #LOC_BDArmory_AIWindow_ExtendTargetDist_Context = Sets when target is too close to turn towards + #LOC_BDArmory_AIWindow_ExtendAbortTime = Abort time + #LOC_BDArmory_AIWindow_ExtendAbortTime_Context = Abort time if failing to gain distance while extending. + #LOC_BDArmory_AIWindow_ExtendMinGainRate = Min Gain Rate + #LOC_BDArmory_AIWindow_ExtendMinGainRate_Context = Minimum rate to be gaining distance for the abort timer. + + // Terrain + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin = Terrain Avoid Min + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin_Context = Turn radius multiplier for ideal craft orientation + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax = Terrain Avoid Max + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax_Context = Turn radius multiplier for inverted craft orientation + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle = Inverted Crit. Angle + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle_Context = Critical angle for inverted terrain avoidance or rolling first + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime = Vessel Reaction Time + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime_Context = Estimate of time required to setup for optimal turning + #LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown = Post-Avoidance Cool-Down + #LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown_Context = Time after avoiding terrain before beginning maneuvers + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = WP Terrain Avoid + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance_Context = Range and strength of waypoint terrain correction + + // Ramming + #LOC_BDArmory_AIWindow_ControlSurfaceLag = Control srf. Lag + #LOC_BDArmory_AIWindow_ControlSurfaceLag_Context = Ram trajectory correction for control surfaces' lag + // Combat + // Up-to-eleven + // Idle / Orbit Behavior + #LOC_BDArmory_AIWindow_Orbit_Context = Direction of non-combat cruising + #LOC_BDArmory_AIWindow_Standby_Context = AI turns on when target enters Guard Range + + // Surface / VTOL / Orbital + #LOC_BDArmory_AIWindow_VehicleType = #LOC_BDArmory_AI_VehicleType + #LOC_BDArmory_AIWindow_VehicleType_Context = Craft operates on this type of terrain + #LOC_BDArmory_AIWindow_MaxSlopeAngle = #LOC_BDArmory_AI_MaxSlopeAngle + #LOC_BDArmory_AIWindow_MaxSlopeAngle_Context = Max angle of terrain craft will drive up + #LOC_BDArmory_AIWindow_CruiseSpeed = #LOC_BDArmory_AI_CruiseSpeed + #LOC_BDArmory_AIWindow_CruiseSpeed_Context = #LOC_BDArmory_AIWindow_IdleSpeed_Context + #LOC_BDArmory_AIWindow_CombatSpeed = #LOC_BDArmory_AI_CombatSpeed + #LOC_BDArmory_AIWindow_CombatSpeed_Context = Target speed for combat without powered steering + #LOC_BDArmory_AIWindow_CombatAltitude = #LOC_BDArmory_AI_CombatAltitude + #LOC_BDArmory_AIWindow_CombatAltitude_Context = Default cruising altitude/depth + #LOC_BDArmory_AIWindow_TargetPitch = #LOC_BDArmory_AI_TargetPitch + #LOC_BDArmory_AIWindow_TargetPitch_Context = Desired craft Attitude Angle + #LOC_BDArmory_AIWindow_MaxDrift = #LOC_BDArmory_AI_MaxDrift + #LOC_BDArmory_AIWindow_MaxDrift_Context = Max angle craft veers off prograde during cornering + #LOC_BDArmory_AIWindow_MaxPitchAngle = #LOC_BDArmory_AI_MaxPitchAngle + #LOC_BDArmory_AIWindow_MaxPitchAngle_Context = Max angle to pitch at while moving + #LOC_BDArmory_AIWindow_BankAngle = #LOC_BDArmory_AI_BankAngle + #LOC_BDArmory_AIWindow_BankAngle_Context = #LOC_BDArmory_AIWindow_BankLimiter_Context + #LOC_BDArmory_AIWindow_WeaveFactor = #LOC_BDArmory_AI_WeaveFactor + #LOC_BDArmory_AIWindow_WeaveFactor_Context = Strength of weaving when under fire + #LOC_BDArmory_AIWindow_MaxBankAngle = #LOC_BDArmory_AI_MaxBankAngle + #LOC_BDArmory_AIWindow_MaxBankAngle_Context = Max angle to roll at while turning + #LOC_BDArmory_AIWindow_BroadsideAttack = #LOC_BDArmory_AI_BroadsideAttack + #LOC_BDArmory_AIWindow_BroadsideAttack_Context = Craft orientation towards target when attacking + #LOC_BDArmory_AIWindow_MinEngagementRange = #LOC_BDArmory_AI_MinEngagementRange + #LOC_BDArmory_AIWindow_MinEngagementRange_Context = Minimum range AI will engage target + #LOC_BDArmory_AIWindow_MaxEngagementRange = #LOC_BDArmory_AI_MaxEngagementRange + #LOC_BDArmory_AIWindow_MaxEngagementRange_Context = Maximum range AI will engage target + #LOC_BDArmory_AIWindow_ForceFiringRange = #LOC_BDArmory_AI_ForceFiringRange + #LOC_BDArmory_AIWindow_ForceFiringRange_Context = Within this range AI always fires without throttle + #LOC_BDArmory_AIWindow_MaintainEngagementRange = #LOC_BDArmory_AI_MaintainEngagementRange + #LOC_BDArmory_AIWindow_MaintainEngagementRange_Context = Craft will stop/reverse at minimum Range + #LOC_BDArmory_AIWindow_ManeuverRCS = #LOC_BDArmory_AI_ManeuverRCS + #LOC_BDArmory_AIWindow_ManeuverRCS_Context = RCS usage for maneuvers + #LOC_BDArmory_AIWindow_FiringRCS = #LOC_BDArmory_AI_FiringRCS + #LOC_BDArmory_AIWindow_FiringRCS_Context = Use RCS to manage velocity while firing + #LOC_BDArmory_AIWindow_ReverseEngines = #LOC_BDArmory_AI_ReverseEngines + #LOC_BDArmory_AIWindow_ReverseEngines_Context = Use backwards engines for reverse thrust + #LOC_BDArmory_AIWindow_EngineRCSRotation = #LOC_BDArmory_AI_EngineRCSRotation + #LOC_BDArmory_AIWindow_EngineRCSRotation_Context = Use engines perpendicular to thrust axis for RCS rotation + #LOC_BDArmory_AIWindow_EngineRCSTranslation =#LOC_BDArmory_AI_EngineRCSTranslation + #LOC_BDArmory_AIWindow_EngineRCSTranslation_Context = Use engines perpendicular to thrust axis for RCS translation + #LOC_BDArmory_AIWindow_OrbitalPIDActive = #LOC_BDArmory_AI_OrbitalPIDActive + #LOC_BDArmory_AIWindow_OrbitalPIDActive_Context = PID active condition + #LOC_BDArmory_AIWindow_RollMode = #LOC_BDArmory_AI_RollMode + #LOC_BDArmory_AIWindow_RollMode_Context = When PID is active, AI will roll to present this side of the ship to the target + #LOC_BDArmory_AIWindow_MinObstacleMass = #LOC_BDArmory_AI_MinObstacleMass + #LOC_BDArmory_AIWindow_MinObstacleMass_Context = Minimum mass of obstacle to trigger avoidance + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection = #LOC_BDArmory_AI_PreferredBroadsideDirection + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context = Which side of craft to present towards target + #LOC_BDArmory_AIWindow_ManeuverSpeed = #LOC_BDArmory_AI_ManeuverSpeed + #LOC_BDArmory_AIWindow_ManeuverSpeed_Context = Maximum speed relative to target when maneuvering + #LOC_BDArmory_AIWindow_minFiringSpeed = #LOC_BDArmory_AI_FiringSpeedMin + #LOC_BDArmory_AIWindow_minFiringSpeed_Context = Minimum speed relative to target when firing + #LOC_BDArmory_AIWindow_FiringSpeed = #LOC_BDArmory_AI_FiringSpeedLimit + #LOC_BDArmory_AIWindow_FiringSpeed_Context = Maximum speed relative to target when firing + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit = #LOC_BDArmory_AI_AngularSpeedLimit + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit_Context = Maximum angular speed relative to target when firing + #LOC_BDArmory_AIWindow_EvasionErraticness = #LOC_BDArmory_AI_EvasionErraticness + #LOC_BDArmory_AIWindow_EvasionErraticness_Context = Amount of variation in evasion direction + #LOC_BDArmory_AIWindow_EvasionRCS = #LOC_BDArmory_AI_EvasionRCS + #LOC_BDArmory_AIWindow_EvasionRCS_Context = Use RCS to evade incoming fire + #LOC_BDArmory_AIWindow_EvasionEngines = #LOC_BDArmory_AI_EvasionEngines + #LOC_BDArmory_AIWindow_EvasionEngines_Context = Use engine thrust to evade incoming fire + + + // AI infolink + // Pilot AI + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_PidHelp_SteerPower = 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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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 and RR - The initial learning rate and roll relevance (RR will auto-update to that of the best tuning). Altitude and Speed - The target altitude and speed to use while tuning. Recentering Distance - The distance from the start (in km) at which the craft will return to the starting area between epochs. 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_infolink_Pilot_AltHelp = Altitude settings control the desired flight envelope of the AI + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_Def = Default Altitude - This is the Altitude the AI will seek to return to when not in combat or extending. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_AltHelp_bombing = Bombing Altitude - The AI will try to maintain level flight at this altitude when performing a bombing run (doesn't apply to torpedoes). + + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp = Speed settings control the desired Airspeeds of the craft over a variety of flight and combat conditions + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_SpeedHelp_idle = Idle Speed - This sets the non-combat cruise airspeed the AI will maintain while orbiting or flying to position. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_SpeedHelp_ABpriority = AB Priority - This controls the level of requested acceleration at which the AI will turn on/off the afterburners. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_SpeedHelp_BrakingPriority = Braking Priority - This controls the aggressiveness of the AI using the brakes when allowed. + + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp = Control Limits set limits on craft control authority under various conditions + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_ControlHelp_modeSwitches = Post-Stall AoA Mode-Switch controls when the plane will switch steering modes due to being beyond the AoA threshold. + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_Immelmann = Immelmann Turn Angle and Bias - When flying to a target within the angle of the Immelmann Turn directly behind the craft, the craft will simply pitch up / down instead of rolling. When not close to the min altitude, the Immelmann Pitch-Up Bias will force pitching up or down when the current pitching rate (°/s) is within the limit, otherwise the current pitching direction is used. + + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_EvadeHelp_Evade = Min Evasion Time, Distance Threshold, Time Threshold, Min Range Threshold - 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 Min Range Threshold sets the range that the attacker has to be beyond to trigger evasion.\nThe Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes.\nThe Evade Missiles Kinematically toggle tells the AI to use beyond-visual-range (BVR) maneuvers, like crank, beam, and turn cold to evade BVR missiles. The Time to Impact Before Evade setting on the weapons manager should be set to the max when "Evade Missiles Kinematically" is enabled. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_EvadeHelp_ExtendVel = Extend Target Velocity Factor - This is a multiplier of current velocity that tells the AI at what relative velocity it should consider extending. Less than 1, the target craft must be slower, greater than 1, faster. A setting of 0.9 would tell the AI to extend if the target is moving less than 90% of its vel, etc. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_EvadeHelp_ExtendAbortTime = Extend Abort Time, Extend Min Gain Rate - These tell the AI to abort extending if it hasn't made sufficient gains within this time (below the min gain rate, the timer increases at a rate of 1; below the min gain rate, the timer decreases at a rate of 0.5). Extending then enters a 5s cooldown period. + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendToggle = Extend Toggle - This enables or disables extending against air targets (extending against ground targets is not affected). - #LOC_BDArmory_VehicleType = Vehicle type - #LOC_BDArmory_MaxSlopeAngle = Max slope angle - #LOC_BDArmory_CruiseSpeed = Cruise speed - #LOC_BDArmory_TargetPitch = Moving pitch - #LOC_BDArmory_BankAngle = Bank angle - #LOC_BDArmory_BroadsideAttack = Attack vector - #LOC_BDArmory_BroadsideAttack_enabledText = Broadside - #LOC_BDArmory_BroadsideAttack_disabledText = Bow - #LOC_BDArmory_MinEngagementRange = Min engagement range - #LOC_BDArmory_MaxEngagementRange = Max engagement range - #LOC_BDArmory_ManeuverRCS = RCS active - #LOC_BDArmory_ManeuverRCS_enabledText = Maneuvers - #LOC_BDArmory_ManeuverRCS_disabledText = Combat - #LOC_BDArmory_MinObstacleMass = Min obstacle mass - #LOC_BDArmory_PreferredBroadsideDirection = Preferred broadside direction - #LOC_BDArmory_GoesUp = Goes up to - #LOC_BDArmory_GoesUp_enabledText = eleven - #LOC_BDArmory_GoesUp_disabledText = ten + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_miscHelp = Misc Settings - These control non-combat behaviors that don't fit elsewhere. + #LOC_BDArmory_AIWindow_infolink_Pilot_orbitHelp = Orbit Direction - This is the direction the AI will orbit when idling, either Clockwise or Counterclockwise. + #LOC_BDArmory_AIWindow_infolink_Pilot_standbyHelp = Standby Toggle - If enabled, the AI will automatically turn on when targets enter its Guard Range. + + // Surface AI + #LOC_BDArmory_AIWindow_infolink_Surface_Type = 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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_Drift = Max Drift - This sets the maximum the vessel will veer off prograde when turning. + #LOC_BDArmory_AIWindow_infolink_Surface_Bank = Bank Angle - This tells the AI the maximum it should allow the vessel to bank or heel over during maneuvers. + #LOC_BDArmory_AIWindow_infolink_Surface_Weave = Weave Factor - When under fire or a missile is incoming, the craft tries weaving to throw off the attacker's aim. This controls the strength of that weaving. + #LOC_BDArmory_AIWindow_infolink_Surface_SteerPower = 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_AIWindow_infolink_Surface_SteerDamping = 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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_RCS = RCS - The RCS setting toggles whether the AI should use RCS thrusters to assist in maneuvering. + #LOC_BDArmory_AIWindow_infolink_Surface_AvoidMass = Mass Avoidance - This sets the minimum mass that an obstace needs to be in order to avoid it instead of ramming it. + #LOC_BDArmory_AIWindow_infolink_Surface_MaintainMinRange = Maintain Min Range - This toggles if a Land vehicle will come to a stop/reverse to maintain distance from a target vs veering away to circle or extend. + #LOC_BDArmory_AIWindow_infolink_Surface_Altitude = Combat Altitude - The depth for the submarines to cruise / engage at. + + // VTOL AI + #LOC_BDArmory_AIWindow_infolink_VTOL_PID = 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.\n - 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.\n - 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.\n - Steer Damping (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_infolink_VTOL_Altitudes = Default Altitude - The altitude (AGL) to cruise at when outside of combat.\nMin and Max Altitudes - The minimum and maximum altitudes the AI will try to fly to. Outside of this range, the AI will either climb or dive to return to the range of altitudes. + #LOC_BDArmory_AIWindow_infolink_VTOL_Speeds = Max and Combat Speeds - The maximum speed for maneuvering and the target speed for combat. + #LOC_BDArmory_AIWindow_infolink_VTOL_Control = Max Pitch and Bank Angles - This tells the AI the maximum it should allow the vessel to pitch or bank (roll) over during maneuvers.\nBroadside Dir - Broadside Direction tells the AI whether it should favor presenting the Port or Starboard broadside towards the target.\nRCS - Whether to use RCS thrusters for maneuvering or only when in combat. + #LOC_BDArmory_AIWindow_infolink_VTOL_Combat = Weave Factor - When under fire or a missile is incoming, the craft tries weaving to throw off the attacker's aim. This controls the strength of that weaving.\nMin, 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. + + // Orbital AI + #LOC_BDArmory_AIWindow_infolink_Orbital_PID = The PID (Proportional Integral Derivative) Controller calculates the difference between desired and actual output, then applies corrections based on P, I, and D values. The stock KSP SAS control steers the craft (Inactive) unless the PID control is enabled when aiming and firing weapons (Firing) or all maneuevers (Everything).\n - 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.\n - 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.\n - Steer Damping (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.\n - Max Error - This sets a clamp on the max angle error used by the PID controller. Lowering this decreases overshoot, but the overall response will be slower. Raising this value will make the overall response faster, but increase overshoot unless the PID is well-tuned. If you have a well-tuned PID, adjusting this value can help limit overshoot or increase your overall response speed. + #LOC_BDArmory_AIWindow_infolink_Orbital_Combat = 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 which direction it should present towards the target, this is only active when PID control is active. When not attacking, the AI will roll to present this side to the target.\n\nMin Engagement Range - This sets the minimum distance from the target the AI will attempt to maneuver to be within in order to engage.\n\nZero Throttle Firing Range - Within this distance from the target (but outside of Min Engagement Range) the AI will always fire weapons without manipulating throttle. + #LOC_BDArmory_AIWindow_infolink_Orbital_Speeds = Maneuver, Firing, and Angular Speeds - Speed at which the AI will maneuver at, and firing speed and angular speed limits during combat. + #LOC_BDArmory_AIWindow_infolink_Orbital_Control = RCS - Whether to use RCS to manage velocity relative to the target when firing and whether to use RCS thrusters for maneuvering or only when in combat.\n\nReverse Engines - If enabled, AI will use backwards engines when able. Automatically disabled if no reverse engines are on the craft. + #LOC_BDArmory_AIWindow_infolink_Orbital_Evasion = Min Evasion Time, Distance Threshold, Time Threshold, Min Range Threshold - 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 Min Range Threshold sets the range that the attacker has to be beyond to trigger evasion.\nThe RCS Evasion and Thrust Evasion toggles determine whether the AI attempts to evade gunfire using RCS and/or engine thrust.\nThe Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes. + + // Missile Config #LOC_BDArmory_DeployAltitude = Deploy Altitude #LOC_BDArmory_EngageRangeMin = Engage Range Min #LOC_BDArmory_EngageRangeMax = Engage Range Max @@ -337,30 +1343,69 @@ Localization #LOC_BDArmory_MaxOffBoresight = Max Off Boresight #LOC_BDArmory_DetonationDistanceOverride = Detonation Distance Override #LOC_BDArmory_DetonateAtMinimumDistance = Detonate At Min Dist + #LOC_BDArmory_UseStaticMaxLaunchRange = Dynamic/Static Max Range + #LOC_BDArmory_ProximityTriggerDistance = Warhead Detonation Dist + #LOC_BDArmory_clustermissileTriggerDistance = Submunition Launch Distance #LOC_BDArmory_DropTime = Drop Time - #LOC_BDArmory_InCargoBay = In Cargo Bay:\u0020 + #LOC_BDArmory_InCargoBay = In Stock Cargo Bay:\u0020 + #LOC_BDArmory_InCustomCargoBay = Custom Bay Toggle:\u0020 + #LOC_BDArmory_DeployableWeapon = Deploy Wep Toggle:\u0020 #LOC_BDArmory_DetonationTime = Detonation Time #LOC_BDArmory_BallisticOvershootFactor = Ballistic Overshoot factor #LOC_BDArmory_BallisticAnglePath = Ballistic Angle path + #LOC_BDArmory_Missile_CruiseSpeed = Cruise Speed #LOC_BDArmory_CruiseAltitude = Cruise Altitude #LOC_BDArmory_CruisePredictionTime = Cruise prediction time + #LOC_BDArmory_CruisePopup = Cruise Popup Attack #LOC_BDArmory_GPSTarget = GPS Target - #LOC_BDArmory_ChangetoLowAltitudeRange = Change to Low Altitude Range + #LOC_BDArmory_ChangetoLowAltitudeRange = Change to High Altitude Range #LOC_BDArmory_MaxAltitude = Max Altitude #LOC_BDArmory_TerminalGuidance = Terminal Guidance:\u0020 #LOC_BDArmory_Direction = Direction:\u0020 #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 + #LOC_BDArmory_OrdnanceAvailable = Ordnance Available #LOC_BDArmory_MissileAssign = Missile Assign #LOC_BDArmory_CurrentLocks = Current Locks + #LOC_BDArmory_Offset = Ordnance Offset + #LOC_BDArmory_Deploy_Time = Deploy Time + + // Safety Systems + #LOC_BDArmory_SSTank = Self-Sealing Tank + #LOC_BDArmory_SSTank_On = Add Self-Sealing Tank + #LOC_BDArmory_SSTank_Off = Remove Self-Sealing Tank + #LOC_BDArmory_CASE = C.A.S.E. Tier + #LOC_BDArmory_CASE_Sim = Detonation Sim + #LOC_BDArmory_FireBottles = Fire Bottles + #LOC_BDArmory_FB_Remaining = Fire Bottles Left + #LOC_BDArmory_FIS = Fuel Inerting System + #LOC_BDArmory_FIS_On = Add Fuel Inerting System + #LOC_BDArmory_FIS_Off = Remove Fuel Inerting System + #LOC_BDArmory_Armorcockpit_On = Add Armored Cockpit + #LOC_BDArmory_Armorcockpit_Off = Remove Armored Cockpit + #LOC_BDArmory_AddedCost = Added cost + #LOC_BDArmory_AddedMass = Safety Systems Mass + #LOC_BDArmory_DryMass = Dry mass + // Turret Config #LOC_BDArmory_MaxPitch = Max Pitch #LOC_BDArmory_MinPitch = Min Pitch #LOC_BDArmory_YawRange = Yaw Range + #LOC_BDArmory_YawStandbyAngle = Yaw Standby Angle #LOC_BDArmory_FireLimits = Fire Limits #LOC_BDArmory_FireLimits_disabledText = None #LOC_BDArmory_FireLimits_enabledText = In range @@ -369,13 +1414,13 @@ Localization #LOC_BDArmory_MaxDetonationRange = Max Detonation Range #LOC_BDArmory_Barrage = Barrage #LOC_BDArmory_ToggleBarrage = Toggle Barrage + #LOC_BDArmory_AimOverrideFalse = Aim With This Weapon + #LOC_BDArmory_AimOverrideTrue = Revert Default Aim #LOC_BDArmory_ReturnTurret = Return Turret #LOC_BDArmory_ToggleAnimation = Toggle Animation + #LOC_BDArmory_TurretID = Custom Turret ID - #LOC_BDArmory_NextTankSetup = Next tank setup - #LOC_BDArmory_PreviousTankSetup = Previous tank setup - #LOC_BDArmory_NextTexture = Next Texture - + // Missile UI #LOC_BDArmory_FireMissile = Fire Missile #LOC_BDArmory_Detonate = Detonate #LOC_BDArmory_Resupply = Resupply @@ -384,6 +1429,8 @@ Localization #LOC_BDArmory_ToggleTurret = Toggle Turret #LOC_BDArmory_TurretEnabled = Turret Enabled #LOC_BDArmory_AutoReturn = Auto-Return + #LOC_BDArmory_TurretLoft = Lofted Aimpoint + #LOC_BDArmory_TurretLoftFac = Loft Velocity Factor #LOC_BDArmory_MissileTurretFireFOV = Fire FOV #LOC_BDArmory_HideUI = Hide Weapon Name UI #LOC_BDArmory_ShowUI = Set Weapon Name UI @@ -391,26 +1438,33 @@ Localization #LOC_BDArmory_SetWeaponGroupUI = Set Weapon Group UI #LOC_BDArmory_Fire = Fire #LOC_BDArmory_ToggleRadar = Toggle Radar + #LOC_BDArmory_ToggleIRST = Toggle IRST + #LOC_BDArmory_DynamicRadar = Disable Radar vs ARMs + // WM Config #LOC_BDArmory_GuardMode = Guard Mode:\u0020 #LOC_BDArmory_Team = Team + #LOC_BDArmory_Allies = Allies #LOC_BDArmory_Weapon = Weapon + #LOC_BDArmory_FiringPriority = Selection Priority + #LOC_BDArmory_weaponChannel = Weapon Channel #LOC_BDArmory_FiringInterval = Firing Interval - #LOC_BDArmory_FiringBurstLength = Firing Burst Length + #LOC_BDArmory_FiringBurstLength = Firing Burst Length (Time) + #LOC_BDArmory_FiringBurstCount = Firing Burst Length (Count) #LOC_BDArmory_FiringTolerance = Firing Angle #LOC_BDArmory_FieldOfView = Field of View #LOC_BDArmory_VisualRange = Visual Range #LOC_BDArmory_GunsRange = Guns Range - #LOC_BDArmory_MissilesORTarget = Missiles/Target + #LOC_BDArmory_MissilesRange = Use Dynamic Launch Zone + #LOC_BDArmory_MissilesOnTarget = Missiles/Target + #LOC_BDArmory_FireAngleOverride_Enable = Enable Firing Angle Override + #LOC_BDArmory_FireAngleOverride_Disable = Disable Firing Angle Override + #LOC_BDArmory_BurstLengthOverride_Enable = Enable Burst Length Override + #LOC_BDArmory_BurstLengthOverride_Disable = Disable Burst Length Override + #LOC_BDArmory_FiringAngle = Firing Angle - #LOC_BDArmory_false = False - #LOC_BDArmory_true = True - - #LOC_BDArmory_AddedCost = Added cost - #LOC_BDArmory_DryMass = Dry mass - #LOC_BDArmory_Enabled = Enabled - #LOC_BDArmory_Disabled = Disabled - #LOC_BDArmory_Enable = Enable + #LOC_BDArmory_dynamic = Dynamic + #LOC_BDArmory_static = Static #LOC_BDArmory_Status = Status #LOC_BDArmory_Toggle = Toggle @@ -419,7 +1473,6 @@ Localization #LOC_BDArmory_ShowGroupEditor_disabledText = open Group GUI #LOC_BDArmory_DeactivationDepth = Deactivation Depth #LOC_BDArmory_Hitpoints = Hitpoints - #LOC_BDArmory_ArmorThickness = Armor Thickness #LOC_BDArmory_FireCountermeasure = Fire Countermeasure #LOC_BDArmory_TogglePilot = Toggle Pilot @@ -429,16 +1482,131 @@ Localization #LOC_BDArmory_SelectTeam = Select Team #LOC_BDArmory_OpenGUI = Open GUI - //ammo switch + #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 = Armour-piercing - #LOC_BDArmory_Ammo_Flak = Flak + #LOC_BDArmory_Ammo_AP = Armor-Piercing + #LOC_BDArmory_Ammo_SAP = Semi-Armor-Piercing + #LOC_BDArmory_Ammo_Flak = Proximity #LOC_BDArmory_Ammo_Explosive = Explosive #LOC_BDArmory_Ammo_HE = High-Explosive #LOC_BDArmory_Ammo_Shaped = Shaped Charge #LOC_BDArmory_Ammo_Kinetic = Kinetic + #LOC_BDArmory_Ammo_EMP = E.M.P. + #LOC_BDArmory_Ammo_Choker = Choker + #LOC_BDArmory_Ammo_Impulse = Impulse + #LOC_BDArmory_Ammo_Gravitic = Gravitic + #LOC_BDArmory_Ammo_Incendiary = Incendiary + #LOC_BDArmory_Ammo_Nuclear = Nuclear + #LOC_BDArmory_Ammo_Beehive = Beehive + #LOC_BDArmory_NextTankSetup = Next tank setup + #LOC_BDArmory_PreviousTankSetup = Previous tank setup + + // Team Icons + #LOC_BDArmory_Icons_title = BDArmory Team UI Icons + #LOC_BDArmory_Icons_PSA = Press F4 to toggle stock vessel icons + #LOC_BDArmory_Enable_Icons = Enable Team Icons + #LOC_BDArmory_Icon_show_self = Show Self + #LOC_BDArmory_Icon_teams = Enable Team Labels + #LOC_BDArmory_Icon_names = Enable Vessel Labels + #LOC_BDArmory_Icon_score = Enable Score + #LOC_BDArmory_Icon_healthbars = Enable Healthbars + #LOC_BDArmory_Icon_missiles = Missile Icons + #LOC_BDArmory_Icon_missile_text = Missile Labels + #LOC_BDArmory_Icon_debris = Debris Icons + #LOC_BDArmory_Icon_persist = Do not hide with UI + #LOC_BDArmory_Icon_threats = Vessel Threat Indicators + #LOC_BDArmory_Icon_pointers = Offscreen Icon Pointers + #LOC_BDArmory_Icon_scale = Icon Scale: + #LOC_BDArmory_Icon_opacity = Opacity: + #LOC_BDArmory_Icon_distance_threshold = Distance Threshold: + #LOC_BDArmory_Icon_max_distance_threshold = Max Distance Threshold: + #LOC_BDArmory_Icon_telemetry = Enable Telemetry + #LOC_BDArmory_Icon_StoreTeamColors = Store Team Colors + #LOC_BDArmory_Icon_colorget = Apply + + // 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_RAMFoam = Radar Absorbent Foam + #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 + #LOC_BDArmory_ArmorDuctility = Ductility + #LOC_BDArmory_ArmorDiffusivity = Diffusivity + #LOC_BDArmory_ArmorMaxTemp = Safe Temp + #LOC_BDArmory_ArmorDensity = Density + #LOC_BDArmory_ArmorMass = Armor Mass + #LOC_BDArmory_ArmorCost = Cost + #LOC_BDArmory_ArmorCurrent = Current Armor + #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_partTreeVisualizer = Toggle Part Tree Visualizer + #LOC_BDArmory_checkVessel = Check Vessel Legality + #LOC_BDArmory_ArmorSelect = Select Armor Material + #LOC_BDArmory_DryMassWhitelist = Resources Counted as Drymass + #LOC_BDArmory_ArmorTool = BDA Craft Utilities Tool + #LOC_BDArmory_Armor_HullMat = Current Hull Material + #LOC_BDArmory_Armor_ArmorType = Armor Type + #LOC_BDArmory_Armor_Hullmass = Adjusted Part Mass + #LOC_BDArmory_BulletResist = Kinetic Resist. + #LOC_BDArmory_ExplosionResist = Explosive Resist. + #LOC_BDArmory_LaserResist = Laser Resist. + #LOC_BDArmory_ArmorShatterWarning = Penetrating hit will shatter armor + #LOC_BDArmory_ArmorToolPartCount = Part Count Exceeded! + #LOC_BDArmory_ArmorToolEngineCount = Too Many Engines: + #LOC_BDArmory_ArmorToolEngineCountFloor = Too Few Engines: + #LOC_BDArmory_ArmorToolTWR = TWR Exceeded: + #LOC_BDArmory_ArmorToolLTW = LTW Exceeded: + #LOC_BDArmory_ArmorToolMaxMass = Mass Limit Exceeded: + #LOC_BDArmory_ArmorToolMaxPoints = Point Limit Exceeded: + #LOC_BDArmory_ArmorToolIllegalParts = Illegal Parts: + #LOC_BDArmory_ArmorToolNonCockpit = not attached to cockpit + #LOC_BDArmory_ArmorToolOversizedPWings = pWings exceeding max Lift - check Lift Visualizer + #LOC_BDArmory_ArmorToolVesselLegal = Vessel legal! + + + // Missile & CM Settings + #LOC_BDArmory_Settings_MissileCMToggle = Show Missile & Countermeasure Settings + #LOC_BDArmory_Settings_AspectedRCS = Real-Time Aspected RCS + #LOC_BDArmory_Settings_AspectedRCSOverallRCSWeight = Overall RCS Weight + #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 + + // Texture switching + #LOC_BDArmory_NextTexture = Next Texture } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ja.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ja.cfg new file mode 100644 index 000000000..de4daf2ee --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ja.cfg @@ -0,0 +1,1578 @@ +// Notes: +// - The indentation provides fold region info for IDEs. +// - #LOC_A = #LOC_B is valid. +// - Check for duplicates with: grep -o '^\s*#LOC[^ ]\+' en-us.cfg |tr -d ' '|sort|uniq -c|grep -v '\s1' +// - Propagate changes in en-us.cfg to the other localisation files by running 'python3 ../_Other\ Stuff/localisation_organisation_sync.py' in the BDArmory/BDArmory folder. + +Localization +{ + ja + { + // Generic + #LOC_BDArmory_Generic_OK = 完了 + #LOC_BDArmory_Generic_Cancel = キャンセル + #LOC_BDArmory_Generic_New = 新規作成 + #LOC_BDArmory_Generic_On = オン + #LOC_BDArmory_Generic_Off = オフ + #LOC_BDArmory_On = オン + #LOC_BDArmory_Off = オフ + #LOC_BDArmory_Generic_Hide = 非表示 + #LOC_BDArmory_Generic_Show = 表示 + #LOC_BDArmory_Generic_Load = ロード + #LOC_BDArmory_Generic_Save = 保存 + //#LOC_BDArmory_Generic_Reload = ??? Reload + #LOC_BDArmory_Generic_Help = ヘルプ + #LOC_BDArmory_Generic_Select = 選択 + #LOC_BDArmory_Generic_SaveandClose = 保存 + #LOC_BDArmory_VesselStatus_Landed = (着陸) + #LOC_BDArmory_VesselStatus_Splashed = (スプラッシュ) + #LOC_BDArmory_VesselStatus_Underwater = (水中) + #LOC_BDArmory_false = 無効 + #LOC_BDArmory_true = 有効 + #LOC_BDArmory_Enabled = 有効 + #LOC_BDArmory_Disabled = 無効 + #LOC_BDArmory_Enable = 有効 + #LOC_BDArmory_Disable = 無効 + + // WM Window + #LOC_BDArmory_WMWindow_title = 武器マネージャー + #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_barrageStagger = ばらつき + #LOC_BDArmory_WMWindow_rippleText3 = 発射速度: <<1>> RPM + #LOC_BDArmory_WMWindow_rippleText4 = 発射速度: オフ + #LOC_BDArmory_WMWindow_ListWeapons = 武器 + #LOC_BDArmory_WMWindow_GuardMenu = ガードモード + #LOC_BDArmory_WMWindow_ModulesToggle = モジュール + #LOC_BDArmory_WMWindow_NoWeaponManager = 武器マネージャーが見つかりません + + // WM Guard Menu + #LOC_BDArmory_WMWindow_NoneWeapon = なし + #LOC_BDArmory_WMWindow_GuardMode = ガードモード <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = 発射間隔 + #LOC_BDArmory_WMWindow_BurstLength = バーストの長さ + #LOC_BDArmory_WMWindow_FiringTolerance = 発射角度 + #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 = すべてのターゲット + // Advanced Targeting + #LOC_BDArmory_Settings_Adv_Targeting = 高度なターゲッティング + #LOC_BDArmory_Selecttargeting = ターゲッティングオプションを選択 + #LOC_BDArmory_targetSetting = ターゲッティング + #LOC_BDArmory_TargetCOM = CoM + #LOC_BDArmory_Weapons = 武器 + #LOC_BDArmory_Engines = エンジン + #LOC_BDArmory_Command = コクピット + #LOC_BDArmory_Mass = 最も重いパーツ + #LOC_BDArmory_Random = ランダムなパーツ + + // WM Target Priority + #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 = 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を守る + + // WM Modules + #LOC_BDArmory_WMWindow_RadarWarning = レーダー警告受信機 + #LOC_BDArmory_WMWindow_GPSCoordinator = GPSコーディネーター + #LOC_BDArmory_WMWindow_WingCommand = 飛行コマンド + // WM GPS Module + #LOC_BDArmory_WMWindow_GPSTarget = GPSターゲット + #LOC_BDArmory_WMWindow_NoTarget = ターゲットなし + + // WM infolink + // WM infolink Weapons + #LOC_BDArmory_WMWindow_Weapons_Desc = 武器 - このタブには、機体上のすべての武器/武器グループが表示されます。武器(グループ)名をクリックすると、その武器を選択して有効にし、銃、ロケット、レーザーの場合は手動で発射できるようになります。ミサイル兵器の場合、ミサイルを発射する前に、「武装解除」を「武装」に切り替える必要があります + #LOC_BDArmory_WMWindow_Ripple_Salvo_Desc = ミサイルを選択すると、発射速度の設定が表示されます。これは、トリガーが押された場合にミサイルを順次発射する速度を設定します。発射速度が毎分1500発未満の銃、ロケット、またはレーザーには、代わりに連射/無制限の切り替えがあります。同じタイプ/武器グループの複数の武器が存在する場合、連射モードでは各武器が順番に発射されます。無制限モードでは、すべての武器が同時に発射されます + + // WM infolink Guard Menu + #LOC_BDArmory_WMWindow_GuardTab_Desc = ガード - この設定は、AIが武器を使用する方法とタイミングを制御します + #LOC_BDArmory_WMWindow_FiringInterval_Desc = 発射間隔 - これは、AIがターゲットをスキャンする頻度を秒単位で設定し、選択した武器を発射する速度に変換されます + #LOC_BDArmory_WMWindow_BurstLength_desc = バーストの長さ - 選択した武器をAIが発射し続ける時間を秒単位で制御します。0の場合、AIは0.5秒間発射します + #LOC_BDArmory_WMWindow_FiringTolerance_desc = 発射角度 - これは、AIがターゲット上にあると見なし、武器を発射するタイミングを制御します。角度が1の場合、ターゲットは、ターゲット半径の幅に選択した武器の広がりを加えたものに等しいターゲットコーン内にある必要があります。より正確な武器はターゲットコーンが狭くなり、その逆も同様です。角度2は、ターゲット半径などの幅の2倍のターゲットコーンです。ターゲットがこのターゲットコーン内にある場合、AIは現在選択されている武器を発射します + #LOC_BDArmory_WMWindow_FieldofView_desc = 視野 - AIの視野を制御します。360°は、すべての方向を見ることができることを意味します。360°未満の値は、AIがこの幅の円錐内のターゲットしか認識できないことを意味します + #LOC_BDArmory_WMWindow_VisualRange_desc = 視認範囲 - これは、AIが見ることができる距離です。この値よりも近いターゲットが表示され、攻撃するために移動します。この値以外のターゲットは、レーダーによって発見する必要があります + #LOC_BDArmory_WMWindow_GunsRange_desc = 銃の射程 - 機体上のすべての主砲、ロケット弾、レーザーの最大武器射程を設定します。デフォルトでは、これは航空機に搭載されている最長距離の非ミサイル兵器に設定されています。AIは、この範囲外の機体への発砲を試みません + #LOC_BDArmory_WMWindow_MultiTargetNum_desc = 最大砲塔ターゲット数 - 複数の砲塔を持つ機体の場合、砲塔が独立してターゲットを絞って攻撃できるターゲット数を設定し、機体が複数のターゲットと同時に交戦できるようにします + #LOC_BDArmory_WMWindow_MultiMissileTgtNum_desc = 最大ミサイルターゲット数 - 複数のミサイルを搭載した機体の場合、AIがミサイルで攻撃しようとする異なるターゲット数を設定し、現在のターゲットで指定された数のミサイルが発射されると新しいターゲットに切り替えます + #LOC_BDArmory_WMWindow_MissilesTgt_desc = ターゲット当たりのミサイル数 - AIがターゲットを発射するミサイル数を設定します。この数のミサイルが発射されると、AIは、以前に発射されたミサイルが命中するか、破壊された場合にのみ、追加のミサイルを発射します + #LOC_BDArmory_WMWindow_TargetType_desc = 高度なターゲティングボタン - これにより、AIのカスタムターゲティング設定を設定し、重心をターゲットにする代わりに、武器、エンジン、コマンドポッド、最も重い部品、またはこれらの組み合わせをターゲットにすることができます + #LOC_BDArmory_WMWindow_EngageType_desc = 交戦オプションボタン - 機体上のすべての武器の武器交戦オプション(空中、地上、ミサイル、SLW)を一度にすばやく設定できます + + // WM infolink Target Priority + #LOC_BDArmory_WMWindow_Prioritues_Desc = ターゲットの優先度 - このタブでは、AIのターゲット設定の動作を設定します + #LOC_BDArmory_WMWindow_targetBias_desc = ターゲットの優先度 - これは、AIが他の新しいターゲットよりも現在のターゲットをどれだけ優先するかを設定します。値が大きいほど、現在のターゲットに対するAIの優先度が大きくなります + #LOC_BDArmory_WMWindow_targetPreference_desc = ターゲット攻撃設定 - AIの優先ターゲットタイプを設定します。値が小さいほど、AIは地上のターゲットを好むようになり、値が大きいほど、空中ターゲットの優先度が高くなります + #LOC_BDArmory_WMWindow_targetProximity_desc = ターゲット距離 - AIの優先ターゲット距離を設定します。より近いターゲットを優先するには高く設定し、より遠いターゲットを優先するには低く設定します + #LOC_BDArmory_WMWindow_targetAngletoTarget_desc = ターゲットに対する角度 - これは、機体のプログレードベクトルからより近い角度にあるターゲットに対するAIの好みに重みを付けます。値が大きいほど、航空機の真正面にあるターゲットへの重みが大きくなります + #LOC_BDArmory_WMWindow_targetAngleDist_desc = 角度/距離 - これは、航空機のプログレードからのターゲットの角度に基づいて、ターゲットに対するAIの好みをターゲット距離で割った重み付けします。値が大きいほど、クラフトの前と近くのターゲットが優先され、値を低くするとその逆が優先されます + #LOC_BDArmory_WMWindow_targetAccel_desc = ターゲット加速度 - 加速度の速いターゲットに対して高い値重みターゲティングを優先し、遅いターゲットに対して低い値を設定します + #LOC_BDArmory_WMWindow_targetClosingTime_desc = 終了時間の短縮 - 値が大きいほど、ターゲットの選択は、最も早く到達できるよりもターゲットに重み付けされ、低い値は、飛行時間が長いターゲットに向かって重み付けされます + #LOC_BDArmory_WMWindow_targetgunNumber_desc = 武器順位 - ターゲットの武器数に対するターゲティングの優先順位を重み付けします。値が大きいほど、より多くの武器を持つターゲットが優先され、値が低いほど、より少ない武器が優先されます + #LOC_BDArmory_WMWindow_targetMass_desc = 目標の質量 - これは、より重いまたはより軽い機体へのターゲティングの優先順位を重み付けします。より大きなターゲット質量に向かって重みを付けます + #LOC_BDArmory_WMWindow_targetDmg_desc = ターゲットダメージ - ターゲットが被ったダメージ量に基づいてターゲティング設定に重みを付けます。値が大きいほど、残りのターゲットヘルスが少なくなります + #LOC_BDArmory_WMWindow_targetAllies_desc = 攻撃する味方が少ない - これは、現在味方の攻撃を受けていない機体に対するターゲット設定の重み付けです。高い値は未交戦の機体を優先し、低い値は味方が交戦している機体を優先します + #LOC_BDArmory_WMWindow_targetThreat_desc = ターゲットの脅威 - 高い値は、現在この機体を撃っている機体に対してターゲティングを優先し、低い値は攻撃している機体を無視する方向に重みを付けます + #LOC_BDArmory_WMWindow_targetVIP_desc = VIPを攻撃/VIPを守る。これらの重みターゲティングは、敵のVIPターゲットを攻撃すること、または高い値に設定されている場合は味方VIPと交戦しているターゲットを攻撃し、低い値に設定されている場合はこれらのターゲットを無視します + + // Settings Window + #LOC_BDArmory_Settings_Title = 設定 + #LOC_BDArmory_Settings_AdvancedUserSettings = 高度なユーザー設定 + // Section Toggles + #LOC_BDArmory_Settings_GeneralSettingsToggle = ゲーム設定 + #LOC_BDArmory_Settings_GraphicsSettingsToggle = グラフィック/UI設定 + #LOC_BDArmory_Settings_SliderSettingsToggle = 動作設定 + #LOC_BDArmory_Settings_RadarSettingsToggle = レーダー設定 + #LOC_BDArmory_Settings_GameModesSettingsToggle = ゲームモード + #LOC_BDArmory_Settings_OtherSettingsToggle = その他の設定 + #LOC_BDArmory_Settings_CompSettingsToggle = 対戦設定 + #LOC_BDArmory_Settings_GMSettingsToggle = GM設定 + + // Graphics / UI + #LOC_BDArmory_Settings_DebugSettingsToggle = デバッグ + #LOC_BDArmory_Settings_AIToolbarButton = AIツールバー + #LOC_BDArmory_Settings_VMToolbarButton = VMツールバー + //#LOC_BDArmory_Settings_UIScale = ??? UI Scale + //#LOC_BDArmory_Settings_UIScaleFollowsStock = ??? Follow Stock + #LOC_BDArmory_Settings_Instakill = 即時破壊 + #LOC_BDArmory_Settings_InfiniteAmmo = 無限弾薬 + #LOC_BDArmory_Settings_InfiniteMissiles = 無限兵器 + //#LOC_BDArmory_Settings_InfiniteCountermeasures = ??? Infinite Countermeasures + #LOC_BDArmory_Settings_BulletFX = 銃弾FX + #LOC_BDArmory_Settings_BulletHits = 銃弾の命中 + //#LOC_BDArmory_Settings_WaterHitFX = ??? Water Hit FX + //#LOC_BDArmory_Settings_LightFX = ??? Light FX + //#LOC_BDArmory_Settings_PerfOptions = ??? Enable FX + #LOC_BDArmory_Settings_EjectShells = シェルの排出 + #LOC_BDArmory_Settings_VesselRelativeBulletChecks = 機体関連の銃弾チェック + #LOC_BDArmory_Settings_AimAssist = エイムアシスト + #LOC_BDArmory_Settings_AimAssistMode_Target = エイムアシストモード(ターゲット) + #LOC_BDArmory_Settings_AimAssistMode_Aimer = エイムアシストモード(エイム) + #LOC_BDArmory_Settings_GUIBackgroundOpacity = 背景の不透明度 + #LOC_BDArmory_Settings_DrawAimers = 照準を描画 + + // Debugging + #LOC_BDArmory_Settings_DebugTelemetry = 遠隔スクリーン + #LOC_BDArmory_Settings_DebugLines = デバッグライン + #LOC_BDArmory_Settings_DebugAI = AI + #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_ResetScrollZoom = ズームのリセット + + // Gameplay FIXME These need more sorting + #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 = 永続的なFX + #LOC_BDArmory_Settings_DisableKillTimer = キルタイマーを無効化 + #LOC_BDArmory_Settings_TraceVessels = ベッセルパスのトレースの自動有効化 + #LOC_BDArmory_Settings_TraceVesselsManualStart = 追跡を開始 + #LOC_BDArmory_Settings_TraceVesselsManualStop = 追跡を終了 + //#LOC_BDArmory_Settings_AutoLogTimeSync = ??? Auto-Enable Time-Sync Logging + //#LOC_BDArmory_Settings_LogTimeSyncInterval = ??? Time-Sync Log Interval + //#LOC_BDArmory_Settings_LogTimeSyncStart = ??? Start Logging + //#LOC_BDArmory_Settings_LogTimeSyncStop = ??? Stop Logging + #LOC_BDArmory_Settings_ShowEditorSubcategories = サブカテゴリのエディタを表示 + #LOC_BDArmory_Settings_AutocategorizeParts = パーツの自動分類 + #LOC_BDArmory_Settings_waterDrag = 水中弾丸ドラッグ + #LOC_BDArmory_Settings_AutoLoadToKSC = KSCを自動でロード + #LOC_BDArmory_Settings_GenerateCleanSave = クリーンセーブを作成する + #LOC_BDArmory_Settings_AutoDisableUI = UIの自動無効化 + #LOC_BDArmory_Settings_AutoResumeTournaments = 自動でトーナメントを再開 + //#LOC_BDArmory_Settings_AutoResumeContinuousSpawn = ??? Auto-Resume Cts Spawn + #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_DisableRamming = ラミングを無効化 + #LOC_BDArmory_Settings_DefaultFFATargeting = デフォルトのFFAターゲッティング + #LOC_BDArmory_Settings_TagMode = タグモード + #LOC_BDArmory_Settings_PaintballMode = ペイントボールモード + #LOC_BDArmory_Settings_DumbIRSeekers = フレア除去を無効化 + #LOC_BDArmory_Settings_RunwayProject = 滑走路プロジェクト + //#LOC_BDArmory_Settings_CompChecks = ??? Use AI/WM Overrides + #LOC_BDArmory_Settings_RunwayProjectRound = 滑走路プロジェクトラウンド + #LOC_BDArmory_Settings_BattleDamage = 戦闘ダメージ + #LOC_BDArmory_Settings_GravityHacks = 死亡時の重力の増加 + #LOC_BDArmory_Settings_AutoEnableVesselSwitching = 機体切り替えの自動有効化 + #LOC_BDArmory_Settings_AutonomousCombatSeats = 自動戦闘席 + #LOC_BDArmory_Settings_DestroyWMWhenNotControlled = 制御されていないWMを破棄 + #LOC_BDArmory_Settings_DisplayCompetitionStatus = 対戦状況の表示 + #LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI = UIを非表示でステータスを表示 + #LOC_BDArmory_Settings_CameraSwitchIncludeMissiles = カメラの切り替え: ミサイルを含む + #LOC_BDArmory_Settings_ScrollZoomPrevention = スクロールズームの防止 + #LOC_BDArmory_Settings_ResetHP = パーツの最大HPをリセット + #LOC_BDArmory_Settings_ResetArmor = パーツの装甲をリセット + #LOC_BDArmory_Settings_ResetHull = 消耗品のリセット + #LOC_BDArmory_Settings_RestoreKAL = KALを戻す + //#LOC_BDArmory_Settings_DisableGuardModeOnSpawn = ??? Disable Guard Mode on Spawn + #LOC_BDArmory_Settings_IntakeHack = インテークをハック + #LOC_BDArmory_Settings_PWingsHack = プウィングエッジリフト + #LOC_BDArmory_Settings_PWingsThickHP = プウィングの厚さベースの質量/HP + #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_gungame_progression = ??? Keep progresson on respawn + //#LOC_BDArmory_settings_gungame_cycle = ??? Cycle List + // General Sliders + #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 = B.D.爆発倍数 + #LOC_BDArmory_Settings_ArmorExplosivePenetrationResistanceMultiplier = 装甲爆発耐性倍数 + #LOC_BDArmory_Settings_BuildingDamageMultiplier = 建物ダメージ倍数 + #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = 即時ダメージ倍数 + #LOC_BDArmory_Settings_SecondaryEffectDuration = 特殊武器の効果持続時間 + #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = 弾道軌道、シム、倍数 + #LOC_BDArmory_Settings_ArmorMassMultiplier = 装甲質量倍数 + #LOC_BDArmory_Settings_DebrisCleanUpDelay = デブリ除去の遅延 + #LOC_BDArmory_Settings_NumericInputSelfUpdate = 数値入力の自己更新 + #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 = HPスケールのしきい値 + #LOC_BDArmory_Settings_HP_Clamp = 最大HP + //#LOC_BDArmory_Settings_Max_Armor_Limit = ??? Max Armor Limit + + // Game Modes + // Heart-Bleed + #LOC_BDArmory_Settings_HeartBleed = 心拍 + #LOC_BDArmory_Settings_HeartBleedRate = 心拍数 + #LOC_BDArmory_Settings_HeartBleedInterval = 心拍の間隔 + #LOC_BDArmory_Settings_HeartBleedThreshold = 心拍のしきい値 + + // Resource Steal + #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 = CM転送率 + + // Asteroids + #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 = 機体の広がりに従う + + // 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_MutatorGungame = ??? GunGame Progression + #LOC_BDArmory_Settings_MutatorTimed = タイマーで適用 + #LOC_BDArmory_Settings_MutatorDuration = 間隔 + #LOC_BDArmory_UI_MutatorStart = アクティブなグローバルミューテーター + #LOC_BDArmory_UI_MutatorShuffle = ミューテーターがシャッフルされました! + #LOC_BDArmory_Settings_MutatorNum = アクティブなミューテーターの数 + #LOC_BDArmory_Settings_MutatorIcons = ミューテーターのアイコンを表示 + + #LOC_BDArmory_Settings_WaypointsMode = ウェイポイントモード + //#LOC_BDArmory_Settings_GLimitsMode = ??? Override G-Force Limits + + // Battle Damage + #LOC_BDArmory_Settings_BDSettingsToggle = 戦闘ダメージの設定 + #LOC_BDArmory_Settings_BD_Proc = プロセスの頻度 + //#LOC_BDArmory_Settings_BD_Proc_Pen = ??? Proc Min Penetration + #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_JointStrength = 構造のダメージ + #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 = 戦闘ダメージを許可 + + // Radar / Other Settings + #LOC_BDArmory_Settings_RWRWindowScale = RWR画面の大きさ + #LOC_BDArmory_Settings_RadarWindowScale = レーダー画面の大きさ + #LOC_BDArmory_Settings_LogarithmicRWRDisplay = 対戦時のRWR画面 + #LOC_BDArmory_Settings_TargetWindowScale = Target画面の大きさ + #LOC_BDArmory_Settings_TargetWindowInvertMouse = マウスを反転(ターゲット画面) + #LOC_BDArmory_Settings_TriggerHold = トリガーをホールド + #LOC_BDArmory_Settings_UIVolume = UIの大きさ + #LOC_BDArmory_Settings_WeaponVolume = 武器の大きさ + //#LOC_BDArmory_Settings_IGNORE_TERRAIN_CHECK = ??? Detection Ignores Terrain + //#LOC_BDArmory_Settings_CHECK_WATER_TERRAIN = ??? Detection Checks Water + //#LOC_BDArmory_Settings_RADAR_NOTCHING = ??? Radar Notching + //#LOC_BDArmory_Settings_Notching_Factor = ??? Notch Effectiveness Factor + //#LOC_BDArmory_Settings_Notching_SCR_Factor = ??? Notch SCR Factor + + // Competition / Tournament + #LOC_BDArmory_Settings_CompetitionDistance = 対戦距離 + #LOC_BDArmory_Settings_CompetitionDuration = 対戦時間 + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparation = チームの分割 + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparationPerMember = / メンバー + #LOC_BDArmory_Settings_CompetitionFinalGracePeriod = 最終猶予期間 + #LOC_BDArmory_Settings_CompetitionInitialGracePeriod = 初期猶予期間 + #LOC_BDArmory_Settings_CompetitionKillTimer = 着陸キルタイマー + #LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay = 観戦者削除の遅延 + #LOC_BDArmory_Settings_CompetitionKillerGMFrequency = キラーGM周波数 + #LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod = キラーGM猶予期間 + #LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh = 高度の制限高 + #LOC_BDArmory_Settings_CompetitionAltitudeLimitLow = 高度の制限低 + //#LOC_BDArmory_Settings_CompetitionGMWeaponKill = ??? Kill Weaponless Craft + //#LOC_BDArmory_Settings_CompetitionGMEngineKill = ??? Kill Engineless Craft + //#LOC_BDArmory_Settings_CompetitionGMDisableKill = ??? Kill Disabled Craft + //#LOC_BDArmory_Settings_CompetitionGMHPKill = ??? Kill Damaged Craft + //#LOC_BDArmory_Settings_CompetitionGMKillDelay = ??? GM Kill Delay + #LOC_BDArmory_Settings_CompetitionStarting = 対戦開始中 + #LOC_BDArmory_Settings_DogfightCompetition = 対戦 + #LOC_BDArmory_Settings_StartCompetition = 対戦を開始 + #LOC_BDArmory_Settings_StopCompetition = 対戦を終了 + #LOC_BDArmory_Settings_StartCompetitionNow = すぐに対戦を開始 + #LOC_BDArmory_Settings_CompetitionStartNowAfter = すぐにコンプ遅延を開始 + #LOC_BDArmory_Settings_CompetitionStartDespiteFailures = コンプを開始 + #LOC_BDArmory_Settings_StartRapidDeployment = 素早く展開を開始 + #LOC_BDArmory_Settings_StartOrbitalDeployment = 軌道展開の開始 + #LOC_BDArmory_Settings_LowGravDeployment = 低重力離陸対戦の開始 + #LOC_BDArmory_Settings_EditInputs = 編集 + #LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart = 対戦開始時に設定を閉じる + #LOC_BDArmory_Settings_CompetitionWaypointTimeThreshold = ウェイポイント時間のしきい値 + + // BDA Remote (defunct) + #LOC_BDArmory_BDARemoteOrchestration_Title = 遠隔編成 + #LOC_BDArmory_Settings_RemoteLogging = 遠隔編成 + #LOC_BDArmory_Settings_RemoteInterheatDelay = Inter-heat Delay + #LOC_BDArmory_Settings_RemoteSync = 遠隔編成経由で実行 + #LOC_BDArmory_Settings_CompetitionID = 対戦ID + + // Input Settings + #LOC_BDArmory_InputSettings_Weapons = 武器 + #LOC_BDArmory_InputSettings_TargetingPod = ターゲッティングポッド + #LOC_BDArmory_InputSettings_Radar = レーダー + #LOC_BDArmory_InputSettings_VesselSwitcher = チームの管理 + #LOC_BDArmory_InputSettings_Tournament = トーナメント + #LOC_BDArmory_InputSettings_TimeScaling = Time Scaling + //#LOC_BDArmory_InputSettings_TemporarilyShowMouse = ??? Temporarily Show Mouse + #LOC_BDArmory_InputSettings_GUI = GUI + #LOC_BDArmory_InputSettings_BackBtn = 戻る + #LOC_BDArmory_InputSettings_recordedInput = キーまたはボタンを押します + #LOC_BDArmory_InputSettings_SetKey = キーの設定 + #LOC_BDArmory_InputSettings_Clear = リセット + + // Weapon Config + #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_saveClose = ??? Save & Close + #LOC_BDArmory_reset = 元に戻す + //#LOC_BDArmory_applyTo = ??? Apply To + //#LOC_BDArmory_WeaponGroup = ??? Weapon Group GUI + //#LOC_BDArmory_AddToWpnGroup = ??? Add to Weapon Group: + //#LOC_BDArmory_thisWeapon = ??? this weapon + //#LOC_BDArmory_SymmetricWeapons = ??? symmetric weapons + + #LOC_BDArmory_CustomFireKey = カスタムファイアキー + #LOC_BDArmory_SetCustomFireKey = カスタム Fire キーを設定する + + #LOC_BDArmory_EjectVelocity = 発射速度 + #LOC_BDArmory_TNTMass = TNT当量 + #LOC_BDArmory_BlastRadius = 爆発半径 + #LOC_BDArmory_WeaponName = 武器名\u0020 + #LOC_BDArmory_GuidanceType = 誘導の種類\u0020 + #LOC_BDArmory_TargetingMode = ターゲッティングモード\u0020 + #LOC_BDArmory_ActiveRadarRange = 有効なレーダー範囲 + //#LOC_BDArmory_MissileCMRange = ??? Countermeasure Range + //#LOC_BDArmory_MissileCMInterval = ??? Countermeasure Interval + + // Adjustable Rails + #LOC_BDArmory_Rails = レール + #LOC_BDArmory_IncreaseHeight = 高さ ++ + #LOC_BDArmory_DecreaseHeight = 高さ -- + #LOC_BDArmory_IncreaseLength = 長さ ++ + #LOC_BDArmory_DecreaseLength = 長さ -- + #LOC_BDArmory_RailsPlus = レール ++ + #LOC_BDArmory_RailsMinus = レール -- + + // Vessel Spawner + #LOC_BDArmory_BDAVesselSpawner_Title = 機体のスポーン + // Spawn Options + #LOC_BDArmory_Settings_SpawnOptions = スポーン設定 + #LOC_BDArmory_Settings_SpawnDistanceFactor = スポーン距離倍率 + //#LOC_BDArmory_Settings_SpawnRefHeading = ??? Reference Heading + #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_CSFollowsCentroid = ??? Spawn Point Follows Centroid (CS) + #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = 連続シングルスポーン (S) + #LOC_BDArmory_Settings_SpawnRandomOrder = ランダムスポーン順 (S) + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = 対戦を自動で開始 + //#LOC_BDArmory_Settings_SpawnInitialVelocity = ??? Air-Spawn With Idle Speed + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = ここにスポーン + #LOC_BDArmory_Settings_OutOfAmmoKillTime = 弾切れキル時間 (CS) + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = 機体のスポーンポイントをここに設定 + #LOC_BDArmory_Settings_SaveSpawnLoc = 保存場所 + #LOC_BDArmory_Settings_ClearDebrisNow = デブリを除去 + #LOC_BDArmory_Settings_ClearBystandersNow = バイスタンダーを除去 + // Fill Seats + #LOC_BDArmory_Settings_SpawnFillSeats = 座席を埋める + #LOC_BDArmory_Settings_SpawnFillSeats_Minimal = 最小 + #LOC_BDArmory_Settings_SpawnFillSeats_Default = コクピット又はコマンドシート + #LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints = 全てのコントロールポイント + #LOC_BDArmory_Settings_SpawnFillSeats_Cabins = キャビン + + // Teams + #LOC_BDArmory_Settings_Teams = チーム + #LOC_BDArmory_Settings_Teams_FFA = FFA + #LOC_BDArmory_Settings_Teams_Folders = フォルダーごと/クラフトファイルごと + #LOC_BDArmory_Settings_Teams_Custom_Template = カスタムテンプレート + #LOC_BDArmory_Settings_Teams_SplitEvenly = 均等に分割 + + #LOC_BDArmory_Settings_SpawnFilesLocation = クラフトファイルの場所 + // Custom Spawn Templates + #LOC_BDArmory_Settings_CustomSpawnTemplateOptions = スポーンテンプレート設定 + #LOC_BDArmory_Settings_SpawnOnly = スポーンのみ + #LOC_BDArmory_Settings_SpawnAndStartCompetition = スポーンして対戦を開始 + #LOC_BDArmory_Settings_CustomSpawnTemplate_ReplaceTeam = チームを変換 + #LOC_BDArmory_Settings_CustomSpawnTemplate_TemplateSelection = テンプレートを選択 + //#LOC_BDArmory_Settings_CustomSpawnTemplate_SaveCraftToTemplate = ??? Save Craft URLs + + // Observers + #LOC_BDArmory_Settings_Observers = 観戦者 + #LOC_BDArmory_ObserverSelection_Title = 観戦者を選択 + #LOC_BDArmory_ObserverSelection_SelectAll = 全て選択 + #LOC_BDArmory_ObserverSelection_SelectNone = 選択を解除 + + // Interesting Spawn Locations + #LOC_BDArmory_Settings_SpawnLocations = スポーン位置 + #LOC_BDArmory_Settings_WarpHere = ここにワープ + #LOC_BDArmory_Settings_Planet = 惑星を選択 + + // Tournament Options + #LOC_BDArmory_Settings_TournamentOptions = トーナメント設定 + #LOC_BDArmory_Settings_TournamentStyle = トーナメントの種類 + #LOC_BDArmory_Settings_TournamentRoundType = ラウンドの種類 + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = リスポーン間の遅延 + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = ラウンド間のタイムワープ + //#LOC_BDArmory_Settings_TournamentTimeWarpDaylight = ??? Daylight + #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_TournamentNPCsPerHeat = 体力ごとのNPC数 + #LOC_BDArmory_Settings_TournamentSetup = トーナメントのセットアップ + #LOC_BDArmory_Settings_TournamentRun = トーナメントを開始 + #LOC_BDArmory_Settings_TournamentStop = トーナメントを終了 + + // Waypoints + #LOC_BDArmory_Settings_WaypointsOptions = ウェイポイントの設定 + #LOC_BDArmory_Settings_WaypointsOneAtATime = 一つずつ + #LOC_BDArmory_Settings_WaypointsInfFuelAtStart = 最初のウェイポイントまで無限燃料 + #LOC_BDArmory_Settings_WaypointsShow = ウェイポイントを表示 + + #LOC_BDArmory_Settings_SingleSpawn = シングルスポーん + #LOC_BDArmory_Settings_ContinuousSpawning = 連続スポーン + #LOC_BDArmory_Settings_CancelSpawning = スポーンをキャンセル + + // Waypoint GUI + //#LOC_BDArmory_BDAWaypointBuilder_Title = ??? Waypoint Course Tool + //#LOC_BDArmory_WP_LoadCourse = ??? Load Course + //#LOC_BDArmory_WP_NewCourse = ??? New Course + //#LOC_BDArmory_WP_ChooseCourse = ??? Select Course + //#LOC_BDArmory_WP_Create = ??? Create + //#LOC_BDArmory_WP_Record = ??? Record + //#LOC_BDArmory_WP_TimeStep = ??? Timestep (s) + //#LOC_BDArmory_WP_Recording = ??? Recording Course.... + //#LOC_BDArmory_WP_FinishRecording = ??? Finish Recording + //#LOC_BDArmory_WP_Spawnpoint = ??? Spawnpoint + //#LOC_BDArmory_WP_AddGate = ??? Add Gate + //#LOC_BDArmory_WP_Waypoint = ??? Waypoint + //#LOC_BDArmory_WP_SpeedLimit = ??? Speed limit + //#LOC_BDArmory_WP_Increment = ??? Increment + //#LOC_BDArmory_WP_MaxLaps = ??? Max Laps + //#LOC_BDArmory_WP_GuardActivate = ??? Activate Guard After + //#LOC_BDArmory_WP_CourseDefaults = ??? Use Course Settings + //#LOC_BDArmory_WP_SelectModel = ??? Waypoint Type + + // Vessel Mover + #LOC_BDArmory_VesselMover_Title = 機体の移動 + #LOC_BDArmory_VesselMover_VesselSelection = 機体を選択 + #LOC_BDArmory_VesselMover_CrewSelection = 搭乗員を選択 + #LOC_BDArmory_VesselMover_MoveVessel = 機体を動かす + #LOC_BDArmory_VesselMover_SpawnVessel = 機体をスポーン + #LOC_BDArmory_VesselMover_RecoverVessel = 機体の回収 + #LOC_BDArmory_VesselMover_ChooseCrew = 搭乗員を選択 + #LOC_BDArmory_VesselMover_PlaceAfterSpawn = スポーン後の位置 + //#LOC_BDArmory_VesselMover_DeconflictVesselName = ??? Deconflict Vessel Name + #LOC_BDArmory_VesselMover_PlaceVessel = 機体を設置 + #LOC_BDArmory_VesselMover_DropVessel = 機体を落とす + #LOC_BDArmory_VesselMover_InstantLowering = 即時降下 + #LOC_BDArmory_VesselMover_ClassicChooser = クラフトファイルを選択 + #LOC_BDArmory_VesselMover_EnableBrakes = ブレーキを有効化 + #LOC_BDArmory_VesselMover_EnableSAS = SASを有効化 + #LOC_BDArmory_VesselMover_MinLowerSpeed = 最大低速速度 + #LOC_BDArmory_VesselMover_LowerFast = 下に配置 + #LOC_BDArmory_VesselMover_BelowWater = 水中 + #LOC_BDArmory_VesselMover_DontWorryAboutCollisions = 衝突を回避しない + #LOC_BDArmory_VesselMover_Any = 任意 + #LOC_BDArmory_VesselMover_ReallyRemoveKerbals = 本当にカーバルを削除しますか? + #LOC_BDArmory_VesselMover_Help_Movement = 移動 + #LOC_BDArmory_VesselMover_Help_Roll = ロール + #LOC_BDArmory_VesselMover_Help_Pitch = ピッチ + #LOC_BDArmory_VesselMover_Help_Yaw = ヨー + #LOC_BDArmory_VesselMover_Help_AutoRotateRocket = ロケットの自動回転 + #LOC_BDArmory_VesselMover_Help_AutoRotatePlane = 飛行機の自動回転 + #LOC_BDArmory_VesselMover_Help_CycleAltitudes = サイクルプリセット高度: Tab、Shift+Tab + #LOC_BDArmory_VesselMover_Help_ResetAltitude = 高度をリセット + #LOC_BDArmory_VesselMover_Help_AdjustAltitude = 高度を調整 + #LOC_BDArmory_VesselMover_CloseOnCompetitionStart = 対戦開始時に閉じる + + // Craft Browser + #LOC_BDArmory_CraftBrowser_InvalidParts = 無効な部品 + #LOC_BDArmory_CraftBrowser_UnknownModules = モジュール + #LOC_BDArmory_CraftBrowser_Clear = クリア + #LOC_BDArmory_CraftBrowser_ClearAll = 全てクリア + #LOC_BDArmory_CraftBrowser_Refresh = 更新 + #LOC_BDArmory_CraftBrowser_Parts = 部品 + #LOC_BDArmory_CraftBrowser_Mass = 質量 + #LOC_BDArmory_CraftBrowser_Version = バージョン + #LOC_BDArmory_CraftBrowser_Craft = クラフト + #LOC_BDArmory_CraftBrowser_Folder = フォルダー + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnails = ??? Generate Missing Thumbnails + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsRecurse = ??? Recurse subfolders + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingFor = ??? Generating thumbnail for + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingForCraftIn = ??? Generating thumbnail for craft in + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFinished = ??? Finished generating thumbnails. + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFailure = ??? Unable to capture thumbnail of + + // Scores + #LOC_BDArmory_BDAScores_Title = トーナメントスコア + #LOC_BDArmory_BDAScores_Weights = スコアの量 + #LOC_BDArmory_BDAScores_Round = ラウンド + #LOC_BDArmory_BDAScores_Heat = 体力 + //#LOC_BDArmory_BDAScores_Unlimited = ??? Unlimited + //#LOC_BDArmory_BDAScores_Score = ??? Score + //#LOC_BDArmory_BDAScores_Lives = ??? Lives + + // Staging Icons + #LOC_BDArmory_ProtoStageIconInfo_Reloading = リロード + #LOC_BDArmory_ProtoStageIconInfo_Overheat = オーバーヒート + #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = 弾切れ + //#LOC_BDArmory_ProtoStageIconInfo_CMsOut = ??? CMs Depleted + + // Wing Commander + #LOC_BDArmory_WingCommander_Title = ウィングコマンダー + #LOC_BDArmory_WingCommander_Guiname1 = フォーメーションの広がり + #LOC_BDArmory_WingCommander_Guiname2 = フォーメーションの遅延 + #LOC_BDArmory_WingCommander_Guiname3 = トグルGUI + #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右クリックでキャンセル + + // Vessel Switcher + #LOC_BDArmory_BDAVesselSwitcher_Title = チームの管理 + + // Evolution + #LOC_BDArmory_Evolution_Title = BDAエボリューション + #LOC_BDArmory_Evolution_Options = エボリューション設定 + #LOC_BDArmory_Evolution_HeatsPerGroup = グループ当たりヒート数 + #LOC_BDArmory_Evolution_MutationsPerHeat = Mutations per Heat + #LOC_BDArmory_Evolution_AdversariesPerHeat = Adversaries per Heat + #LOC_BDArmory_Evolution_ID = エボリューション + #LOC_BDArmory_Evolution_Status = 状態 + #LOC_BDArmory_Evolution_Group = グループ + #LOC_BDArmory_Evolution_Heat = Heat + + // Modular Missile, Custom Weapons + #LOC_BDArmory_StagesNumber = ステージ番号 + #LOC_BDArmory_StageToTriggerOnProximity = 近接時にトリガーするステージ + #LOC_BDArmory_RollCorrection = ロール補正 + #LOC_BDArmory_RollCorrection_enabledText = ロール有効 + #LOC_BDArmory_RollCorrection_disabledText = ロール無効 + //#LOC_BDArmory_MissileIFF = ??? Seeker IFF + //#LOC_BDArmory_MissileIFF_enabledText = ??? IFF enabled + //#LOC_BDArmory_MissileIFF_disabledText = ??? IFF disabled + #LOC_BDArmory_TimeBetweenStages = ステージ間の時間 + #LOC_BDArmory_AI_MinSpeedGuidance = 誘導前の最小速度 + #LOC_BDArmory_ClearanceRadius = クリアランス半径 + #LOC_BDArmory_ClearanceLength = クリアランスの長さ + #LOC_BDArmory_showRFGUI = 武器名エディタを表示 + #LOC_BDArmory_showRFGUI_enabledText = 武器名GUI + #LOC_BDArmory_showRFGUI_disabledText = GUI + + // WM (PAW) + // Target Priority + #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 = ターゲットTWR + #LOC_BDArmory_TargetPriority_ShorterClosingTime = 閉じる時間の短縮 + #LOC_BDArmory_TargetPriority_TargetWeaponNumber = ターゲットの武器番号 + #LOC_BDArmory_TargetPriority_TargetMass = ターゲットの質量 + #LOC_BDArmory_TargetPriority_TargetDmg = ターゲットのダメージ + #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を攻撃 + + // Countermeasures + #LOC_BDArmory_Countermeasure_Settings = 対策設定 + #LOC_BDArmory_EvadeThreshold = 回避前の衝突までの時間 + #LOC_BDArmory_CMThreshold = CM前の影響時間 + #LOC_BDArmory_CMRepetition = シーケンスごとのフレア・ダンプ + #LOC_BDArmory_CMInterval = フレア・ダンプ間隔時間 + #LOC_BDArmory_CMWaitTime = フレア・シーケンス再起動の遅延 + #LOC_BDArmory_ChaffRepetition = シーケンスごとのチャフダンプ + #LOC_BDArmory_ChaffInterval = チャフダンプ間隔時間 + #LOC_BDArmory_ChaffWaitTime = チャフシーケンス再起動の遅延 + //#LOC_BDArmory_SmokeRepetition = ??? Smoke Launch per Sequence + //#LOC_BDArmory_SmokeInterval = ??? Smoke Launch Interval Time + //#LOC_BDArmory_SmokeWaitTime = ??? Smoke Sequence Restart Delay + #LOC_BDArmory_ChaffFactor = チャフ感受性 + //#LOC_BDArmory_NonGuardModeCMs = ??? Non-GuardMode CMs + + #LOC_BDArmory_IsVIP = VIPかどうか + #LOC_BDArmory_IsVIP_enabledText = はい + #LOC_BDArmory_IsVIP_disabledText = いいえ + //#LOC_BDArmory_WM_IsPrimaryWM = ??? Is Primary + + // AI (PAW) + // Pilot AI + // PID + #LOC_BDArmory_AI_PID = PIDコントローラー + #LOC_BDArmory_AI_SteerPower = 操縦力(P) + #LOC_BDArmory_AI_SteerKi = 操縦補正(I) + #LOC_BDArmory_AI_SteerDamping = 操縦減衰(D) + //#LOC_BDArmory_AI_SteerMaxError = ??? Steer Max Error + + // Dynamic damping + #LOC_BDArmory_AI_DynamicSteerDamping = 動的操縦減衰 + #LOC_BDArmory_AI_DynamicDamping = 動的減衰 + #LOC_BDArmory_AI_DynamicDampingMin = オフターゲット減衰 + #LOC_BDArmory_AI_DynamicDampingMax = オフターゲット減衰 + #LOC_BDArmory_AI_DynamicDampingFactor = 動的減衰要素 + + // 3-axis damping + //#LOC_BDArmory_AI_3AxisSteerDamping = ??? 3-Axis Steer Damping + + // 3-axis static damping + //#LOC_BDArmory_AI_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + //#LOC_BDArmory_AI_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + //#LOC_BDArmory_AI_SteerDampingRoll = ??? Steer Damping Roll (Dr) + + // 3-axis dynamic damping + #LOC_BDArmory_AI_DynamicDampingPitch = 動的減衰ピッチ + #LOC_BDArmory_AI_DynamicDampingPitchMin = オフターゲット減衰ピッチ + #LOC_BDArmory_AI_DynamicDampingPitchMax = オフターゲット減衰ピッチ + #LOC_BDArmory_AI_DynamicDampingPitchFactor = ピッチ減衰要素 + #LOC_BDArmory_AI_DynamicDampingYaw = 動的減衰ヨー + #LOC_BDArmory_AI_DynamicDampingYawMin = オフターゲット減衰ヨー + #LOC_BDArmory_AI_DynamicDampingYawMax = オフターゲット減衰ヨー + #LOC_BDArmory_AI_DynamicDampingYawFactor = ヨー減衰要素 + #LOC_BDArmory_AI_DynamicDampingRoll = 動的減衰ロー + #LOC_BDArmory_AI_DynamicDampingRollMin = オフターゲット減衰ロー + #LOC_BDArmory_AI_DynamicDampingRollMax = オフターゲット減衰ロー + #LOC_BDArmory_AI_DynamicDampingRollFactor = ロー減衰要素 + + // Auto-tuning + #LOC_BDArmory_AI_PID_AutoTune = PID自動調整 + #LOC_BDArmory_AI_PID_AutoTuning_Loss = 自動調整の損失 + #LOC_BDArmory_AI_PID_AutoTuning_NumSamples = サンプル数の自動調整 + #LOC_BDArmory_AI_PID_AutoTuning_FastResponseRelevance = 自動調整の高速応答関連性 + #LOC_BDArmory_AI_PID_AutoTuning_InitialLearningRate = 初期学習率の自動調整 + //#LOC_BDArmory_AI_PID_AutoTuning_InitialRollRelevance = ??? Auto-Tuning Initial Roll Relevance + #LOC_BDArmory_AI_PID_AutoTuning_Speed = 自動調整速度 + #LOC_BDArmory_AI_PID_AutoTuning_Altitude = 自動調整高度 + #LOC_BDArmory_AI_PID_AutoTuning_RecenteringDistance = オートチューニング再センター距離(km) + #LOC_BDArmory_AI_PID_AutoTuning_FixedP = 自動調整を固定 + #LOC_BDArmory_AI_PID_AutoTuning_ClampMaximums = 最大自動調整クランプ + //#LOC_BDArmory_AI_PID_AutoTuning_Summary = ??? Auto-Tuning Summary + + // Altitudes + #LOC_BDArmory_AI_Altitudes = 高度 + #LOC_BDArmory_AI_DefaultAltitude = デフォルトの代替 + #LOC_BDArmory_AI_MinAltitude = 最低高度 + #LOC_BDArmory_AI_MaxAltitude = 最高高度(AGL) + #LOC_BDArmory_AI_HardMinAltitude = 最最低高度 + //#LOC_BDArmory_AI_BombingAltitude = ??? Bombing Altitude + //#LOC_BDArmory_AI_DiveBombing = ??? Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AI_Speeds = 速度 + #LOC_BDArmory_AI_MaxSpeed = 最高速度 + #LOC_BDArmory_AI_TakeOffSpeed = 最高速度 + #LOC_BDArmory_AI_MinSpeed = 最小戦闘速度 + #LOC_BDArmory_AI_StrafingSpeed = 機銃発射速度 + #LOC_BDArmory_AI_IdleSpeed = アイドル時の速度 + #LOC_BDArmory_AI_ABPriority = アフターバーナーの優先度 + #LOC_BDArmory_AI_ABOverrideThreshold = アフターバーナーオーバーライドの限界値 + //#LOC_BDArmory_AI_BrakingPriority = ??? Braking Priority + + // Control + #LOC_BDArmory_AI_ControlLimits = 制御権限の制限 + #LOC_BDArmory_AI_SteerLimiter = 操縦制限 + #LOC_BDArmory_AI_LowSpeedSteerLimiter = 低速操縦制限 + #LOC_BDArmory_AI_LowSpeedLimiterSpeed = 低速制限速度 + #LOC_BDArmory_AI_HighSpeedSteerLimiter = 高速操縦制限 + #LOC_BDArmory_AI_HighSpeedLimiterSpeed = 高速制限速度 + #LOC_BDArmory_AI_AltitudeSteerLimiterFactor = 高度操舵制限倍率 + #LOC_BDArmory_AI_AltitudeSteerLimiterAltitude = 高度操縦制限高度 + #LOC_BDArmory_AI_AttitudeLimiter = 姿勢制限 + #LOC_BDArmory_AI_BankLimiter = 姿勢制限 + #LOC_BDArmory_AI_WaypointPreRollTime = ウェイポイントのプレロール時間 + #LOC_BDArmory_AI_WaypointYawAuthorityTime = ウェイポイントヨーオーソリティ時間 + #LOC_BDArmory_AI_MaxAllowedGForce = 最大G + #LOC_BDArmory_AI_MaxAllowedAoA = 最大AoA + #LOC_BDArmory_AI_PostStallAoA = ポストストールAoAモードスイッチ + #LOC_BDArmory_AI_ImmelmannTurnAngle = インメルマン旋回角 + //#LOC_BDArmory_AI_ImmelmannPitchUpBias = ??? Immelmann Pitch-Up Bias + + // Evade / Extend + #LOC_BDArmory_AI_EvadeExtend = 回避/拡張 + #LOC_BDArmory_AI_ExtendMultiplier = 倍率の拡張 + #LOC_BDArmory_AI_ExtendDistanceAirToAir = 空対空距離の延長 + #LOC_BDArmory_AI_ExtendAngleAirToAir = 空対空角度の延長 + #LOC_BDArmory_AI_ExtendDistanceAirToGroundGuns = 空対空距離の延長(銃) + #LOC_BDArmory_AI_ExtendDistanceAirToGround = 空対地距離の延長 + #LOC_BDArmory_AI_ExtendTargetVel = ターゲット速度係数の拡張 + #LOC_BDArmory_AI_ExtendTargetAngle = ターゲット角度の延長 + #LOC_BDArmory_AI_ExtendTargetDist = ターゲット距離の延長 + #LOC_BDArmory_AI_ExtendAbortTime = 中止時間の延長 + //#LOC_BDArmory_AI_ExtendMinGainRate = ??? Extend Min Gain Rate + #LOC_BDArmory_AI_ExtendToggle = トグルの延長(空対空) + #LOC_BDArmory_AI_MinEvasionTime = 最小回避時間 + #LOC_BDArmory_AI_EvasionNonlinearity = 回避/拡張非線形性 + #LOC_BDArmory_AI_EvasionThreshold = 回避距離しきい値 + //#LOC_BDArmory_AI_EvasionErraticness = ??? Evasion Erraticness + #LOC_BDArmory_AI_EvasionTimeThreshold = 回避時間のしきい値 + //#LOC_BDArmory_AI_EvasionMinRangeThreshold = ??? Evasion Min Range Threshold + #LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe = ターゲットを回避しない + //#LOC_BDArmory_AI_EvasionMissileKinematic = ??? Kinematic Msl Evasion + #LOC_BDArmory_AI_CollisionAvoidanceThreshold = 機体回避値 + #LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod = 機体回避の予測 + #LOC_BDArmory_AI_CollisionAvoidanceStrength = 機体回避強度 + #LOC_BDArmory_AI_StandoffDistance = スタンドオフ距離 + + // Terrain + #LOC_BDArmory_AI_Terrain = 地形回避 + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMin = 最小地形回避調整 + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMax = 最大地形回避調整 + #LOC_BDArmory_AI_TerrainAvoidanceCriticalAngle = 反転地形回避臨界角 + #LOC_BDArmory_AI_TerrainAvoidanceVesselReactionTime = 機体の反応時間 + #LOC_BDArmory_AI_TerrainAvoidancePostAvoidanceCoolDown = 回避後のクールダウン + #LOC_BDArmory_AI_WaypointTerrainAvoidance = ウェイポイント地形の回避 + + // Ramming + #LOC_BDArmory_AI_Ramming = ラミング + #LOC_BDArmory_AI_ControlSurfaceLag = ラミングコントロールサーフェスのラグ + #LOC_BDArmory_AI_AllowRamming = ラミングを許可 + #LOC_BDArmory_AI_AllowRammingGroundTargets = 地上ターゲットを含める + + // Ejection (unused) + #LOC_BDArmory_AI_Ejection = 発射 + #LOC_BDArmory_AI_EjectOnImpendingDoom = 偶然の場合は発射 + + #LOC_BDArmory_AI_SliderResolution = スライダーの解像度 + // Idle / Orbit Behavior + #LOC_BDArmory_AI_Orbit = 軌道方向\u0020 + #LOC_BDArmory_AI_Orbit_Starboard = 右舷 (CW) + #LOC_BDArmory_AI_Orbit_Port = ポート(CCW) + #LOC_BDArmory_AI_Orbit_Random = 両方(CW/CCW) + #LOC_BDArmory_AI_Standby = スタンバイモード + + // Up-to-eleven + #LOC_BDArmory_AI_UnclampTuning = クランプ解除調整\u0020 + #LOC_BDArmory_AI_UnclampTuning_enabledText = クランプなし + #LOC_BDArmory_AI_UnclampTuning_disabledText = クランプ + + // Surface / VTOL / Orbital AI + #LOC_BDArmory_AI_VehicleType = 車両の種類 + #LOC_BDArmory_AI_MaxSlopeAngle = 最大傾斜角 + #LOC_BDArmory_AI_CruiseSpeed = 巡航速度 + #LOC_BDArmory_AI_CombatSpeed = 戦闘速度 + #LOC_BDArmory_AI_CombatAltitude = 戦闘高度 + #LOC_BDArmory_AI_TargetPitch = 移動ピッチ + #LOC_BDArmory_AI_MaxDrift = 最大ドリフト + #LOC_BDArmory_AI_MaxPitchAngle = 最大ピッチ角 + #LOC_BDArmory_AI_BankAngle = バンク角 + //#LOC_BDArmory_AI_WeaveFactor = ??? Weave Factor + #LOC_BDArmory_AI_MaxBankAngle = 最大バンク角 + #LOC_BDArmory_AI_BroadsideAttack = 攻撃ベクトル + #LOC_BDArmory_AI_BroadsideAttack_enabledText = ブロードサイド + #LOC_BDArmory_AI_BroadsideAttack_disabledText = 弓 + #LOC_BDArmory_AI_MinEngagementRange = 最小エンゲージメント範囲 + #LOC_BDArmory_AI_MaxEngagementRange = 最大交戦範囲 + //#LOC_BDArmory_AI_ForceFiringRange = ??? Zero Throttle Firing Range + //#LOC_BDArmory_AI_MaintainEngagementRange = ??? Maintain Min Range + #LOC_BDArmory_AI_ManeuverRCS = RCSアクティブ + #LOC_BDArmory_AI_ManeuverRCS_enabledText = 作戦 + #LOC_BDArmory_AI_ManeuverRCS_disabledText = 戦闘 + //#LOC_BDArmory_AI_FiringRCS = ??? RCS While Firing + //#LOC_BDArmory_AI_FiringRCS_enabledText = ??? Manage Velocity + //#LOC_BDArmory_AI_FiringRCS_disabledText = ??? Maneuvers Only + //#LOC_BDArmory_AI_ReverseEngines = ??? Reverse Engines + //#LOC_BDArmory_AI_EngineRCSRotation = ??? Engine RCS (Rotation) + //#LOC_BDArmory_AI_EngineRCSTranslation = ??? Engine RCS (Translation) + //#LOC_BDArmory_AI_OrbitalPIDActive = ??? PID Active For + //#LOC_BDArmory_AI_RollMode = ??? Broadside Dir + #LOC_BDArmory_AI_MinObstacleMass = 最小障害物質量 + #LOC_BDArmory_AI_PreferredBroadsideDirection = 優先ブロードサイド ディレクトリ + #LOC_BDArmory_AI_GoesUp = まで上がります + #LOC_BDArmory_AI_GoesUp_enabledText = 十一 + #LOC_BDArmory_AI_GoesUp_disabledText = 十 + //#LOC_BDArmory_AI_ManeuverSpeed = ??? Maneuver Speed + //#LOC_BDArmory_AI_FiringSpeedMin = ??? Min Firing Speed + //#LOC_BDArmory_AI_FiringSpeedLimit = ??? Max Firing Speed + //#LOC_BDArmory_AI_AngularSpeedLimit = ??? Angular Speed Limit + //#LOC_BDArmory_AI_EvasionRCS = ??? RCS Evasion + //#LOC_BDArmory_AI_EvasionEngines = ??? Thrust Evasion + + // AI GUI + #LOC_BDArmory_AIWindow_title = AIマネージャー + #LOC_BDArmory_AIWindow_infoLink = 詳細情報 + #LOC_BDArmory_AIWindow_NoAI = 機体にAIが見つかりません + // Sections + //#LOC_BDArmory_AIWindow_PID = ??? PID + //#LOC_BDArmory_AIWindow_Altitudes = ??? Altitudes + //#LOC_BDArmory_AIWindow_Speeds = ??? Speeds + #LOC_BDArmory_AIWindow_Control = コントロール + #LOC_BDArmory_AIWindow_EvadeExtend = 回避/延長 + #LOC_BDArmory_AIWindow_Terrain = 地形 + //#LOC_BDArmory_AIWindow_Ramming = ??? Ramming + //#LOC_BDArmory_AIWindow_Combat = ??? Combat + #LOC_BDArmory_AIWindow_Misc = その他 + + // Panel + // Pilot + // PID + #LOC_BDArmory_AIWindow_SteerPower = 制御入力乗算器 + #LOC_BDArmory_AIWindow_SteerPower_ContextLow = <- 遅い + #LOC_BDArmory_AIWindow_SteerPower_ContextHigh = 速い -> + //#LOC_BDArmory_AIWindow_SteerKi = ??? Steer Correction (I) + #LOC_BDArmory_AIWindow_SteerKi_ContextLow = <- アンダーシュート + #LOC_BDArmory_AIWindow_SteerKi_ContextHigh = オーバーシュート -> + //#LOC_BDArmory_AIWindow_SteerDamping = ??? Steer Damping (D) + #LOC_BDArmory_AIWindow_SteerDamping_ContextLow = <- ぐらつく + #LOC_BDArmory_AIWindow_SteerDamping_ContextHigh = 硬い -> + //#LOC_BDArmory_AIWindow_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextLow = <- ぐらつく + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextHigh = 硬い -> + //#LOC_BDArmory_AIWindow_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextLow = <- ぐらつく + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextHigh = 硬い -> + //#LOC_BDArmory_AIWindow_SteerDampingRoll = ??? Steer Damping Roll (Dr) + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextLow = <- ぐらつく + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextHigh = 硬い -> + //#LOC_BDArmory_AIWindow_SteerMaxError = ??? Max Error + //#LOC_BDArmory_AIWindow_SteerMaxError_ContextLow = ??? <- Slow & Easy Tuning + //#LOC_BDArmory_AIWindow_SteerMaxError_ContextHigh = ??? Fast & Harder Tuning -> + //#LOC_BDArmory_AIWindow_DynDampMin = ??? Off-target Damping + #LOC_BDArmory_AIWindow_DynDampMin_Context = 最小減衰 + //#LOC_BDArmory_AIWindow_DynDampMax = ??? On-target Damping + #LOC_BDArmory_AIWindow_DynDampMax_Context = 最大減衰 + //#LOC_BDArmory_AIWindow_DynDampMult = ??? Dynamic Damping Factor + #LOC_BDArmory_AIWindow_DynDampMult_Context = 減衰の大きさ + + // Auto-tuning + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = A-T サンプル数 + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextLow = <- 精度が低い + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextHigh = より正確に -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = A-T高速応答 + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextLow = <- ダンピングの向上 + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextHigh = より速い応答 -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = A-T初期LR + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate_Context = PID 値の変化が大きすぎる場合は、この値を下げます。 + //#LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance = ??? A-T Initial RR + //#LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance_Context = ??? How much the roll errors contribute to the loss + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = A-Tスピード + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed_Context = オートチューニングの目標速度 + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = A-T 高度 + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude_Context = これより±min 高度以内に留まるようにしてください + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance = A-T 再センター幅 + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance_Context = Distance from start at which re-centering is triggered + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = A-T固定フィールド + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = A-T クランプ最大値 + // PID fixed fields + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P = ??? P + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I = ??? I + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D = ??? D + // 3-axis damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch = ??? Dp + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw = ??? Dy + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll = ??? Dr + // Dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OffTarget = ??? DOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OnTarget = ??? DOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_Factor = ??? DF + // 3-axis dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OffTarget = ??? DpOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OnTarget = ??? DpOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_Factor = ??? DpF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OffTarget = ??? DyOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OnTarget = ??? DyOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_Factor = ??? DyF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OffTarget = ??? DrOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OnTarget = ??? DrOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_Factor = ??? DrF + + // Altitudes + //#LOC_BDArmory_AIWindow_DefaultAltitude = ??? Default Alt. + #LOC_BDArmory_AIWindow_DefaultAltitude_Context = AIはアイドル状態になるとこれに戻ります + //#LOC_BDArmory_AIWindow_MinAltitude = ??? Min Altitude + #LOC_BDArmory_AIWindow_MinAltitude_Context = AIはこれを超えようとします + //#LOC_BDArmory_AIWindow_MaxAltitude = ??? Max Altitude (AGL) + #LOC_BDArmory_AIWindow_MaxAltitude_Context = AIはこれ以下に留まろうとする + //#LOC_BDArmory_AIWindow_BombingAltitude = ??? Bombing Altitude + //#LOC_BDArmory_AIWindow_BombingAltitude_Context = ??? AI tries to bomb at this altitude + //#LOC_BDArmory_AIWindow_DiveBomb = ??? Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AIWindow_MaxSpeed = 最大戦闘速度 + #LOC_BDArmory_AIWindow_MaxSpeed_Context = AI はこの対気速度以下に留まります + #LOC_BDArmory_AIWindow_TakeOffSpeed = #LOC_BDArmory_AI_TakeOffSpeed + #LOC_BDArmory_AIWindow_TakeOffSpeed_Context = Speed AI は離陸時にピッチ入力を開始します + #LOC_BDArmory_AIWindow_MinSpeed = #LOC_BDArmory_AI_MinSpeed + #LOC_BDArmory_AIWindow_MinSpeed_Context = この速度を下回ると、AI はエネルギーを回復しようとします + #LOC_BDArmory_AIWindow_StrafingSpeed = #LOC_BDArmory_AI_StrafingSpeed + #LOC_BDArmory_AIWindow_StrafingSpeed_Context = 対地攻撃速度 + #LOC_BDArmory_AIWindow_IdleSpeed = #LOC_BDArmory_AI_IdleSpeed + #LOC_BDArmory_AIWindow_IdleSpeed_Context = 非戦闘巡航速度 + #LOC_BDArmory_AIWindow_ABPriority = #LOC_BDArmory_AI_ABPriority + #LOC_BDArmory_AIWindow_ABPriority_Context = アフターバーナーを有効にするためのしきい値を変更します + #LOC_BDArmory_AIWindow_ABOverrideThreshold = アフターバーナーオーバーライド + #LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = この速度以下でフルスロットルの場合はアフターバーナーを強制的に使用します + #LOC_BDArmory_AIWindow_BrakingPriority = #LOC_BDArmory_AI_BrakingPriority + //#LOC_BDArmory_AIWindow_BrakingPriority_Context = ??? Prioritize using brakes to slow down when allowed + + // Control + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = 低速リミッター + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter_Context = 低速制限以下の制限制御 + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = ローリミッター速度 + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed_Context = AI はこの速度以下では低速リミッターを使用します + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = 高速リミッター + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter_Context = 高速制限を超える制御を制限 + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = ハイリミッター速度 + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed_Context = AI はこの速度を超えると上限に完全に制限されます + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Alt ステア係数 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor_Context = 高度に基づいてステア制限を増減する係数 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Alt ステアリング高度 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude_Context = ステア制限の減少/増加を開始する高度 + //#LOC_BDArmory_AIWindow_BankLimiter = ??? Bank Angle Limit + #LOC_BDArmory_AIWindow_BankLimiter_Context = 最大ロール角 + //#LOC_BDArmory_AIWindow_MaxAllowedGForce = ??? Max G + #LOC_BDArmory_AIWindow_MaxAllowedGForce_Context = マニューバはこの G 制限を超えないよう努めます + //#LOC_BDArmory_AIWindow_MaxAllowedAoA = ??? Max AoA + #LOC_BDArmory_AIWindow_MaxAllowedAoA_Context = マニューバ AoA はこの AoA を超えないよう努めます + #LOC_BDArmory_AIWindow_WaypointPreRollTime = WP プリロール時間 + #LOC_BDArmory_AIWindow_WaypointPreRollTime_Context = ウェイポイントに到達する前にローリングを開始する + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = WP ヨー認証時間 + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime_Context = ウェイポイントに近づくときのヨー応答を増加します + #LOC_BDArmory_AIWindow_PostStallAoA = ストール後の AoA + #LOC_BDArmory_AIWindow_PostStallAoA_Context = この AoA を超えた失速後に飛行モードを切り替える + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle = ブラインドコーン角度の読み取り + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle_Context = クラフトはこのコーン内のターゲットを狙うためにピッチアップするだけです + //#LOC_BDArmory_AIWindow_ImmelmannPitchUpBias = ??? Immelmann Pitch-Up Bias + //#LOC_BDArmory_AIWindow_ImmelmannPitchUpBias_Context = ??? < Down — Bias direction on current pitch rate — Up > + + // Evade / Extend + #LOC_BDArmory_AIWindow_Evade = 回避 + #LOC_BDArmory_AIWindow_MinEvasionTime = #LOC_BDArmory_AI_MinEvasionTime + #LOC_BDArmory_AIWindow_MinEvasionTime_Context = AI が攻撃を回避する最小時間 + #LOC_BDArmory_AIWindow_EvasionThreshold = 距離のしきい値 + #LOC_BDArmory_AIWindow_EvasionThreshold_Context = この距離内に砲火が入ってきた場合は回避します + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = 時間のしきい値 + #LOC_BDArmory_AIWindow_EvasionTimeThreshold_Context = 射撃を受けて回避を引き起こすまでの最小時間 + //#LOC_BDArmory_AIWindow_EvasionMinRangeThreshold = ??? Min Range Threshold + //#LOC_BDArmory_AIWindow_EvasionMinRangeThreshold_Context = ??? Only evade if the attacker is beyond this range. + #LOC_BDArmory_AIWindow_EvasionNonlinearity = 非線形性の回避/拡張 + #LOC_BDArmory_AIWindow_EvasionNonlinearity_Context = 回避・伸長時の振動の強さ + + #LOC_BDArmory_AIWindow_Avoidance = 血管の回避 + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = 距離のしきい値 + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold_Context = この距離内で接近してくる航空機を回避してください + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = 先読み時間 + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod_Context = AI が衝突を探すのは何秒前か + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = 応答強度 + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength_Context = AI が到来する航空機からどれだけ困難に離脱するか + #LOC_BDArmory_AIWindow_StandoffDistance = スタンドオフ距離 + #LOC_BDArmory_AIWindow_StandoffDistance_Context = AI がターゲットに近づこうとする距離 + + #LOC_BDArmory_AIWindow_Extend = 拡大 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = 距離A2A + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = 空対空距離の延長 + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = アングルA2A + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = 伸長時の希望上昇角度 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = 長距離 A2G 砲 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = 空対地距離の延長 (砲) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = 距離 A2G + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = 空対地距離の延長 + #LOC_BDArmory_AIWindow_ExtendTargetVel = ターゲットレベル要素 + #LOC_BDArmory_AIWindow_ExtendTargetVel_Context = ターゲットが遅すぎて方向を向くことができない場合に設定します + #LOC_BDArmory_AIWindow_ExtendTargetAngle = 目標角度 + #LOC_BDArmory_AIWindow_ExtendTargetAngle_Context = ターゲットが回転半径の外側にあるときに設定します + #LOC_BDArmory_AIWindow_ExtendTargetDist = 目標距離 + #LOC_BDArmory_AIWindow_ExtendTargetDist_Context = ターゲットが近すぎて振り向くことができない場合に設定します + #LOC_BDArmory_AIWindow_ExtendAbortTime = 中止時間 + #LOC_BDArmory_AIWindow_ExtendAbortTime_Context = 延長中に距離を稼げなかった場合は時間を中止します。 + //#LOC_BDArmory_AIWindow_ExtendMinGainRate = ??? Min Gain Rate + //#LOC_BDArmory_AIWindow_ExtendMinGainRate_Context = ??? Minimum rate to be gaining distance for the abort timer. + + // Terrain + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin = 地形回避最小値 + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin_Context = 理想的な機体の向きを実現する回転半径乗数 + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax = 地形回避最大値 + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax_Context = 反転したクラフトの向きの回転半径乗数 + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle = 逆クリティカル角度 + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle_Context = 逆地形回避またはローリングファーストの臨界角 + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime = 機体の反応時間 + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime_Context = 最適な旋回のためのセットアップに必要な時間の見積もり + //#LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown = ??? Post-Avoidance Cool-Down + //#LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown_Context = ??? Time after avoiding terrain before beginning maneuvers + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = WP 地形回避 + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance_Context = ウェイポイントの地形補正の範囲と強度 + + // Ramming + #LOC_BDArmory_AIWindow_ControlSurfaceLag = コントロールサーフ。遅れ + #LOC_BDArmory_AIWindow_ControlSurfaceLag_Context = 舵面の遅れに対するラム軌道補正 + // Combat + // Up-to-eleven + // Idle / Orbit Behavior + #LOC_BDArmory_AIWindow_Orbit_Context = 非戦闘巡航の方向 + #LOC_BDArmory_AIWindow_Standby_Context = ターゲットがガード範囲に入るとAIがオンになります + + // Surface / VTOL / Orbital + #LOC_BDArmory_AIWindow_VehicleType = #LOC_BDArmory_AI_VehicleType + #LOC_BDArmory_AIWindow_VehicleType_Context = クラフトはこのタイプの地形で動作します + #LOC_BDArmory_AIWindow_MaxSlopeAngle = #LOC_BDArmory_AI_MaxSlopeAngle + #LOC_BDArmory_AIWindow_MaxSlopeAngle_Context = 地形クラフトの最大角度が上昇します + #LOC_BDArmory_AIWindow_CruiseSpeed = #LOC_BDArmory_AI_CruiseSpeed + #LOC_BDArmory_AIWindow_CruiseSpeed_Context = #LOC_BDArmory_AIWindow_IdleSpeed_Context + #LOC_BDArmory_AIWindow_CombatSpeed = #LOC_BDArmory_AI_CombatSpeed + //#LOC_BDArmory_AIWindow_CombatSpeed_Context = ??? Target speed for combat without powered steering + #LOC_BDArmory_AIWindow_CombatAltitude = #LOC_BDArmory_AI_CombatAltitude + //#LOC_BDArmory_AIWindow_CombatAltitude_Context = ??? Default cruising altitude/depth + #LOC_BDArmory_AIWindow_TargetPitch = #LOC_BDArmory_AI_TargetPitch + #LOC_BDArmory_AIWindow_TargetPitch_Context = 希望する機体の姿勢角度 + #LOC_BDArmory_AIWindow_MaxDrift = #LOC_BDArmory_AI_MaxDrift + #LOC_BDArmory_AIWindow_MaxDrift_Context = 最大角度の機体がコーナリング中に順行から逸れる + #LOC_BDArmory_AIWindow_MaxPitchAngle = #LOC_BDArmory_AI_MaxPitchAngle + //#LOC_BDArmory_AIWindow_MaxPitchAngle_Context = ??? Max angle to pitch at while moving + #LOC_BDArmory_AIWindow_BankAngle = #LOC_BDArmory_AI_BankAngle + #LOC_BDArmory_AIWindow_BankAngle_Context = #LOC_BDArmory_AIWindow_BankLimiter_Context + #LOC_BDArmory_AIWindow_WeaveFactor = #LOC_BDArmory_AI_WeaveFactor + //#LOC_BDArmory_AIWindow_WeaveFactor_Context = ??? Strength of weaving when under fire + #LOC_BDArmory_AIWindow_MaxBankAngle = #LOC_BDArmory_AI_MaxBankAngle + //#LOC_BDArmory_AIWindow_MaxBankAngle_Context = ??? Max angle to roll at while turning + #LOC_BDArmory_AIWindow_BroadsideAttack = #LOC_BDArmory_AI_BroadsideAttack + #LOC_BDArmory_AIWindow_BroadsideAttack_Context = 攻撃時のターゲットに向けたクラフトの向き + #LOC_BDArmory_AIWindow_MinEngagementRange = #LOC_BDArmory_AI_MinEngagementRange + #LOC_BDArmory_AIWindow_MinEngagementRange_Context = AI がターゲットと交戦する最小範囲 + #LOC_BDArmory_AIWindow_MaxEngagementRange = #LOC_BDArmory_AI_MaxEngagementRange + #LOC_BDArmory_AIWindow_MaxEngagementRange_Context = AI がターゲットと交戦する最大範囲 + #LOC_BDArmory_AIWindow_ForceFiringRange = #LOC_BDArmory_AI_ForceFiringRange + //#LOC_BDArmory_AIWindow_ForceFiringRange_Context = ??? Within this range AI always fires without throttle + #LOC_BDArmory_AIWindow_MaintainEngagementRange = #LOC_BDArmory_AI_MaintainEngagementRange + //#LOC_BDArmory_AIWindow_MaintainEngagementRange_Context = ??? Craft will stop/reverse at minimum Range + #LOC_BDArmory_AIWindow_ManeuverRCS = #LOC_BDArmory_AI_ManeuverRCS + #LOC_BDArmory_AIWindow_ManeuverRCS_Context = RCS利用条件 + #LOC_BDArmory_AIWindow_FiringRCS = #LOC_BDArmory_AI_FiringRCS + //#LOC_BDArmory_AIWindow_FiringRCS_Context = ??? Use RCS to manage velocity while firing + #LOC_BDArmory_AIWindow_ReverseEngines = #LOC_BDArmory_AI_ReverseEngines + //#LOC_BDArmory_AIWindow_ReverseEngines_Context = ??? Use backwards engines for reverse thrust + #LOC_BDArmory_AIWindow_EngineRCSRotation = #LOC_BDArmory_AI_EngineRCSRotation + //#LOC_BDArmory_AIWindow_EngineRCSRotation_Context = ??? Use engines perpendicular to thrust axis for RCS rotation + #LOC_BDArmory_AIWindow_EngineRCSTranslation = #LOC_BDArmory_AI_EngineRCSTranslation + //#LOC_BDArmory_AIWindow_EngineRCSTranslation_Context = ??? Use engines perpendicular to thrust axis for RCS translation + #LOC_BDArmory_AIWindow_OrbitalPIDActive = #LOC_BDArmory_AI_OrbitalPIDActive + //#LOC_BDArmory_AIWindow_OrbitalPIDActive_Context = ??? PID active condition + #LOC_BDArmory_AIWindow_RollMode = #LOC_BDArmory_AI_RollMode + //#LOC_BDArmory_AIWindow_RollMode_Context = ??? When PID is active, AI will roll to present this side of the ship to the target + #LOC_BDArmory_AIWindow_MinObstacleMass = #LOC_BDArmory_AI_MinObstacleMass + #LOC_BDArmory_AIWindow_MinObstacleMass_Context = 回避をトリガーする障害物の最小質量 + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection = #LOC_BDArmory_AI_PreferredBroadsideDirection + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context = ターゲットに向けてクラフトのどちら側を提示するか + #LOC_BDArmory_AIWindow_ManeuverSpeed = #LOC_BDArmory_AI_ManeuverSpeed + //#LOC_BDArmory_AIWindow_ManeuverSpeed_Context = ??? Maximum speed relative to target when maneuvering + #LOC_BDArmory_AIWindow_minFiringSpeed = #LOC_BDArmory_AI_FiringSpeedMin + //#LOC_BDArmory_AIWindow_minFiringSpeed_Context = ??? Minimum speed relative to target when firing + #LOC_BDArmory_AIWindow_FiringSpeed = #LOC_BDArmory_AI_FiringSpeedLimit + //#LOC_BDArmory_AIWindow_FiringSpeed_Context = ??? Maximum speed relative to target when firing + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit = #LOC_BDArmory_AI_AngularSpeedLimit + //#LOC_BDArmory_AIWindow_FiringAngularVelocityLimit_Context = ??? Maximum angular speed relative to target when firing + #LOC_BDArmory_AIWindow_EvasionErraticness = #LOC_BDArmory_AI_EvasionErraticness + //#LOC_BDArmory_AIWindow_EvasionErraticness_Context = ??? Amount of variation in evasion direction + #LOC_BDArmory_AIWindow_EvasionRCS = #LOC_BDArmory_AI_EvasionRCS + //#LOC_BDArmory_AIWindow_EvasionRCS_Context = ??? Use RCS to evade incoming fire + #LOC_BDArmory_AIWindow_EvasionEngines = #LOC_BDArmory_AI_EvasionEngines + //#LOC_BDArmory_AIWindow_EvasionEngines_Context = ??? Use engine thrust to evade incoming fire + + + // AI infolink + // Pilot AI + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp = PID (比例積分微分) コントローラーは、希望の出力と実際の出力の差を計算し、P、I、および D 値に基づいて補正を適用します。これは船を操縦するために使用されます。通常、チューニングには 3 つの値すべてを調整する必要があります。まず Steer Mult から始めて、必要に応じて Ki と Damping を調整します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_SteerPower = Steer Power (P) - これは制御入力です。低すぎると、航空機はその制御権限の全範囲を下回って使用することになります。高すぎると、クラフトが必要以上に適用され、目的の方向を超えてしまいます。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_SteerKi = Steer Ki (I) - これは、P と D からの累積誤差を修正するために適用される誤差修正です。少なすぎると、クラフトは常に望ましい方向を下回ります。多すぎるとオーバーシュートします。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_Steerdamp = Steer Damp (D) - これは微分値です。これは、航空機の新しい方向への回転がどの程度減衰されるかを表します。低すぎると、正しい向きを超えて機体が振動してしまいます。高すぎると、減衰が方向の変化に対抗しすぎて、航空機がそれほど速く回転しなくなります。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_Dyndamp = ダイナミック ダンピング - ターゲットへの角度に基づいて、最小ダンピング値から最大ダンピング値までダンピングを動的に調整します。値が低いほど、ターゲット角度の変化に対するダイナミック ダンピング値はより線形になります。値が高いほど、ターゲットから遠ざかるときは減衰が減少し、ターゲットに近いときは減衰が増加します。これは 3 つの制御軸すべてに当てはまります。軸ダンピングを個別に制御するには、関連するピッチ/ロール/ヨーダイナミックダンピングを有効にします。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_AutoTune = PID 自動調整 - これにより、AI が勾配降下法を使用して、一定範囲の機首方位に旋回してそれらの方向で安定する飛行機の能力を最適化する自動 PID 調整モードが有効になります。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_AutoTune_details = \n - 最小化される損失は、θ ∈ (30°,120°) の範囲で ∫f(x,θ)dθ です (中点リーマン和を使用)。ここで、f(x,θ) は\n ∫(δp²· (α+t²)/θ² + γ・δr²・(α+t)/100/θ)dt\n現在の PID 値 (x) と機首方位変化 (θ) について、δp は指向誤差、δr はロールです誤差、α は高速応答関連性、γ はロール関連性 (ポインティング エラーとロール エラーによる影響のバランスをとるために、時間の経過とともに自動的に調整されます)。\n - 使用方法: 飛行機が飛行すると (非戦闘中)、自動チューニングを有効にし、スライダーを目的の値に設定します (デフォルトは適切な開始点であり、SPH で事前に設定できます。一部のスライダーを調整すると自動チューニングが再開されます)。その後、自動チューニングが適切な値になるまで実行されます。学習率 (LR) が 1e-3 未満に低下すると、自動的に停止します。 PID 値は損失が最も低い値に戻り、SPH で復元できるように保存されます。\n - パラメータ: サンプル数 - リーマン和で使用される方位変化 (θ) の数。値を大きくすると、勾配内のノイズが減少します。高速応答 (α) - 初期のポインティング エラーとロール エラーをどれだけ重み付けするか。初期 LR - 初期学習率。高度と速度 - 調整中に使用する目標の高度と速度。固定 P - P を固定し、他のフィールドのみを調整します。最大値をクランプ - 調整された値をスライダーの制限内に保ちます。\n - 推奨事項: 1. 自動調整の高度と速度を、戦闘での使用が想定される値に設定します。 2. 5 ~ 10 倍のタイム スケーリングを使用します。 3. 山岳地を避けてください。 4. 最初に動的減衰を使用せずに調整し、その結果を動的減衰の開始点として使用します。すべての減衰値は調整された静的減衰値に設定され、動的減衰係数は 1 に設定されます。 5. PID 値は (現在) 設定されているため、固定点への飛行に最適化されているため、調整された I 値は戦闘中の移動目標には最適ではない可能性があり、わずかに大きい I が望ましい場合があります。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp = 高度設定は AI の望ましい飛行エンベロープを制御します + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_Def = デフォルトの高度 - これは、戦闘中または拡張中でないときに AI が戻ろうとする高度です。 + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_min = 最低高度 - AI が上昇を開始する前に、航空機がどの高度を超えて降下する必要があるかを設定します。機体を引き上げるために十分なスペースを確保してください。 + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_max = 最大高度 - 有効にすると、これは最小高度の逆になります。これは、AI がダイビングを開始する前に航空機がどの高度を超える必要があるかを設定します。 + //#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_bombing = ??? Bombing Altitude - The AI will try to maintain level flight at this altitude when performing a bombing run (doesn't apply to torpedoes). + + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp = 速度設定は、さまざまな飛行条件や戦闘条件で航空機の望ましい対気速度を制御します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_min = 最小速度と最大速度 - 最大速度は、戦闘中または体当たり中に AI が到達しようとする対気速度ですが、それを超えることはありません。この速度を超えると、AI は最大速度を下回るまでブレーキをかけます。最小速度は、目標速度に関係なく、AI が戦闘操作を実行する最小速度です。この速度を下回る場合、AI は壊れて延長し、最小速度を超えるまで加速します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_takeoff = 離陸速度 - AI がアクティブになったときに航空機が着陸した場合、離陸速度は、AI が離陸のためにピッチアップを開始する前に航空機が到達する必要がある速度です。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_idle = アイドル速度 - AI が定位置まで周回中または飛行中に維持する非戦闘巡航対気速度を設定します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_gnd = 機銃掃射速度 - これは、地上目標を攻撃するときに AI が使用する対気速度です。地上ターゲットが移動している場合、掃射速度は地上ターゲットの速度を追加します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABpriority = AB 優先度 - AI がアフターバーナーをオン/オフするときに要求される加速レベルを制御します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABOverrideThreshold = AB オーバーライドしきい値 - この速度しきい値を下回ると、スロットルが最大の場合、AI はアフターバーナーをオンにします。 + //#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_BrakingPriority = ??? Braking Priority - This controls the aggressiveness of the AI using the brakes when allowed. + + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp = 管理制限は、さまざまな条件下で船舶の管理権限に制限を設定します + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_limiters = ステア リミッター - ステア リミッターは航空機の制御権限を制限します。低速リミッターは、低速制限速度以下の場合に AI 制御権限を設定します。高速リミッターは、高速制限速度以上の場合に AI 制御権限を設定します。下限速度と上限速度の間では、リミッター値は下限値から上限値まで直線的に変化します。高度ステア リミッターは、(高度/制限)^ 係数による制限を超える高度に基づいてステア制限を調整します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_bank = 最大バンク - 最大許容バンク角を設定します。 180 未満の場合、AI は操縦中に水平からこの角度を超えて回転しません。 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_clamps = 最大許容 G と AoA - 最大許容 G は、AI がこの数以下の G を引く操作に制限します。最大許容 AoA は、AI が使用できる最大攻撃角度を制限します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_modeSwitches = Post-Stall AoA Mode-Switch は、AoA しきい値を超えたために飛行機がいつステアリング モードを切り替えるかを制御します。インメルマン ターンの角度内で目標に向かって飛行する場合、機体は回転するのではなく、単純にピッチアップします。 + //#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_Immelmann = ??? Immelmann Turn Angle and Bias - When flying to a target within the angle of the Immelmann Turn directly behind the craft, the craft will simply pitch up / down instead of rolling. When not close to the min altitude, the Immelmann Pitch-Up Bias will force pitching up or down when the current pitching rate (°/s) is within the limit, otherwise the current pitching direction is used. + + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp = 回避/拡張は、銃撃、ミサイル、その他の航空機などの到来する脅威に対して AI がどのように反応するか、および AI がそれ自体と相対的に他の航空機の位置にどのように反応するかの両方を制御します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Evade = 最小回避時間、距離しきい値、時間しきい値、ターゲットを回避しない - これら 4 つの設定は、AI がいつ回避するかを制御します。 Min Evasion Time は、AI が回避操作を行う秒数を設定します。距離のしきい値は、回避をトリガーするために銃撃がどれだけ近づく必要があるかを設定します。時間しきい値は、AI が回避を開始する前に攻撃を受け続ける必要がある時間を設定します。 「ターゲットを回避しない」トグルは、現在のターゲットからの銃撃を回避目的で無視するかどうかを決定します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Nonlinearity = 回避/伸長の非線形性 - これは、航空機が回避または伸長中に行う飛行方向を中心とした振動の半径 (度単位) を制御します。これにより、航空機が回避/延長する際に直線的に飛行することがなくなります。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Dodge = 船舶回避 - これら 3 つの設定は、衝突の可能性に対して AI がどのように反応するかを設定します。次の先読み期間内に別の船舶が回避しきい値内に到達すると予測された場合、AI はそれを回避しようとします。回避強度は、予測された衝突を回避するために AI がどれだけ速く方向を変えようとするかを決定します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_standoff = スタンドオフ距離は、AI が戦闘中にターゲットの航空機に近づく最も近い距離です。スタンドオフ距離よりも近い場合、AI はターゲットまでの距離を増やすためにブレーキをかけます。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Extend = 距離の延長 - これらの設定は、AI がさまざまな種類のターゲットに対してどこまで延長するかを制御します。拡張角度は、AI が空中目標に対して拡張するときに高度を上げようとするか、または高度を下げようとするかを制御します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendVars = ターゲット速度係数、ターゲット角度、ターゲット距離 - これら 3 つの設定は、AI がいつ伸びるかを制御します。拡張するには、AI が前方に検出コーンを投影し、ターゲットまでの距離と相対速度をチェックします。デフォルトでは、ターゲットが AI の前方の 78 度の円錐の外側にあり、400 メートルより近く、対気速度が遅い場合、AI は延長します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendVel = ターゲット速度係数の拡張 - これは、どの相対速度で拡張を考慮すべきかを AI に指示します。 1 未満の場合はターゲット クラフトが遅くなり、1 より大きい場合は高速になる必要があります。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendAngle = ターゲット角度の拡張 - これは検出コーンの幅を設定し、視野と有効回転半径の組み合わせとして考えることができます。航空機の回転半径が大きいほど、この値は大きくなります。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendDist = ターゲットの距離を延長 - ターゲットに対してより良い角度を取得するために AI が延長される前に、ターゲットがどれだけ近づく必要があるかを設定します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendAbortTime = 中止時間を延長 - この時間内に利益が得られなかった場合、AI に延長を中止するよう指示します。延長すると 5 秒のクールダウン期間に入ります。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendToggle = 拡張トグル - 空中ターゲットに対する拡張を有効または無効にします (地上ターゲットに対する拡張は影響を受けません)。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_TerrainHelp = 地形回避は、機体の回転半径をスケーリングして地形衝突距離を生成し、引き上げる必要があるかどうかを AI に伝えることで、地面との衝突を予測するために使用されます。地形回避最小値は、飛行機が地面と平行で、ピッチアップのみが必要な最適な飛行条件に基づいています。 Terrain Avoid Max は、飛行機が地面に対して反転している最悪のシナリオに基づいており、ピッチアップする前に 180 度回転する必要があります。逆地形回避の臨界角は、垂直方向のロール角を決定します。飛行機が反転中に地形を回避しようとするか、最初にロールアップしようとする地形法線。操縦翼面展開時間は、地形を回避するために操縦翼面を調整するのにかかる平均時間の推定値です。ウェイポイントの地形回避は、航空機と現在のウェイポイントの間にある地形による、飛行方向への調整の範囲と強度に影響します。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_RamHelp = 体当たりは、弾薬がなくなった場合、または機能する武器がなくなった場合に、航空機が他の航空機に体当たりを試みるべきかどうかを制御します。体当たりが有効になっていない場合、AI は操縦を続けますが、交戦することはできません。コントロール サーフェス ラグは、コントロール サーフェスが完全な偏向に達するまでにかかる時間に基づいて、AI が衝突予測をどの程度修正するかを設定します。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_miscHelp = その他の設定 - これらは、他の場所に当てはまらない非戦闘動作を制御します。 + #LOC_BDArmory_AIWindow_infolink_Pilot_orbitHelp = 軌道方向 - これは、アイドリング時に AI が軌道を描く方向で、時計回りまたは反時計回りのいずれかです。 + #LOC_BDArmory_AIWindow_infolink_Pilot_standbyHelp = スタンバイ切り替え - 有効にすると、ターゲットがガード範囲に入ると AI が自動的にオンになります。 + + // Surface AI + #LOC_BDArmory_AIWindow_infolink_Surface_Type = 車両タイプ - これは、船舶が陸上車両であるか、船舶であるか、または陸上と水上での運用が可能な水陸両用船であるかを AI に伝えます。 + #LOC_BDArmory_AIWindow_infolink_Surface_Slopes = スロープ角度とターゲットピッチ - これらは船舶の角度制約を設定します。 Slope Angle は、AI が上昇しようとする最大傾斜を設定します。ターゲットピッチは、船舶が維持しようとする望ましい船舶の姿勢 (地面に対するピッチ角) を設定します。 + #LOC_BDArmory_AIWindow_infolink_Surface_Speeds = 巡航速度と最大速度は船舶の速度を設定します。巡航速度は、AI が非戦闘中に維持しようとする速度です。最大速度は、AI が戦闘中に到達しようとする速度です。 + #LOC_BDArmory_AIWindow_infolink_Surface_Drift = 最大ドリフト - 旋回時に船舶が順行から逸れる最大値を設定します。 + #LOC_BDArmory_AIWindow_infolink_Surface_Bank = Bank Angle -これにより、操船中に船舶がバンクまたはヒールオーバーできる最大値が AI に通知されます。 + //#LOC_BDArmory_AIWindow_infolink_Surface_Weave = ??? Weave Factor - When under fire or a missile is incoming, the craft tries weaving to throw off the attacker's aim. This controls the strength of that weaving. + #LOC_BDArmory_AIWindow_infolink_Surface_SteerPower = Steer Mult - これは、Surface AI PID コントローラーの比例入力です。低すぎると、航空機はその制御権限の全範囲を下回って使用することになります。高すぎると、クラフトが必要以上に適用され、目的の方向を超えてしまいます。 + #LOC_BDArmory_AIWindow_infolink_Surface_SteerDamping = Steer Damp - これは、Surface AI PID コントローラーの微分入力です。これは、新しい方向への機体の回転がどの程度減衰されるかを表します。低すぎると、正しい向きを超えて機体が振動してしまいます。高すぎると、減衰が方向の変化に対抗しすぎて、航空機がそれほど速く回転しなくなります。 + #LOC_BDArmory_AIWindow_infolink_Surface_Orientation = Attack Vector、Broadside Dir - これらは、AI がターゲットにどのように接近して攻撃するかを設定します。 Attack Vector 設定は、AI に、ターゲットに向けて船首を向けて交戦するか、それともターゲットに向けて船の舷側を向けるかを指示します。舷側方向は、目標に向かって左舷側か右舷側のどちらを優先するかを AI に指示します。 + #LOC_BDArmory_AIWindow_infolink_Surface_Engagement = 最小、最大交戦距離 - AI が交戦するためにその範囲内に収まるように操作しようとするターゲットからの最小距離と最大距離を設定します。 + #LOC_BDArmory_AIWindow_infolink_Surface_RCS = RCS - RCS 設定は、AI が操縦を支援するために RCS スラスターを使用するかどうかを切り替えます。 + #LOC_BDArmory_AIWindow_infolink_Surface_AvoidMass = 質量回避 - 障害物に体当たりするのではなく回避するために必要な最小質量を設定します。 + //#LOC_BDArmory_AIWindow_infolink_Surface_MaintainMinRange = ??? Maintain Min Range - This toggles if a Land vehicle will come to a stop/reverse to maintain distance from a target vs veering away to circle or extend. + //#LOC_BDArmory_AIWindow_infolink_Surface_Altitude = ??? Combat Altitude - The depth for the submarines to cruise / engage at. + + // VTOL AI + //#LOC_BDArmory_AIWindow_infolink_VTOL_PID = ??? 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.\n - 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.\n - 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.\n - Steer Damping (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_infolink_VTOL_Altitudes = ??? Default Altitude - The altitude (AGL) to cruise at when outside of combat.\nMin and Max Altitudes - The minimum and maximum altitudes the AI will try to fly to. Outside of this range, the AI will either climb or dive to return to the range of altitudes. + //#LOC_BDArmory_AIWindow_infolink_VTOL_Speeds = ??? Max and Combat Speeds - The maximum speed for maneuvering and the target speed for combat. + //#LOC_BDArmory_AIWindow_infolink_VTOL_Control = ??? Max Pitch and Bank Angles - This tells the AI the maximum it should allow the vessel to pitch or bank (roll) over during maneuvers.\nBroadside Dir - Broadside Direction tells the AI whether it should favor presenting the Port or Starboard broadside towards the target.\nRCS - Whether to use RCS thrusters for maneuvering or only when in combat. + //#LOC_BDArmory_AIWindow_infolink_VTOL_Combat = ??? Weave Factor - When under fire or a missile is incoming, the craft tries weaving to throw off the attacker's aim. This controls the strength of that weaving.\nMin, 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. + + // Orbital AI + //#LOC_BDArmory_AIWindow_infolink_Orbital_PID = ??? The PID (Proportional Integral Derivative) Controller calculates the difference between desired and actual output, then applies corrections based on P, I, and D values. The stock KSP SAS control steers the craft (Inactive) unless the PID control is enabled when aiming and firing weapons (Firing) or all maneuevers (Everything).\n - 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.\n - 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.\n - Steer Damping (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.\n - Max Error - This sets a clamp on the max angle error used by the PID controller. Lowering this decreases overshoot, but the overall response will be slower. Raising this value will make the overall response faster, but increase overshoot unless the PID is well-tuned. If you have a well-tuned PID, adjusting this value can help limit overshoot or increase your overall response speed. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Combat = ??? 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 which direction it should present towards the target, this is only active when PID control is active. When not attacking, the AI will roll to present this side to the target.\n\nMin Engagement Range - This sets the minimum distance from the target the AI will attempt to maneuver to be within in order to engage.\n\nZero Throttle Firing Range - Within this distance from the target (but outside of Min Engagement Range) the AI will always fire weapons without manipulating throttle. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Speeds = ??? Maneuver, Firing, and Angular Speeds - Speed at which the AI will maneuver at, and firing speed and angular speed limits during combat. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Control = ??? RCS - Whether to use RCS to manage velocity relative to the target when firing and whether to use RCS thrusters for maneuvering or only when in combat.\n\nReverse Engines - If enabled, AI will use backwards engines when able. Automatically disabled if no reverse engines are on the craft. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Evasion = ??? Min Evasion Time, Distance Threshold, Time Threshold, Min Range Threshold - 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 Min Range Threshold sets the range that the attacker has to be beyond to trigger evasion.\nThe RCS Evasion and Thrust Evasion toggles determine whether the AI attempts to evade gunfire using RCS and/or engine thrust.\nThe Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes. + + // Missile Config + #LOC_BDArmory_DeployAltitude = 高度の展開 + #LOC_BDArmory_EngageRangeMin = 最小エンゲージレンジ + #LOC_BDArmory_EngageRangeMax = 最大交戦範囲 + #LOC_BDArmory_EngageAir = 空気を取り入れる + #LOC_BDArmory_EngageMissile = ミサイルを攻撃する + #LOC_BDArmory_EngageSurface = 係合面 + #LOC_BDArmory_EngageSLW = SLW に参加する + #LOC_BDArmory_DisableEngageOptions = エンゲージメントオプションを無効にする + #LOC_BDArmory_EnableEngageOptions = エンゲージメントオプションを有効にする + #LOC_BDArmory_MaxStaticLaunchRange = 最大静的発射範囲 + #LOC_BDArmory_MinStaticLaunchRange = 最小静的発射範囲 + #LOC_BDArmory_MaxOffBoresight = 最大オフボアサイト + #LOC_BDArmory_DetonationDistanceOverride = 爆発距離オーバーライド + #LOC_BDArmory_DetonateAtMinimumDistance = 最小距離で爆発する + //#LOC_BDArmory_UseStaticMaxLaunchRange = ??? Dynamic/Static Max Range + #LOC_BDArmory_ProximityTriggerDistance = 弾頭の爆発距離 + #LOC_BDArmory_clustermissileTriggerDistance = 弾頭の発射距離 + #LOC_BDArmory_DropTime = ドロップタイム + #LOC_BDArmory_InCargoBay = 在庫ありカーゴベイ:\u0020 + #LOC_BDArmory_InCustomCargoBay = カスタムベイ切り替え:\u0020 + #LOC_BDArmory_DeployableWeapon = Wep の展開切り替え:\u0020 + #LOC_BDArmory_DetonationTime = 爆発時間 + #LOC_BDArmory_BallisticOvershootFactor = 弾道オーバーシュート係数 + #LOC_BDArmory_BallisticAnglePath = 弾道角度パス + //#LOC_BDArmory_Missile_CruiseSpeed = ??? Cruise Speed + #LOC_BDArmory_CruiseAltitude = 巡航高度 + #LOC_BDArmory_CruisePredictionTime = クルーズ予測時間 + //#LOC_BDArmory_CruisePopup = ??? Cruise Popup Attack + #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_DecoupleSpeed = デカップリング速度 + #LOC_BDArmory_LoftMaxAltitude = ロフトの最高高度 + #LOC_BDArmory_LoftRangeOverride = ロフト範囲の上書き + #LOC_BDArmory_LoftAltitudeAdvMax = ロフト最大高度 + #LOC_BDArmory_LoftMinAltitude = ロフトの最低高度 + #LOC_BDArmory_LoftAngle = ロフトの上昇角度 + #LOC_BDArmory_LoftTermAngle = ロフトの終了角度 + #LOC_BDArmory_LoftRangeFac = ロフトのレンジ係数 + #LOC_BDArmory_LoftVelComp = ロフト速度補正 + #LOC_BDArmory_LoftVertVelComp = ロフト垂直速度補正 + //#LOC_BDArmory_LoftAltComp = ??? Loft Altitude Compensation + #LOC_BDArmory_terminalHomingRange = ターミナル誘導の幅 + + #LOC_BDArmory_EMPBlastRadius = EMP爆発範囲 + #LOC_BDArmory_OrdnanceAvailable = 条例が利用可能 + #LOC_BDArmory_MissileAssign = ミサイルの割り当て + #LOC_BDArmory_CurrentLocks = 現在のロック + //#LOC_BDArmory_Offset = ??? Ordnance Offset + //#LOC_BDArmory_Deploy_Time = ??? Deploy Time + + // Safety Systems + #LOC_BDArmory_SSTank = セルフシールタンク + #LOC_BDArmory_SSTank_On = セルフシールタンクの追加 + #LOC_BDArmory_SSTank_Off = セルフシールタンクを取り外します + #LOC_BDArmory_CASE = 場合。階層 + //#LOC_BDArmory_CASE_Sim = ??? Detonation Sim + #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_AddedCost = 追加コスト + #LOC_BDArmory_AddedMass = 安全システムの質量 + #LOC_BDArmory_DryMass = 質量 + + // Turret Config + #LOC_BDArmory_MaxPitch = 最大ピッチ + #LOC_BDArmory_MinPitch = 最小ピッチ + #LOC_BDArmory_YawRange = ヨー範囲 + //#LOC_BDArmory_YawStandbyAngle = ??? Yaw Standby Angle + #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_AimOverrideFalse = ??? Aim With This Weapon + //#LOC_BDArmory_AimOverrideTrue = ??? Revert Default Aim + #LOC_BDArmory_ReturnTurret = リターンタレット + #LOC_BDArmory_ToggleAnimation = アニメーションの切り替え + + // Missile UI + #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_TurretLoft = ??? Lofted Aimpoint + //#LOC_BDArmory_TurretLoftFac = ??? Loft Velocity Factor + #LOC_BDArmory_MissileTurretFireFOV = 火の視野 + #LOC_BDArmory_HideUI = 武器名UIを隠す + #LOC_BDArmory_ShowUI = 武器名の設定UI + #LOC_BDArmory_HideWeaponGroupUI = 武器グループUIを隠す + #LOC_BDArmory_SetWeaponGroupUI = 武器グループUIの設定 + #LOC_BDArmory_Fire = 火 + #LOC_BDArmory_ToggleRadar = レーダーの切り替え + #LOC_BDArmory_ToggleIRST = IRSTを切り替えます + //#LOC_BDArmory_DynamicRadar = ??? Disable Radar vs ARMs + + // WM Config + #LOC_BDArmory_GuardMode = ガードモード:\u0020 + #LOC_BDArmory_Team = チーム + //#LOC_BDArmory_Allies = ??? Allies + #LOC_BDArmory_Weapon = 武器 + #LOC_BDArmory_FiringPriority = 選択の優先順位 + //#LOC_BDArmory_weaponChannel = ??? Weapon Channel + #LOC_BDArmory_FiringInterval = 発射間隔 + #LOC_BDArmory_FiringBurstLength = 発射バースト長(時間) + #LOC_BDArmory_FiringBurstCount = 発射バースト長(回数) + #LOC_BDArmory_FiringTolerance = 発射角度 + #LOC_BDArmory_FieldOfView = 視野 + #LOC_BDArmory_VisualRange = 視覚範囲 + #LOC_BDArmory_GunsRange = ガンズレンジ + #LOC_BDArmory_MissilesRange = 動的発射範囲を使用する + #LOC_BDArmory_MissilesOnTarget = ミサイル/ターゲット + #LOC_BDArmory_FireAngleOverride_Enable = 発射角度のオーバーライドを有効にする + #LOC_BDArmory_FireAngleOverride_Disable = 発射角度オーバーライドを無効にする + #LOC_BDArmory_BurstLengthOverride_Enable = バースト長オーバーライドを有効にする + #LOC_BDArmory_BurstLengthOverride_Disable = バースト長オーバーライドを無効にする + #LOC_BDArmory_FiringAngle = 発射角度 + + //#LOC_BDArmory_dynamic = ??? Dynamic + //#LOC_BDArmory_static = ??? Static + + #LOC_BDArmory_Status = 状態 + #LOC_BDArmory_Toggle = 切り替え + #LOC_BDArmory_ShowGroupEditor = グループエディターを表示 + #LOC_BDArmory_ShowGroupEditor_enabledText = グループGUIを閉じる + #LOC_BDArmory_ShowGroupEditor_disabledText = グループGUIを開く + #LOC_BDArmory_DeactivationDepth = 非アクティブ化の深さ + #LOC_BDArmory_Hitpoints = ヒットポイント + #LOC_BDArmory_FireCountermeasure = 火災対策 + + #LOC_BDArmory_TogglePilot = パイロットの切り替え + #LOC_BDArmory_DeactivatePilot = パイロットを非アクティブ化する + #LOC_BDArmory_ActivatePilot = パイロットをアクティブ化する + + #LOC_BDArmory_SelectTeam = チームを選ぶ + #LOC_BDArmory_OpenGUI = GUIを開く + + #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_Multiple = 重複 + #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 = シェイプドチャージ + #LOC_BDArmory_Ammo_Kinetic = キネティック + #LOC_BDArmory_Ammo_EMP = E.M.P. + #LOC_BDArmory_Ammo_Choker = チョーカー + #LOC_BDArmory_Ammo_Impulse = インパルス + #LOC_BDArmory_Ammo_Gravitic = 重力 + #LOC_BDArmory_Ammo_Incendiary = 焼夷性 + #LOC_BDArmory_Ammo_Nuclear = 核 + #LOC_BDArmory_Ammo_Beehive = 蜂の巣 + #LOC_BDArmory_NextTankSetup = 次の水槽セットアップ + #LOC_BDArmory_PreviousTankSetup = 前回の水槽セットアップ + + // Team Icons + #LOC_BDArmory_Icons_title = BDArmory チームUIアイコン + #LOC_BDArmory_Icons_PSA = F4 を押してストック容器のアイコンを切り替えます + #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_missile_text = ??? Missile Labels + #LOC_BDArmory_Icon_debris = 破片のアイコン + #LOC_BDArmory_Icon_persist = UIで非表示にしないでください + #LOC_BDArmory_Icon_threats = 船舶の脅威指標 + #LOC_BDArmory_Icon_pointers = オフスクリーン アイコン ポインター + #LOC_BDArmory_Icon_scale = アイコンのスケール: + //#LOC_BDArmory_Icon_opacity = ??? Opacity: + #LOC_BDArmory_Icon_distance_threshold = 距離のしきい値: + //#LOC_BDArmory_Icon_max_distance_threshold = ??? Max Distance Threshold: + #LOC_BDArmory_Icon_telemetry = テレメトリを有効にします。 + //#LOC_BDArmory_Icon_StoreTeamColors = ??? Store Team Colors + #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_Titanium = チタン + #LOC_BDArmory_Composites = 複合材料 + //#LOC_BDArmory_RAMFoam = ??? Radar Absorbent Foam + #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_ArmorLiftStacking = リフトスタッキング + #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 = HPビジュアライザーの切り替え + #LOC_BDArmory_ArmorHullVisualizer = ハルビジュアライザの切り替え + #LOC_BDArmory_ArmorLiftVisualizer = リフトビジュアライザの切り替え + //#LOC_BDArmory_partTreeVisualizer = ??? Toggle Part Tree Visualizer + //#LOC_BDArmory_checkVessel = ??? Check Vessel Legality + #LOC_BDArmory_ArmorSelect = 防具素材の選択 + //#LOC_BDArmory_DryMassWhitelist = ??? Resources Counted as Drymass + #LOC_BDArmory_ArmorTool = BDA クラフトユーティリティツール + #LOC_BDArmory_Armor_HullMat = 現在の機体の材質 + #LOC_BDArmory_Armor_ArmorType = 装甲の種類 + #LOC_BDArmory_Armor_Hullmass = 調整後の部品質量 + #LOC_BDArmory_BulletResist = キネティックレジスト + #LOC_BDArmory_ExplosionResist = 爆発耐性 + #LOC_BDArmory_LaserResist = レーザーレジスト + #LOC_BDArmory_ArmorShatterWarning = 貫通攻撃で装甲が粉砕される + //#LOC_BDArmory_ArmorToolPartCount = ??? Part Count Exceeded! + //#LOC_BDArmory_ArmorToolEngineCount = ??? Too Many Engines: + //#LOC_BDArmory_ArmorToolEngineCountFloor = ??? Too Few Engines: + //#LOC_BDArmory_ArmorToolTWR = ??? TWR Exceeded: + //#LOC_BDArmory_ArmorToolLTW = ??? LTW Exceeded: + //#LOC_BDArmory_ArmorToolMaxMass = ??? Mass Limit Exceeded: + //#LOC_BDArmory_ArmorToolMaxPoints = ??? Point Limit Exceeded: + //#LOC_BDArmory_ArmorToolIllegalParts = ??? Illegal Parts: + //#LOC_BDArmory_ArmorToolNonCockpit = ??? not attached to cockpit + //#LOC_BDArmory_ArmorToolOversizedPWings = ??? pWings exceeding max Lift - check Lift Visualizer + //#LOC_BDArmory_ArmorToolVesselLegal = ??? Vessel legal! + + + // Missile & CM Settings + #LOC_BDArmory_Settings_MissileCMToggle = ミサイルと対策設定を表示 + #LOC_BDArmory_Settings_AspectedRCS = リアルタイムアスペクトRCS + #LOC_BDArmory_Settings_AspectedRCSOverallRCSWeight = RCS全体の質量 + #LOC_BDArmory_Settings_AspectedIRSeekers = IRオクルージョンはミサイルに影響を与える + #LOC_BDArmory_Settings_FlareFactor = マックスフレアスタートヒートマルチ + #LOC_BDArmory_Settings_ChaffFactor = チャフの位置。ディストーションマルチ + #LOC_BDArmory_Settings_SmokeDeflectionFactor = 煙の位置。ディストーションマルチ + #LOC_BDArmory_Settings_APSThreshold = APSをトリガーする口径 + + // Texture switching + #LOC_BDArmory_NextTexture = 次のテクスチャ + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ru.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ru.cfg new file mode 100644 index 000000000..0319b777c --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ru.cfg @@ -0,0 +1,1578 @@ +// Notes: +// - The indentation provides fold region info for IDEs. +// - #LOC_A = #LOC_B is valid. +// - Check for duplicates with: grep -o '^\s*#LOC[^ ]\+' en-us.cfg |tr -d ' '|sort|uniq -c|grep -v '\s1' +// - Propagate changes in en-us.cfg to the other localisation files by running 'python3 ../_Other\ Stuff/localisation_organisation_sync.py' in the BDArmory/BDArmory folder. + +Localization +{ + ru + { + // Generic + //#LOC_BDArmory_Generic_OK = ??? OK + #LOC_BDArmory_Generic_Cancel = Отменить + #LOC_BDArmory_Generic_New = Новый + #LOC_BDArmory_Generic_On = Вкл + #LOC_BDArmory_Generic_Off = Выкл + #LOC_BDArmory_On = Вкл. + #LOC_BDArmory_Off = Откл. + #LOC_BDArmory_Generic_Hide = Спрятать + #LOC_BDArmory_Generic_Show = Показать + //#LOC_BDArmory_Generic_Load = ??? Load + //#LOC_BDArmory_Generic_Save = ??? Save + //#LOC_BDArmory_Generic_Reload = ??? Reload + //#LOC_BDArmory_Generic_Help = ??? Help + //#LOC_BDArmory_Generic_Select = ??? Select + #LOC_BDArmory_Generic_SaveandClose = Сохранить и Выйти + #LOC_BDArmory_VesselStatus_Landed = (Посажен) + #LOC_BDArmory_VesselStatus_Splashed = (Уничтожен) + #LOC_BDArmory_VesselStatus_Underwater = (Под водой) + #LOC_BDArmory_false = Ложь + #LOC_BDArmory_true = Истина + #LOC_BDArmory_Enabled = Вкл. + #LOC_BDArmory_Disabled = Выкл. + #LOC_BDArmory_Enable = Вкл. + #LOC_BDArmory_Disable = Выкл. + + // WM Window + #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_barrageStagger = Колебания + #LOC_BDArmory_WMWindow_rippleText3 = Залповый огонь: <<1>> RPM + #LOC_BDArmory_WMWindow_rippleText4 = Залповый огонь: ВЫКЛ + #LOC_BDArmory_WMWindow_ListWeapons = Вооружение + #LOC_BDArmory_WMWindow_GuardMenu = Боевое Меню + #LOC_BDArmory_WMWindow_ModulesToggle = Модули + #LOC_BDArmory_WMWindow_NoWeaponManager = Контроллер Вооружения не найден. + + // WM Guard Menu + #LOC_BDArmory_WMWindow_NoneWeapon = Нет + #LOC_BDArmory_WMWindow_GuardMode = Боевой Режим <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = Огневой Интервал + #LOC_BDArmory_WMWindow_BurstLength = Длина Очереди + #LOC_BDArmory_WMWindow_FiringTolerance = Огневой Угол + #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 = Все Цели + // Advanced Targeting + #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 = Случайные Детали + + // WM Target Priority + #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 + + // WM Modules + #LOC_BDArmory_WMWindow_RadarWarning = Приемник Радиолокационного Предупреждения + #LOC_BDArmory_WMWindow_GPSCoordinator = GPS Координатор + #LOC_BDArmory_WMWindow_WingCommand = Управление Крылом + // WM GPS Module + #LOC_BDArmory_WMWindow_GPSTarget = GPS Цель + #LOC_BDArmory_WMWindow_NoTarget = Нет Цели + + // WM infolink + // WM infolink Weapons + #LOC_BDArmory_WMWindow_Weapons_Desc = Вооружение - Эта вкладка отображает все оружия/группы вооружения на судне. При нажатии на название оружия (группы) оно будет выбрано и активировано, позволяя вручную стрелять из пулемета, ракетной установки или лазера. Если выбрана ракета, переключатель "Триггер разоружен" должен быть переключен на ВООРУЖЕН, прежде чем ракета может быть запущена. + #LOC_BDArmory_WMWindow_Ripple_Salvo_Desc = Ракеты, когда выбраны, будут иметь опцию Залпового огня. Она отвечает за залповый пуск ракет, если триггер вооружен. Пулеметы, ракетные установки и лазеры со скорострельностью ниже 1500 выстрелов в минуту будут иметь переключатель Последовательный огонь/Залп. Если присутствует несколько видов оружия одного типа/группы оружия, режим Последовательного огня приведет к тому, что каждое оружие будет стрелять последовательно. В режиме Залпа все орудия будут стрелять одновременно. + + // WM infolink Guard Menu + #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 = Кнопка "Настройки Атакуемых Целей" - Переключатель, чтобы быстро устанавливать атакуемые цели (Воздушные, Наземные, Ракеты или Торпеды) для всего вооружения сразу. + + // WM infolink Target Priority + #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_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 = Наименьшее кол-во Союзников Атакует - Контролирует приоритетность целей, основываясь на количестве союзников, атакующих эту цель. Большие значения отдают приоритет никем не атакуемым целям, меньшие значения - целям, атакованным союзниками. + #LOC_BDArmory_WMWindow_targetThreat_desc = Угроза Цели - Большие значения отдают приоритет целям, атакующим данный аппарат, меньшие значения игнорируют атакующие данный аппарат цели. + #LOC_BDArmory_WMWindow_targetVIP_desc = Атаковать VIP / Защищать VIP. Отдает приоритет атаке вражеских VIP целей или атаке целей, которые атакуют VIP союзника, если установлены большие значения, и игнорирует их, если установлены маленькие значения. + + // Settings Window + #LOC_BDArmory_Settings_Title = Настройки BDArmory + #LOC_BDArmory_Settings_AdvancedUserSettings = Расширенные Настройки + // Section Toggles + #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 = Настройки Ограничений + + // Graphics / UI + #LOC_BDArmory_Settings_DebugSettingsToggle = Отладка + #LOC_BDArmory_Settings_AIToolbarButton = Кнопка ИИ на Панели Инструментов + //#LOC_BDArmory_Settings_VMToolbarButton = ??? VM Toolbar Button + //#LOC_BDArmory_Settings_UIScale = ??? UI Scale + //#LOC_BDArmory_Settings_UIScaleFollowsStock = ??? Follow Stock + #LOC_BDArmory_Settings_Instakill = Мгновенное Убийство + #LOC_BDArmory_Settings_InfiniteAmmo = Бесконечные Боеприпасы + //#LOC_BDArmory_Settings_InfiniteMissiles = ??? Infinite Ordnance + //#LOC_BDArmory_Settings_InfiniteCountermeasures = ??? Infinite Countermeasures + #LOC_BDArmory_Settings_BulletFX = Bullet FX + #LOC_BDArmory_Settings_BulletHits = Пулевые Попадания + //#LOC_BDArmory_Settings_WaterHitFX = ??? Water Hit FX + //#LOC_BDArmory_Settings_LightFX = ??? Light FX + //#LOC_BDArmory_Settings_PerfOptions = ??? Enable FX + #LOC_BDArmory_Settings_EjectShells = Отбрасывать Гильзы + #LOC_BDArmory_Settings_VesselRelativeBulletChecks = Проверять Пули на Аппарате + #LOC_BDArmory_Settings_AimAssist = Помощь в Наведении + //#LOC_BDArmory_Settings_AimAssistMode_Target = ??? Aim Assist Mode (Target) + //#LOC_BDArmory_Settings_AimAssistMode_Aimer = ??? Aim Assist Mode (Aimer) + #LOC_BDArmory_Settings_GUIBackgroundOpacity = Непрозрачность Фона Интерфейса + #LOC_BDArmory_Settings_DrawAimers = Целеуказатели + + // Debugging + #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_ResetScrollZoom = ??? Reset Scroll-Zoom + + // Gameplay FIXME These need more sorting + #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_AutoLogTimeSync = ??? Auto-Enable Time-Sync Logging + //#LOC_BDArmory_Settings_LogTimeSyncInterval = ??? Time-Sync Log Interval + //#LOC_BDArmory_Settings_LogTimeSyncStart = ??? Start Logging + //#LOC_BDArmory_Settings_LogTimeSyncStop = ??? Stop Logging + #LOC_BDArmory_Settings_ShowEditorSubcategories = Показать Подкатегории для Разработчика + #LOC_BDArmory_Settings_AutocategorizeParts = Автоматическая Классификация Деталей + #LOC_BDArmory_Settings_waterDrag = Подводное Сопротивление Пуль + #LOC_BDArmory_Settings_AutoLoadToKSC = Автоматическая Загрузка в Космический Центр + #LOC_BDArmory_Settings_GenerateCleanSave = Создать Чистое Сохранение + //#LOC_BDArmory_Settings_AutoDisableUI = ??? Auto-Disable UI + #LOC_BDArmory_Settings_AutoResumeTournaments = Автоматическое Продолжение Турнира + //#LOC_BDArmory_Settings_AutoResumeContinuousSpawn = ??? Auto-Resume Cts Spawn + #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_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_CompChecks = ??? Use AI/WM Overrides + #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_CameraSwitchIncludeMissiles = ??? Camera Switch: Incl. Missiles + //#LOC_BDArmory_Settings_ScrollZoomPrevention = ??? Scroll-Zoom Prevention + #LOC_BDArmory_Settings_ResetHP = Установить Макс. Здоровье Всем Деталям + #LOC_BDArmory_Settings_ResetArmor = Сбросить Значения Брони + #LOC_BDArmory_Settings_ResetHull = Сбросить Материалы + //#LOC_BDArmory_Settings_RestoreKAL = ??? Restore KAL + //#LOC_BDArmory_Settings_DisableGuardModeOnSpawn = ??? Disable Guard Mode on Spawn + #LOC_BDArmory_Settings_IntakeHack = Хакерские Заборники + //#LOC_BDArmory_Settings_PWingsHack = ??? Pwing Edge Lift + //#LOC_BDArmory_Settings_PWingsThickHP = ??? PWing Thickness Based Mass/HP + #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_gungame_progression = ??? Keep progresson on respawn + //#LOC_BDArmory_settings_gungame_cycle = ??? Cycle List + // General Sliders + #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_ArmorExplosivePenetrationResistanceMultiplier = ??? Armor Explosion Resistance Multiplier + //#LOC_BDArmory_Settings_BuildingDamageMultiplier = ??? Building Damage Multiplier + #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = Множитель Имплозивного Урона + #LOC_BDArmory_Settings_SecondaryEffectDuration = Продолжительность Эффектов Особого Вооружения + #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = Множитель Баллистической Траектории + //#LOC_BDArmory_Settings_ArmorMassMultiplier = ??? Armor Mass Multiplier + #LOC_BDArmory_Settings_DebrisCleanUpDelay = Задержка Удаления Обломков + //#LOC_BDArmory_Settings_NumericInputSelfUpdate = ??? Numeric Input Self Update + #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_Max_Armor_Limit = ??? Max Armor Limit + + // Game Modes + // Heart-Bleed + #LOC_BDArmory_Settings_HeartBleed = Кровотечение + #LOC_BDArmory_Settings_HeartBleedRate = Частота Кровотечения + #LOC_BDArmory_Settings_HeartBleedInterval = Задержка Кровотеченя + #LOC_BDArmory_Settings_HeartBleedThreshold = Порог Кровотечения + + // Resource Steal + #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 = Норма Кражи Контрмер + + // Asteroids + #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 = Отслеживает Разброс Аппаратов + + // 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_MutatorGungame = ??? GunGame Progression + #LOC_BDArmory_Settings_MutatorTimed = Применить на Таймер + #LOC_BDArmory_Settings_MutatorDuration = Продолжительность + #LOC_BDArmory_UI_MutatorStart = Глобальные Мутации + #LOC_BDArmory_UI_MutatorShuffle = Мутации Перемешались! + #LOC_BDArmory_Settings_MutatorNum = Кол-во Мутаций + #LOC_BDArmory_Settings_MutatorIcons = Значки Мутаций + + #LOC_BDArmory_Settings_WaypointsMode = Режим Вейпоинтов + //#LOC_BDArmory_Settings_GLimitsMode = ??? Override G-Force Limits + + // Battle Damage + #LOC_BDArmory_Settings_BDSettingsToggle = Настройки Боевых Повреждений + #LOC_BDArmory_Settings_BD_Proc = Частота Процесса + //#LOC_BDArmory_Settings_BD_Proc_Pen = ??? Proc Min Penetration + #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_JointStrength = ??? Structural Damage + #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 = Разрешить Боевые Повреждения + + // Radar / Other Settings + #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_IGNORE_TERRAIN_CHECK = ??? Detection Ignores Terrain + //#LOC_BDArmory_Settings_CHECK_WATER_TERRAIN = ??? Detection Checks Water + //#LOC_BDArmory_Settings_RADAR_NOTCHING = ??? Radar Notching + //#LOC_BDArmory_Settings_Notching_Factor = ??? Notch Effectiveness Factor + //#LOC_BDArmory_Settings_Notching_SCR_Factor = ??? Notch SCR Factor + + // Competition / Tournament + #LOC_BDArmory_Settings_CompetitionDistance = Дистанция Состязания + #LOC_BDArmory_Settings_CompetitionDuration = Продолжительность Состязания + //#LOC_BDArmory_Settings_CompetitionIntraTeamSeparation = ??? Intra-Team Separation + //#LOC_BDArmory_Settings_CompetitionIntraTeamSeparationPerMember = ??? / Member + #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_CompetitionGMWeaponKill = ??? Kill Weaponless Craft + //#LOC_BDArmory_Settings_CompetitionGMEngineKill = ??? Kill Engineless Craft + //#LOC_BDArmory_Settings_CompetitionGMDisableKill = ??? Kill Disabled Craft + //#LOC_BDArmory_Settings_CompetitionGMHPKill = ??? Kill Damaged Craft + //#LOC_BDArmory_Settings_CompetitionGMKillDelay = ??? GM Kill Delay + #LOC_BDArmory_Settings_CompetitionStarting = Начало Состязания... + #LOC_BDArmory_Settings_DogfightCompetition = Воздушный Бой + #LOC_BDArmory_Settings_StartCompetition = Начать Состязание + #LOC_BDArmory_Settings_StopCompetition = Остановить Состязание + #LOC_BDArmory_Settings_StartCompetitionNow = Начать Состязание СЕЙЧАС + #LOC_BDArmory_Settings_CompetitionStartNowAfter = Задержка Ручного Начала Состязания + //#LOC_BDArmory_Settings_CompetitionStartDespiteFailures = ??? Start Comp. Despite Failures + #LOC_BDArmory_Settings_StartRapidDeployment = Начать Оперативное Развертывание + #LOC_BDArmory_Settings_StartOrbitalDeployment = Начать Орбитальное Развертывание + #LOC_BDArmory_Settings_LowGravDeployment = Начать Состязание с Низкой Гравитацией + #LOC_BDArmory_Settings_EditInputs = Редактировать Входные Данные + #LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart = Закрывать Окно Настроек при Начале Состязания + //#LOC_BDArmory_Settings_CompetitionWaypointTimeThreshold = ??? Waypoint Time Threshold + + // BDA Remote (defunct) + #LOC_BDArmory_BDARemoteOrchestration_Title = BDA Удаленная Аранжировка + #LOC_BDArmory_Settings_RemoteLogging = Удаленная Аранжировка + #LOC_BDArmory_Settings_RemoteInterheatDelay = Задержка между Нагревами + #LOC_BDArmory_Settings_RemoteSync = Запустить через Удаленную Аранжировку + #LOC_BDArmory_Settings_CompetitionID = ID Состязания + + // Input Settings + #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_TemporarilyShowMouse = ??? Temporarily Show Mouse + #LOC_BDArmory_InputSettings_GUI = Интерфейс + #LOC_BDArmory_InputSettings_BackBtn = Назад + #LOC_BDArmory_InputSettings_recordedInput = Нажмите кнопку. + #LOC_BDArmory_InputSettings_SetKey = Установить Кнопку + #LOC_BDArmory_InputSettings_Clear = Очистить + + // Weapon Config + #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_saveClose = ??? Save & Close + #LOC_BDArmory_reset = Сбросить + //#LOC_BDArmory_applyTo = ??? Apply To + //#LOC_BDArmory_WeaponGroup = ??? Weapon Group GUI + //#LOC_BDArmory_AddToWpnGroup = ??? Add to Weapon Group: + //#LOC_BDArmory_thisWeapon = ??? this weapon + //#LOC_BDArmory_SymmetricWeapons = ??? symmetric weapons + + //#LOC_BDArmory_CustomFireKey = ??? Custom Fire Key + //#LOC_BDArmory_SetCustomFireKey = ??? Set Custom Fire Key + + #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_MissileCMRange = ??? Countermeasure Range + //#LOC_BDArmory_MissileCMInterval = ??? Countermeasure Interval + + // Adjustable Rails + #LOC_BDArmory_Rails = Направляющие + #LOC_BDArmory_IncreaseHeight = Высота ++ + #LOC_BDArmory_DecreaseHeight = Высота -- + #LOC_BDArmory_IncreaseLength = Длина ++ + #LOC_BDArmory_DecreaseLength = Длина -- + #LOC_BDArmory_RailsPlus = Кол-во Направляющих ++ + #LOC_BDArmory_RailsMinus = Кол-во Направляющих -- + + // Vessel Spawner + #LOC_BDArmory_BDAVesselSpawner_Title = BDA Спавнер Аппаратов + // Spawn Options + #LOC_BDArmory_Settings_SpawnOptions = Параметры Размещения + #LOC_BDArmory_Settings_SpawnDistanceFactor = Коэфицент Дальности Размещения + //#LOC_BDArmory_Settings_SpawnRefHeading = ??? Reference Heading + #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_CSFollowsCentroid = ??? Spawn Point Follows Centroid (CS) + #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = Непрерывное Размещение (S) + #LOC_BDArmory_Settings_SpawnRandomOrder = Случайный Порядок Размещения (S) + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = Начинать Состязания Автоматически + //#LOC_BDArmory_Settings_SpawnInitialVelocity = ??? Air-Spawn With Idle Speed + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = Разместить Спавнер Здесь + #LOC_BDArmory_Settings_OutOfAmmoKillTime = Время Убийства из-за Отсутствия Боеприпасов (CS) + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Установить Точку Размещения Здесь + #LOC_BDArmory_Settings_SaveSpawnLoc = Сохранить Локацию + #LOC_BDArmory_Settings_ClearDebrisNow = Удалить Обломки + #LOC_BDArmory_Settings_ClearBystandersNow = Удалить Незадействованные Аппараты + // Fill Seats + #LOC_BDArmory_Settings_SpawnFillSeats = Заполнить Места + #LOC_BDArmory_Settings_SpawnFillSeats_Minimal = По Минимуму + #LOC_BDArmory_Settings_SpawnFillSeats_Default = Кабины Пилотов и Боевые Кресла + #LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints = Все Точки Управления + #LOC_BDArmory_Settings_SpawnFillSeats_Cabins = Также Кабины + + // Teams + #LOC_BDArmory_Settings_Teams = Команды + #LOC_BDArmory_Settings_Teams_FFA = FFA + #LOC_BDArmory_Settings_Teams_Folders = Для Каждой Папки / Для Каждого Файла + //#LOC_BDArmory_Settings_Teams_Custom_Template = ??? Custom Template + #LOC_BDArmory_Settings_Teams_SplitEvenly = Разделить Равномерно на + + #LOC_BDArmory_Settings_SpawnFilesLocation = Расположение Файлов Аппаратов + // 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_TemplateSelection = ??? Template Selection + //#LOC_BDArmory_Settings_CustomSpawnTemplate_SaveCraftToTemplate = ??? Save Craft URLs + + // Observers + //#LOC_BDArmory_Settings_Observers = ??? Observers + //#LOC_BDArmory_ObserverSelection_Title = ??? Observer Selection + //#LOC_BDArmory_ObserverSelection_SelectAll = ??? Select All + //#LOC_BDArmory_ObserverSelection_SelectNone = ??? Select None + + // Interesting Spawn Locations + #LOC_BDArmory_Settings_SpawnLocations = Интересные Места Размещения + #LOC_BDArmory_Settings_WarpHere = Переместить Сюда + #LOC_BDArmory_Settings_Planet = Выбрать Планету + + // Tournament Options + #LOC_BDArmory_Settings_TournamentOptions = Настройки Турнира + #LOC_BDArmory_Settings_TournamentStyle = Стиль Турнира + //#LOC_BDArmory_Settings_TournamentRoundType = ??? Round Type + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = Задержка Между Нагревами + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = Ускорение Времени между Раундами + //#LOC_BDArmory_Settings_TournamentTimeWarpDaylight = ??? Daylight + #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_TournamentNPCsPerHeat = ??? NPCs Per Heat + #LOC_BDArmory_Settings_TournamentSetup = Устроить Турнир + #LOC_BDArmory_Settings_TournamentRun = Начать Турнир + #LOC_BDArmory_Settings_TournamentStop = Остановить Турнир + + // Waypoints + #LOC_BDArmory_Settings_WaypointsOptions = Настройки Вейпоинтов + #LOC_BDArmory_Settings_WaypointsOneAtATime = По Одному за Раз + #LOC_BDArmory_Settings_WaypointsInfFuelAtStart = Бесконечное Топливо до Первого Вейпоинта + #LOC_BDArmory_Settings_WaypointsShow = Показать Вейпоинты + + #LOC_BDArmory_Settings_SingleSpawn = Одиночное Размещение + #LOC_BDArmory_Settings_ContinuousSpawning = Продолжительное Размещение + #LOC_BDArmory_Settings_CancelSpawning = Отменить Размещение + + // Waypoint GUI + //#LOC_BDArmory_BDAWaypointBuilder_Title = ??? Waypoint Course Tool + //#LOC_BDArmory_WP_LoadCourse = ??? Load Course + //#LOC_BDArmory_WP_NewCourse = ??? New Course + //#LOC_BDArmory_WP_ChooseCourse = ??? Select Course + //#LOC_BDArmory_WP_Create = ??? Create + //#LOC_BDArmory_WP_Record = ??? Record + //#LOC_BDArmory_WP_TimeStep = ??? Timestep (s) + //#LOC_BDArmory_WP_Recording = ??? Recording Course.... + //#LOC_BDArmory_WP_FinishRecording = ??? Finish Recording + //#LOC_BDArmory_WP_Spawnpoint = ??? Spawnpoint + //#LOC_BDArmory_WP_AddGate = ??? Add Gate + //#LOC_BDArmory_WP_Waypoint = ??? Waypoint + //#LOC_BDArmory_WP_SpeedLimit = ??? Speed limit + //#LOC_BDArmory_WP_Increment = ??? Increment + //#LOC_BDArmory_WP_MaxLaps = ??? Max Laps + //#LOC_BDArmory_WP_GuardActivate = ??? Activate Guard After + //#LOC_BDArmory_WP_CourseDefaults = ??? Use Course Settings + //#LOC_BDArmory_WP_SelectModel = ??? Waypoint Type + + // Vessel Mover + //#LOC_BDArmory_VesselMover_Title = ??? BDA Vessel Mover + //#LOC_BDArmory_VesselMover_VesselSelection = ??? Vessel Selection + //#LOC_BDArmory_VesselMover_CrewSelection = ??? Crew Selection + //#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_DeconflictVesselName = ??? Deconflict Vessel Name + //#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 + + // Craft Browser + //#LOC_BDArmory_CraftBrowser_InvalidParts = ??? INVALID PARTS + //#LOC_BDArmory_CraftBrowser_UnknownModules = ??? Modules + //#LOC_BDArmory_CraftBrowser_Clear = ??? Clear + //#LOC_BDArmory_CraftBrowser_ClearAll = ??? Clear All + //#LOC_BDArmory_CraftBrowser_Refresh = ??? Refresh + //#LOC_BDArmory_CraftBrowser_Parts = ??? Parts + //#LOC_BDArmory_CraftBrowser_Mass = ??? Mass + //#LOC_BDArmory_CraftBrowser_Version = ??? Version + //#LOC_BDArmory_CraftBrowser_Craft = ??? Craft + //#LOC_BDArmory_CraftBrowser_Folder = ??? Folder + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnails = ??? Generate Missing Thumbnails + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsRecurse = ??? Recurse subfolders + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingFor = ??? Generating thumbnail for + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingForCraftIn = ??? Generating thumbnail for craft in + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFinished = ??? Finished generating thumbnails. + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFailure = ??? Unable to capture thumbnail of + + // Scores + //#LOC_BDArmory_BDAScores_Title = ??? Tournament Scores + //#LOC_BDArmory_BDAScores_Weights = ??? Score Weights + //#LOC_BDArmory_BDAScores_Round = ??? Round + //#LOC_BDArmory_BDAScores_Heat = ??? Heat + //#LOC_BDArmory_BDAScores_Unlimited = ??? Unlimited + //#LOC_BDArmory_BDAScores_Score = ??? Score + //#LOC_BDArmory_BDAScores_Lives = ??? Lives + + // Staging Icons + #LOC_BDArmory_ProtoStageIconInfo_Reloading = Перезарядка + #LOC_BDArmory_ProtoStageIconInfo_Overheat = Перегрев + #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = Нет Боеприпасов + //#LOC_BDArmory_ProtoStageIconInfo_CMsOut = ??? CMs Depleted + + // Wing Commander + #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ПКМ для отмены. + + // Vessel Switcher + #LOC_BDArmory_BDAVesselSwitcher_Title = BDA Переключатель Аппаратов + + // 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 = Нагрев + + // Modular Missile, Custom Weapons + #LOC_BDArmory_StagesNumber = Кол-во Ступеней + #LOC_BDArmory_StageToTriggerOnProximity = Детонирующая Ступень + #LOC_BDArmory_RollCorrection = Корректирование Крена + #LOC_BDArmory_RollCorrection_enabledText = Крен вкл. + #LOC_BDArmory_RollCorrection_disabledText = Крен выкл. + //#LOC_BDArmory_MissileIFF = ??? Seeker IFF + //#LOC_BDArmory_MissileIFF_enabledText = ??? IFF enabled + //#LOC_BDArmory_MissileIFF_disabledText = ??? IFF disabled + #LOC_BDArmory_TimeBetweenStages = Время Между Ступенями + #LOC_BDArmory_AI_MinSpeedGuidance = Мин. Время до Наведения + #LOC_BDArmory_ClearanceRadius = Радиус зазора + #LOC_BDArmory_ClearanceLength = Длина зазора + #LOC_BDArmory_showRFGUI = Показать Редактор Имени Оружия + #LOC_BDArmory_showRFGUI_enabledText = Интерфейс Имени Оружия + #LOC_BDArmory_showRFGUI_disabledText = Интерфейс + + // WM (PAW) + // Target Priority + #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_TargetDmg = ??? Target Damage + #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 Цели + + // Countermeasures + #LOC_BDArmory_Countermeasure_Settings = Настройки Контрмер + //#LOC_BDArmory_EvadeThreshold = ??? Time to Impact Before Evade + #LOC_BDArmory_CMThreshold = Время Пуска Контрмер до Контакта + #LOC_BDArmory_CMRepetition = Кол-во Пусков Вспышек за Цикл + #LOC_BDArmory_CMInterval = Интервал между Пусками Вспышек + #LOC_BDArmory_CMWaitTime = Интервал между Циклами Пуска Вспышек + #LOC_BDArmory_ChaffRepetition = Кол-во Пусков Отражателей за Цикл + #LOC_BDArmory_ChaffInterval = Интервал между Пусками Отражателей + #LOC_BDArmory_ChaffWaitTime = Интервал между Циклами Пуска Вспышек + //#LOC_BDArmory_SmokeRepetition = ??? Smoke Launch per Sequence + //#LOC_BDArmory_SmokeInterval = ??? Smoke Launch Interval Time + //#LOC_BDArmory_SmokeWaitTime = ??? Smoke Sequence Restart Delay + #LOC_BDArmory_ChaffFactor = Восприимчивость Дипольных Отражателей + //#LOC_BDArmory_NonGuardModeCMs = ??? Non-GuardMode CMs + + #LOC_BDArmory_IsVIP = VIP + #LOC_BDArmory_IsVIP_enabledText = Да + #LOC_BDArmory_IsVIP_disabledText = Нет + //#LOC_BDArmory_WM_IsPrimaryWM = ??? Is Primary + + // AI (PAW) + // Pilot AI + // PID + #LOC_BDArmory_AI_PID = PID Контроллер + #LOC_BDArmory_AI_SteerPower = Сила (P) + #LOC_BDArmory_AI_SteerKi = Корректировка (I) + #LOC_BDArmory_AI_SteerDamping = Амортизация (D) + //#LOC_BDArmory_AI_SteerMaxError = ??? Steer Max Error + + // Dynamic damping + #LOC_BDArmory_AI_DynamicSteerDamping = Динамическая Амортизация + #LOC_BDArmory_AI_DynamicDamping = Динамическая Амортизация + #LOC_BDArmory_AI_DynamicDampingMin = Амортизация (От Цели) + #LOC_BDArmory_AI_DynamicDampingMax = Амортизация (На Цель) + #LOC_BDArmory_AI_DynamicDampingFactor = Коэфицент Динамической Амортизации + + // 3-axis damping + //#LOC_BDArmory_AI_3AxisSteerDamping = ??? 3-Axis Steer Damping + + // 3-axis static damping + //#LOC_BDArmory_AI_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + //#LOC_BDArmory_AI_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + //#LOC_BDArmory_AI_SteerDampingRoll = ??? Steer Damping Roll (Dr) + + // 3-axis dynamic damping + #LOC_BDArmory_AI_DynamicDampingPitch = Амортизация Тангажа + #LOC_BDArmory_AI_DynamicDampingPitchMin = Амортизация Тангажа (От Цели) + #LOC_BDArmory_AI_DynamicDampingPitchMax = Амортизация Тангажа (На Цель) + #LOC_BDArmory_AI_DynamicDampingPitchFactor = Коэфицент Дин. Амортизации Тангажа + #LOC_BDArmory_AI_DynamicDampingYaw = Амортизация Рысканья + #LOC_BDArmory_AI_DynamicDampingYawMin = Амортизация Рысканья (От Цели) + #LOC_BDArmory_AI_DynamicDampingYawMax = Амортизация Рысканья (На Цель) + #LOC_BDArmory_AI_DynamicDampingYawFactor = Коэфицент Дин. Амортизации Рысканья + #LOC_BDArmory_AI_DynamicDampingRoll = Амортизация Крена + #LOC_BDArmory_AI_DynamicDampingRollMin = Амортизация Крена (От Цели) + #LOC_BDArmory_AI_DynamicDampingRollMax = Амортизация Крена (На Цель) + #LOC_BDArmory_AI_DynamicDampingRollFactor = Коэфицент Дин. Амортизации Крена + + // Auto-tuning + #LOC_BDArmory_AI_PID_AutoTune = PID Авто-Настройка + #LOC_BDArmory_AI_PID_AutoTuning_Loss = Потери Авто-Настройки + #LOC_BDArmory_AI_PID_AutoTuning_NumSamples = Кол-во Образцов Авто-Настройки + #LOC_BDArmory_AI_PID_AutoTuning_FastResponseRelevance = Важность Быстрого Реагирования Авто-Настройки + #LOC_BDArmory_AI_PID_AutoTuning_InitialLearningRate = Исходная Скорость Обучения Авто-Настройки + //#LOC_BDArmory_AI_PID_AutoTuning_InitialRollRelevance = ??? Auto-Tuning Initial Roll Relevance + #LOC_BDArmory_AI_PID_AutoTuning_Speed = Скорость для Авто-Настройки + #LOC_BDArmory_AI_PID_AutoTuning_Altitude = Высота для Авто-Настройки + //#LOC_BDArmory_AI_PID_AutoTuning_RecenteringDistance = ??? Auto-Tuning Recentering Distance (km) + #LOC_BDArmory_AI_PID_AutoTuning_FixedP = Фиксированная Сила (P) + #LOC_BDArmory_AI_PID_AutoTuning_ClampMaximums = Максимальная Фиксация Авто-Настройки + //#LOC_BDArmory_AI_PID_AutoTuning_Summary = ??? Auto-Tuning Summary + + // Altitudes + #LOC_BDArmory_AI_Altitudes = Высота Полета + #LOC_BDArmory_AI_DefaultAltitude = Обычная Высота Полета + #LOC_BDArmory_AI_MinAltitude = Мин. Высота Полета + #LOC_BDArmory_AI_MaxAltitude = Макс. Высота Полета (AGL) + //#LOC_BDArmory_AI_HardMinAltitude = ??? Hard Min Altitude + //#LOC_BDArmory_AI_BombingAltitude = ??? Bombing Altitude + //#LOC_BDArmory_AI_DiveBombing = ??? Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AI_Speeds = Скорость + #LOC_BDArmory_AI_MaxSpeed = Макс. Скорость + #LOC_BDArmory_AI_TakeOffSpeed = Взлетная Скорость + #LOC_BDArmory_AI_MinSpeed = Мин. Боевая Скорость + #LOC_BDArmory_AI_StrafingSpeed = Скорость Обстрела + #LOC_BDArmory_AI_IdleSpeed = Крейсерская Скорость + #LOC_BDArmory_AI_ABPriority = Приоритетность Форсажа + //#LOC_BDArmory_AI_ABOverrideThreshold = ??? Afterburner Override Threshold + //#LOC_BDArmory_AI_BrakingPriority = ??? Braking Priority + + // Control + #LOC_BDArmory_AI_ControlLimits = Контрольные Ограничения + #LOC_BDArmory_AI_SteerLimiter = Предел Разворота + #LOC_BDArmory_AI_LowSpeedSteerLimiter = Минимальная Скорость Разворота + #LOC_BDArmory_AI_LowSpeedLimiterSpeed = Минимальный Предел Скорости + #LOC_BDArmory_AI_HighSpeedSteerLimiter = Максимальная Скорость Разворота + #LOC_BDArmory_AI_HighSpeedLimiterSpeed = Максимальный Предел Скорости + #LOC_BDArmory_AI_AltitudeSteerLimiterFactor = Коэф. Предела Высоты Разворота + #LOC_BDArmory_AI_AltitudeSteerLimiterAltitude = Предел Высоты Разворота + #LOC_BDArmory_AI_AttitudeLimiter = Предел Высоты + #LOC_BDArmory_AI_BankLimiter = Предел Угла + #LOC_BDArmory_AI_WaypointPreRollTime = Время Упрежд. Крена (Вейпоинты) + #LOC_BDArmory_AI_WaypointYawAuthorityTime = Время Рысканья (Вейпоинты) + #LOC_BDArmory_AI_MaxAllowedGForce = Макс. Перегрузка + #LOC_BDArmory_AI_MaxAllowedAoA = Макс. Угол Атаки + //#LOC_BDArmory_AI_PostStallAoA = ??? Post-Stall AoA Mode-Switch + //#LOC_BDArmory_AI_ImmelmannTurnAngle = ??? Immelmann Turn Angle + //#LOC_BDArmory_AI_ImmelmannPitchUpBias = ??? Immelmann Pitch-Up Bias + + // Evade / Extend + #LOC_BDArmory_AI_EvadeExtend = Уклонение/Увеличение + #LOC_BDArmory_AI_ExtendMultiplier = Увеличить Множитель + #LOC_BDArmory_AI_ExtendDistanceAirToAir = Увеличить Дистанцию Воздух-Воздух + #LOC_BDArmory_AI_ExtendAngleAirToAir = Увеличить Угол Воздух-Воздух + #LOC_BDArmory_AI_ExtendDistanceAirToGroundGuns = Увел. Дистанцию Воздух-Земля (Пушки) + #LOC_BDArmory_AI_ExtendDistanceAirToGround = Увеличить Дистанцию Воздух-Земля + #LOC_BDArmory_AI_ExtendTargetVel = Коэфицент Скорости Цели + #LOC_BDArmory_AI_ExtendTargetAngle = Увеличить Угол до Цели + #LOC_BDArmory_AI_ExtendTargetDist = Увеличить Расстояние до Цели + //#LOC_BDArmory_AI_ExtendAbortTime = ??? Extend Abort Time + //#LOC_BDArmory_AI_ExtendMinGainRate = ??? Extend Min Gain Rate + #LOC_BDArmory_AI_ExtendToggle = Увеличение Расстояния + #LOC_BDArmory_AI_MinEvasionTime = Мин. Время Уклонения + #LOC_BDArmory_AI_EvasionNonlinearity = Нелинейность Уклонения/Увеличения + #LOC_BDArmory_AI_EvasionThreshold = Пороговая Дистанция Уклонения + //#LOC_BDArmory_AI_EvasionErraticness = ??? Evasion Erraticness + #LOC_BDArmory_AI_EvasionTimeThreshold = Пороговое Время Уклонения + //#LOC_BDArmory_AI_EvasionMinRangeThreshold = ??? Evasion Min Range Threshold + #LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe = Не Уклоняться от Цели + //#LOC_BDArmory_AI_EvasionMissileKinematic = ??? Kinematic Msl Evasion + #LOC_BDArmory_AI_CollisionAvoidanceThreshold = Порог Уклонения Аппарата + #LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod = Упреждение Уклонения Аппарата + #LOC_BDArmory_AI_CollisionAvoidanceStrength = Сила Уклонения Аппарата + #LOC_BDArmory_AI_StandoffDistance = Удерживаемая Дистанцию + + // Terrain + #LOC_BDArmory_AI_Terrain = Избежание Крушения об Землю + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMin = Мин. Избегание Крушения + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMax = Макс. Избегание Крушения + //#LOC_BDArmory_AI_TerrainAvoidanceCriticalAngle = ??? Inverted Terrain Avoidance Critical Angle + //#LOC_BDArmory_AI_TerrainAvoidanceVesselReactionTime = ??? Vessel Reaction Time + //#LOC_BDArmory_AI_TerrainAvoidancePostAvoidanceCoolDown = ??? Post-Avoidance Cool-Down + #LOC_BDArmory_AI_WaypointTerrainAvoidance = Избегание Крушения (Вейпоинты) + + // Ramming + #LOC_BDArmory_AI_Ramming = Таран + #LOC_BDArmory_AI_ControlSurfaceLag = Задержка Управления при Таране + #LOC_BDArmory_AI_AllowRamming = Разрешить Таран + //#LOC_BDArmory_AI_AllowRammingGroundTargets = ??? Include Ground Targets + + // Ejection (unused) + #LOC_BDArmory_AI_Ejection = Катапультирование + #LOC_BDArmory_AI_EjectOnImpendingDoom = Катапультирование в Критической Ситуации + + #LOC_BDArmory_AI_SliderResolution = Разрешение Ползунков + // Idle / Orbit Behavior + #LOC_BDArmory_AI_Orbit = Направление по орбите \u0020 + #LOC_BDArmory_AI_Orbit_Starboard = Штирборт (CW) + #LOC_BDArmory_AI_Orbit_Port = Порт (CCW) + #LOC_BDArmory_AI_Orbit_Random = Любой (CW/CCW) + #LOC_BDArmory_AI_Standby = Режим Ожидания + + // Up-to-eleven + #LOC_BDArmory_AI_UnclampTuning = Разблокировать настройки\u0020 + #LOC_BDArmory_AI_UnclampTuning_enabledText = Разблокировано + #LOC_BDArmory_AI_UnclampTuning_disabledText = Заблокировано + + // Surface / VTOL / Orbital AI + #LOC_BDArmory_AI_VehicleType = Тип Аппарата + #LOC_BDArmory_AI_MaxSlopeAngle = Макс. Угол Уклона + #LOC_BDArmory_AI_CruiseSpeed = Крейсерская Скорость + #LOC_BDArmory_AI_CombatSpeed = Боевая Скорость + #LOC_BDArmory_AI_CombatAltitude = Боевая Высота + #LOC_BDArmory_AI_TargetPitch = Moving Pitch + #LOC_BDArmory_AI_MaxDrift = Максимальный Дрифт + #LOC_BDArmory_AI_MaxPitchAngle = Max Pitch Angle + #LOC_BDArmory_AI_BankAngle = Bank Angle + //#LOC_BDArmory_AI_WeaveFactor = ??? Weave Factor + #LOC_BDArmory_AI_MaxBankAngle = Max Bank Angle + #LOC_BDArmory_AI_BroadsideAttack = Вектор Атаки + #LOC_BDArmory_AI_BroadsideAttack_enabledText = Прямой + #LOC_BDArmory_AI_BroadsideAttack_disabledText = Парабола + #LOC_BDArmory_AI_MinEngagementRange = Мин. Дистанция Атаки + #LOC_BDArmory_AI_MaxEngagementRange = Макс. Дистанция Атаки + //#LOC_BDArmory_AI_ForceFiringRange = ??? Zero Throttle Firing Range + //#LOC_BDArmory_AI_MaintainEngagementRange = ??? Maintain Min Range + #LOC_BDArmory_AI_ManeuverRCS = РСУ Активна + #LOC_BDArmory_AI_ManeuverRCS_enabledText = Маневры + #LOC_BDArmory_AI_ManeuverRCS_disabledText = Сражение + //#LOC_BDArmory_AI_FiringRCS = ??? RCS While Firing + //#LOC_BDArmory_AI_FiringRCS_enabledText = ??? Manage Velocity + //#LOC_BDArmory_AI_FiringRCS_disabledText = ??? Maneuvers Only + //#LOC_BDArmory_AI_ReverseEngines = ??? Reverse Engines + //#LOC_BDArmory_AI_EngineRCSRotation = ??? Engine RCS (Rotation) + //#LOC_BDArmory_AI_EngineRCSTranslation = ??? Engine RCS (Translation) + //#LOC_BDArmory_AI_OrbitalPIDActive = ??? PID Active For + //#LOC_BDArmory_AI_RollMode = ??? Broadside Dir + #LOC_BDArmory_AI_MinObstacleMass = Мин. Масса Препятствия + #LOC_BDArmory_AI_PreferredBroadsideDirection = Предпочтительное Направление + #LOC_BDArmory_AI_GoesUp = Макс. Значения + #LOC_BDArmory_AI_GoesUp_enabledText = 11 + #LOC_BDArmory_AI_GoesUp_disabledText = 10 + //#LOC_BDArmory_AI_ManeuverSpeed = ??? Maneuver Speed + //#LOC_BDArmory_AI_FiringSpeedMin = ??? Min Firing Speed + //#LOC_BDArmory_AI_FiringSpeedLimit = ??? Max Firing Speed + //#LOC_BDArmory_AI_AngularSpeedLimit = ??? Angular Speed Limit + //#LOC_BDArmory_AI_EvasionRCS = ??? RCS Evasion + //#LOC_BDArmory_AI_EvasionEngines = ??? Thrust Evasion + + // AI GUI + #LOC_BDArmory_AIWindow_title = ИИ-контроллер + #LOC_BDArmory_AIWindow_infoLink = Инфолинк + #LOC_BDArmory_AIWindow_NoAI = ИИ-контроллер не найден. + // Sections + //#LOC_BDArmory_AIWindow_PID = ??? PID + //#LOC_BDArmory_AIWindow_Altitudes = ??? Altitudes + //#LOC_BDArmory_AIWindow_Speeds = ??? Speeds + #LOC_BDArmory_AIWindow_Control = Контроль + #LOC_BDArmory_AIWindow_EvadeExtend = Энергия + #LOC_BDArmory_AIWindow_Terrain = Земля + //#LOC_BDArmory_AIWindow_Ramming = ??? Ramming + //#LOC_BDArmory_AIWindow_Combat = ??? Combat + #LOC_BDArmory_AIWindow_Misc = Разное + + // Panel + // Pilot + // PID + #LOC_BDArmory_AIWindow_SteerPower = Контролирует установленный множитель + #LOC_BDArmory_AIWindow_SteerPower_ContextLow = <- Вялый + #LOC_BDArmory_AIWindow_SteerPower_ContextHigh = Резкий -> + //#LOC_BDArmory_AIWindow_SteerKi = ??? Steer Correction (I) + #LOC_BDArmory_AIWindow_SteerKi_ContextLow = <- Недолет + #LOC_BDArmory_AIWindow_SteerKi_ContextHigh = Перелет -> + //#LOC_BDArmory_AIWindow_SteerDamping = ??? Steer Damping (D) + #LOC_BDArmory_AIWindow_SteerDamping_ContextLow = <- Шаткий + #LOC_BDArmory_AIWindow_SteerDamping_ContextHigh = Стабильный -> + //#LOC_BDArmory_AIWindow_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextLow = <- Шаткий + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextHigh = Стабильный -> + //#LOC_BDArmory_AIWindow_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextLow = <- Шаткий + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextHigh = Стабильный -> + //#LOC_BDArmory_AIWindow_SteerDampingRoll = ??? Steer Damping Roll (Dr) + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextLow = <- Шаткий + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextHigh = Стабильный -> + //#LOC_BDArmory_AIWindow_SteerMaxError = ??? Max Error + //#LOC_BDArmory_AIWindow_SteerMaxError_ContextLow = ??? <- Slow & Easy Tuning + //#LOC_BDArmory_AIWindow_SteerMaxError_ContextHigh = ??? Fast & Harder Tuning -> + //#LOC_BDArmory_AIWindow_DynDampMin = ??? Off-target Damping + #LOC_BDArmory_AIWindow_DynDampMin_Context = Минимальная Амортизация + //#LOC_BDArmory_AIWindow_DynDampMax = ??? On-target Damping + #LOC_BDArmory_AIWindow_DynDampMax_Context = Максимальная Амортизация + //#LOC_BDArmory_AIWindow_DynDampMult = ??? Dynamic Damping Factor + #LOC_BDArmory_AIWindow_DynDampMult_Context = Величина Амортизации + + // Auto-tuning + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = Кол-во Примеров + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextLow = <- Менее точный + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextHigh = Более точный -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = Скор. Отклика + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextLow = <- Сильная амортизация + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextHigh = Быстрое реагирование -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = Изначальная скорость обучения + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate_Context = Понизить, если изменения значений в PID слишком большие + //#LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance = ??? A-T Initial RR + //#LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance_Context = ??? How much the roll errors contribute to the loss + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = Скорость + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed_Context = Целевая скорость авто-настройки + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = Высота + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude_Context = Старайться оставаться в пределах этой высоты + //#LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance = ??? A-T Recentering Distance + //#LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance_Context = ??? Distance from start at which re-centering is triggered + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = Фиксированные Поля А-Н + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = Макс. Зажим + // PID fixed fields + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P = ??? P + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I = ??? I + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D = ??? D + // 3-axis damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch = ??? Dp + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw = ??? Dy + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll = ??? Dr + // Dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OffTarget = ??? DOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OnTarget = ??? DOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_Factor = ??? DF + // 3-axis dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OffTarget = ??? DpOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OnTarget = ??? DpOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_Factor = ??? DpF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OffTarget = ??? DyOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OnTarget = ??? DyOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_Factor = ??? DyF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OffTarget = ??? DrOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OnTarget = ??? DrOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_Factor = ??? DrF + + // Altitudes + //#LOC_BDArmory_AIWindow_DefaultAltitude = ??? Default Alt. + #LOC_BDArmory_AIWindow_DefaultAltitude_Context = ИИ возвращается к этому при бездействии + //#LOC_BDArmory_AIWindow_MinAltitude = ??? Min Altitude + #LOC_BDArmory_AIWindow_MinAltitude_Context = ИИ пытается оставаться над этим + //#LOC_BDArmory_AIWindow_MaxAltitude = ??? Max Altitude (AGL) + #LOC_BDArmory_AIWindow_MaxAltitude_Context = ИИ пытается оставаться под этим + //#LOC_BDArmory_AIWindow_BombingAltitude = ??? Bombing Altitude + //#LOC_BDArmory_AIWindow_BombingAltitude_Context = ??? AI tries to bomb at this altitude + //#LOC_BDArmory_AIWindow_DiveBomb = ??? Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AIWindow_MaxSpeed = Максимальная скорость в бою + #LOC_BDArmory_AIWindow_MaxSpeed_Context = ИИ остается ниже этой скорости полета + #LOC_BDArmory_AIWindow_TakeOffSpeed = #LOC_BDArmory_AI_TakeOffSpeed + #LOC_BDArmory_AIWindow_TakeOffSpeed_Context = Скорость вхождения ИИ в тангаж + #LOC_BDArmory_AIWindow_MinSpeed = #LOC_BDArmory_AI_MinSpeed + #LOC_BDArmory_AIWindow_MinSpeed_Context = ИИ будет удаляться ниже этого + #LOC_BDArmory_AIWindow_StrafingSpeed = #LOC_BDArmory_AI_StrafingSpeed + #LOC_BDArmory_AIWindow_StrafingSpeed_Context = Скорость атаки наземных целей + #LOC_BDArmory_AIWindow_IdleSpeed = #LOC_BDArmory_AI_IdleSpeed + #LOC_BDArmory_AIWindow_IdleSpeed_Context = Крейсерская скорость + #LOC_BDArmory_AIWindow_ABPriority = #LOC_BDArmory_AI_ABPriority + #LOC_BDArmory_AIWindow_ABPriority_Context = Приоритет включения форсажа + //#LOC_BDArmory_AIWindow_ABOverrideThreshold = ??? Afterburner Override + //#LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = ??? Force use of afterburner if below this speed and full throttle + #LOC_BDArmory_AIWindow_BrakingPriority = #LOC_BDArmory_AI_BrakingPriority + //#LOC_BDArmory_AIWindow_BrakingPriority_Context = ??? Prioritize using brakes to slow down when allowed + + // Control + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = Наим. Предел Скорости Поворота + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter_Context = Ограничивает контроль ниже наименьшего лимита скорости + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = Наим. Предел Скорости + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed_Context = ИИ использует минимальный лимит скорости ниже этого + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = Наиб. Предел Скорости Поворота + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter_Context = Ограничивает контроль выше наименьшего лимита скорости + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = Наиб. Предел Скорости + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed_Context = ИИ полностью ограничен максимальным пределом высоты + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Фактор Высоты Поворота + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor_Context = Коэфицент доя уменьшения/увеличения лимита разворота, основываясь на высоте + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Лимит Высоты Поворота + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude_Context = Высота для начала уменьшения/увеличения лимита разворота + //#LOC_BDArmory_AIWindow_BankLimiter = ??? Bank Angle Limit + #LOC_BDArmory_AIWindow_BankLimiter_Context = Макс. угол крена + //#LOC_BDArmory_AIWindow_MaxAllowedGForce = ??? Max G + #LOC_BDArmory_AIWindow_MaxAllowedGForce_Context = Перегрузка не будет превышать это значение + //#LOC_BDArmory_AIWindow_MaxAllowedAoA = ??? Max AoA + #LOC_BDArmory_AIWindow_MaxAllowedAoA_Context = Угол атаки не будет превышать это значение + #LOC_BDArmory_AIWindow_WaypointPreRollTime = Время Упрежд. Крена (ВП) + #LOC_BDArmory_AIWindow_WaypointPreRollTime_Context = Начать крен заранее до достижения вейпоинта + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = Время Рысканья (ВП) + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime_Context = Увеличить отклик рысканья во время приближения к вейпоинту + //#LOC_BDArmory_AIWindow_PostStallAoA = ??? Post-Stall AoA + //#LOC_BDArmory_AIWindow_PostStallAoA_Context = ??? Switch flight modes for post-stall beyond this AoA + //#LOC_BDArmory_AIWindow_ImmelmannTurnAngle = ??? Immelmann Turn Angle + //#LOC_BDArmory_AIWindow_ImmelmannTurnAngle_Context = ??? Craft will just pitch up to aim at a target in this cone + //#LOC_BDArmory_AIWindow_ImmelmannPitchUpBias = ??? Immelmann Pitch-Up Bias + //#LOC_BDArmory_AIWindow_ImmelmannPitchUpBias_Context = ??? < Down — Bias direction on current pitch rate — Up > + + // Evade / Extend + //#LOC_BDArmory_AIWindow_Evade = ??? Evasion + #LOC_BDArmory_AIWindow_MinEvasionTime = #LOC_BDArmory_AI_MinEvasionTime + #LOC_BDArmory_AIWindow_MinEvasionTime_Context = Минимальное время уклонения ИИ от атаки + #LOC_BDArmory_AIWindow_EvasionThreshold = Дист. Уклонения + #LOC_BDArmory_AIWindow_EvasionThreshold_Context = Избегает вражеского огня внутри этого + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Порог Уклонения + #LOC_BDArmory_AIWindow_EvasionTimeThreshold_Context = Минимальное время обстрела для активации уклонения + //#LOC_BDArmory_AIWindow_EvasionMinRangeThreshold = ??? Min Range Threshold + //#LOC_BDArmory_AIWindow_EvasionMinRangeThreshold_Context = ??? Only evade if the attacker is beyond this range. + #LOC_BDArmory_AIWindow_EvasionNonlinearity = Нелинейность Уклон./Увелич. + #LOC_BDArmory_AIWindow_EvasionNonlinearity_Context = Сила колебаний во время уклонения/увеличения расстояния + + //#LOC_BDArmory_AIWindow_Avoidance = ??? Vessel Avoidance + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Дист. Уклонения Аппарата + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold_Context = Уклоняться от вражеского аппарата на этой дистанции + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Упрежд. Уклонение Аппарата + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod_Context = Как далеко ИИ упреждает столкновение + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Сила Уклонения Аппарата + //#LOC_BDArmory_AIWindow_CollisionAvoidanceStrength_Context = ??? How hard the AI will break away from incoming craft + #LOC_BDArmory_AIWindow_StandoffDistance = Держать Дистанцию + #LOC_BDArmory_AIWindow_StandoffDistance_Context = Минимальная дистанция, на которую ИИ будет приближаться к цели + + //#LOC_BDArmory_AIWindow_Extend = ??? Extension + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Увел. Дист. В-В + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = Увеличить Угол (Воздух-Воздух) + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = Увел. Угол + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = Желаемый угол подъема при увеличении + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Увел. Дист. В-З (Пушки) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = Увеличить расстояние до земли (Пулеметы) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Увел. Дист. В-З + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = Увеличить Дистанцию Воздух-Земля + #LOC_BDArmory_AIWindow_ExtendTargetVel = Коэф. Скорости Цели + #LOC_BDArmory_AIWindow_ExtendTargetVel_Context = Устанавливает, когда цель слишком медленна, чтобы поворачивать к ней + #LOC_BDArmory_AIWindow_ExtendTargetAngle = Угол до Цели + #LOC_BDArmory_AIWindow_ExtendTargetAngle_Context = Устанавливает, когда цель вне радиуса поворота + #LOC_BDArmory_AIWindow_ExtendTargetDist = Дист. до Цели + #LOC_BDArmory_AIWindow_ExtendTargetDist_Context = Устанавливает, когда цель слишком близко, чтобы поворачивать к ней + //#LOC_BDArmory_AIWindow_ExtendAbortTime = ??? Abort time + //#LOC_BDArmory_AIWindow_ExtendAbortTime_Context = ??? Abort time if failing to gain distance while extending. + //#LOC_BDArmory_AIWindow_ExtendMinGainRate = ??? Min Gain Rate + //#LOC_BDArmory_AIWindow_ExtendMinGainRate_Context = ??? Minimum rate to be gaining distance for the abort timer. + + // Terrain + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin = Мин. Уклонение у Земли + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin_Context = Множитель радиусса разворота для идеальной ориентации аппарата + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax = Макс. Уклонение у Земли + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax_Context = Множитель радиусса разворота для обратной ориентации аппарата + //#LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle = ??? Inverted Crit. Angle + //#LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle_Context = ??? Critical angle for inverted terrain avoidance or rolling first + //#LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime = ??? Vessel Reaction Time + //#LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime_Context = ??? Estimate of time required to setup for optimal turning + //#LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown = ??? Post-Avoidance Cool-Down + //#LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown_Context = ??? Time after avoiding terrain before beginning maneuvers + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = Уклонение у Земли (Вейпоинты) + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance_Context = Расстояние и сила корректировки избежания крушения в режиме вейпоинтов + + // Ramming + #LOC_BDArmory_AIWindow_ControlSurfaceLag = Задержка Элевонов + #LOC_BDArmory_AIWindow_ControlSurfaceLag_Context = Корректировка траектории тарана для задержки элевонов + // Combat + // Up-to-eleven + // Idle / Orbit Behavior + #LOC_BDArmory_AIWindow_Orbit_Context = Направление курсирования + #LOC_BDArmory_AIWindow_Standby_Context = ИИ включается, когда цель входит в боевой радиус + + // Surface / VTOL / Orbital + #LOC_BDArmory_AIWindow_VehicleType = #LOC_BDArmory_AI_VehicleType + #LOC_BDArmory_AIWindow_VehicleType_Context = Аппарат действует на этом типе рельефа + #LOC_BDArmory_AIWindow_MaxSlopeAngle = #LOC_BDArmory_AI_MaxSlopeAngle + #LOC_BDArmory_AIWindow_MaxSlopeAngle_Context = Максимальный наклон поверхности, по которой аппарат будет ехать вверх + #LOC_BDArmory_AIWindow_CruiseSpeed = #LOC_BDArmory_AI_CruiseSpeed + #LOC_BDArmory_AIWindow_CruiseSpeed_Context = #LOC_BDArmory_AIWindow_IdleSpeed_Context + #LOC_BDArmory_AIWindow_CombatSpeed = #LOC_BDArmory_AI_CombatSpeed + //#LOC_BDArmory_AIWindow_CombatSpeed_Context = ??? Target speed for combat without powered steering + #LOC_BDArmory_AIWindow_CombatAltitude = #LOC_BDArmory_AI_CombatAltitude + //#LOC_BDArmory_AIWindow_CombatAltitude_Context = ??? Default cruising altitude/depth + #LOC_BDArmory_AIWindow_TargetPitch = #LOC_BDArmory_AI_TargetPitch + #LOC_BDArmory_AIWindow_TargetPitch_Context = Desired craft Attitude Angle + #LOC_BDArmory_AIWindow_MaxDrift = #LOC_BDArmory_AI_MaxDrift + #LOC_BDArmory_AIWindow_MaxDrift_Context = Max angle craft veers off prograde during cornering + #LOC_BDArmory_AIWindow_MaxPitchAngle = #LOC_BDArmory_AI_MaxPitchAngle + //#LOC_BDArmory_AIWindow_MaxPitchAngle_Context = ??? Max angle to pitch at while moving + #LOC_BDArmory_AIWindow_BankAngle = #LOC_BDArmory_AI_BankAngle + #LOC_BDArmory_AIWindow_BankAngle_Context = #LOC_BDArmory_AIWindow_BankLimiter_Context + #LOC_BDArmory_AIWindow_WeaveFactor = #LOC_BDArmory_AI_WeaveFactor + //#LOC_BDArmory_AIWindow_WeaveFactor_Context = ??? Strength of weaving when under fire + #LOC_BDArmory_AIWindow_MaxBankAngle = #LOC_BDArmory_AI_MaxBankAngle + //#LOC_BDArmory_AIWindow_MaxBankAngle_Context = ??? Max angle to roll at while turning + #LOC_BDArmory_AIWindow_BroadsideAttack = #LOC_BDArmory_AI_BroadsideAttack + #LOC_BDArmory_AIWindow_BroadsideAttack_Context = Ориентация аппарата относительно цели во время атаки + #LOC_BDArmory_AIWindow_MinEngagementRange = #LOC_BDArmory_AI_MinEngagementRange + #LOC_BDArmory_AIWindow_MinEngagementRange_Context = Мин. дальность атаки ИИ + #LOC_BDArmory_AIWindow_MaxEngagementRange = #LOC_BDArmory_AI_MaxEngagementRange + #LOC_BDArmory_AIWindow_MaxEngagementRange_Context = Макс. дальность атаки ИИ + #LOC_BDArmory_AIWindow_ForceFiringRange = #LOC_BDArmory_AI_ForceFiringRange + //#LOC_BDArmory_AIWindow_ForceFiringRange_Context = ??? Within this range AI always fires without throttle + #LOC_BDArmory_AIWindow_MaintainEngagementRange = #LOC_BDArmory_AI_MaintainEngagementRange + //#LOC_BDArmory_AIWindow_MaintainEngagementRange_Context = ??? Craft will stop/reverse at minimum Range + #LOC_BDArmory_AIWindow_ManeuverRCS = #LOC_BDArmory_AI_ManeuverRCS + #LOC_BDArmory_AIWindow_ManeuverRCS_Context = Условия использования РСУ + #LOC_BDArmory_AIWindow_FiringRCS = #LOC_BDArmory_AI_FiringRCS + //#LOC_BDArmory_AIWindow_FiringRCS_Context = ??? Use RCS to manage velocity while firing + #LOC_BDArmory_AIWindow_ReverseEngines = #LOC_BDArmory_AI_ReverseEngines + //#LOC_BDArmory_AIWindow_ReverseEngines_Context = ??? Use backwards engines for reverse thrust + #LOC_BDArmory_AIWindow_EngineRCSRotation = #LOC_BDArmory_AI_EngineRCSRotation + //#LOC_BDArmory_AIWindow_EngineRCSRotation_Context = ??? Use engines perpendicular to thrust axis for RCS rotation + #LOC_BDArmory_AIWindow_EngineRCSTranslation = #LOC_BDArmory_AI_EngineRCSTranslation + //#LOC_BDArmory_AIWindow_EngineRCSTranslation_Context = ??? Use engines perpendicular to thrust axis for RCS translation + #LOC_BDArmory_AIWindow_OrbitalPIDActive = #LOC_BDArmory_AI_OrbitalPIDActive + //#LOC_BDArmory_AIWindow_OrbitalPIDActive_Context = ??? PID active condition + #LOC_BDArmory_AIWindow_RollMode = #LOC_BDArmory_AI_RollMode + //#LOC_BDArmory_AIWindow_RollMode_Context = ??? When PID is active, AI will roll to present this side of the ship to the target + #LOC_BDArmory_AIWindow_MinObstacleMass = #LOC_BDArmory_AI_MinObstacleMass + #LOC_BDArmory_AIWindow_MinObstacleMass_Context = Минимальная масса объекта, от которого необходимо уклоняться + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection = #LOC_BDArmory_AI_PreferredBroadsideDirection + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context = Сторона аппарата, которая должна смотреть на цель + #LOC_BDArmory_AIWindow_ManeuverSpeed = #LOC_BDArmory_AI_ManeuverSpeed + //#LOC_BDArmory_AIWindow_ManeuverSpeed_Context = ??? Maximum speed relative to target when maneuvering + #LOC_BDArmory_AIWindow_minFiringSpeed = #LOC_BDArmory_AI_FiringSpeedMin + //#LOC_BDArmory_AIWindow_minFiringSpeed_Context = ??? Minimum speed relative to target when firing + #LOC_BDArmory_AIWindow_FiringSpeed = #LOC_BDArmory_AI_FiringSpeedLimit + //#LOC_BDArmory_AIWindow_FiringSpeed_Context = ??? Maximum speed relative to target when firing + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit = #LOC_BDArmory_AI_AngularSpeedLimit + //#LOC_BDArmory_AIWindow_FiringAngularVelocityLimit_Context = ??? Maximum angular speed relative to target when firing + #LOC_BDArmory_AIWindow_EvasionErraticness = #LOC_BDArmory_AI_EvasionErraticness + //#LOC_BDArmory_AIWindow_EvasionErraticness_Context = ??? Amount of variation in evasion direction + #LOC_BDArmory_AIWindow_EvasionRCS = #LOC_BDArmory_AI_EvasionRCS + //#LOC_BDArmory_AIWindow_EvasionRCS_Context = ??? Use RCS to evade incoming fire + #LOC_BDArmory_AIWindow_EvasionEngines = #LOC_BDArmory_AI_EvasionEngines + //#LOC_BDArmory_AIWindow_EvasionEngines_Context = ??? Use engine thrust to evade incoming fire + + + // AI infolink + // Pilot AI + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_PidHelp_SteerPower = 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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_AltHelp = Altitude settings control the desired flight envelope of the AI + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_Def = Default Altitude - This is the Altitude the AI will seek to return to when not in combat or extending. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_AltHelp_bombing = ??? Bombing Altitude - The AI will try to maintain level flight at this altitude when performing a bombing run (doesn't apply to torpedoes). + + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp = Speed settings control the desired Airspeeds of the craft over a variety of flight and combat conditions + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_SpeedHelp_idle = Idle Speed - This sets the non-combat cruise airspeed the AI will maintain while orbiting or flying to position. + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_SpeedHelp_ABpriority = AB Priority - This controls the level of requested acceleration at which the AI will turn on/off the afterburners. + //#LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_SpeedHelp_BrakingPriority = ??? Braking Priority - This controls the aggressiveness of the AI using the brakes when allowed. + + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp = Control Limits set limits on craft control authority under various conditions + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_ControlHelp_modeSwitches = ??? Post-Stall AoA Mode-Switch controls when the plane will switch steering modes due to being beyond the AoA threshold. + //#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_Immelmann = ??? Immelmann Turn Angle and Bias - When flying to a target within the angle of the Immelmann Turn directly behind the craft, the craft will simply pitch up / down instead of rolling. When not close to the min altitude, the Immelmann Pitch-Up Bias will force pitching up or down when the current pitching rate (°/s) is within the limit, otherwise the current pitching direction is used. + + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_EvadeHelp_ExtendAbortTime = ??? Extend Abort Time, Extend Min Gain Rate - These tell the AI to abort extending if it hasn't made sufficient gains within this time (below the min gain rate, the timer increases at a rate of 1; below the min gain rate, the timer decreases at a rate of 0.5). Extending then enters a 5s cooldown period. + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendToggle = Extend Toggle - This enables or disables extending against air targets (extending against ground targets is not affected). + + #LOC_BDArmory_AIWindow_infolink_Pilot_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_infolink_Pilot_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_infolink_Pilot_miscHelp = Misc Settings - These control non-combat behaviors that don't fit elsewhere. + #LOC_BDArmory_AIWindow_infolink_Pilot_orbitHelp = Orbit Direction - This is the direction the AI will orbit when idling, either Clockwise or Counterclockwise. + #LOC_BDArmory_AIWindow_infolink_Pilot_standbyHelp = Standby Toggle - If enabled, the AI will automatically turn on when targets enter its Guard Range. + + // Surface AI + #LOC_BDArmory_AIWindow_infolink_Surface_Type = 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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_Drift = Max Drift - This sets the maximum the vessel will veer off prograde when turing. + #LOC_BDArmory_AIWindow_infolink_Surface_Bank = Bank Angle - This tells the AI the maximum it should allow the vessel to bank or heel over during maneuvers. + //#LOC_BDArmory_AIWindow_infolink_Surface_Weave = ??? Weave Factor - When under fire or a missile is incoming, the craft tries weaving to throw off the attacker's aim. This controls the strength of that weaving. + #LOC_BDArmory_AIWindow_infolink_Surface_SteerPower = 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_AIWindow_infolink_Surface_SteerDamping = 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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_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_AIWindow_infolink_Surface_RCS = RCS - The RCS setting toggles whether the AI should use RCS thrusters to assist in maneuvering. + #LOC_BDArmory_AIWindow_infolink_Surface_AvoidMass = Mass Avoidance - This sets the minimum mass that an obstace needs to be in order to avoid it instead of ramming it. + //#LOC_BDArmory_AIWindow_infolink_Surface_MaintainMinRange = ??? Maintain Min Range - This toggles if a Land vehicle will come to a stop/reverse to maintain distance from a target vs veering away to circle or extend. + //#LOC_BDArmory_AIWindow_infolink_Surface_Altitude = ??? Combat Altitude - The depth for the submarines to cruise / engage at. + + // VTOL AI + //#LOC_BDArmory_AIWindow_infolink_VTOL_PID = ??? 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.\n - 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.\n - 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.\n - Steer Damping (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_infolink_VTOL_Altitudes = ??? Default Altitude - The altitude (AGL) to cruise at when outside of combat.\nMin and Max Altitudes - The minimum and maximum altitudes the AI will try to fly to. Outside of this range, the AI will either climb or dive to return to the range of altitudes. + //#LOC_BDArmory_AIWindow_infolink_VTOL_Speeds = ??? Max and Combat Speeds - The maximum speed for maneuvering and the target speed for combat. + //#LOC_BDArmory_AIWindow_infolink_VTOL_Control = ??? Max Pitch and Bank Angles - This tells the AI the maximum it should allow the vessel to pitch or bank (roll) over during maneuvers.\nBroadside Dir - Broadside Direction tells the AI whether it should favor presenting the Port or Starboard broadside towards the target.\nRCS - Whether to use RCS thrusters for maneuvering or only when in combat. + //#LOC_BDArmory_AIWindow_infolink_VTOL_Combat = ??? Weave Factor - When under fire or a missile is incoming, the craft tries weaving to throw off the attacker's aim. This controls the strength of that weaving.\nMin, 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. + + // Orbital AI + //#LOC_BDArmory_AIWindow_infolink_Orbital_PID = ??? The PID (Proportional Integral Derivative) Controller calculates the difference between desired and actual output, then applies corrections based on P, I, and D values. The stock KSP SAS control steers the craft (Inactive) unless the PID control is enabled when aiming and firing weapons (Firing) or all maneuevers (Everything).\n - 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.\n - 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.\n - Steer Damping (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.\n - Max Error - This sets a clamp on the max angle error used by the PID controller. Lowering this decreases overshoot, but the overall response will be slower. Raising this value will make the overall response faster, but increase overshoot unless the PID is well-tuned. If you have a well-tuned PID, adjusting this value can help limit overshoot or increase your overall response speed. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Combat = ??? 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 which direction it should present towards the target, this is only active when PID control is active. When not attacking, the AI will roll to present this side to the target.\n\nMin Engagement Range - This sets the minimum distance from the target the AI will attempt to maneuver to be within in order to engage.\n\nZero Throttle Firing Range - Within this distance from the target (but outside of Min Engagement Range) the AI will always fire weapons without manipulating throttle. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Speeds = ??? Maneuver, Firing, and Angular Speeds - Speed at which the AI will maneuver at, and firing speed and angular speed limits during combat. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Control = ??? RCS - Whether to use RCS to manage velocity relative to the target when firing and whether to use RCS thrusters for maneuvering or only when in combat.\n\nReverse Engines - If enabled, AI will use backwards engines when able. Automatically disabled if no reverse engines are on the craft. + //#LOC_BDArmory_AIWindow_infolink_Orbital_Evasion = ??? Min Evasion Time, Distance Threshold, Time Threshold, Min Range Threshold - 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 Min Range Threshold sets the range that the attacker has to be beyond to trigger evasion.\nThe RCS Evasion and Thrust Evasion toggles determine whether the AI attempts to evade gunfire using RCS and/or engine thrust.\nThe Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes. + + // Missile Config + #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_UseStaticMaxLaunchRange = ??? Dynamic/Static Max Range + #LOC_BDArmory_ProximityTriggerDistance = Дист. Детонации Боеголовки + //#LOC_BDArmory_clustermissileTriggerDistance = ??? Submunition Launch Distance + #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_Missile_CruiseSpeed = ??? Cruise Speed + #LOC_BDArmory_CruiseAltitude = Крейсерская Высота + #LOC_BDArmory_CruisePredictionTime = Крейсерское время упреждения + //#LOC_BDArmory_CruisePopup = ??? Cruise Popup Attack + #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_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 = Радиус ЭМИ Взрыва + #LOC_BDArmory_OrdnanceAvailable = Доступная Скорость Отбрасывания + #LOC_BDArmory_MissileAssign = Назначение Ракеты + #LOC_BDArmory_CurrentLocks = Текущие Цели + //#LOC_BDArmory_Offset = ??? Ordnance Offset + //#LOC_BDArmory_Deploy_Time = ??? Deploy Time + + // Safety Systems + #LOC_BDArmory_SSTank = Самозапечатываемый Бак + #LOC_BDArmory_SSTank_On = Вкл. Самозапечатываемый Бак + #LOC_BDArmory_SSTank_Off = Выкл. Самозапечатываемый Бак + #LOC_BDArmory_CASE = C.A.S.E. Tier + //#LOC_BDArmory_CASE_Sim = ??? Detonation Sim + #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_AddedCost = Доп. стоимость + #LOC_BDArmory_AddedMass = Масса Системы Безопасности + #LOC_BDArmory_DryMass = Сухая Масса + + // Turret Config + #LOC_BDArmory_MaxPitch = Макс. Тангаж + #LOC_BDArmory_MinPitch = Мин. Тангаж + #LOC_BDArmory_YawRange = Диапазон Рысканья + //#LOC_BDArmory_YawStandbyAngle = ??? Yaw Standby Angle + #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_AimOverrideFalse = ??? Aim With This Weapon + //#LOC_BDArmory_AimOverrideTrue = ??? Revert Default Aim + #LOC_BDArmory_ReturnTurret = Вернуть Турель + #LOC_BDArmory_ToggleAnimation = Анимация + + // Missile UI + #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_TurretLoft = ??? Lofted Aimpoint + //#LOC_BDArmory_TurretLoftFac = ??? Loft Velocity Factor + #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_DynamicRadar = ??? Disable Radar vs ARMs + + // WM Config + #LOC_BDArmory_GuardMode = Боевой Режим:\u0020 + #LOC_BDArmory_Team = Команда + //#LOC_BDArmory_Allies = ??? Allies + #LOC_BDArmory_Weapon = Оружие + #LOC_BDArmory_FiringPriority = Приоритет Использования + //#LOC_BDArmory_weaponChannel = ??? Weapon Channel + #LOC_BDArmory_FiringInterval = Огневой Интервал + #LOC_BDArmory_FiringBurstLength = Длина Очереди (Время) + #LOC_BDArmory_FiringBurstCount = Длина Очереди (Кол-во) + #LOC_BDArmory_FiringTolerance = Огневой Угол + #LOC_BDArmory_FieldOfView = Поле Зрения + #LOC_BDArmory_VisualRange = Визуальная Дистанция + #LOC_BDArmory_GunsRange = Дальность Пулеметов + //#LOC_BDArmory_MissilesRange = ??? Use Dynamic Launch Zone + #LOC_BDArmory_MissilesOnTarget = Ракеты/Цель + #LOC_BDArmory_FireAngleOverride_Enable = Вкл. Коррекцию Огневого Угла + #LOC_BDArmory_FireAngleOverride_Disable = Вкл. Коррекцию Огневого Угла + #LOC_BDArmory_BurstLengthOverride_Enable = Вкл. Коррекцию Длины Очереди + #LOC_BDArmory_BurstLengthOverride_Disable = Выкл. Коррекцию Длины Очереди + #LOC_BDArmory_FiringAngle = Огневой Угол + + //#LOC_BDArmory_dynamic = ??? Dynamic + //#LOC_BDArmory_static = ??? Static + + #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_Multiple = ??? Multiple + #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 = Рой + #LOC_BDArmory_NextTankSetup = След. наполнение бака + #LOC_BDArmory_PreviousTankSetup = Пред. наполнение бака + + // 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_missile_text = ??? Missile Labels + #LOC_BDArmory_Icon_debris = Значки Обломков + #LOC_BDArmory_Icon_persist = Не прятать с интерфейсом + #LOC_BDArmory_Icon_threats = Индикатор Угрозы + #LOC_BDArmory_Icon_pointers = Указатели на Значки + #LOC_BDArmory_Icon_scale = Размеры Значков: + //#LOC_BDArmory_Icon_opacity = ??? Opacity: + #LOC_BDArmory_Icon_distance_threshold = Пороговая Дистанция: + //#LOC_BDArmory_Icon_max_distance_threshold = ??? Max Distance Threshold: + #LOC_BDArmory_Icon_telemetry = Вкл. Телеметрию; + //#LOC_BDArmory_Icon_StoreTeamColors = ??? Store Team Colors + #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_Titanium = ??? Titanium + //#LOC_BDArmory_Composites = ??? Composites + //#LOC_BDArmory_RAMFoam = ??? Radar Absorbent Foam + #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_ArmorLiftStacking = Суммирование Подъемной Силы + #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_partTreeVisualizer = ??? Toggle Part Tree Visualizer + //#LOC_BDArmory_checkVessel = ??? Check Vessel Legality + #LOC_BDArmory_ArmorSelect = Выбрать Материал Брони + //#LOC_BDArmory_DryMassWhitelist = ??? Resources Counted as Drymass + #LOC_BDArmory_ArmorTool = BDA Инструмент Систем Аппарата + #LOC_BDArmory_Armor_HullMat = Текущий Материал Корпуса + #LOC_BDArmory_Armor_ArmorType = Тип Брони + #LOC_BDArmory_Armor_Hullmass = Масса Измененных Деталей + #LOC_BDArmory_BulletResist = Кин. Сопротивление + #LOC_BDArmory_ExplosionResist = Взрывоустойчивость + #LOC_BDArmory_LaserResist = Лазер. Сопротивление + #LOC_BDArmory_ArmorShatterWarning = Проникающий удар разрушит броню + //#LOC_BDArmory_ArmorToolPartCount = ??? Part Count Exceeded! + //#LOC_BDArmory_ArmorToolEngineCount = ??? Too Many Engines: + //#LOC_BDArmory_ArmorToolEngineCountFloor = ??? Too Few Engines: + //#LOC_BDArmory_ArmorToolTWR = ??? TWR Exceeded: + //#LOC_BDArmory_ArmorToolLTW = ??? LTW Exceeded: + //#LOC_BDArmory_ArmorToolMaxMass = ??? Mass Limit Exceeded: + //#LOC_BDArmory_ArmorToolMaxPoints = ??? Point Limit Exceeded: + //#LOC_BDArmory_ArmorToolIllegalParts = ??? Illegal Parts: + //#LOC_BDArmory_ArmorToolNonCockpit = ??? not attached to cockpit + //#LOC_BDArmory_ArmorToolOversizedPWings = ??? pWings exceeding max Lift - check Lift Visualizer + //#LOC_BDArmory_ArmorToolVesselLegal = ??? Vessel legal! + + + // Missile & CM Settings + //#LOC_BDArmory_Settings_MissileCMToggle = ??? Show Missile & Countermeasure Settings + //#LOC_BDArmory_Settings_AspectedRCS = ??? Real-Time Aspected RCS + //#LOC_BDArmory_Settings_AspectedRCSOverallRCSWeight = ??? Overall RCS Weight + //#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 + + // Texture switching + #LOC_BDArmory_NextTexture = След. Текстура + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/zh-cn.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/zh-cn.cfg index b0845c0ad..02b6b8ef5 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/zh-cn.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/zh-cn.cfg @@ -1,266 +1,1578 @@ +// Notes: +// - The indentation provides fold region info for IDEs. +// - #LOC_A = #LOC_B is valid. +// - Check for duplicates with: grep -o '^\s*#LOC[^ ]\+' en-us.cfg |tr -d ' '|sort|uniq -c|grep -v '\s1' +// - Propagate changes in en-us.cfg to the other localisation files by running 'python3 ../_Other\ Stuff/localisation_organisation_sync.py' in the BDArmory/BDArmory folder. + Localization { - zh-cn - { - #LOC_BDArmory_Generic_Cancel = 取消 - #LOC_BDArmory_Generic_New = 新建 - #LOC_BDArmory_Generic_On = 开 - #LOC_BDArmory_Generic_Off = 关 - #LOC_BDArmory_VesselStatus_Landed = (着陆) - #LOC_BDArmory_VesselStatus_Splashed = (溅落) - - #LOC_BDArmory_WMWindow_title = BDA武器管理器 - #LOC_BDArmory_WMWindow_GuardModebtn = 警戒模式 - #LOC_BDArmory_WMWindow_ArmedText = 扳机 - #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 = Ripple: <<1>> RPM - #LOC_BDArmory_WMWindow_rippleText4 = Ripple: OFF - #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 = Burst Length - #LOC_BDArmory_WMWindow_FieldofView = 视场范围 - #LOC_BDArmory_WMWindow_VisualRange = 视距 - #LOC_BDArmory_WMWindow_GunsRange = 枪炮射程 - #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 = 无目标 - #LOC_BDArmory_Settings_Title = BDArmory设置 - #LOC_BDArmory_Settings_Instakill = 一击必杀 - #LOC_BDArmory_Settings_InfiniteAmmo = 无限子弹 - #LOC_BDArmory_Settings_BulletHits = 子弹击中 - #LOC_BDArmory_Settings_EjectShells = 跳弹 - #LOC_BDArmory_Settings_AimAssist = 瞄准辅助 - #LOC_BDArmory_Settings_DrawAimers = Draw Aimers - #LOC_BDArmory_Settings_DebugLines = Debug线条 - #LOC_BDArmory_Settings_DebugLabels = Debug标签 - #LOC_BDArmory_Settings_RemoteFiring = 远程射击 - #LOC_BDArmory_Settings_ClearanceCheck = 安全间距检查 - #LOC_BDArmory_Settings_AmmoGauges = 弹药列表 - #LOC_BDArmory_Settings_ShellCollisions = 弹药碰撞 - #LOC_BDArmory_Settings_BulletHoleDecals = 弹孔显示 - #LOC_BDArmory_Settings_PerformanceLogging = 性能日志 - #LOC_BDArmory_Settings_ShowEditorSubcategories = 航天大楼里显示BDA分类 - #LOC_BDArmory_Settings_AutocategorizeParts = 自动分类部件 - #LOC_BDArmory_Settings_MaxBulletHoles = 最大弹孔数量 - #LOC_BDArmory_Settings_PeaceMode = 和平模式 - #LOC_BDArmory_Settings_RWRWindowScale = RWR窗口比例 - #LOC_BDArmory_Settings_RadarWindowScale = 雷达窗口比例 - #LOC_BDArmory_Settings_TargetWindowScale = 目标窗口比例 - #LOC_BDArmory_Settings_TriggerHold = 长按扳机 - #LOC_BDArmory_Settings_UIVolume = 用户界面大小 - #LOC_BDArmory_Settings_WeaponVolume = 武器大小 - #LOC_BDArmory_Settings_DogfightCompetition = 缠斗比赛 - #LOC_BDArmory_Settings_CompetitionDistance = 比赛距离 - #LOC_BDArmory_Settings_StartCompetition = 开始比赛 - #LOC_BDArmory_Settings_CompetitionStarting = 开始比赛中... - #LOC_BDArmory_Settings_EditInputs = 编辑键位 - #LOC_BDArmory_Generic_SaveandClose = 保存退出 - #LOC_BDArmory_InputSettings_Weapons = 武器 - #LOC_BDArmory_InputSettings_TargetingPod = 瞄准吊舱 - #LOC_BDArmory_InputSettings_Radar = 雷达 - #LOC_BDArmory_InputSettings_VesselSwitcher = 载具切换 - #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 = Formation Lag - #LOC_BDArmory_WingCommander_Guiname3 = 切换界面 - #LOC_BDArmory_WingCommander_SelectAll = 选择全部 - #LOC_BDArmory_WingCommander_CommandSelf = Command Self - #LOC_BDArmory_WingCommander_Follow = 跟随 - #LOC_BDArmory_WingCommander_FlyToPos = Fly To Pos - #LOC_BDArmory_WingCommander_AttackPos = Attack Pos - #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 = Lag - #LOC_BDArmory_WingCommander_ScreenMessage = 选择目标坐标.\n右键取消. - - #LOC_BDArmory_BDAVesselSwitcher_Title = BDA 载具切换 - - //GUI Names - #LOC_BDArmory_EjectVelocity = 喷射速度 - #LOC_BDArmory_TNTMass = TNT质量等效 - #LOC_BDArmory_BlastRadius = 爆炸半径 - #LOC_BDArmory_WeaponName = 武器名\u0020 - #LOC_BDArmory_GuidanceType = 制导类型\u0020 - #LOC_BDArmory_TargetingMode = 目标模式\u0020 - #LOC_BDArmory_ActiveRadarRange = 主动雷达范围 - #LOC_BDArmory_SteerLimiter = 制导限制 - #LOC_BDArmory_StagesNumber = 分级编号 - #LOC_BDArmory_StageToTriggerOnProximity = 在接近时分级引爆 - #LOC_BDArmory_SteerDamping = 制导阻尼 - #LOC_BDArmory_SteerFactor = Steer Factor - #LOC_BDArmory_RollCorrection = 翻滚修正 - #LOC_BDArmory_RollCorrection_enabledText = Roll enabled - #LOC_BDArmory_RollCorrection_disabledText = Roll disabled - #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_DefaultAltitude = 默认高度 - #LOC_BDArmory_MinAltitude = 最低高度 - #LOC_BDArmory_SteerKi = Steer Ki - #LOC_BDArmory_MaxSpeed = 最大速度 - #LOC_BDArmory_MaxDrift = Max drift - #LOC_BDArmory_TakeOffSpeed = 起飞速度 - #LOC_BDArmory_MinSpeed = 最低战斗速度 - #LOC_BDArmory_IdleSpeed = 怠速 - #LOC_BDArmory_maxAllowedGForce = 最大G力 - #LOC_BDArmory_maxAllowedAoA = 最大攻角 - #LOC_BDArmory_Orbit = 绕\u0020 - #LOC_BDArmory_Orbit_enabledText = 右舷 (顺时针) - #LOC_BDArmory_Orbit_disabledText = Port (逆时针) - #LOC_BDArmory_UnclampTuning = Unclamp tuning\u0020 - #LOC_BDArmory_UnclampTuning_enabledText = 分离 - #LOC_BDArmory_UnclampTuning_disabledText = 夹紧 - #LOC_BDArmory_StandbyMode = 待机模式 - #LOC_BDArmory_On = 开 - #LOC_BDArmory_Off = 关 - #LOC_BDArmory_VehicleType = 车辆类型 - #LOC_BDArmory_MaxSlopeAngle = 最大坡度角 - #LOC_BDArmory_CruiseSpeed = 巡航速度 - #LOC_BDArmory_TargetPitch = 移动俯仰 - #LOC_BDArmory_BankAngle = 倾斜角 - #LOC_BDArmory_BroadsideAttack = 攻击方向 - #LOC_BDArmory_BroadsideAttack_enabledText = 侧面 - #LOC_BDArmory_BroadsideAttack_disabledText = 前 - #LOC_BDArmory_MinEngagementRange = 最小遭遇范围 - #LOC_BDArmory_MaxEngagementRange = 最大遭遇范围 - #LOC_BDArmory_ManeuverRCS = RCS启动 - #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 - #LOC_BDArmory_Rails = Rails - #LOC_BDArmory_DeployAltitude = 分离高度 - #LOC_BDArmory_EngageRangeMin = 遭遇范围最小值 - #LOC_BDArmory_EngageRangeMax = 遭遇范围最大值 - #LOC_BDArmory_EngageAir = Engage空气 - #LOC_BDArmory_EngageMissile = Engage导弹 - #LOC_BDArmory_EngageSurface = Engage地面 - #LOC_BDArmory_EngageSLW = Engage SLW - #LOC_BDArmory_DisableEngageOptions = Disable Engage Options - #LOC_BDArmory_EnableEngageOptions = Enable Engage Options - #LOC_BDArmory_MaxStaticLaunchRange = 最大静态发射距离 - #LOC_BDArmory_MinStaticLaunchRange = 最小静态发射距离 - #LOC_BDArmory_MaxOffBoresight = Max Off Boresight - #LOC_BDArmory_DetonationDistanceOverride = 爆轰距离覆盖 - #LOC_BDArmory_DropTime = 下降时间 - #LOC_BDArmory_InCargoBay = 在弹仓:\u0020 - #LOC_BDArmory_DetonationTime = 爆炸时间 - #LOC_BDArmory_BallisticOvershootFactor = Ballistic Overshoot factor - #LOC_BDArmory_BallisticAnglePath = Ballistic Angle path - #LOC_BDArmory_CruiseAltitude = 巡航高度 - #LOC_BDArmory_CruisePredictionTime = 巡航预测时间 - #LOC_BDArmory_GPSTarget = GPS目标 - #LOC_BDArmory_FiringInterval = 射击间隔 - #LOC_BDArmory_FiringBurstLength = Firing Burst Length - #LOC_BDArmory_FieldOfView = 视场范围 - #LOC_BDArmory_VisualRange = 可视距离 - #LOC_BDArmory_GunsRange = 武器范围 - #LOC_BDArmory_MissilesORTarget = 导弹/目标 - #LOC_BDArmory_GaurdMode = 警戒模式:\u0020 - #LOC_BDArmory_Team = 小队 - #LOC_BDArmory_Weapon = 武器 - #LOC_BDArmory_Direction = 方向:\u0020 - #LOC_BDArmory_Direction_disabledText = 横向 - #LOC_BDArmory_Direction_enabledText = 向前 - #LOC_BDArmory_DecoupleSpeed = 分离速度 - #LOC_BDArmory_MaxAltitude = 最大高度 - #LOC_BDArmory_TerminalGuidance = 终端制导:\u0020 - #LOC_BDArmory_false = 否 - #LOC_BDArmory_true = 是 - #LOC_BDArmory_TurretEnabled = 炮塔开启 - #LOC_BDArmory_AutoReturn = 自动返回 - #LOC_BDArmory_AddedCost = 增加成本 - #LOC_BDArmory_DryMass = 干质量 - #LOC_BDArmory_Enabled = 开启 - #LOC_BDArmory_Enable = Enable - #LOC_BDArmory_EMPBlastRadius = EMP Blast Radius - #LOC_BDArmory_OrdinanceAvailable = Ordinance Available - #LOC_BDArmory_MissileAssign = 导弹分配 - #LOC_BDArmory_CurrentLocks = 当前锁定 - #LOC_BDArmory_MaxPitch = Max Pitch - #LOC_BDArmory_MinPitch = Min Pitch - #LOC_BDArmory_YawRange = Yaw Range - #LOC_BDArmory_FireLimits = 开火限制 - #LOC_BDArmory_FireLimits_disabledText = 无 - #LOC_BDArmory_FireLimits_enabledText = 在范围内 - #LOC_BDArmory_DefaultDetonationRange = 引信启动爆炸距离\u0020 - #LOC_BDArmory_ProximityFuzeRadius = Proximity Fuze Radius - #LOC_BDArmory_MaxDetonationRange = 最大爆炸范围 - #LOC_BDArmory_Barrage = 弹幕射击 - #LOC_BDArmory_ToggleBarrage = 开关弹幕射击 - #LOC_BDArmory_Status = 状态 - #LOC_BDArmory_Toggle = 切换 - #LOC_BDArmory_ShowGroupEditor = Show Group Editor - #LOC_BDArmory_ShowGroupEditor_enabledText = close Group GUI - #LOC_BDArmory_ShowGroupEditor_disabledText = open Group GUI - #LOC_BDArmory_DeactivationDepth = Deactivation Depth - #LOC_BDArmory_Hitpoints = 生命值 - #LOC_BDArmory_ArmorThickness = 装甲厚度 - #LOC_BDArmory_FireCountermeasure = 开火反制 - #LOC_BDArmory_IncreaseHeight = 高度 ++ - #LOC_BDArmory_DecreaseHeight = 高度 -- - #LOC_BDArmory_IncreaseLength = 长度 ++ - #LOC_BDArmory_DecreaseLength = 长度 -- - #LOC_BDArmory_Detonate = 引爆 - #LOC_BDArmory_TogglePilot = 开关自动驾驶 - #LOC_BDArmory_DeactivatePilot = 手动驾驶 - #LOC_BDArmory_ActivatePilot = 自动驾驶 - #LOC_BDArmory_FireMissile = 发射导弹 - #LOC_BDArmory_GuidanceMode = 制导模式 - #LOC_BDArmory_Jettison = 投弃 - #LOC_BDArmory_ToggleTurret = 切换炮塔 - #LOC_BDArmory_HideUI = 隐藏武器名界面 - #LOC_BDArmory_ShowUI = 设置武器名界面 - #LOC_BDArmory_RailsPlus = Rails++ - #LOC_BDArmory_RailsMinus = Rails-- - #LOC_BDArmory_ChangetoLowAltitudeRange = 转向低空飞行 - #LOC_BDArmory_SelectTeam = 选择队伍 - #LOC_BDArmory_OpenGUI = 打开界面 - #LOC_BDArmory_ReturnTurret = 返回炮塔 - #LOC_BDArmory_ToggleAnimation = 开关动画 - #LOC_BDArmory_NextTankSetup = 下个配置箱 - #LOC_BDArmory_PreviousTankSetup = 上个配置箱 - #LOC_BDArmory_Resupply = Resupply - #LOC_BDArmory_ToggleRadar = 开关雷达 - #LOC_BDArmory_NextTexture = 下一贴图 - #LOC_BDArmory_HideWeaponGroupUI = 隐藏武器组界面 - #LOC_BDArmory_SetWeaponGroupUI = 设置武器组界面 - #LOC_BDArmory_Fire = 开火 + zh-cn + { + // Generic + #LOC_BDArmory_Generic_OK = 好 + #LOC_BDArmory_Generic_Cancel = 取消 + #LOC_BDArmory_Generic_New = 新建 + #LOC_BDArmory_Generic_On = 开 + #LOC_BDArmory_Generic_Off = 关 + #LOC_BDArmory_On = 开 + #LOC_BDArmory_Off = 关 + #LOC_BDArmory_Generic_Hide = 隐藏 + #LOC_BDArmory_Generic_Show = 显示 + #LOC_BDArmory_Generic_Load = 加载 + #LOC_BDArmory_Generic_Save = 保存 + #LOC_BDArmory_Generic_Reload = 重新加载 + #LOC_BDArmory_Generic_Help = 帮助 + #LOC_BDArmory_Generic_Select = 选择 + #LOC_BDArmory_Generic_SaveandClose = 保存退出 + #LOC_BDArmory_VesselStatus_Landed = (着陆) + #LOC_BDArmory_VesselStatus_Splashed = (溅落) + #LOC_BDArmory_VesselStatus_Underwater = (水下) + #LOC_BDArmory_false = 否 + #LOC_BDArmory_true = 是 + #LOC_BDArmory_Enabled = 启用 + #LOC_BDArmory_Disabled = 禁用 + #LOC_BDArmory_Enable = 启用 + #LOC_BDArmory_Disable = 禁用 + + // WM Window + #LOC_BDArmory_WMWindow_title = BDA武器管理器 + #LOC_BDArmory_WMWindow_GuardModebtn = 警戒模式 + #LOC_BDArmory_WMWindow_ArmedText = 扳机 + #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_barrageStagger = 交错 + #LOC_BDArmory_WMWindow_rippleText3 = 齐射射速: <<1>> RPM + #LOC_BDArmory_WMWindow_rippleText4 = 齐射射速:关 + #LOC_BDArmory_WMWindow_ListWeapons = 所有武器 + #LOC_BDArmory_WMWindow_GuardMenu = 警戒菜单 + #LOC_BDArmory_WMWindow_ModulesToggle = 模块 + #LOC_BDArmory_WMWindow_NoWeaponManager = 没有找到武器管理器. + + // WM Guard Menu + #LOC_BDArmory_WMWindow_NoneWeapon = 无 + #LOC_BDArmory_WMWindow_GuardMode = 警戒模式 <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = 射击间隔 + #LOC_BDArmory_WMWindow_BurstLength = 点射长度 + #LOC_BDArmory_WMWindow_FiringTolerance = 射击角度 + #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 = 所有目标 + // Advanced Targeting + #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 = 随机 + + // WM Target Priority + #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 = 目标推重比 + #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 + + // WM Modules + #LOC_BDArmory_WMWindow_RadarWarning = 雷达告警接收机 + #LOC_BDArmory_WMWindow_GPSCoordinator = GPS 坐标面板 + #LOC_BDArmory_WMWindow_WingCommand = 僚机控制 + // WM GPS Module + #LOC_BDArmory_WMWindow_GPSTarget = GPS 目标 + #LOC_BDArmory_WMWindow_NoTarget = 无目标 + + // WM infolink + // WM infolink Weapons + #LOC_BDArmory_WMWindow_Weapons_Desc = 武器 - 该选项卡显示飞船上的所有武器/武器组。点击武器(组)名称将选择并激活该武器,允许手动发射枪炮、火箭或激光。如果是导弹武器,则必须将 "扳机已解除 "切换到 "已上膛",然后才能发射导弹。 + #LOC_BDArmory_WMWindow_Ripple_Salvo_Desc = 选择导弹后,会有一个 "齐射射速 "选项。这将设定按住扳机时导弹的连续发射速度。射速低于 1500 发/分钟的火炮、火箭或激光将有一个 "单个/齐射 "切换选项。如果存在同类型/武器组的多件武器,"单个"模式将使每件武器依次开火。齐射模式将使每种武器同时开火。 + + // WM infolink Guard Menu + #LOC_BDArmory_WMWindow_GuardTab_Desc = 警戒模式 - 该设置组控制AI如何以及何时使用载具上的武器。 + #LOC_BDArmory_WMWindow_FiringInterval_Desc = 开火间隔 - 设置AI扫描目标的频率(以秒为单位),也就是发射所选武器的频率。 + #LOC_BDArmory_WMWindow_BurstLength_desc = 点射长度 - 该选项控制AI持续发射所选武器的时间(以秒为单位)。如果为 0,AI将发射 (1/2 * 开火间隔) 秒。 + #LOC_BDArmory_WMWindow_FiringTolerance_desc = 射击角度 - 此项控制AI何时认为自己已瞄准目标并开火。角度为 1 意味着目标必须位于一个目标锥内,其宽度等于目标半径加上所选武器的散布半径。精确度较高的武器的目标锥较窄,反之亦然。角度为 2 的目标锥宽度是目标半径的 2 倍,以此类推。当目标位于目标锥范围内时,AI将发射当前选定的武器。 + #LOC_BDArmory_WMWindow_FieldofView_desc = 视场范围 - 控制AI的视场。设置为 360 意味着它可以看到所有方向的一切;小于 360 的值意味着AI只能看到这么宽的锥形范围内的目标。 + #LOC_BDArmory_WMWindow_VisualRange_desc = 可视距离 - 这是AI可以看到的距离。比这一数值更近的目标将被目视发现,AI将进入交战状态。超出此值的目标则需要雷达才能发现。 + #LOC_BDArmory_WMWindow_GunsRange_desc = 枪炮射程 - 设置载具上所有枪炮、火箭或激光的最大武器射程。默认情况下,它被设置为载具上安装的射程最远的非导弹武器。AI不会尝试向该范围之外的敌人开火。 + #LOC_BDArmory_WMWindow_MultiTargetNum_desc = 炮塔最大目标数 - 对于拥有多个炮塔的载具,该参数设置了炮塔可以独立瞄准和攻击的目标数量,使载具可以同时攻击多个目标。 + #LOC_BDArmory_WMWindow_MultiMissileTgtNum_desc = 导弹最大目标数 - 对于装有多枚导弹的舰船,它设定了AI使用导弹攻击不同目标的次数,一旦向当前目标发射的导弹达到允许的数量,就会切换到新的目标。 + #LOC_BDArmory_WMWindow_MissilesTgt_desc = 导弹/目标 - 设置AI向目标发射导弹的数量。一旦发射了一定数量的导弹,只有当先前发射的导弹被击中或摧毁后,AI才会再发射导弹。 + #LOC_BDArmory_WMWindow_TargetType_desc = 高级瞄准按钮 - 可为AI设置自定义瞄准偏好,使其能够专门瞄准武器、引擎、指令舱、最重的部件或这些部件的组合,而不是瞄准质量中心。 + #LOC_BDArmory_WMWindow_EngageType_desc = 交战选项按钮 - 这是一个切换按钮,可为舰艇上的所有武器同时快速设置武器交战选项--对空、对地、导弹或 水面/水下。 + + // WM infolink Target Priority + #LOC_BDArmory_WMWindow_Prioritues_Desc = 目标优先级 - 配置AI的目标偏好 + #LOC_BDArmory_WMWindow_targetBias_desc = 目标切换意向 - 设定了AI改变当前目标的意愿或不情愿程度. 此值越高, AI更不情愿改变目标. + #LOC_BDArmory_WMWindow_targetPreference_desc = 目标交战偏好 - 设置AI偏好的目标类型。数值越小,AI越倾向于攻击地面目标;数值越大,AI越倾向于攻击空中目标。 + #LOC_BDArmory_WMWindow_targetProximity_desc = 目标距离 - 设置AI对更近或更远目标的偏好。该值越大,越倾向于更近的目标。 + #LOC_BDArmory_WMWindow_targetAngletoTarget_desc = 目标角度 - AI优先选择与飞船的前进方向呈较小角度的目标的权重。数值越大,对飞船正前方目标的权重就越大。 + #LOC_BDArmory_WMWindow_targetAngleDist_desc = 角度/距离 - 根据目标偏离AI前进角度除以目标距离,来权衡AI对目标的偏好。数值越大,越优先攻击飞船前方和附近的目标,数值越小则相反。 + #LOC_BDArmory_WMWindow_targetAccel_desc = 目标推重比 - 数值越高,目标定位越偏向于加速度较快的目标,数值越低,目标定位越偏向于加速度较慢的目标。 + #LOC_BDArmory_WMWindow_targetClosingTime_desc = 接近时间 - 数值越大,越倾向于选择能最快速到达的目标,数值越小,则倾向于选择飞行时间较长的目标。 + #LOC_BDArmory_WMWindow_targetgunNumber_desc = 目标武器数量 - 该参数会根据目标的武器数量来加权选择目标。数值越大,倾向武器数量多的目标,数值越小,倾向武器数量少的目标。 + #LOC_BDArmory_WMWindow_targetMass_desc = 目标重量 - 这将使目标偏向于较重或较轻的船只。数值越大,倾向质量更大目标。 + #LOC_BDArmory_WMWindow_targetDmg_desc = 目标损害程度 - 此项根据目标所受伤害的程度来加权选择目标。数值越高,倾向生命值少的目标。 + #LOC_BDArmory_WMWindow_targetAllies_desc = 友军交战数--这将加权优先瞄准目前无友军交战的目标。高值将优先选择未交战的目标,低值将优先选择正在交战的目标。 + #LOC_BDArmory_WMWindow_targetThreat_desc = 目标威胁 - 高值将使AI优先交战正在向自身射击的目标。 + #LOC_BDArmory_WMWindow_targetVIP_desc = 攻击VIP/保护VIP. 如果设置为高值,AI会优先攻击敌方 VIP 目标,或攻击与友军 VIP 交战的目标,如果设置为低值,则会忽略这些目标. + + // Settings Window + #LOC_BDArmory_Settings_Title = BDArmory 设置 + #LOC_BDArmory_Settings_AdvancedUserSettings = 高级用户设置 + // Section Toggles + #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 = GM 设置 + + // Graphics / UI + #LOC_BDArmory_Settings_DebugSettingsToggle = Debugging + #LOC_BDArmory_Settings_AIToolbarButton = AI 工具栏按钮 + #LOC_BDArmory_Settings_VMToolbarButton = VM 工具栏按钮 + #LOC_BDArmory_Settings_UIScale = 界面尺寸 + //#LOC_BDArmory_Settings_UIScaleFollowsStock = ??? Follow Stock + #LOC_BDArmory_Settings_Instakill = 一击必杀 + #LOC_BDArmory_Settings_InfiniteAmmo = 无限子弹 + #LOC_BDArmory_Settings_InfiniteMissiles = 无限导弹 + //#LOC_BDArmory_Settings_InfiniteCountermeasures = ??? Infinite Countermeasures + #LOC_BDArmory_Settings_BulletFX = 弹丸特效 + #LOC_BDArmory_Settings_BulletHits = 子弹击中 + #LOC_BDArmory_Settings_WaterHitFX = 子弹入水特效 + #LOC_BDArmory_Settings_LightFX = 灯光效果 + #LOC_BDArmory_Settings_PerfOptions = 启用特效 + #LOC_BDArmory_Settings_EjectShells = 弹壳 + #LOC_BDArmory_Settings_VesselRelativeBulletChecks = 载具关联弹丸检查 + #LOC_BDArmory_Settings_AimAssist = 瞄准辅助 + #LOC_BDArmory_Settings_AimAssistMode_Target = 瞄准辅助模式(目标) + #LOC_BDArmory_Settings_AimAssistMode_Aimer = 瞄准辅助模式(准星) + #LOC_BDArmory_Settings_GUIBackgroundOpacity = 界面不透明度 + #LOC_BDArmory_Settings_DrawAimers = 显示准星 + + // Debugging + #LOC_BDArmory_Settings_DebugTelemetry = 遥测数据 + #LOC_BDArmory_Settings_DebugLines = Debug 线条 + #LOC_BDArmory_Settings_DebugAI = AI + #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_ResetScrollZoom = 重设滚轮缩放 + + // Gameplay FIXME These need more sorting + #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_AutoLogTimeSync = 自动启用时间同步日志 + #LOC_BDArmory_Settings_LogTimeSyncInterval = 时间同步日志间隔 + #LOC_BDArmory_Settings_LogTimeSyncStart = 开始记录日志 + #LOC_BDArmory_Settings_LogTimeSyncStop = 停止记录日志 + #LOC_BDArmory_Settings_ShowEditorSubcategories = 航天大楼里显示BDA分类 + #LOC_BDArmory_Settings_AutocategorizeParts = 自动分类部件 + #LOC_BDArmory_Settings_waterDrag = 水下弹丸阻力 + #LOC_BDArmory_Settings_AutoLoadToKSC = 自动加载到KSC + #LOC_BDArmory_Settings_GenerateCleanSave = 创建无冲突存档 + #LOC_BDArmory_Settings_AutoDisableUI = 自动禁用UI + #LOC_BDArmory_Settings_AutoResumeTournaments = 自动恢复比赛 + #LOC_BDArmory_Settings_AutoResumeContinuousSpawn = 自动恢复连续载具生成 + #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_DisableRamming = 禁用自杀式撞击 + #LOC_BDArmory_Settings_DefaultFFATargeting = 默认个人模式 + #LOC_BDArmory_Settings_TagMode = 标记模式 + #LOC_BDArmory_Settings_PaintballMode = 漆弹模式 + #LOC_BDArmory_Settings_DumbIRSeekers = 无法区分热诱弹 + #LOC_BDArmory_Settings_RunwayProject = 赛道项目 + //#LOC_BDArmory_Settings_CompChecks = ??? Use AI/WM Overrides + #LOC_BDArmory_Settings_RunwayProjectRound = 赛道项目回合 + #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 = 显示比赛状态并隐藏UI + #LOC_BDArmory_Settings_CameraSwitchIncludeMissiles = 视角切换包含导弹 + #LOC_BDArmory_Settings_ScrollZoomPrevention = 阻止滚轮缩放 + #LOC_BDArmory_Settings_ResetHP = 重设部件生命值 + #LOC_BDArmory_Settings_ResetArmor = 重设部件装甲 + #LOC_BDArmory_Settings_ResetHull = 重设部件材质 + #LOC_BDArmory_Settings_RestoreKAL = 重置 KAL + //#LOC_BDArmory_Settings_DisableGuardModeOnSpawn = ??? Disable Guard Mode on Spawn + #LOC_BDArmory_Settings_IntakeHack = 超级进气道 + #LOC_BDArmory_Settings_PWingsHack = 自定义机翼翼缘升力 + #LOC_BDArmory_Settings_PWingsThickHP = 基于自定义机翼厚度的质量/生命值 + #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_gungame_progression = 保持重生进度 + #LOC_BDArmory_settings_gungame_cycle = 循环列表 + // General Sliders + #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 = B.D. 爆炸 伤害倍数 + #LOC_BDArmory_Settings_ArmorExplosivePenetrationResistanceMultiplier = 装甲爆炸抗性 + #LOC_BDArmory_Settings_BuildingDamageMultiplier = 建筑伤害倍数 + #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = 内爆伤害倍数 + #LOC_BDArmory_Settings_SecondaryEffectDuration = 特殊武器效果持续时间 + #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = 弹道轨迹模拟倍数 + #LOC_BDArmory_Settings_ArmorMassMultiplier = 装甲重量倍数 + #LOC_BDArmory_Settings_DebrisCleanUpDelay = 残骸清除延迟 + #LOC_BDArmory_Settings_NumericInputSelfUpdate = 自动更新数值输入 + #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_Max_Armor_Limit = ??? Max Armor Limit + + // Game Modes + // Heart-Bleed + #LOC_BDArmory_Settings_HeartBleed = 失血模式 + #LOC_BDArmory_Settings_HeartBleedRate = 失血速率 + #LOC_BDArmory_Settings_HeartBleedInterval = 失血间隔时间 + #LOC_BDArmory_Settings_HeartBleedThreshold = 失血阈值 + + // Resource Steal + #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 = 反制措施掠夺比例 + + // Asteroids + #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 = 跟随载具散布范围 + + // 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_MutatorGungame = 游戏进程触发 + #LOC_BDArmory_Settings_MutatorTimed = 定时触发 + #LOC_BDArmory_Settings_MutatorDuration = 持续时间 + #LOC_BDArmory_UI_MutatorStart = 启用全局突变体 + #LOC_BDArmory_UI_MutatorShuffle = 重新抽取突变体! + #LOC_BDArmory_Settings_MutatorNum = 已激活突变体数量 + #LOC_BDArmory_Settings_MutatorIcons = 显示突变体图标 + + #LOC_BDArmory_Settings_WaypointsMode = 航点模式 + #LOC_BDArmory_Settings_GLimitsMode = 重力加速度限制 + + // Battle Damage + #LOC_BDArmory_Settings_BDSettingsToggle = 战斗伤害设置 + #LOC_BDArmory_Settings_BD_Proc = 处理频率 + //#LOC_BDArmory_Settings_BD_Proc_Pen = ??? Proc Min Penetration + #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_JointStrength = 结构损坏 + #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 = 允许战斗伤害 + + // Radar / Other Settings + #LOC_BDArmory_Settings_RWRWindowScale = 雷达告警接收机(RWR)窗口缩放比例 + #LOC_BDArmory_Settings_RadarWindowScale = 雷达窗口比例 + #LOC_BDArmory_Settings_LogarithmicRWRDisplay = 对数 RWR 显示 + #LOC_BDArmory_Settings_TargetWindowScale = 目标窗口比例 + #LOC_BDArmory_Settings_TargetWindowInvertMouse = 反转鼠标 (目标窗口) + #LOC_BDArmory_Settings_TriggerHold = 长按扳机 + #LOC_BDArmory_Settings_UIVolume = 用户界面大小 + #LOC_BDArmory_Settings_WeaponVolume = 武器大小 + //#LOC_BDArmory_Settings_IGNORE_TERRAIN_CHECK = ??? Detection Ignores Terrain + //#LOC_BDArmory_Settings_CHECK_WATER_TERRAIN = ??? Detection Checks Water + //#LOC_BDArmory_Settings_RADAR_NOTCHING = ??? Radar Notching + //#LOC_BDArmory_Settings_Notching_Factor = ??? Notch Effectiveness Factor + //#LOC_BDArmory_Settings_Notching_SCR_Factor = ??? Notch SCR Factor + + // Competition / Tournament + #LOC_BDArmory_Settings_CompetitionDistance = 比赛距离 + #LOC_BDArmory_Settings_CompetitionDuration = 比赛时长 + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparation = 队内分工 + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparationPerMember = / 成员 + #LOC_BDArmory_Settings_CompetitionFinalGracePeriod = 最终考虑阶段 + #LOC_BDArmory_Settings_CompetitionInitialGracePeriod = 初始考虑阶段 + #LOC_BDArmory_Settings_CompetitionKillTimer = 着陆后摧毁倒计时 + #LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay = 观战者移除延迟 + #LOC_BDArmory_Settings_CompetitionKillerGMFrequency = 杀手GM频率 + #LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod = 杀手GM考虑阶段 + #LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh = 高度上限 + #LOC_BDArmory_Settings_CompetitionAltitudeLimitLow = 高度下限 + #LOC_BDArmory_Settings_CompetitionGMWeaponKill = 摧毁无武装载具 + #LOC_BDArmory_Settings_CompetitionGMEngineKill = 摧毁无引擎载具 + #LOC_BDArmory_Settings_CompetitionGMDisableKill = 摧毁瘫痪载具 + #LOC_BDArmory_Settings_CompetitionGMHPKill = 摧毁受损载具 + #LOC_BDArmory_Settings_CompetitionGMKillDelay = GM摧毁延迟 + #LOC_BDArmory_Settings_CompetitionStarting = 正在开始比赛... + #LOC_BDArmory_Settings_DogfightCompetition = 缠斗比赛 + #LOC_BDArmory_Settings_StartCompetition = 开始比赛 + #LOC_BDArmory_Settings_StopCompetition = 停止比赛 + #LOC_BDArmory_Settings_StartCompetitionNow = 比赛现在开始 + #LOC_BDArmory_Settings_CompetitionStartNowAfter = 比赛即将开始 + #LOC_BDArmory_Settings_CompetitionStartDespiteFailures = 无视错误开始比赛 + #LOC_BDArmory_Settings_StartRapidDeployment = 启动快速部署 + #LOC_BDArmory_Settings_StartOrbitalDeployment = 启动轨道部署 + #LOC_BDArmory_Settings_LowGravDeployment = 开始低重力起飞比赛 + #LOC_BDArmory_Settings_EditInputs = 编辑键位 + #LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart = 开始比赛时关闭设置 + #LOC_BDArmory_Settings_CompetitionWaypointTimeThreshold = 航点时间阈值 + + // BDA Remote (defunct) + #LOC_BDArmory_BDARemoteOrchestration_Title = BDA远程编排 + #LOC_BDArmory_Settings_RemoteLogging = 远程日志记录 + #LOC_BDArmory_Settings_RemoteInterheatDelay = 回合内延迟 + #LOC_BDArmory_Settings_RemoteSync = 远程同步执行 + #LOC_BDArmory_Settings_CompetitionID = 比赛ID + + // Input Settings + #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_TemporarilyShowMouse = ??? Temporarily Show Mouse + #LOC_BDArmory_InputSettings_GUI = 界面 + #LOC_BDArmory_InputSettings_BackBtn = 返回 + #LOC_BDArmory_InputSettings_recordedInput = 按一个键或按钮. + #LOC_BDArmory_InputSettings_SetKey = 设置按键 + #LOC_BDArmory_InputSettings_Clear = 清除 + + // Weapon Config + #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_saveClose = 保存并关闭 + #LOC_BDArmory_reset = 重设 + #LOC_BDArmory_applyTo = 应用到 + #LOC_BDArmory_WeaponGroup = 武器组界面 + #LOC_BDArmory_AddToWpnGroup = 加入武器组: + #LOC_BDArmory_thisWeapon = 此武器 + #LOC_BDArmory_SymmetricWeapons = 对称武器 + + #LOC_BDArmory_CustomFireKey = 自定义开火键 + #LOC_BDArmory_SetCustomFireKey = 设定自定义开火键 + + #LOC_BDArmory_EjectVelocity = 喷射速度 + #LOC_BDArmory_TNTMass = TNT质量等效 + #LOC_BDArmory_BlastRadius = 爆炸半径 + #LOC_BDArmory_WeaponName = 武器名\u0020 + #LOC_BDArmory_GuidanceType = 制导类型\u0020 + #LOC_BDArmory_TargetingMode = 目标模式\u0020 + #LOC_BDArmory_ActiveRadarRange = 主动雷达范围 + //#LOC_BDArmory_MissileCMRange = ??? Countermeasure Range + //#LOC_BDArmory_MissileCMInterval = ??? Countermeasure Interval + + // Adjustable Rails + #LOC_BDArmory_Rails = 挂架 + #LOC_BDArmory_IncreaseHeight = 高度 ++ + #LOC_BDArmory_DecreaseHeight = 高度 -- + #LOC_BDArmory_IncreaseLength = 长度 ++ + #LOC_BDArmory_DecreaseLength = 长度 -- + #LOC_BDArmory_RailsPlus = 挂架++ + #LOC_BDArmory_RailsMinus = 挂架-- + + // Vessel Spawner + #LOC_BDArmory_BDAVesselSpawner_Title = BDA 载具生成器 + // Spawn Options + #LOC_BDArmory_Settings_SpawnOptions = 生成选项 + #LOC_BDArmory_Settings_SpawnDistanceFactor = 生成距离系数 + #LOC_BDArmory_Settings_SpawnRefHeading = 生成参考航向 + #LOC_BDArmory_Settings_SpawnDistance = 生成距离 + #LOC_BDArmory_Settings_SpawnDistanceToggle = 绝对距离与系数切换 + #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_CSFollowsCentroid = 生成点跟随质心 (CS) + #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = 持续单个生成 (S) + #LOC_BDArmory_Settings_SpawnRandomOrder = 随机生成顺序 (S) + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = 自动开始比赛 + #LOC_BDArmory_Settings_SpawnInitialVelocity = 以怠速在空中生成 + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = 在此位置生成 + #LOC_BDArmory_Settings_OutOfAmmoKillTime = 弹药耗尽后销毁时间 (CS) + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = 在此坐标生成载具 + #LOC_BDArmory_Settings_SaveSpawnLoc = 保存生成位置 + #LOC_BDArmory_Settings_ClearDebrisNow = 立即清除残骸 + #LOC_BDArmory_Settings_ClearBystandersNow = 立即清除观战者 + // Fill Seats + #LOC_BDArmory_Settings_SpawnFillSeats = 自动填充座位 + #LOC_BDArmory_Settings_SpawnFillSeats_Minimal = 最小填充 + #LOC_BDArmory_Settings_SpawnFillSeats_Default = 默认填充 + #LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints = 填充所有控制点 + #LOC_BDArmory_Settings_SpawnFillSeats_Cabins = 包含客舱座位 + + // Teams + #LOC_BDArmory_Settings_Teams = 小队设置 + #LOC_BDArmory_Settings_Teams_FFA = 自由混战模式 + #LOC_BDArmory_Settings_Teams_Folders = 每个文件夹 / 每个载具文件 + #LOC_BDArmory_Settings_Teams_Custom_Template = 自定义小队模板 + #LOC_BDArmory_Settings_Teams_SplitEvenly = 平均分配小队 + + #LOC_BDArmory_Settings_SpawnFilesLocation = 载具文件位置 + // Custom Spawn Templates + #LOC_BDArmory_Settings_CustomSpawnTemplateOptions = 载具生成模板选项 + #LOC_BDArmory_Settings_SpawnOnly = 仅生成 + #LOC_BDArmory_Settings_SpawnAndStartCompetition = 生成并开始比赛 + #LOC_BDArmory_Settings_CustomSpawnTemplate_ReplaceTeam = 替换小队 + #LOC_BDArmory_Settings_CustomSpawnTemplate_TemplateSelection = 模板选择 + //#LOC_BDArmory_Settings_CustomSpawnTemplate_SaveCraftToTemplate = ??? Save Craft URLs + + // Observers + #LOC_BDArmory_Settings_Observers = 观战者 + #LOC_BDArmory_ObserverSelection_Title = 观战者选择 + #LOC_BDArmory_ObserverSelection_SelectAll = 全选 + #LOC_BDArmory_ObserverSelection_SelectNone = 全不选 + + // Interesting Spawn Locations + #LOC_BDArmory_Settings_SpawnLocations = 有趣的地方 + #LOC_BDArmory_Settings_WarpHere = 移动到此处 + #LOC_BDArmory_Settings_Planet = 选择星球 + + // Tournament Options + #LOC_BDArmory_Settings_TournamentOptions = 比赛选项 + #LOC_BDArmory_Settings_TournamentStyle = 比赛模式 + #LOC_BDArmory_Settings_TournamentRoundType = 回合模式 + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = 回合间隔时间 + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = 回合间时间加速 + //#LOC_BDArmory_Settings_TournamentTimeWarpDaylight = ??? Daylight + #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_TournamentNPCsPerHeat = 每回合NPC数量 + #LOC_BDArmory_Settings_TournamentSetup = 设置比赛 + #LOC_BDArmory_Settings_TournamentRun = 开始比赛 + #LOC_BDArmory_Settings_TournamentStop = 中止比赛 + + // Waypoints + #LOC_BDArmory_Settings_WaypointsOptions = 航点选项 + #LOC_BDArmory_Settings_WaypointsOneAtATime = 逐个完成航点 + #LOC_BDArmory_Settings_WaypointsInfFuelAtStart = 起点前无限燃料 + #LOC_BDArmory_Settings_WaypointsShow = 显示航点 + + #LOC_BDArmory_Settings_SingleSpawn = 单次生成 + #LOC_BDArmory_Settings_ContinuousSpawning = 持续生成 + #LOC_BDArmory_Settings_CancelSpawning = 取消生成 + + // Waypoint GUI + #LOC_BDArmory_BDAWaypointBuilder_Title = 航点航线构建器 + #LOC_BDArmory_WP_LoadCourse = 加载航线 + #LOC_BDArmory_WP_NewCourse = 新建航线 + #LOC_BDArmory_WP_ChooseCourse = 选择航线 + #LOC_BDArmory_WP_Create = 创建 + #LOC_BDArmory_WP_Record = 录制航线 + #LOC_BDArmory_WP_TimeStep = 时间间隔 + #LOC_BDArmory_WP_Recording = 正在录制航线... + #LOC_BDArmory_WP_FinishRecording = 完成录制 + #LOC_BDArmory_WP_Spawnpoint = 起始点 + #LOC_BDArmory_WP_AddGate = 添加检查点 + #LOC_BDArmory_WP_Waypoint = 航点 + #LOC_BDArmory_WP_SpeedLimit = 速度限制 + #LOC_BDArmory_WP_Increment = 增加 + #LOC_BDArmory_WP_MaxLaps = 最大圈数 + #LOC_BDArmory_WP_GuardActivate = 激活警戒模式 + #LOC_BDArmory_WP_CourseDefaults = 应用航线默认设置 + #LOC_BDArmory_WP_SelectModel = 选择航点模型 + + // Vessel Mover + #LOC_BDArmory_VesselMover_Title = BDA 载具生成器 + #LOC_BDArmory_VesselMover_VesselSelection = 载具选择 + #LOC_BDArmory_VesselMover_CrewSelection = 乘员选择 + #LOC_BDArmory_VesselMover_MoveVessel = 移动载具 + #LOC_BDArmory_VesselMover_SpawnVessel = 载具生成 + #LOC_BDArmory_VesselMover_RecoverVessel = 回收载具 + #LOC_BDArmory_VesselMover_ChooseCrew = 选择乘员 + #LOC_BDArmory_VesselMover_PlaceAfterSpawn = 生成后立即放置 + //#LOC_BDArmory_VesselMover_DeconflictVesselName = ??? Deconflict Vessel Name + #LOC_BDArmory_VesselMover_PlaceVessel = 放置载具 + #LOC_BDArmory_VesselMover_DropVessel = 扔下载具 + #LOC_BDArmory_VesselMover_InstantLowering = 即时降低 + #LOC_BDArmory_VesselMover_ClassicChooser = 经典载具浏览器 + #LOC_BDArmory_VesselMover_EnableBrakes = 启动刹车 + #LOC_BDArmory_VesselMover_EnableSAS = 启动SAS + #LOC_BDArmory_VesselMover_MinLowerSpeed = 最低下降速度 + #LOC_BDArmory_VesselMover_LowerFast = 放置更低 + #LOC_BDArmory_VesselMover_BelowWater = 水下 + #LOC_BDArmory_VesselMover_DontWorryAboutCollisions = 不要避免碰撞 + #LOC_BDArmory_VesselMover_Any = 任意 + #LOC_BDArmory_VesselMover_ReallyRemoveKerbals = 确定移除坎巴拉人吗‽ + #LOC_BDArmory_VesselMover_Help_Movement = 移动 + #LOC_BDArmory_VesselMover_Help_Roll = 滚转 + #LOC_BDArmory_VesselMover_Help_Pitch = 俯仰 + #LOC_BDArmory_VesselMover_Help_Yaw = 偏航 + #LOC_BDArmory_VesselMover_Help_AutoRotateRocket = 自动旋转火箭 + #LOC_BDArmory_VesselMover_Help_AutoRotatePlane = 自动旋转载具 + #LOC_BDArmory_VesselMover_Help_CycleAltitudes = 循环预设高度: Tab、Shift+Tab + #LOC_BDArmory_VesselMover_Help_ResetAltitude = 重设高度 + #LOC_BDArmory_VesselMover_Help_AdjustAltitude = 调整高度 + #LOC_BDArmory_VesselMover_CloseOnCompetitionStart = 比赛开始时关闭 + + // Craft Browser + #LOC_BDArmory_CraftBrowser_InvalidParts = 无效部件 + #LOC_BDArmory_CraftBrowser_UnknownModules = 模块 + #LOC_BDArmory_CraftBrowser_Clear = 清除 + #LOC_BDArmory_CraftBrowser_ClearAll = 全部清除 + #LOC_BDArmory_CraftBrowser_Refresh = 刷新 + #LOC_BDArmory_CraftBrowser_Parts = 部件 + #LOC_BDArmory_CraftBrowser_Mass = 质量 + #LOC_BDArmory_CraftBrowser_Version = 版本 + #LOC_BDArmory_CraftBrowser_Craft = 载具 + #LOC_BDArmory_CraftBrowser_Folder = 文件夹 + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnails = ??? Generate Missing Thumbnails + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsRecurse = ??? Recurse subfolders + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingFor = ??? Generating thumbnail for + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingForCraftIn = ??? Generating thumbnail for craft in + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFinished = ??? Finished generating thumbnails. + //#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFailure = ??? Unable to capture thumbnail of + + // Scores + #LOC_BDArmory_BDAScores_Title = 比赛得分 + #LOC_BDArmory_BDAScores_Weights = 得分权重 + #LOC_BDArmory_BDAScores_Round = 比赛轮次 + #LOC_BDArmory_BDAScores_Heat = 比赛回合 + //#LOC_BDArmory_BDAScores_Unlimited = ??? Unlimited + //#LOC_BDArmory_BDAScores_Score = ??? Score + //#LOC_BDArmory_BDAScores_Lives = ??? Lives + + // Staging Icons + #LOC_BDArmory_ProtoStageIconInfo_Reloading = 正在装弹 + #LOC_BDArmory_ProtoStageIconInfo_Overheat = 过热警告 + #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = 弹药用尽 + //#LOC_BDArmory_ProtoStageIconInfo_CMsOut = ??? CMs Depleted + + // Wing Commander + #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右键点击取消选择 + + // Vessel Switcher + #LOC_BDArmory_BDAVesselSwitcher_Title = BDA 载具切换 + + // 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 = 回合 + + // Modular Missile, Custom Weapons + #LOC_BDArmory_StagesNumber = 分级数量 + #LOC_BDArmory_StageToTriggerOnProximity = 接近触发分级 + #LOC_BDArmory_RollCorrection = 滚转修正 + #LOC_BDArmory_RollCorrection_enabledText = 启用滚转修正 + #LOC_BDArmory_RollCorrection_disabledText = 禁用滚转修正 + //#LOC_BDArmory_MissileIFF = ??? Seeker IFF + //#LOC_BDArmory_MissileIFF_enabledText = ??? IFF enabled + //#LOC_BDArmory_MissileIFF_disabledText = ??? IFF disabled + #LOC_BDArmory_TimeBetweenStages = 分级间隔时间 + #LOC_BDArmory_AI_MinSpeedGuidance = AI制导最低速度 + #LOC_BDArmory_ClearanceRadius = 安全距离半径 + #LOC_BDArmory_ClearanceLength = 安全距离长度 + #LOC_BDArmory_showRFGUI = 显示武器名称界面 + #LOC_BDArmory_showRFGUI_enabledText = 显示武器名称 + #LOC_BDArmory_showRFGUI_disabledText = 隐藏武器名称 + + // WM (PAW) + // Target Priority + #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_TargetDmg = 目标损伤程度 + #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 + + // Countermeasures + #LOC_BDArmory_Countermeasure_Settings = 反制措施设置 + #LOC_BDArmory_EvadeThreshold = 规避前的预测受击时间 + #LOC_BDArmory_CMThreshold = 启用反制措施前的预测受击时间 + #LOC_BDArmory_CMRepetition = 每轮热诱弹发射次数 + #LOC_BDArmory_CMInterval = 热诱弹发射间隔时间 + #LOC_BDArmory_CMWaitTime = 热诱弹轮间等待时间 + #LOC_BDArmory_ChaffRepetition = 每轮箔条发射次数 + #LOC_BDArmory_ChaffInterval = 箔条发射间隔时间 + #LOC_BDArmory_ChaffWaitTime = 箔条轮间等待时间 + #LOC_BDArmory_SmokeRepetition = 每轮烟雾发射次数 + #LOC_BDArmory_SmokeInterval = 烟雾发射间隔时间 + #LOC_BDArmory_SmokeWaitTime = 烟雾轮间等待时间 + #LOC_BDArmory_ChaffFactor = 箔条效能系数 + #LOC_BDArmory_NonGuardModeCMs = 非警戒模式下启用反制措施 + + #LOC_BDArmory_IsVIP = 是否为VIP? + #LOC_BDArmory_IsVIP_enabledText = 是 + #LOC_BDArmory_IsVIP_disabledText = 否 + //#LOC_BDArmory_WM_IsPrimaryWM = ??? Is Primary + + // AI (PAW) + // Pilot AI + // PID + #LOC_BDArmory_AI_PID = PID 控制器 + #LOC_BDArmory_AI_SteerPower = 转向力度 (P) + #LOC_BDArmory_AI_SteerKi = 稳定修正 (I) + #LOC_BDArmory_AI_SteerDamping = 平稳性 (D) + //#LOC_BDArmory_AI_SteerMaxError = ??? Steer Max Error + + // Dynamic damping + #LOC_BDArmory_AI_DynamicSteerDamping = 动态转向调整 + #LOC_BDArmory_AI_DynamicDamping = 动态平稳控制 + #LOC_BDArmory_AI_DynamicDampingMin = 最小平稳值 + #LOC_BDArmory_AI_DynamicDampingMax = 最大平稳值 + #LOC_BDArmory_AI_DynamicDampingFactor = 平稳调整系数 + + // 3-axis damping + //#LOC_BDArmory_AI_3AxisSteerDamping = ??? 3-Axis Steer Damping + + // 3-axis static damping + //#LOC_BDArmory_AI_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + //#LOC_BDArmory_AI_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + //#LOC_BDArmory_AI_SteerDampingRoll = ??? Steer Damping Roll (Dr) + + // 3-axis dynamic damping + #LOC_BDArmory_AI_DynamicDampingPitch = 俯仰平稳控制 + #LOC_BDArmory_AI_DynamicDampingPitchMin = 俯仰最小平稳值 + #LOC_BDArmory_AI_DynamicDampingPitchMax = 俯仰最大平稳值 + #LOC_BDArmory_AI_DynamicDampingPitchFactor = 俯仰平稳调整系数 + #LOC_BDArmory_AI_DynamicDampingYaw = 偏航平稳控制 + #LOC_BDArmory_AI_DynamicDampingYawMin = 偏航最小平稳值 + #LOC_BDArmory_AI_DynamicDampingYawMax = 偏航最大平稳值 + #LOC_BDArmory_AI_DynamicDampingYawFactor = 偏航平稳调整系数 + #LOC_BDArmory_AI_DynamicDampingRoll = 滚转平稳控制 + #LOC_BDArmory_AI_DynamicDampingRollMin = 滚转最小平稳值 + #LOC_BDArmory_AI_DynamicDampingRollMax = 滚转最大平稳值 + #LOC_BDArmory_AI_DynamicDampingRollFactor = 滚转平稳调整系数 + + // Auto-tuning + #LOC_BDArmory_AI_PID_AutoTune = PID 自动调校 + #LOC_BDArmory_AI_PID_AutoTuning_Loss = 自动调校损耗 + #LOC_BDArmory_AI_PID_AutoTuning_NumSamples = 自动调校样本数 + #LOC_BDArmory_AI_PID_AutoTuning_FastResponseRelevance = 自动调校快速反应关联性 + #LOC_BDArmory_AI_PID_AutoTuning_InitialLearningRate = 自动调校初期学习率 + #LOC_BDArmory_AI_PID_AutoTuning_InitialRollRelevance = 自动调校初始滚转相关性 + #LOC_BDArmory_AI_PID_AutoTuning_Speed = 自动调校速度 + #LOC_BDArmory_AI_PID_AutoTuning_Altitude = 自动调校高度 + #LOC_BDArmory_AI_PID_AutoTuning_RecenteringDistance = 自动调校重新定位距离(km) + #LOC_BDArmory_AI_PID_AutoTuning_FixedP = 固定P值自动调校 + #LOC_BDArmory_AI_PID_AutoTuning_ClampMaximums = 自动调校最大值约束 + #LOC_BDArmory_AI_PID_AutoTuning_Summary = 自动调校总结 + + // Altitudes + #LOC_BDArmory_AI_Altitudes = 高度 + #LOC_BDArmory_AI_DefaultAltitude = 默认飞行高度 + #LOC_BDArmory_AI_MinAltitude = 最小飞行高度 + #LOC_BDArmory_AI_MaxAltitude = 最大飞行高度(高于地面) + #LOC_BDArmory_AI_HardMinAltitude = 强制最小高度 + //#LOC_BDArmory_AI_BombingAltitude = ??? Bombing Altitude + //#LOC_BDArmory_AI_DiveBombing = ??? Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AI_Speeds = 速度 + #LOC_BDArmory_AI_MaxSpeed = 最大飞行速度 + #LOC_BDArmory_AI_TakeOffSpeed = 起飞所需速度 + #LOC_BDArmory_AI_MinSpeed = 最低格斗速度 + #LOC_BDArmory_AI_StrafingSpeed = 对地扫射速度 + #LOC_BDArmory_AI_IdleSpeed = 巡航怠速 + #LOC_BDArmory_AI_ABPriority = 后燃室(加力)优先级 + #LOC_BDArmory_AI_ABOverrideThreshold = 后燃室(加力)覆盖阈值 + #LOC_BDArmory_AI_BrakingPriority = 刹车(减速板)优先级 + + // Control + #LOC_BDArmory_AI_ControlLimits = 控制权限限制 + #LOC_BDArmory_AI_SteerLimiter = 操控限制 + #LOC_BDArmory_AI_LowSpeedSteerLimiter = 低速操控限制 + #LOC_BDArmory_AI_LowSpeedLimiterSpeed = 低速限制触发速度 + #LOC_BDArmory_AI_HighSpeedSteerLimiter = 高速操控限制 + #LOC_BDArmory_AI_HighSpeedLimiterSpeed = 高速限制触发速度 + #LOC_BDArmory_AI_AltitudeSteerLimiterFactor = 高度操控限制系数 + #LOC_BDArmory_AI_AltitudeSteerLimiterAltitude = 高度限制触发高度 + #LOC_BDArmory_AI_AttitudeLimiter = 高度限制 + #LOC_BDArmory_AI_BankLimiter = 滚转角度限制 + #LOC_BDArmory_AI_WaypointPreRollTime = 航点预先滚转时间 + #LOC_BDArmory_AI_WaypointYawAuthorityTime = 航点启用偏航时间 + #LOC_BDArmory_AI_MaxAllowedGForce = 最大G力 + #LOC_BDArmory_AI_MaxAllowedAoA = 最大攻角 + #LOC_BDArmory_AI_PostStallAoA = 失速后攻角模式开关 + #LOC_BDArmory_AI_ImmelmannTurnAngle = 英麦曼回旋角度 + #LOC_BDArmory_AI_ImmelmannPitchUpBias = 英麦曼俯仰偏差 + + // Evade / Extend + #LOC_BDArmory_AI_EvadeExtend = 规避/远离 + #LOC_BDArmory_AI_ExtendMultiplier = 远离效果倍增 + #LOC_BDArmory_AI_ExtendDistanceAirToAir = 空对空远离距离 + #LOC_BDArmory_AI_ExtendAngleAirToAir = 空对空远离角度 + #LOC_BDArmory_AI_ExtendDistanceAirToGroundGuns = 空对地远离距离(枪炮) + #LOC_BDArmory_AI_ExtendDistanceAirToGround = 空对地远离距离 + #LOC_BDArmory_AI_ExtendTargetVel = 远离目标速度系数 + #LOC_BDArmory_AI_ExtendTargetAngle = 远离目标角度系数 + #LOC_BDArmory_AI_ExtendTargetDist = 远离目标距离 + #LOC_BDArmory_AI_ExtendAbortTime = 远离取消时间 + //#LOC_BDArmory_AI_ExtendMinGainRate = ??? Extend Min Gain Rate + #LOC_BDArmory_AI_ExtendToggle = 开关远离 (空对空) + #LOC_BDArmory_AI_MinEvasionTime = 最短规避时间 + #LOC_BDArmory_AI_EvasionNonlinearity = 非线性规避/远离 + #LOC_BDArmory_AI_EvasionThreshold = 规避距离阈值 + #LOC_BDArmory_AI_EvasionErraticness = RCS规避不稳定性 + #LOC_BDArmory_AI_EvasionTimeThreshold = 规避时间阈值 + #LOC_BDArmory_AI_EvasionMinRangeThreshold = 规避最小范围阈值 + #LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe = 不规避我的目标 + #LOC_BDArmory_AI_EvasionMissileKinematic = 运动学导弹规避 + #LOC_BDArmory_AI_CollisionAvoidanceThreshold = 载具规避阈值 + #LOC_BDArmory_AI_CollisionAvoidanceLookAheadPeriod = 载具规避预测 + #LOC_BDArmory_AI_CollisionAvoidanceStrength = 载具规避强度 + #LOC_BDArmory_AI_StandoffDistance = 安全距离 + + // Terrain + #LOC_BDArmory_AI_Terrain = 规避地形 + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMin = 地形规避调整最小值 + #LOC_BDArmory_AI_TurnRadiusTwiddleFactorMax = 地形规避调整最大值 + #LOC_BDArmory_AI_TerrainAvoidanceCriticalAngle = 倒置地形规避临界角 + #LOC_BDArmory_AI_TerrainAvoidanceVesselReactionTime = 载具反应时间 + #LOC_BDArmory_AI_TerrainAvoidancePostAvoidanceCoolDown = 规避冷却 + #LOC_BDArmory_AI_WaypointTerrainAvoidance = 航点地形规避 + + // Ramming + #LOC_BDArmory_AI_Ramming = 自杀式撞击 + #LOC_BDArmory_AI_ControlSurfaceLag = 撞击时考虑控制面滞后 + #LOC_BDArmory_AI_AllowRamming = 准许自杀式撞击 + #LOC_BDArmory_AI_AllowRammingGroundTargets = 包括地面目标 + + // Ejection (unused) + #LOC_BDArmory_AI_Ejection = 弹射 + #LOC_BDArmory_AI_EjectOnImpendingDoom = 被击坠即弹射 + + #LOC_BDArmory_AI_SliderResolution = 滑条精度 + // Idle / Orbit Behavior + #LOC_BDArmory_AI_Orbit = 盘旋方向\u0020 + #LOC_BDArmory_AI_Orbit_Starboard = 右舷 (CW) + #LOC_BDArmory_AI_Orbit_Port = 左舷 (CCW) + #LOC_BDArmory_AI_Orbit_Random = 左右均可 (CW/CCW) + #LOC_BDArmory_AI_Standby = 就绪模式 + + // Up-to-eleven + #LOC_BDArmory_AI_UnclampTuning = 大区间调整\u0020 + #LOC_BDArmory_AI_UnclampTuning_enabledText = 大区间 + #LOC_BDArmory_AI_UnclampTuning_disabledText = 小区间 + + // Surface / VTOL / Orbital AI + #LOC_BDArmory_AI_VehicleType = 载具类型 + #LOC_BDArmory_AI_MaxSlopeAngle = 最大坡度角 + #LOC_BDArmory_AI_CruiseSpeed = 巡航速度 + #LOC_BDArmory_AI_CombatSpeed = 战斗速度 + #LOC_BDArmory_AI_CombatAltitude = 战斗高度 + #LOC_BDArmory_AI_TargetPitch = 目标俯仰角 + #LOC_BDArmory_AI_MaxDrift = 最大偏移量 + #LOC_BDArmory_AI_MaxPitchAngle = 最大俯仰角 + #LOC_BDArmory_AI_BankAngle = 滚转角 + #LOC_BDArmory_AI_WeaveFactor = 穿插系数 + #LOC_BDArmory_AI_MaxBankAngle = 最大滚转角 + #LOC_BDArmory_AI_BroadsideAttack = 攻击方向 + #LOC_BDArmory_AI_BroadsideAttack_enabledText = 舷侧 + #LOC_BDArmory_AI_BroadsideAttack_disabledText = 前方 + #LOC_BDArmory_AI_MinEngagementRange = 最小交战距离 + #LOC_BDArmory_AI_MaxEngagementRange = 最大交战距离 + //#LOC_BDArmory_AI_ForceFiringRange = ??? Zero Throttle Firing Range + #LOC_BDArmory_AI_MaintainEngagementRange = 保持最小距离 + #LOC_BDArmory_AI_ManeuverRCS = RCS 启动 + #LOC_BDArmory_AI_ManeuverRCS_enabledText = 机动 + #LOC_BDArmory_AI_ManeuverRCS_disabledText = 战斗 + #LOC_BDArmory_AI_FiringRCS = 射击时的RCS(反应控制系统) + #LOC_BDArmory_AI_FiringRCS_enabledText = 管理速度 + #LOC_BDArmory_AI_FiringRCS_disabledText = 仅进行机动 + #LOC_BDArmory_AI_ReverseEngines = 反向引擎 + #LOC_BDArmory_AI_EngineRCSRotation = 引擎RCS(旋转) + #LOC_BDArmory_AI_EngineRCSTranslation = 引擎RCS(平移) + #LOC_BDArmory_AI_OrbitalPIDActive = 轨道PID激活 + #LOC_BDArmory_AI_RollMode = 侧舷方向 + #LOC_BDArmory_AI_MinObstacleMass = 最小障碍物质量 + #LOC_BDArmory_AI_PreferredBroadsideDirection = 首选舷侧方向 + #LOC_BDArmory_AI_GoesUp = 是否超过极限 + #LOC_BDArmory_AI_GoesUp_enabledText = 超过极限 + #LOC_BDArmory_AI_GoesUp_disabledText = 不超过极限 + #LOC_BDArmory_AI_ManeuverSpeed = 机动速度 + #LOC_BDArmory_AI_FiringSpeedMin = 最小开火速度 + #LOC_BDArmory_AI_FiringSpeedLimit = 最大开火速度 + #LOC_BDArmory_AI_AngularSpeedLimit = 角速度限制 + #LOC_BDArmory_AI_EvasionRCS = 反应控制系统规避 + #LOC_BDArmory_AI_EvasionEngines = 推力规避 + + // AI GUI + #LOC_BDArmory_AIWindow_title = AI 管理器 + #LOC_BDArmory_AIWindow_infoLink = 信息链接 + #LOC_BDArmory_AIWindow_NoAI = 未找到AI + // Sections + #LOC_BDArmory_AIWindow_PID = PID + #LOC_BDArmory_AIWindow_Altitudes = 高度 + #LOC_BDArmory_AIWindow_Speeds = 速度 + #LOC_BDArmory_AIWindow_Control = 控制 + #LOC_BDArmory_AIWindow_EvadeExtend = 规避/远离 + #LOC_BDArmory_AIWindow_Terrain = 地形 + #LOC_BDArmory_AIWindow_Ramming = 撞击 + #LOC_BDArmory_AIWindow_Combat = 战斗 + #LOC_BDArmory_AIWindow_Misc = 杂项 + + // Panel + // Pilot + // PID + #LOC_BDArmory_AIWindow_SteerPower = 操纵强度(P) + #LOC_BDArmory_AIWindow_SteerPower_ContextLow = <- 迟钝 + #LOC_BDArmory_AIWindow_SteerPower_ContextHigh = 灵敏 -> + #LOC_BDArmory_AIWindow_SteerKi = 操纵校正 (I) + #LOC_BDArmory_AIWindow_SteerKi_ContextLow = <- 反应不足 + #LOC_BDArmory_AIWindow_SteerKi_ContextHigh = 反应过强 -> + #LOC_BDArmory_AIWindow_SteerDamping = 操纵阻尼 (D) + #LOC_BDArmory_AIWindow_SteerDamping_ContextLow = <- 摇摆 + #LOC_BDArmory_AIWindow_SteerDamping_ContextHigh = 过稳定 -> + //#LOC_BDArmory_AIWindow_SteerDampingPitch = ??? Steer Damping Pitch (Dp) + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextLow = <- 摇摆 + #LOC_BDArmory_AIWindow_SteerDampingPitch_ContextHigh = 过稳定 -> + //#LOC_BDArmory_AIWindow_SteerDampingYaw = ??? Steer Damping Yaw (Dy) + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextLow = <- 摇摆 + #LOC_BDArmory_AIWindow_SteerDampingYaw_ContextHigh = 过稳定 -> + //#LOC_BDArmory_AIWindow_SteerDampingRoll = ??? Steer Damping Roll (Dr) + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextLow = <- 摇摆 + #LOC_BDArmory_AIWindow_SteerDampingRoll_ContextHigh = 过稳定 -> + //#LOC_BDArmory_AIWindow_SteerMaxError = ??? Max Error + //#LOC_BDArmory_AIWindow_SteerMaxError_ContextLow = ??? <- Slow & Easy Tuning + //#LOC_BDArmory_AIWindow_SteerMaxError_ContextHigh = ??? Fast & Harder Tuning -> + #LOC_BDArmory_AIWindow_DynDampMin = 偏离目标阻尼 + #LOC_BDArmory_AIWindow_DynDampMin_Context = 最小阻尼 + #LOC_BDArmory_AIWindow_DynDampMax = 对准目标阻尼 + #LOC_BDArmory_AIWindow_DynDampMax_Context = 最大阻尼 + #LOC_BDArmory_AIWindow_DynDampMult = 动态阻尼系数 + #LOC_BDArmory_AIWindow_DynDampMult_Context = 阻尼幅度 + + // Auto-tuning + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = 自动调校样本数 + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextLow = <- 不精确 + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples_ContextHigh = 更精确 -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = 自动调校快速响应 + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextLow = <- 更好阻尼 + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance_ContextHigh = 更快响应 -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = 自动调校初始学习率 + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate_Context = 如果 PID 值变化过大,则降低该值 + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance = 自动调校初始滚转率 + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialRollRelevance_Context = 滚转误差对损失的贡献度 + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = 自动调校速度 + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed_Context = 自动调校的目标速度 + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = 自动调校高度 + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude_Context = 尝试保持在 ±此最低高度 + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance = 自动调校重定位距离 + #LOC_BDArmory_AIWindow_PIDAutoTuningRecenteringDistance_Context = 触发重新定位的起始距离 + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = 自动调校固定域 + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = 自动调校最大值约束 + // PID fixed fields + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P = ??? P + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I = ??? I + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D = ??? D + // 3-axis damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch = ??? Dp + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw = ??? Dy + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll = ??? Dr + // Dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OffTarget = ??? DOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OnTarget = ??? DOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_Factor = ??? DF + // 3-axis dynamic damping + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OffTarget = ??? DpOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OnTarget = ??? DpOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_Factor = ??? DpF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OffTarget = ??? DyOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OnTarget = ??? DyOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_Factor = ??? DyF + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OffTarget = ??? DrOff + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OnTarget = ??? DrOn + //#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_Factor = ??? DrF + + // Altitudes + #LOC_BDArmory_AIWindow_DefaultAltitude = 默认高度 + #LOC_BDArmory_AIWindow_DefaultAltitude_Context = 空闲时,AI返回到此高度 + #LOC_BDArmory_AIWindow_MinAltitude = 最小高度 + #LOC_BDArmory_AIWindow_MinAltitude_Context = AI试图保持在此高度以上 + #LOC_BDArmory_AIWindow_MaxAltitude = 最大高度 + #LOC_BDArmory_AIWindow_MaxAltitude_Context = AI试图保持在此高度以下 + //#LOC_BDArmory_AIWindow_BombingAltitude = ??? Bombing Altitude + //#LOC_BDArmory_AIWindow_BombingAltitude_Context = ??? AI tries to bomb at this altitude + //#LOC_BDArmory_AIWindow_DiveBomb = ??? Enable Dive Bombing + + // Speeds + #LOC_BDArmory_AIWindow_MaxSpeed = 最大战斗速度 + #LOC_BDArmory_AIWindow_MaxSpeed_Context = AI将保持在此空速以下 + #LOC_BDArmory_AIWindow_TakeOffSpeed = #LOC_BDArmory_AI_TakeOffSpeed + #LOC_BDArmory_AIWindow_TakeOffSpeed_Context = 起飞时AI开始拉杆速度 + #LOC_BDArmory_AIWindow_MinSpeed = 最小速度 + #LOC_BDArmory_AIWindow_MinSpeed_Context = 如果低于此速度,AI会尝试恢复能量 + #LOC_BDArmory_AIWindow_StrafingSpeed = #LOC_BDArmory_AI_StrafingSpeed + #LOC_BDArmory_AIWindow_StrafingSpeed_Context = 对地攻击速度 + #LOC_BDArmory_AIWindow_IdleSpeed = #LOC_BDArmory_AI_IdleSpeed + #LOC_BDArmory_AIWindow_IdleSpeed_Context = 非作战巡航速度 + #LOC_BDArmory_AIWindow_ABPriority = #LOC_BDArmory_AI_ABPriority + #LOC_BDArmory_AIWindow_ABPriority_Context = 修改启用后燃室阈值 + #LOC_BDArmory_AIWindow_ABOverrideThreshold = 后燃室覆盖 + #LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = 如果速度低于此值且节流阀全开,则强制使用后燃室 + #LOC_BDArmory_AIWindow_BrakingPriority = #LOC_BDArmory_AI_BrakingPriority + #LOC_BDArmory_AIWindow_BrakingPriority_Context = 在允许的情况下,优先使用刹车减速 + + // Control + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = 低速操控受限 + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter_Context = 在低速限值以下操控受限 + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = 受限低速 + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed_Context = 低于此速度时AI操控受限 + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = 高速操控受限 + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter_Context = 在高速限值以上操控受限 + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = 受限高速 + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed_Context = 高于此速度时AI操控受限 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = 高度操控系数 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor_Context = 根据海拔高度减少/增加操控限制系数 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = 高度操控限制 + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude_Context = 开始降低/增加操控受限的高度 + #LOC_BDArmory_AIWindow_BankLimiter = 滚转角度限制 + #LOC_BDArmory_AIWindow_BankLimiter_Context = 最大滚转角 + #LOC_BDArmory_AIWindow_MaxAllowedGForce = 最大G值 + #LOC_BDArmory_AIWindow_MaxAllowedGForce_Context = 操控将尽量不超过 G 力限制 + #LOC_BDArmory_AIWindow_MaxAllowedAoA = 最大攻角 + #LOC_BDArmory_AIWindow_MaxAllowedAoA_Context = 机动将尽量不超过此攻角 + #LOC_BDArmory_AIWindow_WaypointPreRollTime = 航点预滚转时间 + #LOC_BDArmory_AIWindow_WaypointPreRollTime_Context = 在到达航点前开始滚转 + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = 航点偏航时机 + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime_Context = 增加接近航点时的偏航响应 + #LOC_BDArmory_AIWindow_PostStallAoA = 失速后攻角 + #LOC_BDArmory_AIWindow_PostStallAoA_Context = 切换操控模式以应对超出攻角后的失速情况 + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle = 英麦曼回旋角度 + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle_Context = AI会拉杆以瞄准锥体中的目标 + #LOC_BDArmory_AIWindow_ImmelmannPitchUpBias = 英麦曼爬升偏置 + #LOC_BDArmory_AIWindow_ImmelmannPitchUpBias_Context = < 向下 — 当前俯仰率的偏向方向 — 向上 > + + // Evade / Extend + #LOC_BDArmory_AIWindow_Evade = 规避 + #LOC_BDArmory_AIWindow_MinEvasionTime = #LOC_BDArmory_AI_MinEvasionTime + #LOC_BDArmory_AIWindow_MinEvasionTime_Context = AI躲避攻击的最短时间 + #LOC_BDArmory_AIWindow_EvasionThreshold = 距离阈值 + #LOC_BDArmory_AIWindow_EvasionThreshold_Context = 如果在此距离内有来袭火力,则规避 + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = 时间阈值 + #LOC_BDArmory_AIWindow_EvasionTimeThreshold_Context = 触发规避的最小受击时间 + #LOC_BDArmory_AIWindow_EvasionMinRangeThreshold = 最小距离阈值 + #LOC_BDArmory_AIWindow_EvasionMinRangeThreshold_Context = 只有当攻击者在此范围之外时才会规避. + #LOC_BDArmory_AIWindow_EvasionNonlinearity = 非线性规避/远离 + #LOC_BDArmory_AIWindow_EvasionNonlinearity_Context = 在规避/远离时的振荡强度 + + #LOC_BDArmory_AIWindow_Avoidance = 规避载具 + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = 距离阈值 + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold_Context = 在此范围内躲避来袭载具 + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = 规避预测时间 + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod_Context = AI提前多少秒检测碰撞 + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = 反应强度 + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength_Context = AI摆脱来袭飞船的强度 + #LOC_BDArmory_AIWindow_StandoffDistance = 安全距离 + #LOC_BDArmory_AIWindow_StandoffDistance_Context = AI尝试接近目标的距离 + + #LOC_BDArmory_AIWindow_Extend = 远离 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = 空对空远离距离 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = 空对空距离远离 + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = 空对空远离角度 + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = 远离时所期望爬升角度 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = 空对地远离距离(枪炮) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = 空对空距离远离(枪炮) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = 空对地远离距离 + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = 空对地距离远离 + #LOC_BDArmory_AIWindow_ExtendTargetVel = 远离目标速度系数 + #LOC_BDArmory_AIWindow_ExtendTargetVel_Context = 目标太慢而无法指向时设置 + #LOC_BDArmory_AIWindow_ExtendTargetAngle = 远离目标角度系数 + #LOC_BDArmory_AIWindow_ExtendTargetAngle_Context = 目标超出转弯半径时设置 + #LOC_BDArmory_AIWindow_ExtendTargetDist = 远离目标距离 + #LOC_BDArmory_AIWindow_ExtendTargetDist_Context = 目标太近而无法转向时设置 + #LOC_BDArmory_AIWindow_ExtendAbortTime = 远离取消时间 + #LOC_BDArmory_AIWindow_ExtendAbortTime_Context = 远离时未能获得距离的中止时间. + //#LOC_BDArmory_AIWindow_ExtendMinGainRate = ??? Min Gain Rate + //#LOC_BDArmory_AIWindow_ExtendMinGainRate_Context = ??? Minimum rate to be gaining distance for the abort timer. + + // Terrain + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin = 地形规避最小值 + #LOC_BDArmory_AIWindow_TerrainAvoidanceMin_Context = 理想载具方向的转弯半径倍数 + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax = 地形规避最大值 + #LOC_BDArmory_AIWindow_TerrainAvoidanceMax_Context = 倒置载具方向的转弯半径倍数 + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle = 倒置规避临界角 + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle_Context = 倒置地形规避或首先滚转的临界角度 + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime = 反应时间 + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime_Context = 设置最佳转向所需的时间估计 + #LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown = 规避后冷却时间 + #LOC_BDArmory_AIWindow_TerrainAvoidancePostAvoidanceCoolDown_Context = 避开地形后再开始机动的时间 + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = 航点地形规避 + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance_Context = 航点地形校正的范围和强度 + + // Ramming + #LOC_BDArmory_AIWindow_ControlSurfaceLag = 控制面滞后 + #LOC_BDArmory_AIWindow_ControlSurfaceLag_Context = 自杀式撞击中控制面滞后的轨迹校正 + // Combat + // Up-to-eleven + // Idle / Orbit Behavior + #LOC_BDArmory_AIWindow_Orbit_Context = 非战斗巡航的方向 + #LOC_BDArmory_AIWindow_Standby_Context = 目标进入警戒范围时AI启动 + + // Surface / VTOL / Orbital + #LOC_BDArmory_AIWindow_VehicleType = #LOC_BDArmory_AI_VehicleType + #LOC_BDArmory_AIWindow_VehicleType_Context = 载具于此种地形上运转 + #LOC_BDArmory_AIWindow_MaxSlopeAngle = #LOC_BDArmory_AI_MaxSlopeAngle + #LOC_BDArmory_AIWindow_MaxSlopeAngle_Context = AI会尝试驶上的最大地形坡度角 + #LOC_BDArmory_AIWindow_CruiseSpeed = #LOC_BDArmory_AI_CruiseSpeed + #LOC_BDArmory_AIWindow_CruiseSpeed_Context = #LOC_BDArmory_AIWindow_IdleSpeed_Context + #LOC_BDArmory_AIWindow_CombatSpeed = #LOC_BDArmory_AI_CombatSpeed + #LOC_BDArmory_AIWindow_CombatSpeed_Context = 未进行能量机动时的目标速度 + #LOC_BDArmory_AIWindow_CombatAltitude = #LOC_BDArmory_AI_CombatAltitude + #LOC_BDArmory_AIWindow_CombatAltitude_Context = 默认巡航高度/深度 + #LOC_BDArmory_AIWindow_TargetPitch = #LOC_BDArmory_AI_TargetPitch + #LOC_BDArmory_AIWindow_TargetPitch_Context = 期望载具俯仰角 + #LOC_BDArmory_AIWindow_MaxDrift = #LOC_BDArmory_AI_MaxDrift + #LOC_BDArmory_AIWindow_MaxDrift_Context = 载具在拐弯时最大偏离前进方向的角度限制 + #LOC_BDArmory_AIWindow_MaxPitchAngle = #LOC_BDArmory_AI_MaxPitchAngle + #LOC_BDArmory_AIWindow_MaxPitchAngle_Context = 移动时的最大俯仰角 + #LOC_BDArmory_AIWindow_BankAngle = #LOC_BDArmory_AI_BankAngle + #LOC_BDArmory_AIWindow_BankAngle_Context = #LOC_BDArmory_AIWindow_BankLimiter_Context + #LOC_BDArmory_AIWindow_WeaveFactor = #LOC_BDArmory_AI_WeaveFactor + #LOC_BDArmory_AIWindow_WeaveFactor_Context = 受攻击时穿插机动强度 + #LOC_BDArmory_AIWindow_MaxBankAngle = #LOC_BDArmory_AI_MaxBankAngle + #LOC_BDArmory_AIWindow_MaxBankAngle_Context = 转弯时的最大滚转角度 + #LOC_BDArmory_AIWindow_BroadsideAttack = #LOC_BDArmory_AI_BroadsideAttack + #LOC_BDArmory_AIWindow_BroadsideAttack_Context = 攻击时指向目标的方向 + #LOC_BDArmory_AIWindow_MinEngagementRange = #LOC_BDArmory_AI_MinEngagementRange + #LOC_BDArmory_AIWindow_MinEngagementRange_Context = AI与目标交战的最小范围 + #LOC_BDArmory_AIWindow_MaxEngagementRange = #LOC_BDArmory_AI_MaxEngagementRange + #LOC_BDArmory_AIWindow_MaxEngagementRange_Context = AI与目标交战的最大范围 + #LOC_BDArmory_AIWindow_ForceFiringRange = #LOC_BDArmory_AI_ForceFiringRange + //#LOC_BDArmory_AIWindow_ForceFiringRange_Context = ??? Within this range AI always fires without throttle + #LOC_BDArmory_AIWindow_MaintainEngagementRange = #LOC_BDArmory_AI_MaintainEngagementRange + #LOC_BDArmory_AIWindow_MaintainEngagementRange_Context = 载具在最小范围内会刹车/倒车 + #LOC_BDArmory_AIWindow_ManeuverRCS = #LOC_BDArmory_AI_ManeuverRCS + #LOC_BDArmory_AIWindow_ManeuverRCS_Context = RCS 使用状况 + #LOC_BDArmory_AIWindow_FiringRCS = #LOC_BDArmory_AI_FiringRCS + #LOC_BDArmory_AIWindow_FiringRCS_Context = 射击时使用RCS来控制速度 + #LOC_BDArmory_AIWindow_ReverseEngines = #LOC_BDArmory_AI_ReverseEngines + #LOC_BDArmory_AIWindow_ReverseEngines_Context = 使用反向引擎产生反向推力 + #LOC_BDArmory_AIWindow_EngineRCSRotation = #LOC_BDArmory_AI_EngineRCSRotation + //#LOC_BDArmory_AIWindow_EngineRCSRotation_Context = ??? Use engines perpendicular to thrust axis for RCS rotation + #LOC_BDArmory_AIWindow_EngineRCSTranslation = #LOC_BDArmory_AI_EngineRCSTranslation + //#LOC_BDArmory_AIWindow_EngineRCSTranslation_Context = ??? Use engines perpendicular to thrust axis for RCS translation + #LOC_BDArmory_AIWindow_OrbitalPIDActive = #LOC_BDArmory_AI_OrbitalPIDActive + #LOC_BDArmory_AIWindow_OrbitalPIDActive_Context = PID激活条件 + #LOC_BDArmory_AIWindow_RollMode = #LOC_BDArmory_AI_RollMode + #LOC_BDArmory_AIWindow_RollMode_Context = 当PID激活时,AI将使飞船向目标展示这一侧 + #LOC_BDArmory_AIWindow_MinObstacleMass = #LOC_BDArmory_AI_MinObstacleMass + #LOC_BDArmory_AIWindow_MinObstacleMass_Context = 触发规避的障碍物的最小质量 + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection = #LOC_BDArmory_AI_PreferredBroadsideDirection + #LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context = 应该将载具的哪一侧朝向目标 + #LOC_BDArmory_AIWindow_ManeuverSpeed = 机动速度 + #LOC_BDArmory_AIWindow_ManeuverSpeed_Context = 机动时相对目标的最大速度 + #LOC_BDArmory_AIWindow_minFiringSpeed = #LOC_BDArmory_AI_FiringSpeedMin + #LOC_BDArmory_AIWindow_minFiringSpeed_Context = 射击时相对于目标的最低速度 + #LOC_BDArmory_AIWindow_FiringSpeed = 扫射速度 + #LOC_BDArmory_AIWindow_FiringSpeed_Context = 开火时相对于目标的最大速度 + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit = #LOC_BDArmory_AI_AngularSpeedLimit + #LOC_BDArmory_AIWindow_FiringAngularVelocityLimit_Context = 射击时相对于目标的最大角速度 + #LOC_BDArmory_AIWindow_EvasionErraticness = RCS 规避不规则性 + #LOC_BDArmory_AIWindow_EvasionErraticness_Context = 躲避方向的变化幅度 + #LOC_BDArmory_AIWindow_EvasionRCS = #LOC_BDArmory_AI_EvasionRCS + #LOC_BDArmory_AIWindow_EvasionRCS_Context = 使用RCS规避来袭火力 + #LOC_BDArmory_AIWindow_EvasionEngines = #LOC_BDArmory_AI_EvasionEngines + #LOC_BDArmory_AIWindow_EvasionEngines_Context = 利用引擎推力来规避来袭火力 + + + // AI infolink + // Pilot AI + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp = PID (比例积分微分) - 控制器计算预期输出与实际输出之间的差值,然后根据 P、I 和 D 值进行修正。这用于调整载具的方向。调整时通常需要调整所有三个值,首先是操控倍数,然后根据需要调整 Ki 和阻尼。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_SteerPower = Steer Power (P) - 这是控制输入。太低,载具将无法充分发挥其控制能力。过高,载具使用的功率将超过需要,并偏离所需的方向。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_SteerKi = Steer Ki (I) - 这是用于修正 P 和 D 累积误差的误差修正。过多,则会超调。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_Steerdamp = Steer Damp (D) - 这是导数值;这是飞船旋转到新方向时的阻尼值。过低,飞船会因过度转向正确方向而摆动。过高,阻尼将抵消过多的方向变化,飞船将无法快速转向。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_Dyndamp = 动态阻尼 - 根据与目标的角度,从最小阻尼值到最大阻尼值动态调整阻尼。阻尼值越小,动态阻尼值与目标角度变化的线性关系越好;阻尼值越大,当指向远离目标时,阻尼将减小,而当指向靠近目标时,阻尼将增大。这适用于所有三个控制轴。对于单个控制轴阻尼,请启用相关的俯仰/横滚/偏航动态阻尼。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_AutoTune = PID 自动调校 - 启用自动 PID 调整模式,AI将使用梯度下降来优化载具转向一系列航向的能力,并在这些方向上保持稳定。 + #LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_AutoTune_details = \n + + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp = 高度设置可控制人工智能所需的飞行包线 + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_Def = 默认高度 - 这是人工智能在不进行战斗或远离时将返回的高度。 + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_min = 最小高度 - 设置载具下降到什么高度,人工智能会开始爬升;确保为载具留出足够的空间拉起。 + #LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_max = 最大高度 - 如果启用,它是最小高度的倒数;它设定了人工智能开始下降前载具必须超过的高度。 + //#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_bombing = ??? Bombing Altitude - The AI will try to maintain level flight at this altitude when performing a bombing run (doesn't apply to torpedoes). + + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp = 速度设置可控制载具在各种飞行和作战条件下所需的空速 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_min = 最小速度和最大速度 - 最大速度是人工智能在战斗或撞击时试图达到但不超过的空速。如果高于此速度,AI 将刹车直到低于最大速度。最小速度是 AI 执行战斗机动的最低速度,无论目标速度如何。如果低于此速度,AI 将中断并远离以加速,直到高于最小速度。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_takeoff = 起飞速度 - 如果在启动AI时载具降落,起飞速度是载具必须达到的速度,然后AI才会开始拉起以起飞。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_idle = 怠速 - 这设置了AI在盘旋或飞行到位置时将维持的非战斗巡航空速。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_gnd = 扫射速度 - 这是AI在攻击地面目标时将使用的空速。如果地面目标在移动,扫射速度将加上地面目标的速度。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABpriority = 后燃室优先级 - 这控制了AI在什么加速度水平下会开启/关闭后燃室。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABOverrideThreshold = 后燃室覆盖阈值 - 在低于此速度阈值时,如果油门处于最大位置,AI将开启后燃室。 + #LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_BrakingPriority = 刹车优先级 - 这一设置控制了在允许的情况下,AI使用刹车的积极性。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp = 控制限制在不同条件下设置载具控制权限的限制 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_limiters = 操控限制器 - 操控限制器限制了载具的控制权限。低速限制设置了速度低于低速限制速度时的AI控制权限。高速限制设置了当速度高于高速限制速度时的AI控制权限。当速度介于低速度限制和高速度限制之间时,限制器的值会从低限制值线性变化到高限制值。高度操控限制器根据高度超过限制高度来缩放操控限制,公式为(高度/限制高度)^系数。 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_bank = 最大翻滚角 - 这设置了允许的最大翻滚角度。当小于180时,AI在机动过程中不会翻滚超过这么多度的水平线。 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_clamps = 最大允许G值和攻角 - 最大允许G值限制AI不得执行产生大于设定G值的机动。最大允许攻角限制了AI可使用的最大攻角. + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_modeSwitches = 失速后攻角模式切换 - 这控制了载具因超过攻角阈值而切换操控模式的时间。欲指向位于载具正后方的英麦曼回旋角度内的目标时,载具将简单地仰角而不是滚转。 + #LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_Immelmann = 英麦曼回旋角度与偏向 - 当飞行目标位于飞行器直接后方的英麦曼转向角度内时,飞行器将直接向上或向下俯仰,而不是滚转。当飞行高度不接近最小值时,英麦曼爬升偏置将在当前俯仰速率(度/秒)在限制范围内时强制向上或向下俯仰,否则将使用当前的俯仰方向。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp = 规避/远离 - 控制了AI如何对待来自外部威胁,无论是火力、导弹还是其他载具,以及AI如何根据其他载具的相对位置做出响应。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Evade = 最小规避时间,距离阈值,时间阈值,最小范围阈值,不规避我的目标 - 这四个设置控制了AI何时会进行规避。最小规避时间设置了AI进行规避机动的持续时间(以秒为单位)。距离阈值设置了触发规避所需的敌方火力离AI多近。时间阈值设置了AI在受到火力攻击多长时间后开始规避。最小射程阈值设置了攻击者必须超过的射程以触发规避。不规避我的目标切换确定是否忽略来自当前目标的火力攻击,用于规避目的。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Nonlinearity = 非线性规避/远离 - 这控制了在规避或远离时,载具围绕飞往方向产生的摆动半径(以度为单位)。这有助于载具在规避或远离时不以直线飞行。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Dodge = 载具规避 - 这三个设置决定了AI如何应对潜在的碰撞情况。如果预测到另一艘船只将在下一个预测期内进入规避阈值范围内,AI将尝试规避它。规避强度确定了AI将多快尝试改变方向以避免预测的碰撞。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_standoff = 安全距离 - AI在战斗中允许接近目标载具的最近距离。如果距离目标小于安全距离,AI将刹车以增加与目标的距离。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Extend = 远离距离 - 这些设置控制AI在不同类型目标下的远离距离。远离角度控制了AI在对抗空中目标时是否应尝试提升或下降高度。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendVars = 目标速度系数、目标角度、目标距离 - 这三个设置共同控制了AI何时会执行远离操作。要执行远离,AI会在其前方投射一个检测锥体,检查与目标的距离和相对速度。默认情况下,如果目标在AI前方的78度锥体之外,距离小于400米,并且速度较慢,AI将执行远离操作。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendVel = 远离目标速度系数 - 这告诉AI在什么相对速度下应考虑执行远离操作。小于1表示目标载具必须更慢,大于1表示必须更快才会触发远离操作。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendAngle = 远离目标角度 - 这个设置确定了检测锥体的宽度,可以看作是视野范围和有效转弯半径的组合。载具的转弯半径越好,这个值就可以设置得越高。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendDist = 远离目标距离 - 这个设置确定了目标距离AI需要多近才会执行远离操作以获得更好的目标角度。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendAbortTime = 远离中止时间 - 这告诉AI如果在规定时间内没有取得任何进展,就应中止远离操作。之后,远离将进入5秒的冷却期。 + #LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendToggle = 开关远离 - 这个设置用于启用或禁用对空中目标的远离操作(对地面目标的远离操作不受影响)。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_TerrainHelp = 地形规避 - 用于预测与地面的碰撞,通过调整载具的转弯半径来生成地形碰撞距离,以告诉AI是否需要拉升。地形规避最小值基于最佳飞行条件,载具与地面平行且只需要上升。地形规避最大值基于最坏情况,载具倒置到地面,需要旋转180度才能上升。倒置地形规避的临界角度确定了载具相对于地形法线的倾斜角度,在这个角度下,载具将尝试在倒置状态下规避地形或首先进行旋转。载具反应时间是估计载具平均需要多长时间才能进入紧急规避地形的最佳配置。航点地形规避影响了由于地形位于载具和当前航点之间,对目标方向进行调整的范围和强度。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_RamHelp = 自杀式撞击 - 用于控制载具是否应在弹药耗尽或武器失效时尝试撞向其他载具。如果未启用自杀式撞击,AI将继续进行机动,但无法进行攻击。控制面滞后(Control Surface Lag)设置了AI在多长时间内应该根据控制面达到最大偏转的时间来进行碰撞预测的修正。 + + #LOC_BDArmory_AIWindow_infolink_Pilot_miscHelp = 杂项设置 - 这些设置控制了与非战斗行为相关的行为,这些行为不属于其他分类。 + #LOC_BDArmory_AIWindow_infolink_Pilot_orbitHelp = 盘旋方向 - 这是AI在空闲时盘旋的方向,可以是顺时针(Clockwise)或逆时针(Counterclockwise)方向。 + #LOC_BDArmory_AIWindow_infolink_Pilot_standbyHelp = 切换就绪 - 如果启用,当目标进入其警戒范围时,AI将自动开启。 + + // Surface AI + #LOC_BDArmory_AIWindow_infolink_Surface_Type = 载具类型 - 这告诉AI载具是陆地车辆、船只还是既能在陆地上操作又能在水域操作的两栖载具。 + #LOC_BDArmory_AIWindow_infolink_Surface_Slopes = 坡度角,目标俯仰角 - 这些设置为载具设置了角度限制。 坡度角设置了AI将尝试驶上的最大坡度。 目标俯仰角设置了载具应尽力维持的期望姿态(相对于地面的俯仰角度)。 + #LOC_BDArmory_AIWindow_infolink_Surface_Speeds = 巡航速度和最大速度 - 设置了载具的速度。 巡航速度是AI在非战斗状态下维持的速度。 最大速度是AI在战斗状态下尝试达到的速度。 + #LOC_BDArmory_AIWindow_infolink_Surface_Drift = 最大偏移量 - 这设置了在转弯时载具会偏离前进方向的最大值。 + #LOC_BDArmory_AIWindow_infolink_Surface_Bank = 滚转角 - 这告诉AI在机动时载具应允许的最大滚转或倾斜角度。 + #LOC_BDArmory_AIWindow_infolink_Surface_Weave = 穿插系数 - 当受到攻击或导弹来袭时,飞船会尝试穿插机动以摆脱攻击者瞄准。这控制了穿插的强度。 + #LOC_BDArmory_AIWindow_infolink_Surface_SteerPower = 操控倍数 - 这是Surface AI PID控制器的比例输入。 如果设置得太低,载具将使用不到其控制权的全部范围。 如果设置得太高,载具将施加超过所需的控制,从而超出所期望的方向 + #LOC_BDArmory_AIWindow_infolink_Surface_SteerDamping = 操控阻尼 - 这是表面载具 AI PID控制器的导数输入。它控制了载具将旋转到新方向时的阻尼程度。如果设置得太低,载具由于超过正确方向而产生振荡。如果设置得太高,阻尼将抵消太多的方向变化,导致载具转弯速度减慢。 + #LOC_BDArmory_AIWindow_infolink_Surface_Orientation = 攻击方向,首选舷侧方向 - 这些设置了AI如何接近和攻击目标。攻击向量设置告诉AI是让载具的船头指向目标,还是让载具的舷侧指向目标。侧舷方向告诉AI是否应该优先将左舷或右舷对准目标。 + #LOC_BDArmory_AIWindow_infolink_Surface_Engagement = 最小、最大交战距离 - 这些设置了AI会尝试进行机动以便接近目标的最小和最大距离范围,以便进行交战。 + #LOC_BDArmory_AIWindow_infolink_Surface_RCS = RCS - 设置切换了AI是否应该使用RCS推进器来辅助机动。 + #LOC_BDArmory_AIWindow_infolink_Surface_AvoidMass = 避让质量 - AI会避免与其碰撞而不是撞击的障碍物最低质量。 + #LOC_BDArmory_AIWindow_infolink_Surface_MaintainMinRange = 维持最小距离 - 这切换了陆地车辆是否会停下来或倒车以保持与目标的距离,而不是偏离目标进行环绕或远离。 + #LOC_BDArmory_AIWindow_infolink_Surface_Altitude = 战斗高度 - 潜艇巡航/交战的深度。 + + // VTOL AI + #LOC_BDArmory_AIWindow_infolink_VTOL_PID = PID(比例-积分-微分)控制器计算所需输出与实际输出之间的差异,并根据P、I、D值进行修正,以此来控制飞行器的方向。\n - 控制力度(P)- 这是控制输入。如果太低,飞行器将无法使用其全部控制权范围。如果太高,飞行器将过度执行,超出所需方向。\n - 积分修正(I)- 这是用于修正由P和D导致的累积误差的错误校正。如果太少,飞行器将持续未达到预期方向。如果太多,将会超过预期方向。\n - 阻尼(D)- 这是导数值;这决定了飞行器转向新方向时的阻尼程度。如果太低,飞行器将因超调正确方向而振荡。如果太高,阻尼将过度抵消方向变化,飞行器转向速度将减慢。 + #LOC_BDArmory_AIWindow_infolink_VTOL_Altitudes = 默认飞行高度 - 非战斗状态下的巡航高度(离地高度)。\n最小与最大飞行高度 - AI尝试飞行到的最低和最高高度。超出这个范围时,AI将爬升或俯冲以返回到这个高度范围内。 + #LOC_BDArmory_AIWindow_infolink_VTOL_Speeds = 最大与战斗速度 - 用于机动的最高速度和战斗中的目标速度。 + #LOC_BDArmory_AIWindow_infolink_VTOL_Control = 最大俯仰和滚转角度 - 指示AI在机动时允许船只俯仰或滚转的最大角度。\n侧翼方向 - 侧翼方向告诉AI在面向目标时应优先展示左舷还是右舷。\nRCS - 是否在机动时使用RCS推进器,或仅在战斗中使用。 + #LOC_BDArmory_AIWindow_infolink_VTOL_Combat = 穿插系数 - 当受到火力或导弹袭击时,飞行器会尝试穿插飞行以干扰攻击者的瞄准。这控制了穿插动作的强度。\n最小、最大交战范围 - 设置AI试图机动以进入的目标最小和最大距离,以便进行交战。 + + // Orbital AI + #LOC_BDArmory_AIWindow_infolink_Orbital_PID = PID(比例-积分-微分)控制器通过计算期望输出与实际输出之间的差异,并根据P(比例)、I(积分)、D(微分)值进行修正。在KSP中,除非启用了PID控制,否则SAS(稳定辅助系统)在瞄准和射击武器或进行所有操作时不会控制飞船。/n转向力度(P):控制输入值,过低则飞船无法充分利用控制能力,过高则飞船会过度反应。/n转向修正(I):对P和D累积误差进行修正,过少则飞船持续低于期望方向,过多则飞船会过度修正。/n转向阻尼(D):飞船转向新方向时的阻尼程度,过低会导致飞船因过度修正而振荡,过高则会减缓飞船转向速度。/n最大误差:设定PID控制器使用的最大角度误差限制,降低可减少过度修正,但响应会变慢;提高可加快响应速度,但可能增加过度修正,除非PID调节得当。适当调节PID可帮助限制过度修正或提高响应速度。 + #LOC_BDArmory_AIWindow_infolink_Orbital_Combat = 最小交战范围 - 设置AI尝试操纵时与目标交战的最小距离。 + #LOC_BDArmory_AIWindow_infolink_Orbital_Speeds = 机动和扫射速度 - 在战斗射程外和战斗射程内加速到相对于目标的最大速度。 + #LOC_BDArmory_AIWindow_infolink_Orbital_Control = RCS - 是在机动时使用 RCS 推进器,还是仅在战斗时使用。 + #LOC_BDArmory_AIWindow_infolink_Orbital_Evasion = 最小规避时间、距离阈值、时间阈值、最小范围阈值 - 这四个设置控制AI何时进行规避。最小规避时间设置AI执行规避动作的秒数。距离阈值设置来袭火力需达到多近才触发规避。时间阈值设置AI受到攻击多长时间后开始规避。最小范围阈值设置攻击者必须超出的范围以触发规避。\n"不规避我的目标"开关决定是否忽略当前目标的火力,以用于规避目的。 + + // Missile Config + #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_UseStaticMaxLaunchRange = 动态/静态最大范围 + #LOC_BDArmory_ProximityTriggerDistance = 弹头引爆距离 + #LOC_BDArmory_clustermissileTriggerDistance = 子弹药发射距离 + #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_Missile_CruiseSpeed = 巡航速度 + #LOC_BDArmory_CruiseAltitude = 巡航高度 + #LOC_BDArmory_CruisePredictionTime = 巡航预测时间 + //#LOC_BDArmory_CruisePopup = ??? Cruise Popup Attack + #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_DecoupleSpeed = 分离速度 + #LOC_BDArmory_LoftMaxAltitude = 高抛弹道最大高度 + #LOC_BDArmory_LoftRangeOverride = 高抛弹道距离覆盖 + #LOC_BDArmory_LoftAltitudeAdvMax = 高抛弹道最大高度高级设置 + #LOC_BDArmory_LoftMinAltitude = 高抛弹道最低高度 + #LOC_BDArmory_LoftAngle = 高抛弹道角度 + #LOC_BDArmory_LoftTermAngle = 高抛弹道终端角度 + #LOC_BDArmory_LoftRangeFac = 高抛弹道范围系数 + #LOC_BDArmory_LoftVelComp = 速度补偿 + #LOC_BDArmory_LoftVertVelComp = 垂直速度补偿 + #LOC_BDArmory_LoftAltComp = 高度补偿 + #LOC_BDArmory_terminalHomingRange = 终端制导距离 + + #LOC_BDArmory_EMPBlastRadius = EMP 范围 + #LOC_BDArmory_OrdnanceAvailable = 弹药可用 + #LOC_BDArmory_MissileAssign = 导弹分配 + #LOC_BDArmory_CurrentLocks = 当前锁定 + #LOC_BDArmory_Offset = 弹药偏移 + //#LOC_BDArmory_Deploy_Time = ??? Deploy Time + + // Safety Systems + #LOC_BDArmory_SSTank = 自封油箱 + #LOC_BDArmory_SSTank_On = 增加自封油箱 + #LOC_BDArmory_SSTank_Off = 移除自封油箱 + #LOC_BDArmory_CASE = C.A.S.E. 级别 + #LOC_BDArmory_CASE_Sim = 爆炸模拟 + #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_AddedCost = 增加花费 + #LOC_BDArmory_AddedMass = 安全系统质量 + #LOC_BDArmory_DryMass = 干重 + + // Turret Config + #LOC_BDArmory_MaxPitch = 最大俯仰角 + #LOC_BDArmory_MinPitch = 最小俯仰角 + #LOC_BDArmory_YawRange = 旋转范围 + //#LOC_BDArmory_YawStandbyAngle = ??? Yaw Standby Angle + #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_AimOverrideFalse = ??? Aim With This Weapon + //#LOC_BDArmory_AimOverrideTrue = ??? Revert Default Aim + #LOC_BDArmory_ReturnTurret = 炮塔复位 + #LOC_BDArmory_ToggleAnimation = 开关动画 + + // Missile UI + #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_TurretLoft = ??? Lofted Aimpoint + //#LOC_BDArmory_TurretLoftFac = ??? Loft Velocity Factor + #LOC_BDArmory_MissileTurretFireFOV = 开火视野 + #LOC_BDArmory_HideUI = 隐藏武器名称界面 + #LOC_BDArmory_ShowUI = 设置武器名称界面 + #LOC_BDArmory_HideWeaponGroupUI = 隐藏武器组界面 + #LOC_BDArmory_SetWeaponGroupUI = 设置武器组界面 + #LOC_BDArmory_Fire = 开火 + #LOC_BDArmory_ToggleRadar = 开关雷达 + #LOC_BDArmory_ToggleIRST = 开关 IRST + #LOC_BDArmory_DynamicRadar = 禁用雷达对抗反辐射武器 + + // WM Config + #LOC_BDArmory_GuardMode = 警戒模式:\u0020 + #LOC_BDArmory_Team = 小队 + //#LOC_BDArmory_Allies = ??? Allies + #LOC_BDArmory_Weapon = 武器 + #LOC_BDArmory_FiringPriority = 目标优先级 + //#LOC_BDArmory_weaponChannel = ??? Weapon Channel + #LOC_BDArmory_FiringInterval = 射击间隔 + #LOC_BDArmory_FiringBurstLength = 点射长度(时间) + #LOC_BDArmory_FiringBurstCount = 点射长度(数量) + #LOC_BDArmory_FiringTolerance = 射击角度 + #LOC_BDArmory_FieldOfView = 视场范围 + #LOC_BDArmory_VisualRange = 可视距离 + #LOC_BDArmory_GunsRange = 枪炮距离 + #LOC_BDArmory_MissilesRange = 使用动态发射区间 + #LOC_BDArmory_MissilesOnTarget = 导弹/目标 + #LOC_BDArmory_FireAngleOverride_Enable = 启用射击角度覆盖 + #LOC_BDArmory_FireAngleOverride_Disable = 禁用射击角度覆盖 + #LOC_BDArmory_BurstLengthOverride_Enable = 启用点射长度覆盖 + #LOC_BDArmory_BurstLengthOverride_Disable = 禁用点射长度覆盖 + #LOC_BDArmory_FiringAngle = 开火角度 + + #LOC_BDArmory_dynamic = 动态 + #LOC_BDArmory_static = 静态 + + #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_Multiple = 复合 + #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 = 聚能装药 + #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 = AHEAD弹药 + #LOC_BDArmory_NextTankSetup = 下一配置 + #LOC_BDArmory_PreviousTankSetup = 上一配置 + + // Team Icons + #LOC_BDArmory_Icons_title = BDArmory 团队图标 + #LOC_BDArmory_Icons_PSA = 按 F4 开关游戏内置载具图标 + #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_missile_text = 显示导弹图标 + #LOC_BDArmory_Icon_debris = 显示残骸图标 + #LOC_BDArmory_Icon_persist = 按下 F2 时不与用户界面一起隐藏 + #LOC_BDArmory_Icon_threats = 显示威胁指示器 + #LOC_BDArmory_Icon_pointers = 图标在视野外时显示指针代替 + #LOC_BDArmory_Icon_scale = 图标缩放比例: + #LOC_BDArmory_Icon_opacity = 图标不透明度: + #LOC_BDArmory_Icon_distance_threshold = 显示距离阈值: + #LOC_BDArmory_Icon_max_distance_threshold = 最大显示距离阈值: + #LOC_BDArmory_Icon_telemetry = 启用遥测数据; + //#LOC_BDArmory_Icon_StoreTeamColors = ??? Store Team Colors + #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_Titanium = 钛制 + #LOC_BDArmory_Composites = 复合材料 + #LOC_BDArmory_RAMFoam = 吸波涂层 + #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_ArmorLiftStacking = 升力堆叠 + #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_partTreeVisualizer = 开关零件关系树 + //#LOC_BDArmory_checkVessel = ??? Check Vessel Legality + #LOC_BDArmory_ArmorSelect = 选择装甲材质 + #LOC_BDArmory_DryMassWhitelist = 计入干质量的资源 + #LOC_BDArmory_ArmorTool = BDA 装甲工具 + #LOC_BDArmory_Armor_HullMat = 当前装甲材质 + #LOC_BDArmory_Armor_ArmorType = 装甲类型 + #LOC_BDArmory_Armor_Hullmass = 调整部件重量 + #LOC_BDArmory_BulletResist = 动能抗性. + #LOC_BDArmory_ExplosionResist = 爆炸抗性. + #LOC_BDArmory_LaserResist = 激光抗性. + #LOC_BDArmory_ArmorShatterWarning = 击穿将击碎护甲 + //#LOC_BDArmory_ArmorToolPartCount = ??? Part Count Exceeded! + //#LOC_BDArmory_ArmorToolEngineCount = ??? Too Many Engines: + //#LOC_BDArmory_ArmorToolEngineCountFloor = ??? Too Few Engines: + //#LOC_BDArmory_ArmorToolTWR = ??? TWR Exceeded: + //#LOC_BDArmory_ArmorToolLTW = ??? LTW Exceeded: + //#LOC_BDArmory_ArmorToolMaxMass = ??? Mass Limit Exceeded: + //#LOC_BDArmory_ArmorToolMaxPoints = ??? Point Limit Exceeded: + //#LOC_BDArmory_ArmorToolIllegalParts = ??? Illegal Parts: + //#LOC_BDArmory_ArmorToolNonCockpit = ??? not attached to cockpit + //#LOC_BDArmory_ArmorToolOversizedPWings = ??? pWings exceeding max Lift - check Lift Visualizer + //#LOC_BDArmory_ArmorToolVesselLegal = ??? Vessel legal! + + + // Missile & CM Settings + #LOC_BDArmory_Settings_MissileCMToggle = 显示导弹&反制措施设置 + #LOC_BDArmory_Settings_AspectedRCS = 实时 RCS + #LOC_BDArmory_Settings_AspectedRCSOverallRCSWeight = 整体 RCS 权重 + #LOC_BDArmory_Settings_AspectedIRSeekers = 红外遮蔽影响导弹 + #LOC_BDArmory_Settings_FlareFactor = 热诱弹最大热量倍数 + #LOC_BDArmory_Settings_ChaffFactor = 箔条位置诱导性倍数 + #LOC_BDArmory_Settings_SmokeDeflectionFactor = 烟雾位置诱导性倍数 + #LOC_BDArmory_Settings_APSThreshold = 触发APS最低口径 + + // Texture switching + #LOC_BDArmory_NextTexture = 下一贴图 } } 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..fcb30ec4f --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-de-de.cfg @@ -0,0 +1,373 @@ +// Notes: +// - The "_tags" entries are simply common search terms for the part filter in the SPH/VAB. Instead of being the same in all languages, they should be whatever a user in that language would search for. + +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 + + //#loc_BDArmory_agent2_title = ??? Twin Crown Aerospace Industries + + #loc_BDArmory_part_bahaGatlingGun_title = Vulkan-Geschützturm + #loc_BDArmory_part_bahaGatlingGun_description = Eine 6-läufige, drehbare 20x102mm Vulkan-Kanone. + //#loc_BDArmory_part_bahaGatlingGun_tags = ??? BDA gun weap turret 20mm gatling + + #loc_BDArmory_part_bahaTurret_title = .50cal Maschinengewehrgeschützturm + #loc_BDArmory_part_bahaTurret_description = Ein zweiläufiger .50cal Maschinengewehrgeschützturm. + //#loc_BDArmory_part_bahaTurret_tags = ??? BDA gun turret .50cal weap + + #loc_BDArmory_part_bahaABL_title = USAF Laser + #loc_BDArmory_part_bahaABL_description = Ein Hochleistungslaser, der Ziele in Brand setzt. Verbraucht 350 Elektrizitätseinheiten pro Sekunde. + //#loc_BDArmory_part_bahaABL_tags = ??? BDA laser turret beam anti weap + + #loc_BDArmory_part_bahaAdjustableRail_title = Justierbare Raketenschiene + #loc_BDArmory_part_bahaAdjustableRail_description = Aufhängungspunkt für Raketen und Bomben. Abstand und Länge einstellbar. + //#loc_BDArmory_part_bahaAdjustableRail_tags = ??? BDA rail hardpoint missile mount + + #loc_BDArmory_part_bahaAgm86B_title = AGM-86C Marschflugkörper + #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. + //#loc_BDArmory_part_bahaAgm86B_tags = ??? BDA missile GPS cruise guided weap + + #loc_BDArmory_part_bahaAim120_title = AIM-120 AMRAAM Luft-Luft Rakete + #loc_BDArmory_part_bahaAim120_description = Radargeführte Luft-Luft Rakete mittlerer Reichweite. + //#loc_BDArmory_part_bahaAim120_tags = ??? BDA missile radar homing a2a ata ordnance weap + + #loc_BDArmory_part_bahaEMP120_title = AIM-120 AMRAAM Luft-Luft Rakete (EMP) + #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. + //#loc_BDArmory_part_bahaEMP120_tags = ??? BDA missile radar homing a2a ata emp ordnance weap + + #loc_BDArmory_part_bdPilotAI_title = KI Autopilot (für Flugzeuge) + #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) + //#loc_BDArmory_part_bdpilotAI_tags = ??? BDA pilot ai control + + #loc_BDArmory_part_bdDriverAI_title = KI Autopilot (Schiffe und Fahrzeuge) + #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) + //#loc_BDArmory_part_bdDriverAI_tags = ??? BDA driver ai control vee vehicle ground + + //#loc_BDArmory_part_bdVTOLAI_title = ??? AI Vertical Takeoff and Landing Pilot + //#loc_BDArmory_part_bdVTOLAI_desc = ??? Drives your VTOL craft (i.e. helicopters, VTOL jets, airships) on combat and patrol missions 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_bdVTOLAI_tags = ??? BDA pilot ai control helo heli copter vtol + + //#loc_BDArmory_part_bdOrbitalAI_title = ??? AI Orbital Pilot + //#loc_BDArmory_part_bdOrbitalAI_desc = ??? Pilots spacecraft in orbit on combat missions without using your hands. Tune the values based on your ship's unique characteristics. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + //#loc_BDArmory_part_bdOrbitalAI_tags = ??? BDA pilot ai control space spacecraft orbiter orbital + + #loc_BDArmory_part_baha20mmAmmo_title = 20mm Munitionskasten + #loc_BDArmory_part_baha20mmAmmo_description = Munitionskasten für 650 20x102mm Patronen. + //#loc_BDArmory_part_baha20mmAmmo_tags = ??? BDA ammo box 20mm rounds bullet + + #loc_BDArmory_part_baha25mmAmmo_title = 25mm Munitionskasten + #loc_BDArmory_part_baha25mmAmmo_description = Munitionskasten für 625 25x137mm Patronen. + //#loc_BDArmory_part_baha25mmAmmo_tags = ??? BDA ammo box 25mm rounds bullet + + #loc_BDArmory_part_baha30mmAmmo_title = 30mm Munitionskasten + #loc_BDArmory_part_baha30mmAmmo_description = Munitionskasten für 600 30x173mm Patronen. + //#loc_BDArmory_part_baha30mmAmmo_tags = ??? BDA ammo box 30mm rounds bullet + + #loc_BDArmory_part_rocket70mmAmmo_title = 70mm Paketenmunitionskasten + #loc_BDArmory_part_rocket70mmAmmo_description = Raketenmunitionskasten für 48 ungesteuerte 70mm Paketen. + //#loc_BDArmory_part_bahaRocketAmmo_tags = ??? BDA ammo box rocket ffar + + #loc_BDArmory_part_baha50CalAmmo_title = 50cal Munitionskasten + #loc_BDArmory_part_baha50CalAmmo_description = Munitionskasten für 1200 .50 cal Patronen. + //#loc_BDArmory_part_baha50calAmmo_tags = ??? BDA ammo box .50 50cal 12.7 rounds bullet + + #loc_BDArmory_part_UniversalAmmoBoxBDA_title = Universal-Munitionskasten (Altbestand) + #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. + + #loc_BDArmory_part_BDAcUniversalAmmoBox_title = Universal Ammo Box + #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!). + //#loc_BDArmory_part_bahaUABAmmo_tags = ??? BDA ammo box + + #loc_BDArmory_part_bahaCannonShellBox_title = Kanonenkugel-Munitionskiste + #loc_BDArmory_part_bahaCannonShellBox_description = Munitionskiste für 10 Kanonenkugeln. + //#loc_BDArmory_part_bahaCannonAmmo_tags = ??? BDA ammo box shell cannon tank + + #loc_BDArmory_part_BD1x1slopeArmor_title = BD 1x1 dreieckige Panzerplatte + //#loc_BDArmory_part_bahaArmor_tags = ??? BDA armor plate Armo Ship Afv panel + #loc_BDArmory_part_BD1x1slopeArmor_description = Eine stabile, dreieckige 1x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + #loc_BDArmory_part_BD2x1slopeArmor_title = BD 2x1 dreieckige Panzerplatte + #loc_BDArmory_part_BD2x1slopeArmor_description = Eine stabile, dreieckige 2x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + #loc_BDArmory_part_BD1x1panelArmor_title = BD 1x1 Panzerplatte + #loc_BDArmory_part_BD1x1panelArmor_description = Eine stabile 1x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + #loc_BDArmory_part_BD2x1panelArmor_title = BD 2x1 Panzerplatte + #loc_BDArmory_part_BD2x1panelArmor_description = Eine stabile 2x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + #loc_BDArmory_part_BD3x1panelArmor_title = BD 3x1 Panzerplatte + #loc_BDArmory_part_BD3x1panelArmor_description = Eine stabile 3x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + #loc_BDArmory_part_BD4x1panelArmor_title = BD 4x1 Panzerplatte + #loc_BDArmory_part_BD4x1panelArmor_description = Eine stabile 4x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + #loc_BDArmory_part_awacsRadar_title = AWACS luftgestütztes Radarsytem + #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. + //#loc_BDArmory_part_awacsRadar_tags = ??? bda radar awac track detect + + #loc_BDArmory_part_bdammGuidanceModule_title = Modulares Raketenführungssystem (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) + //#loc_BDArmory_part_bdammGuidanceModule_tags = ??? bda missile mmg guid ordnance + + #loc_BDArmory_part_bahaBrowningAnm2_title = Browning .50cal AN/M3 + #loc_BDArmory_part_bahaBrowningAnm2_description = Eine altes .50 cal Maschiengewehr für 50cal Minution. + //#loc_BDArmory_part_bahaBrowningAnm2_tags = ??? bda gun .50 cal 50cal weap + + #loc_BDArmory_part_bahaClusterBomb_title = CBU-87 Streubombe + #loc_BDArmory_part_bahaClusterBomb_description = Diese Bombe öffnet sich nach kurzer Fallzeit, in einer einstellbaren Höhe, und gibt Kleinbomben frei. + //#loc_BDArmory_part_bahaClusterBomb_tags = ??? bda bomb ordnance cluster ugb atg a2g weap + + #loc_BDArmory_part_bahaChaffPod_title = Düppelabwurfvorrichtung + #loc_BDArmory_part_bahaChaffPod_description = Wirft Wolken von Düppeln ab, die die Radarverfolgung stören oder brechen können. + //#loc_BDArmory_part_bahaChaffPod_tags = ??? bda counter cm chaff + + #loc_BDArmory_part_bahaCmPod_title = IR-Täuschkörper-Abwurfvorichtung + #loc_BDArmory_part_bahaCmPod_description = Wirft Täuschkörper ab, die die Zielverfolgung von hitzesuchenden Raketen verwirren können. + //#loc_BDArmory_part_bahaCmPod_tags = ??? bda counter cm flare + + //#loc_BDArmory_part_bahaDecoyPod_title = ??? Decoy Launcher + //#loc_BDArmory_part_bahaDecoyPod_description = ??? Launches Acoustic Decoys for confusing passive sonar torpedoes. + //#loc_BDArmory_part_bahaDecoyPod_tags = ??? bda counter cm decoy + + //#loc_BDArmory_part_bahaSBTPod_title = ??? Bubble Curtain launcher + //#loc_BDArmory_part_bahaSBTPod_description = ??? Launches bubble curtain countermeasures to degrade enemy active sonar. + + #loc_BDArmory_part_bahaECMJammer_title = Elektronische Gegenmaßnahmen AN/ALQ-131 + #loc_BDArmory_part_bahaECMJammer_description = Dieses System erschwert das Aufschalten des Flugzeugs durch feindliches Radar. Kann die Radaraufschaltung brechen. + //#loc_BDArmory_part_bahaECMJammer_tags = ??? bda ecm jamm counter cm + + #loc_BDArmory_part_bahaGau-8_title = GAU-8 Gatling-Kanone + #loc_BDArmory_part_bahaGau-8_description = Siebenläufige Gatling-Kanone für 30x173mm Patronen. + //#loc_BDArmory_part_bahaGau-8_tags = ??? bda gun 30mm gatling gau brrt weap + + #loc_BDArmory_part_bahaGoalKeeper_title = Goalkeeper Nahbereichsverteidigungssystem + #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! + //#loc_BDArmory_part_bahaGoalKeeper_tags = ??? bda gun turret 30mm gatling gau brrt ciws gk weap + + #loc_BDArmory_part_GoalKeeperBDAcMk1_title = GoalkeeperMk1 Nahbereichsverteidigungssystem + #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. + + #loc_BDArmory_part_BDAcGKmk2_title = Goalkeeper MK2 Nahbereichsverteidigungssystem + #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. + + #loc_BDArmory_part_scanLockRadar1_title = Track-while-scan Radarverfolgungssystem + #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. + //#loc_BDArmory_part_scanLockRadar1_tags = ??? bda radar detect track lock scan search fcs + + #loc_BDArmory_part_scanLargeRadar_title = Großes Suchradar + #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. + //#loc_BDArmory_part_scanLargeRadar_tags = ??? bda radar detect scan search + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = FFAR Abschussystem für ungelenkte Raketen + #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. + //#loc_BDArmory_part_F86RL_tags = ??? bda rocket pod launcher a2a flak reload weap + + #loc_BDArmory_part_bahaH70Launcher_title = Abschusssystem-Gondel für ungelenkte Hydra-70 Raketen + #loc_BDArmory_part_bahaH70Launcher_description = Gondel für Montage unter den Tragflächen. Enthält 19 ungelenkte Hydra-70 Raketen. + //#loc_BDArmory_part_bahaH70Launcher_tags = ??? bda rocket pod launcher a2g weap + + #loc_BDArmory_part_bahaH70Turret_title = Bewegliches Abschusssystem für ungelenkte Hydra-70 Raketen + #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. + //#loc_BDArmory_part_bahaH70Turret_tags = ??? bda rocket pod launcher a2g turret weap + + #loc_BDArmory_part_bahaHarm_title = AGM-88 HARM Rakete + #loc_BDArmory_part_bahaHarm_description = Hochgeschwindigkeits-Anti-Radar-Rakete. Diese Rakete lenkt auf Radarziele, die von einem Radar-Warn-Empfänger entdeckt werden. + //#loc_BDArmory_part_bahaHarm_tags = ??? BDA missile antirad homing agm atg ordnance weap + + //HEKV Missile + #loc_BDArmory_part_bahaHEKV1_title = HE-KV-1 Rakete + #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. + //#loc_BDArmory_part_bahaHEKV1_tags = ??? BDA missile radar homing ata a2a ordnance rcs space weap + + //KKV Missile + //#loc_BDArmory_part_bahaKKV_title = ??? Kinetic Kill Vehicle + //#loc_BDArmory_part_bahaKKV_description = ??? The KKV (kinetic kill vehicle) is a IR-guided homing missile that uses reaction control thrusters and a control moment gyroscope to maneuver. It is capable of steering towards targets in a vacuum and has high drag in atmosphere. The KKV relies on kinetic energy to destroy its target and carries no explosives. 6 km/s delta-V. + //#loc_BDArmory_part_bahaKKV_tags = ??? BDA missile radar homing ata a2a ordnance orbital rcs space weap kinetic + + #loc_BDArmory_part_bahaAGM-114_title = AGM-114 Hellfire Rakete + #loc_BDArmory_part_bahaAGM-114_description = Kleine, schnelle, Laser-geführte Lenkrakete. + //#loc_BDArmory_part_bahaAGM-114_tags = ??? BDA missile laser homing atg agm ordnance weap + + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R Hellfire II EMP Rakete + #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. + //#loc_BDArmory_part_bahaAGM-114_EMP_tags = ??? BDA missile laser homing atg agm ordnance emp weap + + #loc_BDArmory_part_bahaHiddenVulcan_title = Vulkan-Kanone (versteckt) + #loc_BDArmory_part_bahaHiddenVulcan_description = Eine 6-läufige 20x102mm Vulkan-Kanone. + //#loc_BDArmory_part_bahaHiddenVulcan_tags = ??? bda gun 20mm gatling weap + + #loc_BDArmory_part_bahaJdamMk83_title = Mk83 JDAM Bombe + #loc_BDArmory_part_bahaJdamMk83_description = 1000 Pfund GPS-geführte Bombe. + //#loc_BDArmory_part_bahaJdamMk83_tags = ??? bda bomb gps ordnance atg homing guid weap + + #loc_BDArmory_part_bahaM102Howitzer_title = M102 Howitzer-Kanone (Radial) + #loc_BDArmory_part_bahaM102Howitzer_description = Eine radial montierte 105mm Kanone. Feuert Kanonenkugeln. + //#loc_BDArmory_part_bahaM102Howitzer_tags = ??? bda gun cannon turret shell howie weap + + #loc_BDArmory_part_bahaM1Abrams_title = M1 Abrams Kanone + #loc_BDArmory_part_bahaM1Abrams_description = Eine 120mm Kanone auf einem gepanzerten Geschützturm. Feuert Kanonenkugeln. + //#loc_BDArmory_part_bahaM1Abrams_tags = ??? bda gun cannon turret shell tank weap + + #loc_BDArmory_part_bahaM230ChainGun_title = M230 Maschinengewehr-Geschützturm + #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. + //#loc_BDArmory_part_bahaM230ChainGun_tags = ??? bda gun chaingun turret 30mm heli weap + + #loc_BDArmory_part_bahaAGM-65_title = AGM-65 Maverick Rakete + #loc_BDArmory_part_bahaAGM-65_description = Lasergeführte Luft-Boden-Rakete mittlerer Durchschlagskraft. + //#loc_BDArmory_part_bahaAGM-65_tags = ??? bda missile laser ordnance atg homing guid weap + + #loc_BDArmory_part_missileTurretTest_title = Jernas Raketen-Geschützturm + #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. + //#loc_BDArmory_part_missileTurretTest_tags = ??? bda missile turret launch rail mount hardpoint radar lock + + #loc_BDArmory_part_bahaMk82Bomb_title = Mk82 Bombe + #loc_BDArmory_part_bahaMk82Bomb_description = Ungelenkte 500-Pfund-Bumbe. + //#loc_BDArmory_part_bahaMk82Bomb_tags = ??? bda bomb ugb ordnance atg weap + + #loc_BDArmory_part_bahaMk82BombBrake_title = Mk82 Bombe 'SnakeEye' + #loc_BDArmory_part_bahaMk82BombBrake_description = Ungelenkte 500-Pfund-Bombe mit Luftbremse. Für Bombardierung aus geringer Höhe verwenden. + + #loc_BDArmory_part_bahaOMillennium_title = Oerlikon Millennium Revolverkanone + #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. + //#loc_BDArmory_part_bahaOMillennium_tags = ??? bda gun turret ciws flak autocannon oerlikon cram 30mm weap + + #loc_BDArmory_part_bahaPac-3_title = PAC-3 Abfangrakete + #loc_BDArmory_part_bahaPac-3_description = Radargelenkte Hochgeschwindigkeits-Boden-Luft-Rakete mittlerer Reichweite. + //#loc_BDArmory_part_bahaPac-3_tags = ??? bda missile radar ordnance ata a2a homing guid sarh sam weap + + #loc_BDArmory_part_patriotLauncherTurret_title = Patriot Raketengeschützturm + #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. + //#loc_BDArmory_part_patriotLauncherTurret_tags = ??? bda missile turret launch rail mount hardpoint sam + + #loc_BDArmory_part_radarDataReceiver_title = Radar-Datenempfänger + #loc_BDArmory_part_radarDataReceiver_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. + //#loc_BDArmory_part_radarDataReceiver_tags = ??? bda radar detect link data lock + + //AN/APG-63 Variants + //#loc_BDArmory_part_bdRadome_variantPitot = ??? Pitot Tube + //#loc_BDArmory_part_bdRadome_variantNoPitot = ??? No Pitot Tube + #loc_BDArmory_part_bdRadome1_title = AN/APG-63 Radom + #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. + //#loc_BDArmory_part_bdRadome1_tags = ??? bda radar radome detect lock track scan + #loc_BDArmory_part_bdRadome1inline_title = AN/APG-63 Inline Radom + #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. + #loc_BDArmory_part_bdRadome1snub_title = AN/APG-63 Radom + //#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_bahaRBS-15Cruise_title = RBS-15 Boden-Boden Marschflugkörper + #loc_BDArmory_part_bahaRBS-15Cruise_description = Marschflugkörper hoher Reichweite und hoher Geschwindigkeit mit Raketenboostern. + //#loc_BDArmory_part_bahaRBS-15Cruise_tags = ??? bda missile ordnance cruise gps guid homing weap + + #loc_BDArmory_part_bahaRBS-15ALCruise_title = RBS-15 Luft-Boden Marschflugkörper + #loc_BDArmory_part_bahaRBS-15ALCruise_description = Marschflugkörper hoher Reichweite und hoher Geschwindigkeit mit Raketenboostern. Variante für Abwurf von einem Flugzeug. + + #loc_BDArmory_part_bdRotBombBay_title = Justierbares, rotierendes Bombenabwurfsystem + #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). + //#loc_BDArmory_part_bdRotBombBay_tags = ??? bda missile rail launch mount rotary rack + #loc_BDArmory_part_bahaS-8Launcher_title = Abschusssystem-Gondel für ungelenkte S-8KOM Raketen + #loc_BDArmory_part_bahaS-8Launcher_description = Gondel für Montage unter den Tragflächen. Enthält 23 ungelenkte S-8KOM Raketen. Aerodynamischer Bugkonus. + + #loc_BDArmory_part_bahaAim9_title = AIM-9 Sidewinder Rakete + #loc_BDArmory_part_bahaAim9_description = Hitzesuchende Rakete kurzer Reichweite. + //#loc_BDArmory_part_bahaAim9_tags = ??? bda missile ordnance heater heatseek ata a2a aam weap + + #loc_BDArmory_part_bdWarheadSmall_title = Kleiner hochexplosiver Gefechtskopf + #loc_BDArmory_part_bdWarheadSmall_description = Ein mit Sprengstoff vollgestopfter Raketengefechtskopf. + //#loc_BDArmory_part_bdWarheadSmall_tags = ??? bda missile bomb ordnance boom weap + + #loc_BDArmory_part_bahaSmokeCmPod_title = Abschussvorrichtung für Rauch-Gegenmaßnahmen + #loc_BDArmory_part_bahaSmokeCmPod_description = Feuert Gegenmaßnahmen, die eine Nebelwand erzeugen. Effektiv gegen Laser-Zielsysteme. + //#loc_BDArmory_part_bahaSmokeCmPod_tags = ??? bda cm counter smoke + + #loc_BDArmory_part_bahaFlirBall_title = FLIR Zielerfassungssystem + #loc_BDArmory_part_bahaFlirBall_description = Horizont-stabilisiertes Infrarot-Kamerasystem mit Zielbeleuchtungs-Laser für die schnelle Detektion und Aufschaltung von Bodenzielen. + + //#loc_BDArmory_part_bahaCamPod_tags = ??? bda detect laser gps cam flir target + #loc_BDArmory_part_bahaCamPod_title = AN/AAQ-28 Zielerfassungssystem + #loc_BDArmory_part_bahaCamPod_description = Horizont-stabilisiertes Infrarot-Kamerasystem mit Zielbeleuchtungs-Laser für die schnelle Detektion und Aufschaltung von Bodenzielen. + + //#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. + //#loc_BDArmory_part_bahaIRSTPod_tags = ??? bda detect heat scan search track ir therm + + #loc_BDArmory_part_towLauncherTurret_title = TOW Abschusssystem + #loc_BDArmory_part_towLauncherTurret_description = Abschusssystem für 4 lasergeführte TWO Anti-Panzer-Raketen. Schwenkbereich 160 Grad, Schusswinkel -8 bis 12 Grad. + //#loc_BDArmory_part_towLauncherTurret_tags = ??? bda missile turret rail mount launch tow + + #loc_BDArmory_part_bahaTowMissile_title = BGM-71 Tow Missile + #loc_BDArmory_part_bahaTowMissile_description = Lasergelenkte Kurzdistanz-Anti-Panzer-Rakete. + //#loc_BDArmory_part_bahaTowMissile_tags = ??? bda missile ordnance laser agm atg weap + + #loc_BDArmory_part_missileController_title = Waffen-Kontrollsystem + #loc_BDArmory_part_missileController_description = Listet alle Waffensysteme. Wähle eins aus und feure es ab - mit einem einzigen Klick. + //#loc_BDArmory_part_missileController_tags = ??? bda wm ai weap + + #loc_BDArmory_part_BDAsonarPod1A_title = BDA MK1 Sonar + #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. + //#loc_BDArmory_part_BDAsonarPod1A_tags = ??? bda detect sonar ship + + #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. + //#loc_BDArmory_part_StingRayBDATorpedo_tags = ??? bda missile ordnance asm torp ship sonar homing guid s2s sts + + //#loc_BDArmory_part_EJ200_title = ??? TFJ-EJ200 "Typhoon" Afterburning Turbofan + //#loc_BDArmory_part_EJ200_description = ??? Word is that this engine was the result of international cooperation, which produced an engine with exceptional performance and potential. + + #loc_BDArmory_part_SaturnAL31_title = Saturn AL-31FM1 Strahltriebwerk mit Nachbrenner + #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. + //#loc_BDArmory_part_SaturnAL31_tags = ??? after aircraft burner engine fighter jet saturn plane propulsion AL + + //GravityGun + #loc_BDArmory_part_GravGun_title = Nullpunkt-Energiefeld-Manipulator + #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). + //#loc_BDArmory_part_GravGun_tags = ??? bda laser gun weap grav + + #loc_BDArmory_part_genie_title = AIR-2 Genie Air-To-Air Rocket + #loc_BDArmory_part_genie_description = 1.5 Kilotonnen Luft-Luft-Nuklearwaffe. + //#loc_BDArmory_part_genie_tags = ??? BDA missile nuke ata a2a + + #loc_BDArmory_part_GAU22_title = GAU-22/A 25x137mm Kanone + #loc_BDArmory_part_GAU22_description = Ein vierläufiges Maschinengewehr für 25x137mm Munition. + //#loc_BDArmory_part_GAU22_tags = ??? BDA gun weap 25mm gatling gau + + #loc_BDArmory_part_sidam_title = Sidam Anti-Air gun + #loc_BDArmory_part_sidam_description = Ein 4-fach anti-Luft Maschinengewehr-Abwehrgeschütz für 25x137 Munition. + //#loc_BDArmory_part_sidam_tags = ??? BDA gun weap 25mm turret AA + + #loc_BDArmory_part_REA_title = BD 1x0.5 Reaktivpanzerung + #loc_BDArmory_part_REA_Panel_description = Eine 1x0.5m Reaktivpanzerplatte. Bringt das kleine bisschen extra-Sicherheit zusätzlich zu existierender Panzerung. + //#loc_BDArmory_part_REA_Panel_tags = ??? bda armor panel era react + + #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. + + #loc_BDArmory_part_TriPanel_title = BD dreieckige Panzerplatte + //#loc_BDArmory_part_TriIsoPanel_title = ??? BD Armor Panel Oblique Triangle + #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. + + #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. + //#loc_BDArmory_part_BombBay_tags = ??? bda missile ordnance bay rail deploy rack + + //#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. + //#loc_BDArmory_part_combatSeat_tags = ??? bda ai wm weap pilot seat chair + + //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_bdRadome1_Gnd_tags = ??? bda detect radar scan track ground + + //#loc_BDArmory_part_AWACS_Legged = ??? Legged + //#loc_BDArmory_part_AWACS_Legless = ??? Legless + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg index 676a3f49e..c9c7bfa8f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg @@ -1,3 +1,6 @@ +// Notes: +// - The "_tags" entries are simply common search terms for the part filter in the SPH/VAB. Instead of being the same in all languages, they should be whatever a user in that language would search for. + Localization { en-us @@ -7,330 +10,364 @@ Localization #loc_BDArmory_agent_title = Bahamuto Dynamics #loc_BDArmory_agent_description = Leading manufacturer of military arms and munitions. The company is also occasionally contracted by the space program for advanced or unique engineering solutions. #loc_BDArmory_part_manufacturer = Bahamuto Dynamics + + #loc_BDArmory_agent2_title = Twin Crown Aerospace Industries - //Vulcan Turret #loc_BDArmory_part_bahaGatlingGun_title = Vulcan Turret - //A 6 barrel 20x102mm rotary cannon. #loc_BDArmory_part_bahaGatlingGun_description = A 6 barrel 20x102mm rotary cannon. + #loc_BDArmory_part_bahaGatlingGun_tags = BDA gun weap turret 20mm gatling - //.50cal Turret #loc_BDArmory_part_bahaTurret_title = .50cal Turret - //A dual barrel .50 cal machine gun. #loc_BDArmory_part_bahaTurret_description = A dual barrel .50 cal machine gun. + #loc_BDArmory_part_bahaTurret_tags = BDA gun turret .50cal weap - //USAF Airborne Laser #loc_BDArmory_part_bahaABL_title = USAF Airborne Laser - //A high powered laser for setting things on fire. Uses 350 electric charge per second. #loc_BDArmory_part_bahaABL_description = A high powered laser for setting things on fire. Uses 350 electric charge per second. + #loc_BDArmory_part_bahaABL_tags = BDA laser turret beam anti weap - //Adjustable Missile Rail #loc_BDArmory_part_bahaAdjustableRail_title = Adjustable Missile Rail - //A rail for mounting missiles. #loc_BDArmory_part_bahaAdjustableRail_description = A rail for mounting missiles. + #loc_BDArmory_part_bahaAdjustableRail_tags = BDA rail hardpoint missile mount - //AGM-86C Cruise Missile #loc_BDArmory_part_bahaAgm86B_title = AGM-86C Cruise Missile - //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 = 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. - - //AIM-120 AMRAAM Missile + #loc_BDArmory_part_bahaAgm86B_tags = BDA missile GPS cruise guided weap + #loc_BDArmory_part_bahaAim120_title = AIM-120 AMRAAM Missile - //Medium range radar guided homing missile. #loc_BDArmory_part_bahaAim120_description = Medium range radar guided homing missile. + #loc_BDArmory_part_bahaAim120_tags = BDA missile radar homing a2a ata ordnance weap + + #loc_BDArmory_part_bahaEMP120_title = AIM-120 AMRAAM EMP 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. + #loc_BDArmory_part_bahaEMP120_tags = BDA missile radar homing a2a ata emp ordnance weap - //AI Pilot Flight Computer #loc_BDArmory_part_bdPilotAI_title = AI Pilot Flight Computer - //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 = 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 = 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). + #loc_BDArmory_part_bdpilotAI_tags = BDA pilot ai control + + #loc_BDArmory_part_bdDriverAI_title = AI Surface Operation Driver + #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_tags = BDA driver ai control vee vehicle ground + + #loc_BDArmory_part_bdVTOLAI_title = AI Vertical Takeoff and Landing Pilot + #loc_BDArmory_part_bdVTOLAI_desc = Drives your VTOL craft (i.e. helicopters, VTOL jets, airships) on combat and patrol missions 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_bdVTOLAI_tags = BDA pilot ai control helo heli copter vtol + + #loc_BDArmory_part_bdOrbitalAI_title = AI Orbital Pilot + #loc_BDArmory_part_bdOrbitalAI_desc = Pilots spacecraft in orbit on combat missions without using your hands. Tune the values based on your ship's unique characteristics. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + #loc_BDArmory_part_bdOrbitalAI_tags = BDA pilot ai control space spacecraft orbiter orbital - //20mm Ammunition Box #loc_BDArmory_part_baha20mmAmmo_title = 20mm Ammunition Box - //Ammo box containing 650 20x102mm rounds. #loc_BDArmory_part_baha20mmAmmo_description = Ammo box containing 650 20x102mm rounds. + #loc_BDArmory_part_baha20mmAmmo_tags = BDA ammo box 20mm rounds bullet + + #loc_BDArmory_part_baha25mmAmmo_title = 25mm Ammunition Box + #loc_BDArmory_part_baha25mmAmmo_description = Ammo box containing 625 25x137mm rounds. + #loc_BDArmory_part_baha25mmAmmo_tags = BDA ammo box 25mm rounds bullet - //30mm Ammunition Box #loc_BDArmory_part_baha30mmAmmo_title = 30mm Ammunition Box - //Ammo box containing 600 30x173mm rounds. #loc_BDArmory_part_baha30mmAmmo_description = Ammo box containing 600 30x173mm rounds. + #loc_BDArmory_part_baha30mmAmmo_tags = BDA ammo box 30mm rounds bullet + + #loc_BDArmory_part_rocket70mmAmmo_title = 70mm Rocket Ammunition Box + #loc_BDArmory_part_rocket70mmAmmo_description = Ammo box containing 48 70mm Rockets. + #loc_BDArmory_part_bahaRocketAmmo_tags = BDA ammo box rocket ffar - //50cal Ammunition Box #loc_BDArmory_part_baha50CalAmmo_title = 50cal Ammunition Box - //Ammo box containing 1200 .50 cal rounds. #loc_BDArmory_part_baha50CalAmmo_description = Ammo box containing 1200 .50 cal rounds. + #loc_BDArmory_part_baha50calAmmo_tags = BDA ammo box .50 50cal 12.7 rounds bullet - //Universal Ammo Box (Legacy) #loc_BDArmory_part_UniversalAmmoBoxBDA_title = Universal Ammo Box (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 = (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. - //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 = 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_bahaUABAmmo_tags = BDA ammo box - //Cannon Ammunition Box #loc_BDArmory_part_bahaCannonShellBox_title = Cannon Ammunition Box - //Ammo box containing 10 cannon shells. #loc_BDArmory_part_bahaCannonShellBox_description = Ammo box containing 10 cannon shells. + #loc_BDArmory_part_bahaCannonAmmo_tags = BDA ammo box shell cannon tank - //BD 1x1 slope Armor #loc_BDArmory_part_BD1x1slopeArmor_title = BD 1x1 slope Armor - //A sturdy 1x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_bahaArmor_tags = BDA armor plate Armo Ship Afv panel #loc_BDArmory_part_BD1x1slopeArmor_description = A sturdy 1x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float - - //BD 2x1 slope Armor + #loc_BDArmory_part_BD2x1slopeArmor_title = BD 2x1 slope Armor - //A sturdy 2x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float #loc_BDArmory_part_BD2x1slopeArmor_description = A sturdy 2x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float - //BD 1x1 panel Armor #loc_BDArmory_part_BD1x1panelArmor_title = BD 1x1 panel Armor - //A sturdy 1x1 Armor plate, perfect for constructing all sorts of things. PS does not float #loc_BDArmory_part_BD1x1panelArmor_description = A sturdy 1x1 Armor plate, perfect for constructing all sorts of things. PS does not float - //BD 2x1 panel Armor #loc_BDArmory_part_BD2x1panelArmor_title = BD 2x1 panel Armor - //A sturdy 2x1 Armor plate, perfect for constructing all sorts of things. PS does not float #loc_BDArmory_part_BD2x1panelArmor_description = A sturdy 2x1 Armor plate, perfect for constructing all sorts of things. PS does not float - //BD 3x1 panel Armor #loc_BDArmory_part_BD3x1panelArmor_title = BD 3x1 panel Armor - //A sturdy 3x1 Armor plate, perfect for constructing all sorts of things. PS does not float #loc_BDArmory_part_BD3x1panelArmor_description = A sturdy 3x1 Armor plate, perfect for constructing all sorts of things. PS does not float - //BD 4x1 panel Armor #loc_BDArmory_part_BD4x1panelArmor_title = BD 4x1 panel Armor - //A sturdy 4x1 Armor plate, perfect for constructing all sorts of things. PS does not float #loc_BDArmory_part_BD4x1panelArmor_description = A sturdy 4x1 Armor plate, perfect for constructing all sorts of things. PS does not float - //AWACS Detection Radar #loc_BDArmory_part_awacsRadar_title = AWACS Detection Radar - //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 = 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_tags = bda radar awac track detect - //Modular Missile Guidance (EXPERIMENTAL) #loc_BDArmory_part_bdammGuidanceModule_title = Modular Missile Guidance (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 = 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) - - //Browning .50cal AN/M2 + #loc_BDArmory_part_bdammGuidanceModule_tags = bda missile mmg guid ordnance + #loc_BDArmory_part_bahaBrowningAnm2_title = Browning .50cal AN/M3 - //An old fixed .50 cal machine gun 50cal ammo #loc_BDArmory_part_bahaBrowningAnm2_description = An old fixed .50 cal machine gun 50cal ammo + #loc_BDArmory_part_bahaBrowningAnm2_tags = bda gun .50 cal 50cal weap - //CBU-87 Cluster Bomb #loc_BDArmory_part_bahaClusterBomb_title = CBU-87 Cluster Bomb - //This bomb splits open and deploys many small bomblets at a certain altitude. #loc_BDArmory_part_bahaClusterBomb_description = This bomb splits open and deploys many small bomblets at a certain altitude. + #loc_BDArmory_part_bahaClusterBomb_tags = bda bomb ordnance cluster ugb atg a2g weap - //Chaff Dispenser #loc_BDArmory_part_bahaChaffPod_title = Chaff Dispenser - //Drops chaff for confusing or breaking radar locks. #loc_BDArmory_part_bahaChaffPod_description = Drops chaff for confusing or breaking radar locks. + #loc_BDArmory_part_bahaChaffPod_tags = bda counter cm chaff - //Flare Dispenser #loc_BDArmory_part_bahaCmPod_title = Flare Dispenser - //Drops flares for confusing heat-seeking missiles. #loc_BDArmory_part_bahaCmPod_description = Drops flares for confusing heat-seeking missiles. + #loc_BDArmory_part_bahaCmPod_tags = bda counter cm flare + + #loc_BDArmory_part_bahaDecoyPod_title = Decoy Launcher + #loc_BDArmory_part_bahaDecoyPod_description = Launches Acoustic Decoys for confusing passive sonar torpedoes. + #loc_BDArmory_part_bahaDecoyPod_tags = bda counter cm decoy + + #loc_BDArmory_part_bahaSBTPod_title = Bubble Curtain launcher + #loc_BDArmory_part_bahaSBTPod_description = Launches bubble curtain countermeasures to degrade enemy active sonar. - //AN/ALQ-131 ECM Jammer #loc_BDArmory_part_bahaECMJammer_title = AN/ALQ-131 ECM Jammer - //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 = 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_tags = bda ecm jamm counter cm - //GAU-8 30x173mm Cannon #loc_BDArmory_part_bahaGau-8_title = GAU-8 30x173mm Cannon - //A 7 barrel 30mm rotary cannon. #loc_BDArmory_part_bahaGau-8_description = A 7 barrel 30mm rotary cannon. + #loc_BDArmory_part_bahaGau-8_tags = bda gun 30mm gatling gau brrt weap - //Goalkeeper CIWS #loc_BDArmory_part_bahaGoalKeeper_title = Goalkeeper CIWS - #loc_BDArmory_part_bahaGoalKeeper_description = 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 = 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 volume search radar. + #loc_BDArmory_part_bahaGoalKeeper_tags = bda gun turret 30mm gatling gau brrt ciws gk weap - //GoalkeeperMk1 CIWS #loc_BDArmory_part_GoalKeeperBDAcMk1_title = GoalkeeperMk1 CIWS - #loc_BDArmory_part_GoalKeeperBDAcMk1_description = 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 = 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. - //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 #loc_BDArmory_part_scanLockRadar1_description = 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_tags = bda radar detect track lock scan search fcs - //Large Detection Radar #loc_BDArmory_part_scanLargeRadar_title = Large Detection Radar #loc_BDArmory_part_scanLargeRadar_description = 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_tags = bda radar detect scan search + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = FFAR Reloadable Rocket Pod + #loc_BDArmory_part_F86RL_description = 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_tags = bda rocket pod launcher a2a flak reload weap - //Hydra-70 Rocket Pod #loc_BDArmory_part_bahaH70Launcher_title = Hydra-70 Rocket Pod - //Holds and fires 19 unguided Hydra-70 rockets. #loc_BDArmory_part_bahaH70Launcher_description = Holds and fires 19 unguided Hydra-70 rockets. + #loc_BDArmory_part_bahaH70Launcher_tags = bda rocket pod launcher a2g weap - //Hydra-70 Rocket Turret #loc_BDArmory_part_bahaH70Turret_title = Hydra-70 Rocket Turret - //Turret pod that holds and fires 32 unguided Hydra-70 rockets. #loc_BDArmory_part_bahaH70Turret_description = Turret pod that holds and fires 32 unguided Hydra-70 rockets. + #loc_BDArmory_part_bahaH70Turret_tags = bda rocket pod launcher a2g turret weap - //AGM-88 HARM Missile #loc_BDArmory_part_bahaHarm_title = AGM-88 HARM Missile - //High-speed anti-radiation missile. This missile will home in on radar sources detected by the Radar Warning Receiver. #loc_BDArmory_part_bahaHarm_description = High-speed anti-radiation missile. This missile will home in on radar sources detected by the Radar Warning Receiver. + #loc_BDArmory_part_bahaHarm_tags = BDA missile antirad homing agm atg ordnance weap - //HE-KV-1 Missile + //HEKV Missile #loc_BDArmory_part_bahaHEKV1_title = HE-KV-1 Missile - //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 = 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 = 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. 3 km/s delta-V. + #loc_BDArmory_part_bahaHEKV1_tags = BDA missile radar homing ata a2a ordnance rcs space weap + + //KKV Missile + #loc_BDArmory_part_bahaKKV_title = Kinetic Kill Vehicle + #loc_BDArmory_part_bahaKKV_description = The KKV (kinetic kill vehicle) is a IR-guided homing missile that uses reaction control thrusters and a control moment gyroscope to maneuver. It is capable of steering towards targets in a vacuum and has high drag in atmosphere. The KKV relies on kinetic energy to destroy its target and carries no explosives. 6 km/s delta-V. + #loc_BDArmory_part_bahaKKV_tags = BDA missile radar homing ata a2a ordnance orbital rcs space weap kinetic - //AGM-114 Hellfire Missile #loc_BDArmory_part_bahaAGM-114_title = AGM-114 Hellfire Missile - //Small, quick, laser guided homing missile. #loc_BDArmory_part_bahaAGM-114_description = Small, quick, laser guided homing missile. + #loc_BDArmory_part_bahaAGM-114_tags = BDA missile laser homing atg agm ordnance weap + + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R Hellfire II EMP + #loc_BDArmory_part_bahaAGM-114_EMP_description = 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_tags = BDA missile laser homing atg agm ordnance emp weap - //Vulcan (Hidden) #loc_BDArmory_part_bahaHiddenVulcan_title = Vulcan (Hidden) - //A 6 barrel 20x102mm rotary cannon. 20x102Ammo #loc_BDArmory_part_bahaHiddenVulcan_description = A 6 barrel 20x102mm rotary cannon. 20x102Ammo + #loc_BDArmory_part_bahaHiddenVulcan_tags = bda gun 20mm gatling weap - //Mk83 JDAM Bomb #loc_BDArmory_part_bahaJdamMk83_title = Mk83 JDAM Bomb - //1000lb GPS-guided bomb. #loc_BDArmory_part_bahaJdamMk83_description = 1000lb GPS-guided bomb. - - //M102 Howitzer (Radial) + #loc_BDArmory_part_bahaJdamMk83_tags = bda bomb gps ordnance atg homing guid weap + #loc_BDArmory_part_bahaM102Howitzer_title = M102 Howitzer (Radial) - //A radially mounted 105mm gun. CannonShells - #loc_BDArmory_part_bahaM102Howitzer_description =A radially mounted 105mm gun. CannonShells + #loc_BDArmory_part_bahaM102Howitzer_description = A radially mounted 105mm gun. CannonShells + #loc_BDArmory_part_bahaM102Howitzer_tags = bda gun cannon turret shell howie weap - //M1 Abrams Cannon #loc_BDArmory_part_bahaM1Abrams_title = M1 Abrams Cannon - //A 120mm cannon on an armored turret. CannonShells #loc_BDArmory_part_bahaM1Abrams_description = A 120mm cannon on an armored turret. CannonShells - - //M230 Chain Gun Turret + #loc_BDArmory_part_bahaM1Abrams_tags = bda gun cannon turret shell tank weap + #loc_BDArmory_part_bahaM230ChainGun_title = M230 Chain Gun Turret - //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 = 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_tags = bda gun chaingun turret 30mm heli weap - //AGM-65 Maverick Missile #loc_BDArmory_part_bahaAGM-65_title = AGM-65 Maverick Missile - //Medium yield laser guided air-to-ground missile. #loc_BDArmory_part_bahaAGM-65_description = Medium yield laser guided air-to-ground missile. - - //Jernas Missile Turret + #loc_BDArmory_part_bahaAGM-65_tags = bda missile laser ordnance atg homing guid weap + #loc_BDArmory_part_missileTurretTest_title = Jernas Missile Turret - //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 = 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_tags = bda missile turret launch rail mount hardpoint radar lock - //Mk82 Bomb #loc_BDArmory_part_bahaMk82Bomb_title = Mk82 Bomb - //500lb unguided bomb. #loc_BDArmory_part_bahaMk82Bomb_description = 500lb unguided bomb. - - //Mk82 SnakeEye Bomb + #loc_BDArmory_part_bahaMk82Bomb_tags = bda bomb ugb ordnance atg weap + #loc_BDArmory_part_bahaMk82BombBrake_title = Mk82 SnakeEye Bomb - //500lb unguided bomb with airbrakes. Use for low altitude bombing. #loc_BDArmory_part_bahaMk82BombBrake_description = 500lb unguided bomb with airbrakes. Use for low altitude bombing. - //Oerlikon Millennium Cannon #loc_BDArmory_part_bahaOMillennium_title = Oerlikon Millennium Cannon - //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 = 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 - - //PAC-3 Intercept Missile + #loc_BDArmory_part_bahaOMillennium_tags = bda gun turret ciws flak autocannon oerlikon cram 30mm weap + #loc_BDArmory_part_bahaPac-3_title = PAC-3 Intercept Missile - //Medium range, high speed, radar-guided surface to air missile. #loc_BDArmory_part_bahaPac-3_description = Medium range, high speed, radar-guided surface to air missile. - - //Patriot Launcher Turret + #loc_BDArmory_part_bahaPac-3_tags = bda missile radar ordnance ata a2a homing guid sarh sam weap + #loc_BDArmory_part_patriotLauncherTurret_title = Patriot Launcher Turret - //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 = 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. - - //Radar Data Receiver + #loc_BDArmory_part_patriotLauncherTurret_tags = bda missile turret launch rail mount hardpoint sam + #loc_BDArmory_part_radarDataReceiver_title = Radar Data Receiver - //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 = 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_tags = bda radar detect link data lock - //AN/APG-63 Radome + //AN/APG-63 Variants + #loc_BDArmory_part_bdRadome_variantPitot = Pitot Tube + #loc_BDArmory_part_bdRadome_variantNoPitot = No Pitot Tube #loc_BDArmory_part_bdRadome1_title = AN/APG-63 Radome #loc_BDArmory_part_bdRadome1_description = 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. - - //AN/APG-63 Inline Radome + #loc_BDArmory_part_bdRadome1_tags = bda radar radome detect lock track scan #loc_BDArmory_part_bdRadome1inline_title = AN/APG-63 Inline Radome #loc_BDArmory_part_bdRadome1inline_description = 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. - - //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 - //Long distance, multi-platform high-speed cruise missile with boosters. #loc_BDArmory_part_bahaRBS-15Cruise_description = Long distance, multi-platform high-speed cruise missile with boosters. + #loc_BDArmory_part_bahaRBS-15Cruise_tags = bda missile ordnance cruise gps guid homing weap + + #loc_BDArmory_part_bahaRBS-15ALCruise_title = RBS-15 Air launched Cruise Missile + #loc_BDArmory_part_bahaRBS-15ALCruise_description = Long distance, multi-platform high-speed cruise missile Air launched variant without external boosters - //Adjustable Rotary Bomb Rack #loc_BDArmory_part_bdRotBombBay_title = Adjustable Rotary Bomb Rack - //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 = 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. - - //S-8KOM Rocket Pod + #loc_BDArmory_part_bdRotBombBay_tags = bda missile rail launch mount rotary rack #loc_BDArmory_part_bahaS-8Launcher_title = S-8KOM Rocket Pod - //Holds and fires 23 unguided S-8KOM rockets. It has an aerodynamic nose cone. #loc_BDArmory_part_bahaS-8Launcher_description = Holds and fires 23 unguided S-8KOM rockets. It has an aerodynamic nose cone. - //AIM-9 Sidewinder Missile #loc_BDArmory_part_bahaAim9_title = AIM-9 Sidewinder Missile - //Short range heat seeking missile. #loc_BDArmory_part_bahaAim9_description = Short range heat seeking missile. + #loc_BDArmory_part_bahaAim9_tags = bda missile ordnance heater heatseek ata a2a aam weap - //Small High Explosive Warhead #loc_BDArmory_part_bdWarheadSmall_title = Small High Explosive Warhead - //A missile nose cone packed with explosives. #loc_BDArmory_part_bdWarheadSmall_description = A missile nose cone packed with explosives. + #loc_BDArmory_part_bdWarheadSmall_tags = bda missile bomb ordnance boom weap - //Smoke Countermeasure Pod #loc_BDArmory_part_bahaSmokeCmPod_title = Smoke Countermeasure Pod - //Fires smoke-screen countermeasures for occluding laser points. #loc_BDArmory_part_bahaSmokeCmPod_description = Fires smoke-screen countermeasures for occluding laser points. + #loc_BDArmory_part_bahaSmokeCmPod_tags = bda cm counter smoke - //FLIR Targeting Ball #loc_BDArmory_part_bahaFlirBall_title = FLIR Targeting Ball - //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 = 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. - //AN/AAQ-28 Targeting Pod + #loc_BDArmory_part_bahaCamPod_tags = bda detect laser gps cam flir target #loc_BDArmory_part_bahaCamPod_title = AN/AAQ-28 Targeting Pod - //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. - //Tow Launcher + #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. + #loc_BDArmory_part_bahaIRSTPod_tags = bda detect heat scan search track ir therm + #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. #loc_BDArmory_part_towLauncherTurret_description = 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_tags = bda missile turret rail mount launch tow - //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 = Short distance, laser beam-riding, wireless anti-tank missile. + #loc_BDArmory_part_bahaTowMissile_tags = bda missile ordnance laser agm atg weap - //Weapon Manager #loc_BDArmory_part_missileController_title = Weapon Manager - //Cycle through missiles/bombs and fire them with a single button. #loc_BDArmory_part_missileController_description = Cycle through missiles/bombs and fire them with a single button. + #loc_BDArmory_part_missileController_tags = bda wm ai weap - //BDAsonarPod1A #loc_BDArmory_part_BDAsonarPod1A_title = BDA MK1 Sonar Pod #loc_BDArmory_part_BDAsonarPod1A_description = 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_tags = bda detect sonar ship - //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_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. + #loc_BDArmory_part_StingRayBDATorpedo_tags = bda missile ordnance asm torp ship sonar homing guid s2s sts + + #loc_BDArmory_part_EJ200_title = TFJ-EJ200 "Typhoon" Afterburning Turbofan + #loc_BDArmory_part_EJ200_description = Word is that this engine was the result of international cooperation, which produced an engine with exceptional performance and potential. + #loc_BDArmory_part_SaturnAL31_title = Saturn AL-31FM1 Afterburning Jet Engine - //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 = 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. - } -} \ No newline at end of file + #loc_BDArmory_part_SaturnAL31_tags = after aircraft burner engine fighter jet saturn plane propulsion AL + + //GravityGun + #loc_BDArmory_part_GravGun_title = Zero-Point Energy Field Manipulator + #loc_BDArmory_part_GravGun_description = 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_tags = bda laser gun weap grav + + #loc_BDArmory_part_genie_title = AIR-2 Genie Air-To-Air Rocket + #loc_BDArmory_part_genie_description = 1.5kt Nuclear Anti-Air Rocket. + #loc_BDArmory_part_genie_tags = BDA missile nuke ata a2a + + #loc_BDArmory_part_GAU22_title = GAU-22/A 25x137mm Cannon + #loc_BDArmory_part_GAU22_description = A 4 barrel 25mm rotary cannon. 25x137mmAmmo. + #loc_BDArmory_part_GAU22_tags = BDA gun weap 25mm gatling gau + + #loc_BDArmory_part_sidam_title = Sidam Anti-Air gun + #loc_BDArmory_part_sidam_description = A salvo-firing quad 25mm anti-air gun. 25x137mmAmmo. + #loc_BDArmory_part_sidam_tags = BDA gun weap 25mm turret AA + + #loc_BDArmory_part_REA_title = BD 1x0.5 Reactive 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. + #loc_BDArmory_part_REA_Panel_tags = bda armor panel era react + + #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. + + #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. + + #loc_BDArmory_part_BombBay_title = Ordnance Bay + #loc_BDArmory_part_BombBay_description = A weapons bay with a deployable ordnance rack. Payload is shielded from the airstream until deployed. + #loc_BDArmory_part_BombBay_tags = bda missile ordnance bay rail deploy rack + + #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. + #loc_BDArmory_part_combatSeat_tags = bda ai wm weap pilot seat chair + + //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_bdRadome1_Gnd_tags = bda detect radar scan track ground + + #loc_BDArmory_part_AWACS_Legged = Legged + #loc_BDArmory_part_AWACS_Legless = Legless + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ja.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ja.cfg new file mode 100644 index 000000000..1a49cb2d1 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ja.cfg @@ -0,0 +1,373 @@ +// Notes: +// - The "_tags" entries are simply common search terms for the part filter in the SPH/VAB. Instead of being the same in all languages, they should be whatever a user in that language would search for. + +Localization +{ + ja + { + #loc_BDArmory_modname = BDArmory + + #loc_BDArmory_agent_title = バハムートダイナミクス + #loc_BDArmory_agent_description = 軍用武器・弾薬の大手メーカー。同社は、先進的またはユニークなエンジニアリング ソリューションのために宇宙プログラムと契約することもあります。 + #loc_BDArmory_part_manufacturer = バハムートダイナミクス + + //#loc_BDArmory_agent2_title = ??? Twin Crown Aerospace Industries + + #loc_BDArmory_part_bahaGatlingGun_title = バルカンタレット + #loc_BDArmory_part_bahaGatlingGun_description = 6 バレル 20x102mm 回転砲 + #loc_BDArmory_part_bahaGatlingGun_tags = BDA ガンウィープタレット 20mm ガトリング + + #loc_BDArmory_part_bahaTurret_title = .50cal タレット + #loc_BDArmory_part_bahaTurret_description = デュアルバレルの 50 口径機関銃。 + #loc_BDArmory_part_bahaTurret_tags = BDA 砲塔 .50cal 武器 + + #loc_BDArmory_part_bahaABL_title = USAF 航空機搭載レーザー + #loc_BDArmory_part_bahaABL_description = 物に火をつけるための高出力レーザー。 1秒あたり350の電荷を消費します。 + #loc_BDArmory_part_bahaABL_tags = BDAレーザータレットビームアンチウィープ + + #loc_BDArmory_part_bahaAdjustableRail_title = 調整可能なミサイルレール + #loc_BDArmory_part_bahaAdjustableRail_description = ミサイルを搭載するためのレール。 + #loc_BDArmory_part_bahaAdjustableRail_tags = BDAレールハードポイントミサイルマウント + + #loc_BDArmory_part_bahaAgm86B_title = AGM-86C 巡航ミサイル + #loc_BDArmory_part_bahaAgm86B_description = 長距離、亜音速、空中発射、GPS 誘導巡航ミサイル。このミサイルにはブースターがないため、巡航速度で空中で発射する必要があります + #loc_BDArmory_part_bahaAgm86B_tags = BDA ミサイル GPS 巡航誘導兵器 + + #loc_BDArmory_part_bahaAim120_title = AIM-120 アムラームミサイル + #loc_BDArmory_part_bahaAim120_description = 中距離レーダー誘導ホーミングミサイル。 + #loc_BDArmory_part_bahaAim120_tags = BDA ミサイル レーダー ホーミング A2A ATA 兵器 + + #loc_BDArmory_part_bahaEMP120_title = AIM-120 アムラーム EMP ミサイル + #loc_BDArmory_part_bahaEMP120_description = 最新の小型EMP弾頭を搭載した中距離レーダー誘導ホーミングミサイル。パルス半径は 100 メートルとそれほど大きくありませんが、非常に効果的です。このミサイルは構造的損傷を最小限に抑えますが、爆発範囲内のすべての電子機器を動作不能にします。 + #loc_BDArmory_part_bahaEMP120_tags = BDA ミサイル レーダー ホーミング a2a ata emp 兵器 + + #loc_BDArmory_part_bdPilotAI_title = AI パイロット フライト コンピューター + #loc_BDArmory_part_bdPilotAI_description = 手を使わずに戦闘航空哨戒任務で飛行機を操縦します。飛行機の固有の飛行特性に基づいて値を調整します。エンジンを手動で有効にしてください。ガード モードでは武器マネージャーと連携して動作します (個別に接続して設定します)。 + #loc_BDArmory_part_bdpilotAI_tags = BDAパイロットAIコントロール + + #loc_BDArmory_part_bdDriverAI_title = AI サーフェス操作ドライバー + #loc_BDArmory_part_bdDriverAI_description = 手を使わずに車、戦車、ボートなどを運転して陸地や海上での戦闘や哨戒任務を遂行します。船の固有の特性に基づいて値を調整します。エンジンを手動で有効にしてください。ガード モードでは武器マネージャーと連携して動作します (個別に接続して設定します)。 (実験的) + #loc_BDArmory_part_bdDriverAI_tags = BDA ドライバー AI 制御 Vee 車両地上 + + #loc_BDArmory_part_bdVTOLAI_title = AI垂直離着陸パイロット + #loc_BDArmory_part_bdVTOLAI_desc = 手を使わずに VTOL 航空機 (ヘリコプター、VTOL ジェット、飛行船など) を戦闘および哨戒任務で運転します。船の固有の特性に基づいて値を調整します。エンジンを手動で有効にしてください。ガード モードでは武器マネージャーと連携して動作します (個別に接続して設定します)。 (実験的) + #loc_BDArmory_part_bdVTOLAI_tags = BDA パイロット AI 制御ヘリコプター vtol + + //#loc_BDArmory_part_bdOrbitalAI_title = ??? AI Orbital Pilot + //#loc_BDArmory_part_bdOrbitalAI_desc = ??? Pilots spacecraft in orbit on combat missions without using your hands. Tune the values based on your ship's unique characteristics. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + //#loc_BDArmory_part_bdOrbitalAI_tags = ??? BDA pilot ai control space spacecraft orbiter orbital + + #loc_BDArmory_part_baha20mmAmmo_title = 20mm弾薬箱 + #loc_BDArmory_part_baha20mmAmmo_description = 20x102mm弾が650発入った弾薬箱。 + #loc_BDArmory_part_baha20mmAmmo_tags = BDA 弾薬箱 20mm 弾丸 + + #loc_BDArmory_part_baha25mmAmmo_title = 25mm弾薬箱 + #loc_BDArmory_part_baha25mmAmmo_description = 25x137mm弾が625発入った弾薬箱。 + #loc_BDArmory_part_baha25mmAmmo_tags = BDA 弾薬箱 25mm 弾丸 + + #loc_BDArmory_part_baha30mmAmmo_title = 30mm弾薬箱 + #loc_BDArmory_part_baha30mmAmmo_description = 30x173mm弾が600発入った弾薬箱。 + #loc_BDArmory_part_baha30mmAmmo_tags = BDA 弾薬箱 30mm 弾丸 + + #loc_BDArmory_part_rocket70mmAmmo_title = 70mmロケット弾薬箱 + #loc_BDArmory_part_rocket70mmAmmo_description = 70mmロケット弾48発が入った弾薬箱。 + #loc_BDArmory_part_bahaRocketAmmo_tags = BDA 弾薬箱ロケット ffar + + #loc_BDArmory_part_baha50CalAmmo_title = 50cal弾薬箱 + #loc_BDArmory_part_baha50CalAmmo_description = 1200 .50 cal 弾が入った弾薬箱。 + #loc_BDArmory_part_baha50calAmmo_tags = BDA弾薬箱.50 50cal 12.7発弾 + + #loc_BDArmory_part_UniversalAmmoBoxBDA_title = ユニバーサル弾薬ボックス (レガシー) + #loc_BDArmory_part_UniversalAmmoBoxBDA_description = (旧式 - 使用しないでください - ファイアースピッターが必要です) 入れたい弾薬をすべて含むスケーラブルな弾薬ボックスには、BDAc Extra と関連して KSP で現在使用されている最大 16'1 インチまでのあらゆる種類の弾薬を選択可能な数量で収納できますリクエストに応じてタイプを追加できます (ファンタジー弾薬は使用しないでください) 注: この部分には依然として Fire Spitter が必要であり、下位互換性のためにここにあります。今後は新しい UniversalAmmo パーツを使用してください。 + + #loc_BDArmory_part_BDAcUniversalAmmoBox_title = ユニバーサル弾薬ボックス + #loc_BDArmory_part_BDAcUniversalAmmoBox_description = 入れたい弾薬をすべて含む拡張可能な弾薬ボックスには、BDAc に関連して KSP で現在使用されている最大 16 フィート 1 インチまでのあらゆる弾薬タイプを選択可能な数量で収納できます。ご要望に応じて追加タイプを追加できます (空想弾薬はご遠慮ください) ) + #loc_BDArmory_part_bahaUABAmmo_tags = BDA弾薬箱 + + #loc_BDArmory_part_bahaCannonShellBox_title = 大砲の弾薬箱 + #loc_BDArmory_part_bahaCannonShellBox_description = 大砲の砲弾が 10 発入った弾薬箱。 + #loc_BDArmory_part_bahaCannonAmmo_tags = BDA弾薬箱シェルキャノンタンク + + #loc_BDArmory_part_BD1x1slopeArmor_title = BD 1x1 スロープアーマー + #loc_BDArmory_part_bahaArmor_tags = BDA 装甲板 装甲船 Afv パネル + #loc_BDArmory_part_BD1x1slopeArmor_description = 頑丈な 1x1 の傾斜アーマー プレートで、あらゆる種類のものの構築に最適です。 PSが浮かない + + #loc_BDArmory_part_BD2x1slopeArmor_title = BD 2x1 スロープアーマー + #loc_BDArmory_part_BD2x1slopeArmor_description = 頑丈な 2x1 の傾斜アーマー プレートで、あらゆる種類のものの構築に最適です。 PSが浮かない + + #loc_BDArmory_part_BD1x1panelArmor_title = BD 1x1 パネル アーマー + #loc_BDArmory_part_BD1x1panelArmor_description = 頑丈な 1x1 アーマー プレートで、あらゆる種類のものの構築に最適です。 PSが浮かない + + #loc_BDArmory_part_BD2x1panelArmor_title = BD 2x1 パネル アーマー + #loc_BDArmory_part_BD2x1panelArmor_description = 頑丈な 2x1 アーマー プレートで、あらゆる種類のものの構築に最適です。 PSが浮かない + + #loc_BDArmory_part_BD3x1panelArmor_title = BD 3x1 パネル アーマー + #loc_BDArmory_part_BD3x1panelArmor_description = 頑丈な 3x1 装甲プレートで、あらゆる種類のものの構築に最適です。 PSが浮かない + + #loc_BDArmory_part_BD4x1panelArmor_title = BD 4x1 パネル アーマー + #loc_BDArmory_part_BD4x1panelArmor_description = 頑丈な 4x1 装甲プレートで、あらゆる種類のものの構築に最適です。 PSが浮かない + + #loc_BDArmory_part_awacsRadar_title = AWACS探知レーダー + #loc_BDArmory_part_awacsRadar_description = 遠距離からの物体を探知できる大型レーダー。このレーダーにはターゲットを追跡したりロックしたりする機能はありません。 + #loc_BDArmory_part_awacsRadar_tags = BDA レーダー awac トラック検出 + + #loc_BDArmory_part_bdammGuidanceModule_title = モジュール式ミサイル誘導 (実験的) + #loc_BDArmory_part_bdammGuidanceModule_description = ミサイル誘導コンピューター。クラフトのユニークな飛行特性に合わせてステアリング設定を手動で調整します。ガイダンスモードを選択します。ターゲットを選択して、ガイダンスを有効にします。エンジンとステージを手動でアクティブにします。 (実験的) + #loc_BDArmory_part_bdammGuidanceModule_tags = BDA ミサイル mmg 指導令 + + #loc_BDArmory_part_bahaBrowningAnm2_title = ブローニング .50cal AN/M3 + #loc_BDArmory_part_bahaBrowningAnm2_description = 古い固定式 50 口径機関銃 50 口径弾薬 + #loc_BDArmory_part_bahaBrowningAnm2_tags = BDA ガン .50 口径 50 口径武器 + + #loc_BDArmory_part_bahaClusterBomb_title = CBU-87 クラスターボム + #loc_BDArmory_part_bahaClusterBomb_description = この爆弾は割れて、特定の高度で多数の小さな子弾を展開します。 + #loc_BDArmory_part_bahaClusterBomb_tags = BDA 爆弾令 クラスター UGB ATG A2G 兵器 + + #loc_BDArmory_part_bahaChaffPod_title = チャフディスペンサー + #loc_BDArmory_part_bahaChaffPod_description = レーダーロックを混乱させたり破壊したりするためにチャフをドロップします。 + #loc_BDArmory_part_bahaChaffPod_tags = BDA カウンター cm チャフ + + #loc_BDArmory_part_bahaCmPod_title = フレアディスペンサー + #loc_BDArmory_part_bahaCmPod_description = 混乱を招く熱追尾ミサイル用のフレアを投下します。 + #loc_BDArmory_part_bahaCmPod_tags = BDA カウンター cm フレア + + //#loc_BDArmory_part_bahaDecoyPod_title = ??? Decoy Launcher + //#loc_BDArmory_part_bahaDecoyPod_description = ??? Launches Acoustic Decoys for confusing passive sonar torpedoes. + //#loc_BDArmory_part_bahaDecoyPod_tags = ??? bda counter cm decoy + + //#loc_BDArmory_part_bahaSBTPod_title = ??? Bubble Curtain launcher + //#loc_BDArmory_part_bahaSBTPod_description = ??? Launches bubble curtain countermeasures to degrade enemy active sonar. + + #loc_BDArmory_part_bahaECMJammer_title = AN/ALQ-131 ECM ジャマー + #loc_BDArmory_part_bahaECMJammer_description = この電子デバイスにより、レーダーが車両をロックしにくくなり、ロックが解除される可能性が高まります。 + #loc_BDArmory_part_bahaECMJammer_tags = bda ecm ジャムカウンター cm + + #loc_BDArmory_part_bahaGau-8_title = GAU-8 30x173mm キャノン + #loc_BDArmory_part_bahaGau-8_description = 7砲身30mm回転砲。 + #loc_BDArmory_part_bahaGau-8_tags = BDA ガン 30mm ガトリング ガウ brrt 武器 + + #loc_BDArmory_part_bahaGoalKeeper_title = ゴールキーパーCIWS + #loc_BDArmory_part_bahaGoalKeeper_description = フル旋回範囲を備えた 7 バレル 30mm 回転砲。 30mm 高性能榴弾は設定距離で自爆しますが、この武器には自動信管タイミング機能がありません。独自の探知および追跡レーダーを備えていますが、これは近距離でのみ有効であり、適切なボリューム捜索レーダーに代わるものではありません。 + #loc_BDArmory_part_bahaGoalKeeper_tags = BDA 砲塔 30mm ガトリング ガウ brrt ciws gk weap + + #loc_BDArmory_part_GoalKeeperBDAcMk1_title = ゴールキーパーMk1 CIWS + #loc_BDArmory_part_GoalKeeperBDAcMk1_description = フル旋回範囲を備えた 7 バレル 30mm 回転式大砲。この MK 1 バージョンは泥だらけの野原の防水シートの下で発見されました。資金繰りに苦しむ民兵やずる賢い政府に最適です (チープスケート バージョン)。レーダーや探知装置がなければ、この砲塔には目標情報が必要です。 (誰かが指をさして「撃て」と叫んでいるが、発砲時に発生する過剰な騒音のため、効果はわずかしかないことが判明している) 30mm 高性能榴弾は飛行に興味を失うと自爆するが、これはこの武器には自動信管タイミング機能がありません。 + + #loc_BDArmory_part_BDAcGKmk2_title = ゴールキーパー MK2 CIWS + #loc_BDArmory_part_BDAcGKmk2_description = フル旋回範囲を備えた 7 バレル 30mm 回転砲。この MK 2 バージョンは、旧 KSC の格納庫後部の周囲でオーバースプレーとペイント缶に覆われているのが発見されました。このバージョンは、初期のターゲットポインターの難聴の発生率を減らすために MK1 から開発されました。この MK2 には、赤外線照準とレーダー データ受信機が装備されているため、MK1 よりも若干の利点があります。30x173mm 高性能榴弾は、飛行への興味を失うまでに少なくともわずかに時間がかかるという点で、MK1 弾薬に比べてわずかな改善にすぎません。目標に到達する可能性は十分にありましたが、この武器には自動信管タイミング機能が装備されていませんでした。 + + #loc_BDArmory_part_scanLockRadar1_title = TWSロッキングレーダー + #loc_BDArmory_part_scanLockRadar1_description = このユニットには中距離探知レーダーと目標追跡レーダーが内蔵されています。このレーダーはターゲットをロックすることができ、ロックされたターゲットを追跡しながらスキャンを継続します (TWS - Track While Scan)。空中捜索と追跡に最適化されており、地上目標の検出と追跡が困難です。 + #loc_BDArmory_part_scanLockRadar1_tags = BDA レーダー検出トラックロックスキャン検索 FCS + + #loc_BDArmory_part_scanLargeRadar_title = 大型探知レーダー + #loc_BDArmory_part_scanLargeRadar_description = 遠距離からの物体を探知できる大型レーダー。このレーダーにはターゲットを追跡したりロックしたりする機能はありません。空中捜索に最適化されており、地上目標の探知は困難です。 + #loc_BDArmory_part_scanLargeRadar_tags = BDA レーダー検出スキャン検索 + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = FFAR リロード可能ロケットポッド + #loc_BDArmory_part_F86RL_description = 空対空用に設計された内蔵型ロケットランチャー。 24 個の無誘導折りたたみフィン空中ロケットを保持します。弾薬箱が空の場合は弾薬箱から再装填できます。 + #loc_BDArmory_part_F86RL_tags = BDA ロケット ポッド ランチャー A2A 高射砲リロード ウィープ + + #loc_BDArmory_part_bahaH70Launcher_title = ヒドラ-70 ロケット ポッド + #loc_BDArmory_part_bahaH70Launcher_description = 19 発の無誘導ハイドラ 70 ロケットを保持し、発射します。 + #loc_BDArmory_part_bahaH70Launcher_tags = BDAロケットポッドランチャーA2G兵器 + + #loc_BDArmory_part_bahaH70Turret_title = Hydra-70 ロケット砲塔 + #loc_BDArmory_part_bahaH70Turret_description = 32 基の無誘導ハイドラ 70 ロケットを保持し発射するタレット ポッド。 + #loc_BDArmory_part_bahaH70Turret_tags = BDA ロケット ポッド ランチャー A2G タレット ウィープ + + #loc_BDArmory_part_bahaHarm_title = AGM-88 ハームミサイル + #loc_BDArmory_part_bahaHarm_description = 高速対放射線ミサイル。このミサイルは、レーダー警報受信機によって検出されたレーダー発信源を狙います。 + #loc_BDArmory_part_bahaHarm_tags = BDA ミサイル対放射線ホーミング AGM ATG 令兵器 + + //HEKV Missile + #loc_BDArmory_part_bahaHEKV1_title = HE-KV-1 ミサイル + #loc_BDArmory_part_bahaHEKV1_description = HE-KV-1 (高性能爆発物破壊車両) は、反応制御スラスターと推力偏向を使用して操縦するレーダー誘導ホーミング ミサイルです。これは、真空中でも目標に向かって操縦できることを意味します。 + #loc_BDArmory_part_bahaHEKV1_tags = BDA ミサイル レーダー ホーミング ata a2a 令 rcs 宇宙兵器 + + //KKV Missile + //#loc_BDArmory_part_bahaKKV_title = ??? Kinetic Kill Vehicle + //#loc_BDArmory_part_bahaKKV_description = ??? The KKV (kinetic kill vehicle) is a IR-guided homing missile that uses reaction control thrusters and a control moment gyroscope to maneuver. It is capable of steering towards targets in a vacuum and has high drag in atmosphere. The KKV relies on kinetic energy to destroy its target and carries no explosives. 6 km/s delta-V. + //#loc_BDArmory_part_bahaKKV_tags = ??? BDA missile radar homing ata a2a ordnance orbital rcs space weap kinetic + + #loc_BDArmory_part_bahaAGM-114_title = AGM-114 ヘルファイアミサイル + #loc_BDArmory_part_bahaAGM-114_description = 小型で高速なレーザー誘導ホーミングミサイル。 + #loc_BDArmory_part_bahaAGM-114_tags = BDA ミサイル レーザー ホーミング ATG AGM 令兵器 + + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R ヘルファイア II EMP + #loc_BDArmory_part_bahaAGM-114_EMP_description = 最新の小型EMP弾頭を搭載した小型、高速のレーザー誘導ホーミングミサイル。パルス半径は小さい (50 メートル) ものの、非常に効果的です。このミサイルは構造的損傷を最小限に抑えますが、爆発範囲内のすべての電子機器を動作不能にします。 + #loc_BDArmory_part_bahaAGM-114_EMP_tags = BDA ミサイル レーザー ホーミング ATG AGM 令 EMP 兵器 + + #loc_BDArmory_part_bahaHiddenVulcan_title = バルカン (非表示) + #loc_BDArmory_part_bahaHiddenVulcan_description = 6 バレル 20x102mm 回転砲。 20x102弾薬 + #loc_BDArmory_part_bahaHiddenVulcan_tags = BDAガン20mmガトリングウェップ + + #loc_BDArmory_part_bahaJdamMk83_title = Mk83 JDAM 爆弾 + #loc_BDArmory_part_bahaJdamMk83_description = 1000ポンドのGPS誘導爆弾。 + #loc_BDArmory_part_bahaJdamMk83_tags = BDA 爆弾 GPS 条例 ATG ホーミング ガイド 武器 + + #loc_BDArmory_part_bahaM102Howitzer_title = M102 榴弾砲 (ラジアル) + #loc_BDArmory_part_bahaM102Howitzer_description = 放射状に装備された 105mm 砲。大砲の砲弾 + #loc_BDArmory_part_bahaM102Howitzer_tags = BDA ガンキャノンタレットシェルハウイーウィープ + + #loc_BDArmory_part_bahaM1Abrams_title = M1 エイブラムス・キャノン + #loc_BDArmory_part_bahaM1Abrams_description = 装甲砲塔に 120mm 砲を搭載。大砲の砲弾 + #loc_BDArmory_part_bahaM1Abrams_tags = BDA ガンキャノン砲塔砲弾タンク武器 + + #loc_BDArmory_part_bahaM230ChainGun_title = M230 チェーンガンタレット + #loc_BDArmory_part_bahaM230ChainGun_description = M230 チェーン ガンは、30x173 弾の高性能榴弾を発射できる単筒自動大砲です。攻撃ヘリコプターでよく使われています。 + #loc_BDArmory_part_bahaM230ChainGun_tags = BDA ガン チェーンガン タレット 30mm ヘリ ウィープ + + #loc_BDArmory_part_bahaAGM-65_title = AGM-65 マーベリック ミサイル + #loc_BDArmory_part_bahaAGM-65_description = 中出力のレーザー誘導空対地ミサイル。 + #loc_BDArmory_part_bahaAGM-65_tags = BDA ミサイル レーザー令 atg ホーミング ガイド 武器 + + #loc_BDArmory_part_missileTurretTest_title = ジャーナスミサイルタレット + #loc_BDArmory_part_missileTurretTest_description = 最大8発の中小型ミサイルを搭載・発射できる砲塔。統合された検出および追跡レーダーが付属しています。ミサイル以外を搭載した場合は保証対象外となります。砲塔を有効にするには、武器マネージャーから搭載されたミサイルを選択します。 + #loc_BDArmory_part_missileTurretTest_tags = BDA ミサイル砲塔発射レールマウントハードポイントレーダーロック + + #loc_BDArmory_part_bahaMk82Bomb_title = Mk82 爆弾 + #loc_BDArmory_part_bahaMk82Bomb_description = 500ポンドの無誘導爆弾。 + #loc_BDArmory_part_bahaMk82Bomb_tags = BDA爆弾UGB条例ATG武器 + + #loc_BDArmory_part_bahaMk82BombBrake_title = Mk82 スネークアイ爆弾 + #loc_BDArmory_part_bahaMk82BombBrake_description = エアブレーキ付きの500ポンド無誘導爆弾。低空爆撃に使用します。 + + #loc_BDArmory_part_bahaOMillennium_title = エリコン ミレニアム キャノン + #loc_BDArmory_part_bahaOMillennium_description = 時限爆発爆発弾を発射する砲塔。近接防空に適しています。バレルの銃口端にある装置は、各弾丸が発射される際の正確な速度を測定し、ターゲットから事前に設定された距離に近づくと自動的に信管を設定して弾丸を爆発させます。 30x173弾薬を使用 + #loc_BDArmory_part_bahaOMillennium_tags = BDA 砲塔 CIWS 高射砲機関砲 エリコン クラム 30mm ウィープ + + #loc_BDArmory_part_bahaPac-3_title = PAC-3迎撃ミサイル + #loc_BDArmory_part_bahaPac-3_description = 中距離、高速、レーダー誘導地対空ミサイル。 + #loc_BDArmory_part_bahaPac-3_tags = BDA ミサイルレーダー令 ATA A2A ホーミングガイド サール サム ウィープ + + #loc_BDArmory_part_patriotLauncherTurret_title = パトリオットランチャータレット + #loc_BDArmory_part_patriotLauncherTurret_description = 最大 16 発の PAC-3 ミサイル (キャニスターごとに 4 発) を保持および発射できる砲塔。ミサイル以外を搭載した場合は保証対象外となります。砲塔を有効にするには、武器マネージャーから搭載されたミサイルを選択します。 + #loc_BDArmory_part_patriotLauncherTurret_tags = BDA ミサイル砲塔 発射レールマウント ハードポイント サム + + #loc_BDArmory_part_radarDataReceiver_title = レーダーデータ受信機 + #loc_BDArmory_part_radarDataReceiver_description = データリンクを介してレーダー接触を表示し、リモートレーダーを介してターゲットをロックできるモジュールですが、単独でスキャンまたはロックすることはできません。隠しミサイル砲台として役立ちます。 + #loc_BDArmory_part_radarDataReceiver_tags = BDA レーダー検出リンク データ ロック + + //AN/APG-63 Variants + //#loc_BDArmory_part_bdRadome_variantPitot = ??? Pitot Tube + //#loc_BDArmory_part_bdRadome_variantNoPitot = ??? No Pitot Tube + #loc_BDArmory_part_bdRadome1_title = AN/APG-63 レドーム + #loc_BDArmory_part_bdRadome1_description = 空気力学的に考慮された前向きのレーダー。 120度の視野内でターゲットをスキャンしてロックできます。空対空戦闘に最適化されており、地上目標をロックするのは困難です。 + #loc_BDArmory_part_bdRadome1_tags = BDA レーダー レドーム検出ロック トラック スキャン + #loc_BDArmory_part_bdRadome1inline_title = AN/APG-63 インライン レドーム + #loc_BDArmory_part_bdRadome1inline_description = 空気力学的に考慮された前向きのレーダー。 120度の視野内でターゲットをスキャンしてロックできます。黒いマークが前方を向いていることを確認してください。空対空戦闘に最適化されており、地上目標をロックするのは困難です。 + #loc_BDArmory_part_bdRadome1snub_title = AN/APG-63 レドーム + #loc_BDArmory_part_bdRadome1snub_description = 空気力学的に考慮された前向きのレーダー。 120度の視野内でターゲットをスキャンしてロックできます。これは地上攻撃専用バージョンで、地上目標に対するパフォーマンスははるかに優れていますが、空対空能力は低下しています。 + + #loc_BDArmory_part_bahaRBS-15Cruise_title = RBS-15 巡航ミサイル + #loc_BDArmory_part_bahaRBS-15Cruise_description = ブースターを備えた長距離、マルチプラットフォーム高速巡航ミサイル。 + #loc_BDArmory_part_bahaRBS-15Cruise_tags = BDA ミサイル令 巡航 GPS ガイド ホーミング武器 + + #loc_BDArmory_part_bahaRBS-15ALCruise_title = RBS-15 航空発射巡航ミサイル + #loc_BDArmory_part_bahaRBS-15ALCruise_description = 長距離、マルチプラットフォーム高速巡航ミサイル外部ブースターのない空中発射型 + + #loc_BDArmory_part_bdRotBombBay_title = 調整可能な回転式爆弾ラック + #loc_BDArmory_part_bdRotBombBay_description = 調整可能な回転爆弾ラック。黄色の矢印は武器を解放する方向を指しているはずです。ミサイルか爆弾のみ。レールごとに 1 つだけ。 + #loc_BDArmory_part_bdRotBombBay_tags = BDA ミサイルレール発射マウントロータリーラック + #loc_BDArmory_part_bahaS-8Launcher_title = S-8KOM ロケットポッド + #loc_BDArmory_part_bahaS-8Launcher_description = 無誘導 S-8KOM ロケット弾を 23 発保持して発射します。空気力学的ノーズコーンを備えています。 + + #loc_BDArmory_part_bahaAim9_title = AIM-9 サイドワインダー ミサイル + #loc_BDArmory_part_bahaAim9_description = 短距離熱追尾ミサイル。 + #loc_BDArmory_part_bahaAim9_tags = BDA ミサイル令 ヒーター ヒートシーク ATA A2A AAM 武器 + + #loc_BDArmory_part_bdWarheadSmall_title = 小型高性能爆発性弾頭 + #loc_BDArmory_part_bdWarheadSmall_description = 爆発物が詰め込まれたミサイルノーズコーン。 + #loc_BDArmory_part_bdWarheadSmall_tags = BDA ミサイル爆弾令 ブーム兵器 + + #loc_BDArmory_part_bahaSmokeCmPod_title = 煙対策ポッド + #loc_BDArmory_part_bahaSmokeCmPod_description = レーザーポイントを遮るための煙幕対策を発射します。 + #loc_BDArmory_part_bahaSmokeCmPod_tags = BDA cm カウンタースモーク + + #loc_BDArmory_part_bahaFlirBall_title = FLIR ターゲティング ボール + #loc_BDArmory_part_bahaFlirBall_description = ターゲット設定と監視に使用されるボール カメラ。地表および地平線安定化機能を備えた高解像度カメラと、ターゲットをペイントするための赤外線レーザーを備えたこのポッドにより、ミサイルの地上ターゲットを迅速に見つけてロックすることができます。 + + #loc_BDArmory_part_bahaCamPod_tags = BDA 検出レーザー GPS カム flir ターゲット + #loc_BDArmory_part_bahaCamPod_title = AN/AAQ-28 ターゲティングポッド + #loc_BDArmory_part_bahaCamPod_description = 照準と監視に使用される照準ポッド。地表および地平線安定化機能を備えた高解像度カメラと、ターゲットをペイントするための赤外線レーザーを備えたこのポッドにより、ミサイルの地上ターゲットを迅速に見つけてロックすることができます。 + + #loc_BDArmory_part_bahaIRSTPod_title = AN/AAQ-42 IRST ポッド + #loc_BDArmory_part_bahaIRSTPod_description = 空力ポッドに収納された前向きの赤外線探索追跡システム。 120 度の視野内で熱痕跡をスキャンして検出できます。空対空での使用に最適化されており、地表目標の検出は困難です。 + #loc_BDArmory_part_bahaIRSTPod_tags = BDA 検出熱スキャン検索トラック IR サーム + + #loc_BDArmory_part_towLauncherTurret_title = 牽引ランチャー + #loc_BDArmory_part_towLauncherTurret_description = 最大4発のTOWミサイルを搭載し発射できる砲塔。 TOWミサイル以外が搭載されている場合は保証対象外となります。砲塔を有効にするには、武器マネージャーから搭載されたミサイルを選択します。 + #loc_BDArmory_part_towLauncherTurret_tags = BDA ミサイル砲塔レールマウント発射牽引 + + #loc_BDArmory_part_bahaTowMissile_title = BGM-71 牽引ミサイル + #loc_BDArmory_part_bahaTowMissile_description = 短距離、レーザービーム搭載、無線対戦車ミサイル。 + #loc_BDArmory_part_bahaTowMissile_tags = BDA ミサイル令 レーザー AGM ATG 兵器 + + #loc_BDArmory_part_missileController_title = 武器マネージャー + #loc_BDArmory_part_missileController_description = ミサイル/爆弾を循環させ、ボタン 1 つで発射します。 + #loc_BDArmory_part_missileController_tags = bda wm ai ウィープ + + #loc_BDArmory_part_BDAsonarPod1A_title = BDA MK1 ソナー ポッド + #loc_BDArmory_part_BDAsonarPod1A_description = BDA MK1 ソナー ポッドは、最良の結果を得るために、水面下に設置された水しぶきや水没した船舶のみを検出できます。船体搭載ソナーとしては、範囲と感度が限られています。 + #loc_BDArmory_part_BDAsonarPod1A_tags = BDAはソナー船を検出します + + #loc_BDArmory_part_StingRayBDATorpedo_title = スティング レイ BDA 軽量魚雷 + #loc_BDArmory_part_StingRayBDATorpedo_description = スティング レイの軽量魚雷船の発射やヘリによる空中投下は潜水艦では使用されません。興味深い事実は、パック ランチャーにこれらを 16 個取り付けることができますが、適切な訓練なしにそのような装置で使用すると、多くの泣き声と手紙の原因となっています。 + #loc_BDArmory_part_StingRayBDATorpedo_tags = BDA ミサイル令 ASM トープシップ ソナー ホーミング ガイド S2S STS + + //#loc_BDArmory_part_EJ200_title = ??? TFJ-EJ200 "Typhoon" Afterburning Turbofan + //#loc_BDArmory_part_EJ200_description = ??? Word is that this engine was the result of international cooperation, which produced an engine with exceptional performance and potential. + + #loc_BDArmory_part_SaturnAL31_title = サターン AL-31FM1 アフターバーニングジェットエンジン + #loc_BDArmory_part_SaturnAL31_description = 可変幾何学推力偏向ノズルと追加推力のためのアフターバーナーを備えた高性能ジェット エンジン。 KTech のエンジニアは、非常に人気の高い J-404 エンジンをベースとして、この商用型を軍事用の強力な動力装置に (高度に) 改造できる可能性があると考えました。このエンジンの可能性を見たBDAcグループは、すぐに新しいMkIIIテストドローン用にライセンスを取得しました。 + #loc_BDArmory_part_SaturnAL31_tags = 航空機バーナーエンジン後 戦闘機 土星飛行機 推進 AL + + //GravityGun + #loc_BDArmory_part_GravGun_title = ゼロポイントエネルギーフィールドマニピュレーター + #loc_BDArmory_part_GravGun_description = 重力を武器にした非常にハイテクな銃。テクノソーサリーとエイリアンのテクノロジーを組み合わせたものと思われるこの兵器は、ターゲットに非ニュートン力を加え、見かけの質量に影響を与えることができます。 Bahamuto Dynamics は、この武器によって被った損害 (あなた自身、財産、または現実構造への) に対して責任を負いません。 + #loc_BDArmory_part_GravGun_tags = BDAレーザーガンウィープグラブ + + #loc_BDArmory_part_genie_title = AIR-2 ジーニー空対空ロケット + #loc_BDArmory_part_genie_description = 1.5kt核対空ロケット。 + #loc_BDArmory_part_genie_tags = BDA ミサイル核 ata a2a + + #loc_BDArmory_part_GAU22_title = GAU-22/A 25x137mm キャノン + #loc_BDArmory_part_GAU22_description = 4連装25mm回転​​砲。 25x137mm弾薬。 + #loc_BDArmory_part_GAU22_tags = BDA ガンウェップ 25mm ガトリングガウ + + #loc_BDArmory_part_sidam_title = シダム対空砲 + #loc_BDArmory_part_sidam_description = 一斉射撃可能な四連装 25mm 対空砲。 25x137mm弾薬。 + #loc_BDArmory_part_sidam_tags = BDA ガンウィープ 25mm 砲塔対空砲 + + #loc_BDArmory_part_REA_title = BD 1x0.5 リアクティブアーマー + #loc_BDArmory_part_REA_Panel_description = Reactive Armor セクションの 1x0.5m セクション。既存の防具の上にさらに保護を追加するのに最適です。 + #loc_BDArmory_part_REA_Panel_tags = BDA アーマーパネル時代の反応 + + #loc_BDArmory_part_Panel_title = BD アーマーパネル + #loc_BDArmory_part_Panel_description = さまざまなサイズに構成でき、さまざまな素材を使用できる頑丈なユニバーサル構造パネルで、あらゆる種類のものの建設や装甲に最適です。 + + #loc_BDArmory_part_TriPanel_title = BD アーマーパネル直角三角形 + #loc_BDArmory_part_TriIsoPanel_title = BD アーマーパネル 斜三角形 + #loc_BDArmory_part_Tripanel_description = さまざまなサイズに構成でき、さまざまな素材を使用できる頑丈なユニバーサル構造パネルで、あらゆる種類のものの建設や装甲に最適です。こちらは三角形ですね。 + + #loc_BDArmory_part_BombBay_title = アニュメント・ベイ + #loc_BDArmory_part_BombBay_description = 展開可能な令ラックを備えた武器庫。ペイロードは展開されるまで気流から保護されます。 + #loc_BDArmory_part_BombBay_tags = BDAミサイル令ベイレール展開ラック + + #loc_BDArmory_part_combatSeat_title = EAS-2 外部戦闘シート + #loc_BDArmory_part_combatSeat_description = 統合されたパイロット AI と武器マネージャーを含む指揮席により、着座したカーバルによって制御される航空機が手を使わずに戦闘航空哨戒任務で飛行機を操縦できるようになります。飛行機の固有の飛行特性に基づいて値を調整します。エンジンを手動で有効にしてください。 + #loc_BDArmory_part_combatSeat_tags = BDA AI WM Weap パイロットシートチェア + + //AN/APG-77v1 ATG Radar + #loc_BDArmory_part_bdRadome1snub_ground_title = APG-77 空対地レーダー (スナブ) + #loc_BDArmory_part_bdRadome1inline_ground_title = APG-77v1 空対地レーダー (インライン) + #loc_BDArmory_part_bdRadome1_ground_title = APG-77v1 空対地レーダー + #loc_BDArmory_part_bdRadome1_Gnd_desc = AN/APG-77v1 は、前向きの、空気力学的に収容されたソリッドステートのアクティブ電子スキャン アレイ (AESA) レーダーです。 120 度の視野内の静止目標および移動目標に対して最大 40 km の動作範囲で完全な空対地機能を提供します。この特定のユニットは地上戦闘用に最適化されており、航空目標をロックするのが困難です。 + #loc_BDArmory_part_bdRadome1_Gnd_tags = BDAはレーダースキャントラックグラウンドを検出します + + #loc_BDArmory_part_AWACS_Legged = 脚付き + #loc_BDArmory_part_AWACS_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..5b4f4d835 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ru.cfg @@ -0,0 +1,373 @@ +// Notes: +// - The "_tags" entries are simply common search terms for the part filter in the SPH/VAB. Instead of being the same in all languages, they should be whatever a user in that language would search for. + +Localization +{ + ru + { + #loc_BDArmory_modname = BD Armory + + #loc_BDArmory_agent_title = Bahamuto Dynamics + #loc_BDArmory_agent_description = Ведущий производитель вооружения и военного оборудования. Компания также переодически сотрудничает с космическими программами для передовых или уникальных инженерных решений. + #loc_BDArmory_part_manufacturer = Bahamuto Dynamics + + //#loc_BDArmory_agent2_title = ??? Twin Crown Aerospace Industries + + #loc_BDArmory_part_bahaGatlingGun_title = Турель Vulcan + #loc_BDArmory_part_bahaGatlingGun_description = 6-ствольная роторная пушка калибра 20x102 мм. + //#loc_BDArmory_part_bahaGatlingGun_tags = ??? BDA gun weap turret 20mm gatling + + #loc_BDArmory_part_bahaTurret_title = Пулемет .50cal + #loc_BDArmory_part_bahaTurret_description = Двухствольный пулемет 50-го калибра. + //#loc_BDArmory_part_bahaTurret_tags = ??? BDA gun turret .50cal weap + + #loc_BDArmory_part_bahaABL_title = Бортовой Лазер USAF + #loc_BDArmory_part_bahaABL_description = Мощный лазер, способный плавить предметы. Потребляет 350 ед. электического заряда в секунду. + //#loc_BDArmory_part_bahaABL_tags = ??? BDA laser turret beam anti weap + + #loc_BDArmory_part_bahaAdjustableRail_title = Регулируемая Ракетная Направляющая + #loc_BDArmory_part_bahaAdjustableRail_description = Направляющая для установки ракет. + //#loc_BDArmory_part_bahaAdjustableRail_tags = ??? BDA rail hardpoint missile mount + + #loc_BDArmory_part_bahaAgm86B_title = Крылатая Ракета AGM-86C + #loc_BDArmory_part_bahaAgm86B_description = Дозвуковая крылатая ракета дальнего действия воздушного базирования с GPS-наведением. У этой ракеты нет ускорителей, так что она должна быть запущена в воздухе на крейсерской скорости. + //#loc_BDArmory_part_bahaAgm86B_tags = ??? BDA missile GPS cruise guided weap + + #loc_BDArmory_part_bahaAim120_title = Ракета AIM-120 AMRAAM + #loc_BDArmory_part_bahaAim120_description = Самонаводящаяся ракета средней дальности с радиолокационным наведением. + //#loc_BDArmory_part_bahaAim120_tags = ??? BDA missile radar homing a2a ata ordnance weap + + #loc_BDArmory_part_bahaEMP120_title = Ракета AIM-120 AMRAAM EMP + #loc_BDArmory_part_bahaEMP120_description = Самонаводящаяся ракета средней дальности с радиолокационным наведением, оснащенная новейшей миниатюрной ЭМИ-боеголовкой. Имея небольшой радиус импульса (100 метров), она достаточно эффективна. Эта ракета наносит минимальный физический ущерб, но выводит из строя всю электронику в радиусе действия. + //#loc_BDArmory_part_bahaEMP120_tags = ??? BDA missile radar homing a2a ata emp ordnance weap + + #loc_BDArmory_part_bdPilotAI_title = ИИ Автопилот + #loc_BDArmory_part_bdPilotAI_description = Управляет вашим самолетом во время боевых вылетов. Настройте значения, основываясь на уникальных ЛТХ вашего самолета. Пожалуйста, активируйте двигатели вручную. Работает совместно с контроллером вооружения в боевом режиме (присоединяется и настраивается отдельно). (EXPERIMENTAL) + //#loc_BDArmory_part_bdpilotAI_tags = ??? BDA pilot ai control + + #loc_BDArmory_part_bdDriverAI_title = ИИ Автопилот (Поверхность) + #loc_BDArmory_part_bdDriverAI_description = Управляет вашей машиной, танком, кораблем и т.д. на боевых миссиях на земле и на воде. Настройте значения, основываясь на уникальных характеристиках вашего аппарата. Пожалуйста, активируйте двигатели вручную. Работает совместно с контроллером вооружения в боевом режиме (присоединяется и настраивается отдельно). (EXPERIMENTAL) + //#loc_BDArmory_part_bdDriverAI_tags = ??? BDA driver ai control vee vehicle ground + + //#loc_BDArmory_part_bdVTOLAI_title = ??? AI Vertical Takeoff and Landing Pilot + //#loc_BDArmory_part_bdVTOLAI_desc = ??? Drives your VTOL craft (i.e. helicopters, VTOL jets, airships) on combat and patrol missions 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_bdVTOLAI_tags = ??? BDA pilot ai control helo heli copter vtol + + //#loc_BDArmory_part_bdOrbitalAI_title = ??? AI Orbital Pilot + //#loc_BDArmory_part_bdOrbitalAI_desc = ??? Pilots spacecraft in orbit on combat missions without using your hands. Tune the values based on your ship's unique characteristics. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + //#loc_BDArmory_part_bdOrbitalAI_tags = ??? BDA pilot ai control space spacecraft orbiter orbital + + #loc_BDArmory_part_baha20mmAmmo_title = 20мм Ящик Боеприпасов + #loc_BDArmory_part_baha20mmAmmo_description = Ящик с боеприпасами, содержащий 650 патронов калибра 20х102 мм. + //#loc_BDArmory_part_baha20mmAmmo_tags = ??? BDA ammo box 20mm rounds bullet + + #loc_BDArmory_part_baha25mmAmmo_title = 25мм Ящик Боеприпасов + #loc_BDArmory_part_baha25mmAmmo_description = Ящик с боеприпасами, содержащий 625 патронов калибра 25х137 мм. + //#loc_BDArmory_part_baha25mmAmmo_tags = ??? BDA ammo box 25mm rounds bullet + + #loc_BDArmory_part_baha30mmAmmo_title = 30мм Ящик Боеприпасов + #loc_BDArmory_part_baha30mmAmmo_description = Ящик с боеприпасами, содержащий 600 патронов калибра 30x173 мм. + //#loc_BDArmory_part_baha30mmAmmo_tags = ??? BDA ammo box 30mm rounds bullet + + #loc_BDArmory_part_rocket70mmAmmo_title = 70мм Ракетный Ящик Боеприпасов + #loc_BDArmory_part_rocket70mmAmmo_description = Ящик с боеприпасами, содержащий 48 ракет калибра 70 мм. + //#loc_BDArmory_part_bahaRocketAmmo_tags = ??? BDA ammo box rocket ffar + + #loc_BDArmory_part_baha50CalAmmo_title = 50cal Ящик Боеприпасов + #loc_BDArmory_part_baha50CalAmmo_description = Ящик с боеприпасами, содержащий 1200 патронов .50 cal. + //#loc_BDArmory_part_baha50calAmmo_tags = ??? BDA ammo box .50 50cal 12.7 rounds bullet + + #loc_BDArmory_part_UniversalAmmoBoxBDA_title = Универсальный Ящик Боеприпасов (Legacy) + #loc_BDArmory_part_UniversalAmmoBoxBDA_description = (Устарел - НЕ РЕКОМЕНДУЕТСЯ К ИСПОЛЬЗОВАНИЮ - Требуется Fire Spitter) Изменяемая коробка с боеприпасами, содержащая любые боеприпасы, которые вы хотите в нее положить, содержит выбранное количество каждого типа боеприпасов до 16'1 дюйма, который в настоящее время используется в KSP совместно с BDA. Дополнительные типы могут быть добавлены по запросу (пожалуйста, без вымышленных патронов) ПРИМЕЧАНИЕ: для этой части по-прежнему требуется FireSpitter, и она приведена здесь для обеспечения обратной совместимости. В дальнейшем используйте новый универсальный ящик боеприпасов. + + #loc_BDArmory_part_BDAcUniversalAmmoBox_title = Универсальный Ящик Боеприпасов + #loc_BDArmory_part_BDAcUniversalAmmoBox_description = Изменяемая коробка с боеприпасами, содержащая любые боеприпасы, которые вы хотите в нее положить, содержит выбранное количество каждого типа боеприпасов до 16'1 дюйма, который в настоящее время используется в KSP совместно с BDA. Дополнительные типы могут быть добавлены по запросу (пожалуйста, без вымышленных патронов) + //#loc_BDArmory_part_bahaUABAmmo_tags = ??? BDA ammo box + + #loc_BDArmory_part_bahaCannonShellBox_title = Ящик Артиллерийских Боеприпасов + #loc_BDArmory_part_bahaCannonShellBox_description = Ящик с боеприпасами, содержащий 10 артиллерийских снарядов. + //#loc_BDArmory_part_bahaCannonAmmo_tags = ??? BDA ammo box shell cannon tank + + #loc_BDArmory_part_BD1x1slopeArmor_title = Наклонная Броня BD 1x1 + //#loc_BDArmory_part_bahaArmor_tags = ??? BDA armor plate Armo Ship Afv panel + #loc_BDArmory_part_BD1x1slopeArmor_description = Прочная наклонная бронированная панель 1х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + #loc_BDArmory_part_BD2x1slopeArmor_title = Наклонная Броня BD 2x1 + #loc_BDArmory_part_BD2x1slopeArmor_description = Прочная наклонная бронированная панель 2х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + #loc_BDArmory_part_BD1x1panelArmor_title = Бронированная панель BD 1x1 + #loc_BDArmory_part_BD1x1panelArmor_description = Прочная бронированная панель 1х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + #loc_BDArmory_part_BD2x1panelArmor_title = Бронированная панель BD 2x1 + #loc_BDArmory_part_BD2x1panelArmor_description = Прочная бронированная панель 2х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + #loc_BDArmory_part_BD3x1panelArmor_title = Бронированная панель BD 3x1 + #loc_BDArmory_part_BD3x1panelArmor_description = Прочная бронированная панель 3х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + #loc_BDArmory_part_BD4x1panelArmor_title = Бронированная панель BD 4x1 + #loc_BDArmory_part_BD4x1panelArmor_description = Прочная бронированная панель 4х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + #loc_BDArmory_part_awacsRadar_title = Радар Обнаружения AWACS + #loc_BDArmory_part_awacsRadar_description = Большой радар, способный обнаруживать объекты на большом расстоянии. Этот радар не имеет возможности сопровождать или захватывать цели. + //#loc_BDArmory_part_awacsRadar_tags = ??? bda radar awac track detect + + #loc_BDArmory_part_bdammGuidanceModule_title = Наведение Модульной Ракеты (EXPERIMENTAL) + #loc_BDArmory_part_bdammGuidanceModule_description = Компьютер наведения ракеты. Вручную настройте рулевое управление, основываясь на уникальных характеристиках ракеты. Выберите тип наведения. Выберите цель, затем включите наведение. Активируйте двигатели и ступени вручную. (EXPERIMENTAL) + //#loc_BDArmory_part_bdammGuidanceModule_tags = ??? bda missile mmg guid ordnance + + #loc_BDArmory_part_bahaBrowningAnm2_title = .50cal AN/M3 Браунинга + #loc_BDArmory_part_bahaBrowningAnm2_description = Старый .50 cal пулемет. + //#loc_BDArmory_part_bahaBrowningAnm2_tags = ??? bda gun .50 cal 50cal weap + + #loc_BDArmory_part_bahaClusterBomb_title = Кластерная Бомба CBU-87 + #loc_BDArmory_part_bahaClusterBomb_description = Эта бомба раскрывается и выпускает множество маленьких снарядов на определенной высоте. + //#loc_BDArmory_part_bahaClusterBomb_tags = ??? bda bomb ordnance cluster ugb atg a2g weap + + #loc_BDArmory_part_bahaChaffPod_title = Раздатчик Дипольных Отражателей + #loc_BDArmory_part_bahaChaffPod_description = Сбрасывает отражатели для запутывания ракет с радиолокационным наведением. + //#loc_BDArmory_part_bahaChaffPod_tags = ??? bda counter cm chaff + + #loc_BDArmory_part_bahaCmPod_title = Раздатчик Тепловых Ловушек + #loc_BDArmory_part_bahaCmPod_description = Сбрасывает ловушки для запутывания ракет с тепловым наведением. + //#loc_BDArmory_part_bahaCmPod_tags = ??? bda counter cm flare + + //#loc_BDArmory_part_bahaDecoyPod_title = ??? Decoy Launcher + //#loc_BDArmory_part_bahaDecoyPod_description = ??? Launches Acoustic Decoys for confusing passive sonar torpedoes. + //#loc_BDArmory_part_bahaDecoyPod_tags = ??? bda counter cm decoy + + //#loc_BDArmory_part_bahaSBTPod_title = ??? Bubble Curtain launcher + //#loc_BDArmory_part_bahaSBTPod_description = ??? Launches bubble curtain countermeasures to degrade enemy active sonar. + + #loc_BDArmory_part_bahaECMJammer_title = AN/ALQ-131 РЭБ-Глушитель + #loc_BDArmory_part_bahaECMJammer_description = Это электронное устройство затрудняет радарное наведение на ваш транспорт и увеличивает ваши шансы сбросить захват. + //#loc_BDArmory_part_bahaECMJammer_tags = ??? bda ecm jamm counter cm + + #loc_BDArmory_part_bahaGau-8_title = GAU-8 30x173мм Пулемет + #loc_BDArmory_part_bahaGau-8_description = 7-ствольная 30-мм роторный пулемет. + //#loc_BDArmory_part_bahaGau-8_tags = ??? bda gun 30mm gatling gau brrt weap + + #loc_BDArmory_part_bahaGoalKeeper_title = Goalkeeper CIWS + #loc_BDArmory_part_bahaGoalKeeper_description = 7-ствольная 30-мм поворотная пушка с полным диапазоном поворота. 30-миллиметровые осколочно-фугасные снаряды самоподрываются на заданном расстоянии, но это оружие не имеет функции автоматического выбора времени срабатывания предохранителя. У него есть свой собственный радар обнаружения и слежения, но он эффективен только на ближнем расстоянии и не заменяет надлежащий поисковый радар. + //#loc_BDArmory_part_bahaGoalKeeper_tags = ??? bda gun turret 30mm gatling gau brrt ciws gk weap + + #loc_BDArmory_part_GoalKeeperBDAcMk1_title = Goalkeeper Mk1 CIWS + #loc_BDArmory_part_GoalKeeperBDAcMk1_description = 7-ствольная 30-мм поворотная пушка с полным диапазоном поворота. Версию MK1 мы нашли под брезентом в поле. Идеально подходит для стесненных в средствах ополченцев и изворотливых правительств (версия для скупердяев) Не имея радара, эта турель требует получение информации о цели из альтернативного источника (кто-то, указывающий пальцем и кричащий "стреляй в это", оказался малоэффективным из-за чрезмерного шума, производимого при стрельбе из оружия). 30-миллиметровые осколочно-фугасные снаряды самоподрываются на заданном расстоянии, но это оружие не имеет функции автоматического выбора времени срабатывания предохранителя. + + #loc_BDArmory_part_BDAcGKmk2_title = Goalkeeper MK2 CIWS + #loc_BDArmory_part_BDAcGKmk2_description = 7-ствольная 30-мм поворотная пушка с полным диапазоном поворота. Версию MK2 мы нашли в спрее и банках из-под краски за ангаром в старом космическом центре. Разработан на основе MK1 для снижения частоты потери отклика у ранних целеуказателей. MK2 имеет некоторые небольшие преимущества по сравнению с MK1, оснащен инфракрасным прицеливанием и приемником радиолокационных данных. Осколочно-фугасные патроны 30x173 мм являются лишь небольшим улучшением по сравнению с боеприпасами MK1 в том смысле, что им, по крайней мере, требуется немного больше времени, чтобы потерять интерес к полету, и поэтому у них есть хорошие шансы достичь цели, но это оружие никогда не оснащалось функцией автоматического выбора времени срабатывания взрывателя. + + #loc_BDArmory_part_scanLockRadar1_title = Захватывающий Радар TWS + #loc_BDArmory_part_scanLockRadar1_description = Это устройство оснащено радаром обнаружения средней дальности и встроенным радаром сопровождения цели. Этот радар способен захватывать цели и будет продолжать сканирование во время сопровождения захваченной цели (TWS - Track While Scan). Он оптимизирован для воздушного поиска и сопровождения и испытывает трудности с обнаружением и сопровождением наземных целей. + //#loc_BDArmory_part_scanLockRadar1_tags = ??? bda radar detect track lock scan search fcs + + #loc_BDArmory_part_scanLargeRadar_title = Большой Радар Обнаружения + #loc_BDArmory_part_scanLargeRadar_description = Большой радар, способный обнаруживать объекты на большом расстоянии. Этот радар не имеет возможности сопровождать или захватывать цели. Он оптимизирован для воздушного поиска и испытывает трудности с обнаружением наземных целей. + //#loc_BDArmory_part_scanLargeRadar_tags = ??? bda radar detect scan search + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = Перезаряжаемая Ракетная Установка FFAR + #loc_BDArmory_part_F86RL_description = Встраиваемая ракетная установка, предназначенная для использования в режиме "Воздух-воздух". Вмещает 24 неуправляемые воздушные ракеты со складывающимися килями. Может быть перезаряжен из ящика боеприпасов, если ракеты закончатся. + //#loc_BDArmory_part_F86RL_tags = ??? bda rocket pod launcher a2a flak reload weap + + #loc_BDArmory_part_bahaH70Launcher_title = Ракетная Установка Hydra-70 + #loc_BDArmory_part_bahaH70Launcher_description = Вмещает и запускает 19 неуправляемых ракет Hydra-70. + //#loc_BDArmory_part_bahaH70Launcher_tags = ??? bda rocket pod launcher a2g weap + + #loc_BDArmory_part_bahaH70Turret_title = Ракетная Турель Hydra-70 + #loc_BDArmory_part_bahaH70Turret_description = Турельный отсек, вмещающий и выпускающий 32 неуправляемые ракеты Hydra-70. + //#loc_BDArmory_part_bahaH70Turret_tags = ??? bda rocket pod launcher a2g turret weap + + #loc_BDArmory_part_bahaHarm_title = Ракета AGM-88 HARM + #loc_BDArmory_part_bahaHarm_description = Высокоскоростная противорадиационная ракета. Эта ракета нацелится на источники радиоволн, обнаруженные приемником радиолокационного предупреждения. + //#loc_BDArmory_part_bahaHarm_tags = ??? BDA missile antirad homing agm atg ordnance weap + + //HEKV Missile + #loc_BDArmory_part_bahaHEKV1_title = Ракета HE-KV-1 + #loc_BDArmory_part_bahaHEKV1_description = HE-KV-1 (High explosive kill vehicle) это самонаводящаяся ракета с радиолокационным наведением, которая использует реактивную систему управления и изменяемый вектор тяги для маневрирования. Это означает, что она способна направляться на цели в вакууме. + //#loc_BDArmory_part_bahaHEKV1_tags = ??? BDA missile radar homing ata a2a ordnance rcs space weap + + //KKV Missile + //#loc_BDArmory_part_bahaKKV_title = ??? Kinetic Kill Vehicle + //#loc_BDArmory_part_bahaKKV_description = ??? The KKV (kinetic kill vehicle) is a IR-guided homing missile that uses reaction control thrusters and a control moment gyroscope to maneuver. It is capable of steering towards targets in a vacuum and has high drag in atmosphere. The KKV relies on kinetic energy to destroy its target and carries no explosives. 6 km/s delta-V. + //#loc_BDArmory_part_bahaKKV_tags = ??? BDA missile radar homing ata a2a ordnance orbital rcs space weap kinetic + + #loc_BDArmory_part_bahaAGM-114_title = Ракета AGM-114 Hellfire + #loc_BDArmory_part_bahaAGM-114_description = Маленькая, быстрая самонаводящаяся ракета с лазерным наведением. + //#loc_BDArmory_part_bahaAGM-114_tags = ??? BDA missile laser homing atg agm ordnance weap + + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R Hellfire II EMP + #loc_BDArmory_part_bahaAGM-114_EMP_description = Маленькая, быстрая самонаводящаяся ракета с лазерным наведением, оснащенная новейшей миниатюрной ЭМИ-боеголовкой. Имея маленький радиус импульса (50 метров), она достаточно эффективна. Эта ракета наносит минимальный физический ущерб, но выводит из строя всю электронику в радиусе действия. + //#loc_BDArmory_part_bahaAGM-114_EMP_tags = ??? BDA missile laser homing atg agm ordnance emp weap + + #loc_BDArmory_part_bahaHiddenVulcan_title = Vulcan (Скрытый) + #loc_BDArmory_part_bahaHiddenVulcan_description = 6-ствольная роторная пушка калибра 20х102 мм. + //#loc_BDArmory_part_bahaHiddenVulcan_tags = ??? bda gun 20mm gatling weap + + #loc_BDArmory_part_bahaJdamMk83_title = Бомба Mk83 JDAM + #loc_BDArmory_part_bahaJdamMk83_description = 1000-килограммовая бомба с GPS-наведением. + //#loc_BDArmory_part_bahaJdamMk83_tags = ??? bda bomb gps ordnance atg homing guid weap + + #loc_BDArmory_part_bahaM102Howitzer_title = M102 Howitzer (Радиальный) + #loc_BDArmory_part_bahaM102Howitzer_description = Радиально установленное 105-мм орудие. + //#loc_BDArmory_part_bahaM102Howitzer_tags = ??? bda gun cannon turret shell howie weap + + #loc_BDArmory_part_bahaM1Abrams_title = Башня M1 Abrams + #loc_BDArmory_part_bahaM1Abrams_description = 120-мм орудие на бронированной башне. + //#loc_BDArmory_part_bahaM1Abrams_tags = ??? bda gun cannon turret shell tank weap + + #loc_BDArmory_part_bahaM230ChainGun_title = Ленточный Пулемет M230 + #loc_BDArmory_part_bahaM230ChainGun_description = Ленточный пулемет M230 представляет собой одноствольную автоматическую пушку, стреляющую осколочно-фугасными снарядами 30х173 калибра. Он обычно используется на ударных вертолетах. + //#loc_BDArmory_part_bahaM230ChainGun_tags = ??? bda gun chaingun turret 30mm heli weap + + #loc_BDArmory_part_bahaAGM-65_title = Ракета AGM-65 Maverick + #loc_BDArmory_part_bahaAGM-65_description = Ракета класса "воздух-земля" с лазерным наведением средней мощности. + //#loc_BDArmory_part_bahaAGM-65_tags = ??? bda missile laser ordnance atg homing guid weap + + #loc_BDArmory_part_missileTurretTest_title = Ракетная Турель Jernas + #loc_BDArmory_part_missileTurretTest_description = Башня, способная вмещать и выпускать до 8 ракет малого и среднего размера. Поставляется со встроенным радаром обнаружения и сопровождения. Гарантия аннулируется, если будет установлено что-либо, кроме ракет. Чтобы активировать башню, выберите установленную ракету в контроллере вооружения. + //#loc_BDArmory_part_missileTurretTest_tags = ??? bda missile turret launch rail mount hardpoint radar lock + + #loc_BDArmory_part_bahaMk82Bomb_title = Бомба Mk82 + #loc_BDArmory_part_bahaMk82Bomb_description = 500-килограммовая неуправляемая бомба. + //#loc_BDArmory_part_bahaMk82Bomb_tags = ??? bda bomb ugb ordnance atg weap + + #loc_BDArmory_part_bahaMk82BombBrake_title = Бомба Mk82 SnakeEye + #loc_BDArmory_part_bahaMk82BombBrake_description = 500-килограммовая неуправляемая бомба с аэротормозами. Используется для бомбометания на малой высоте. + + #loc_BDArmory_part_bahaOMillennium_title = Пушка Тысячелетия Эрликона + #loc_BDArmory_part_bahaOMillennium_description = Турель, которая стреляет разрывными снарядами с временной детонацией. Подходит для ближней противовоздушной обороны. Устройство на дульном конце ствола измеряет точную скорость каждого выстрела и автоматически устанавливает предохранитель для детонации снаряда, когда он приближается к заданному расстоянию от цели. Использует снаряды 30x173 калибра. + //#loc_BDArmory_part_bahaOMillennium_tags = ??? bda gun turret ciws flak autocannon oerlikon cram 30mm weap + + #loc_BDArmory_part_bahaPac-3_title = Ракета-Перехватчик PAC-3 + #loc_BDArmory_part_bahaPac-3_description = Высокоскоростная ракета класса "земля-воздух" средней дальности с радарным наведением. + //#loc_BDArmory_part_bahaPac-3_tags = ??? bda missile radar ordnance ata a2a homing guid sarh sam weap + + #loc_BDArmory_part_patriotLauncherTurret_title = Пусковая Установка Системы Patriot + #loc_BDArmory_part_patriotLauncherTurret_description = Башня, способная вмещать и выпускать до 16 ракет PAC-3 (по 4 на канистру). Гарантия аннулируется, если будет установлено что-либо, кроме ракет. Чтобы активировать башню, выберите установленную ракету в контроллере вооружения. + //#loc_BDArmory_part_patriotLauncherTurret_tags = ??? bda missile turret launch rail mount hardpoint sam + + #loc_BDArmory_part_radarDataReceiver_title = Приемник Радиолокационного Предупреждения + #loc_BDArmory_part_radarDataReceiver_description = Модуль, который может отображать контакты радара по каналу передачи данных и захватывать цели с помощью удаленного радара, но не может сканировать или захватывать самостоятельно. Полезно для скрытой ракетной батареи. + //#loc_BDArmory_part_radarDataReceiver_tags = ??? bda radar detect link data lock + + //AN/APG-63 Variants + //#loc_BDArmory_part_bdRadome_variantPitot = ??? Pitot Tube + //#loc_BDArmory_part_bdRadome_variantNoPitot = ??? No Pitot Tube + #loc_BDArmory_part_bdRadome1_title = Обтекатель AN/APG-63 + #loc_BDArmory_part_bdRadome1_description = Направленный вперед аэродинамически размещенный радар. Он может сканировать и захватывать цели в пределах 120-градусного поля зрения. Он оптимизирован для ведения воздушного боя и испытывает трудности с захватом наземных целей. + //#loc_BDArmory_part_bdRadome1_tags = ??? bda radar radome detect lock track scan + #loc_BDArmory_part_bdRadome1inline_title = Встроенный Обтекатель AN/APG-63 + #loc_BDArmory_part_bdRadome1inline_description = Направленный вперед аэродинамически размещенный радар. Он может сканировать и захватывать цели в пределах 120-градусного поля зрения. Убедитесь, что черные метки направлены вперед. Он оптимизирован для ведения воздушного боя и испытывает трудности с захватом наземных целей. + #loc_BDArmory_part_bdRadome1snub_title = Короткий Обтекатель AN/APG-63 + #loc_BDArmory_part_bdRadome1snub_description = Направленный вперед аэродинамически размещенный радар. Он может сканировать и захватывать цели в пределах 120-градусного поля зрения. Это специальная версия для атаки наземных целей, но уменьшенными возможностями в воздушном бою. + + #loc_BDArmory_part_bahaRBS-15Cruise_title = Крылатая Ракета RBS-15 + #loc_BDArmory_part_bahaRBS-15Cruise_description = Многоплатформенная высокоскоростная крылатая ракета большой дальности с ускорителями. + //#loc_BDArmory_part_bahaRBS-15Cruise_tags = ??? bda missile ordnance cruise gps guid homing weap + + #loc_BDArmory_part_bahaRBS-15ALCruise_title = Крылатая Ракета RBS-15 Воздушного Базирования + #loc_BDArmory_part_bahaRBS-15ALCruise_description = Многоплатформенная высокоскоростная крылатая ракета дальнего действия воздушного базирования без внешних ускорителей. + + #loc_BDArmory_part_bdRotBombBay_title = Регулируемая Поворотная Бомбовая Стойка + #loc_BDArmory_part_bdRotBombBay_description = Регулируемая поворотная бомбовая стойка. Желтая стрелка должна указывать в направлении запуска оружия. Только ракеты или бомбы. Только по одной на направляющую. + //#loc_BDArmory_part_bdRotBombBay_tags = ??? bda missile rail launch mount rotary rack + #loc_BDArmory_part_bahaS-8Launcher_title = Ракетная Установка S-8KOM + #loc_BDArmory_part_bahaS-8Launcher_description = Вмещает и запускает 23 неуправляемые ракеты S-8KOM. Имеет аэродинамический носовой обтекатель. + + #loc_BDArmory_part_bahaAim9_title = Ракета AIM-9 Sidewinder + #loc_BDArmory_part_bahaAim9_description = Ракета малой дальности с тепловым наведением. + //#loc_BDArmory_part_bahaAim9_tags = ??? bda missile ordnance heater heatseek ata a2a aam weap + + #loc_BDArmory_part_bdWarheadSmall_title = Малая Фугасная Боеголовка + #loc_BDArmory_part_bdWarheadSmall_description = Носовой обтекатель ракеты, начиненный взрывчаткой. + //#loc_BDArmory_part_bdWarheadSmall_tags = ??? bda missile bomb ordnance boom weap + + #loc_BDArmory_part_bahaSmokeCmPod_title = Модуль Дымовой Завесы + #loc_BDArmory_part_bahaSmokeCmPod_description = Активирует дымовую завесу для блокирования лазерного облучения. + //#loc_BDArmory_part_bahaSmokeCmPod_tags = ??? bda cm counter smoke + + #loc_BDArmory_part_bahaFlirBall_title = Шар Наведения FLIR + #loc_BDArmory_part_bahaFlirBall_description = Шаровая камера, используемая для наведения на цель и наблюдения. Оснащенный камерой высокого разрешения и системой стабилизации, а также инфракрасным лазером для помечения целей, этот модуль позволяет быстро находить и захватывать наземные цели для ракет. + + //#loc_BDArmory_part_bahaCamPod_tags = ??? bda detect laser gps cam flir target + #loc_BDArmory_part_bahaCamPod_title = Модуль Наведения AN/AAQ-28 + #loc_BDArmory_part_bahaCamPod_description = Модуль наведения, используемый для наведения на цель и наблюдения. Оснащенный камерой высокого разрешения и системой стабилизации, а также инфракрасным лазером для помечения целей, этот модуль позволяет быстро находить и захватывать наземные цели для ракет. + + #loc_BDArmory_part_bahaIRSTPod_title = Инфракрасный Модуль AN/AAQ-42 + #loc_BDArmory_part_bahaIRSTPod_description = Направленная вперед инфракрасная система поиска и сопровождения, размещенная в аэродинамической капсуле. Может сканировать и обнаруживать тепловые сигнатуры в пределах 120-градусного поля зрения. Он оптимизирован для поиска воздушных целей и испытывает трудности с обнаружением наземных целей. + //#loc_BDArmory_part_bahaIRSTPod_tags = ??? bda detect heat scan search track ir therm + + #loc_BDArmory_part_towLauncherTurret_title = Пусковая Установка TOW + #loc_BDArmory_part_towLauncherTurret_description = Турель, способная вмещать и выпускать до 4 ракет TOW. Гарантия аннулируется, если установлено что-либо, кроме ракет TOW. Чтобы активировать турель, выберите установленную ракету в контроллере вооружения. + //#loc_BDArmory_part_towLauncherTurret_tags = ??? bda missile turret rail mount launch tow + + #loc_BDArmory_part_bahaTowMissile_title = Ракета BGM-71 TOW + #loc_BDArmory_part_bahaTowMissile_description = Противотанковая ракета малой дальности с лазерным наведением. + //#loc_BDArmory_part_bahaTowMissile_tags = ??? bda missile ordnance laser agm atg weap + + #loc_BDArmory_part_missileController_title = Контроллер Вооружения + #loc_BDArmory_part_missileController_description = Переключайтесь между ракетами/бомбами и запускайте их с помощью одной кнопки. + //#loc_BDArmory_part_missileController_tags = ??? bda wm ai weap + + #loc_BDArmory_part_BDAsonarPod1A_title = Сонар BDA MK1 + #loc_BDArmory_part_BDAsonarPod1A_description = Сонар BDA MK1 может обнаруживать только подводные объекты. Как встроенный сонар, он имеет ограниченную дальность действия и чувствительность. + //#loc_BDArmory_part_BDAsonarPod1A_tags = ??? bda detect sonar ship + + #loc_BDArmory_part_StingRayBDATorpedo_title = Легкая Торпеда Sting Ray BDA + #loc_BDArmory_part_StingRayBDATorpedo_description = Легкая Торпеда Sting Ray предназначена для запуска с корабля и сброса с вертолета, не используются на подводных лодках. Интересный факт, вы можете поместить 16 таких в пусковую установку, хотя использование их в таком устройстве без надлежащей подготовки стало причиной большого количества слез и писем начальству. + //#loc_BDArmory_part_StingRayBDATorpedo_tags = ??? bda missile ordnance asm torp ship sonar homing guid s2s sts + + //#loc_BDArmory_part_EJ200_title = ??? TFJ-EJ200 "Typhoon" Afterburning Turbofan + //#loc_BDArmory_part_EJ200_description = ??? Word is that this engine was the result of international cooperation, which produced an engine with exceptional performance and potential. + + #loc_BDArmory_part_SaturnAL31_title = Турбореактивный Двигатель АЛ-31ФМ1 «Сатурн» + #loc_BDArmory_part_SaturnAL31_description = Высокоэффективный реактивный двигатель с изменяемым вектором тяги и форсажной камерой. Основываясь на популярном двигателе J-404, инженеры KTech увидели потенциал в (значительной) модификации коммерческого варианта в мощную силовую установку для военного использования. Заметив потенциал двигателя, BDAc group немедленно лицензировала его для своего нового тестового дрона MkIII. + //#loc_BDArmory_part_SaturnAL31_tags = ??? after aircraft burner engine fighter jet saturn plane propulsion AL + + //GravityGun + #loc_BDArmory_part_GravGun_title = Энергетический Манипулятор Нулевых Колебаний + #loc_BDArmory_part_GravGun_description = Очень высокотехнологичная пушка, которая превратила гравитацию в оружие. По-видимому, это оружие, основанное на сочетании техно-магии и инопланетных технологий, способно воздействовать на цели неньютоновскими силами, а также влиять на их массу. Bahamuto Dynamics не несет ответственности за любой ущерб (вам, имуществу или структуре реальности), причиненный этим оружием. + //#loc_BDArmory_part_GravGun_tags = ??? bda laser gun weap grav + + #loc_BDArmory_part_genie_title = Ракета Воздух-Воздух AIR-2 Genie + #loc_BDArmory_part_genie_description = Ядерная противовоздушная ракета мощностью 1,5 кт. + //#loc_BDArmory_part_genie_tags = ??? BDA missile nuke ata a2a + + #loc_BDArmory_part_GAU22_title = Пулемет GAU-22/A 25x137мм + #loc_BDArmory_part_GAU22_description = 4-ствольная 25-мм роторная пушка. + //#loc_BDArmory_part_GAU22_tags = ??? BDA gun weap 25mm gatling gau + + #loc_BDArmory_part_sidam_title = Зенитное Орудие Sidam + #loc_BDArmory_part_sidam_description = 25-миллиметровое зенитное орудие для залпового огня. + //#loc_BDArmory_part_sidam_tags = ??? BDA gun weap 25mm turret AA + + #loc_BDArmory_part_REA_title = Динамическая Защита BD 1x0.5 + #loc_BDArmory_part_REA_Panel_description = Модуль динамической защиты размером 1x0,5 м. Отлично подходит для небольшой дополнительной защиты поверх существующей брони. + //#loc_BDArmory_part_REA_Panel_tags = ??? bda armor panel era react + + #loc_BDArmory_part_Panel_title = Бронированная Панель BD + #loc_BDArmory_part_Panel_description = Прочная универсальная структурная панель, которая может быть сконфигурирована под различные размеры и материалы, идеально подходит для конструирования или бронирования всевозможных вещей. + + #loc_BDArmory_part_TriPanel_title = Треугольная Бронированная Панель BD + #loc_BDArmory_part_TriIsoPanel_title = Треугольная Бронированная Панель BD + #loc_BDArmory_part_Tripanel_description = Прочная универсальная структурная панель, которая может быть сконфигурирована под различные размеры и материалы, идеально подходит для конструирования или бронирования всевозможных вещей. Эта треугольная. + + #loc_BDArmory_part_BombBay_title = Оружейный Отсек + #loc_BDArmory_part_BombBay_description = Выдвигаемый отсек для хранения оружия. Полезная нагрузка защищена от воздушного потока до момента развертывания. + //#loc_BDArmory_part_BombBay_tags = ??? bda missile ordnance bay rail deploy rack + + //#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. + //#loc_BDArmory_part_combatSeat_tags = ??? bda ai wm weap pilot seat chair + + //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_bdRadome1_Gnd_tags = ??? bda detect radar scan track ground + + //#loc_BDArmory_part_AWACS_Legged = ??? Legged + //#loc_BDArmory_part_AWACS_Legless = ??? Legless + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-zh-cn.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-zh-cn.cfg index f13d3ddac..002b75a5a 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-zh-cn.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-zh-cn.cfg @@ -1,339 +1,373 @@ -Localization -{ - zh-cn - { - #loc_BDArmory_modname = BD Armory - - #loc_BDArmory_agent_title = 巴哈姆特动力 - #loc_BDArmory_agent_description = 军用武器与弹药业界的领军品牌。该公司同事也和太空计划有长期合作关系,为其提供独特的工程解决方案。 - #loc_BDArmory_part_manufacturer = Bahamuto Dynamics - - //Vulcan Turret - #loc_BDArmory_part_bahaGatlingGun_title = “火神”炮塔 - //A 6 barrel 20x102mm rotary cannon. - #loc_BDArmory_part_bahaGatlingGun_description = 六管20x102mm旋转机炮炮塔。 - - //.50cal Turret - #loc_BDArmory_part_bahaTurret_title = .50口径机枪塔 - //A dual barrel .50 cal machine gun. - #loc_BDArmory_part_bahaTurret_description = 双管.50口径机枪。 - - //USAF Airborne Laser - #loc_BDArmory_part_bahaABL_title = 美国空军空基激光器 - //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先进中程空对空导弹 - //Medium range radar guided homing missile. - #loc_BDArmory_part_bahaAim120_description = 中距雷达制导导弹。 - - //AI Pilot Flight Computer - #loc_BDArmory_part_bdPilotAI_title = AI自动驾驶飞控计算机 - //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 = 在空战和巡逻等飞行任务中解放你的双手!请根据你的飞机实际性能调节各项飞行参数,手动启动引擎。在防卫模式下可以与武器管理器(需要另外安装和设置)协同运作。(实验功能) - - //20mm Ammunition Box - #loc_BDArmory_part_baha20mmAmmo_title = 20mm弹药箱 - //Ammo box containing 650 20x102mm rounds. - #loc_BDArmory_part_baha20mmAmmo_description = 装有650发20x102mm弹药的弹药箱。 - - //30mm Ammunition Box - #loc_BDArmory_part_baha30mmAmmo_title = 30mm弹药箱 - //Ammo box containing 600 30x173mm rounds. - #loc_BDArmory_part_baha30mmAmmo_description = 装有600发30x173mm弹药的弹药箱。 - - //50cal Ammunition Box - #loc_BDArmory_part_baha50CalAmmo_title = .50弹药箱 - //Ammo box containing 1200 .50 cal rounds. - #loc_BDArmory_part_baha50CalAmmo_description = 装有1200发.50口径弹药的弹药箱。 - - //Universal Ammo Box (Legacy) - #loc_BDArmory_part_UniversalAmmoBoxBDA_title = 通用弹药箱(遗产) - //(Obsolete - DO NOT USE - Requires Fire Spitter) 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 uponn request (no fantasy ammo please). NOTE: this part still requires Fire Spitter, and is here for backwards compatability. Use the new Universal Ammo Box going forward - #loc_BDArmory_part_UniversalAmmoBoxBDA_description = (过时 - 请勿使用 - 需要火Spitter)可以装入任何所需弹药而且尺寸可调的弹药箱,容量可变内容可选,最大可以装入16.1英寸口径弹药。支持的弹药包括现今用于BDAc的所有自带以及附加种类,需要添加新种类请与我们联系(不存在的弹药不行谢谢)。注意:此部分仍需要火Spitter,此处是为了向后兼容。 使用新的通用弹药盒向前发展。 - - //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 uponn request (no fantasy ammo please) - #loc_BDArmory_part_BDAcUniversalAmmoBox_description = 可以装入任何所需弹药而且尺寸可调的弹药箱,容量可变内容可选,最大可以装入16.1英寸口径弹药。支持的弹药包括现今用于BDAc的所有自带以及附加种类,需要添加新种类请与我们联系(不存在的弹药不行谢谢)。 - - //Cannon Ammunition Box - #loc_BDArmory_part_bahaCannonShellBox_title = 火炮弹药箱 - //Ammo box containing 10 cannon shells. - #loc_BDArmory_part_bahaCannonShellBox_description = 装有10发火炮弹药(CannonShells)的弹药箱。 - - //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 = 一块结实的1x1斜面装甲板,适用于建造各种产品。注:并不能漂浮。 - - //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 = 一块结实的2x1斜面装甲板,适用于建造各种产品。注:并不能漂浮。 - - //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 = 一块结实的1x1装甲板,适用于建造各种产品。注:并不能漂浮。 - - //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 = 一块结实的2x1装甲板,适用于建造各种产品。注:并不能漂浮。 - - //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 = 一块结实的3x1装甲板,适用于建造各种产品。注:并不能漂浮。 - - //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 = 一块结实的4x1装甲板,适用于建造各种产品。注:并不能漂浮。 - - //AWACS Detection Radar - #loc_BDArmory_part_awacsRadar_title = 空中预警系统探测雷达 - //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 = 导弹引导模块(实验功能) - //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 = 一台导弹制导计算机。请根据你的导弹实际性能调节各项飞行参数,设置制导模式,锁定目标启用制导,手动控制启动引擎和分级顺序即可。(实验功能) - - //Browning .50cal AN/M2 - #loc_BDArmory_part_bahaBrowningAnm2_title = 勃朗宁.50口径AN/M3机枪 - //An old fixed .50 cal machine gun 50cal ammo - #loc_BDArmory_part_bahaBrowningAnm2_description = 旧式.50口径固定机枪。 - - //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 30x173mm机炮 - //A 7 barrel 30mm rotary cannon. - #loc_BDArmory_part_bahaGau-8_description = 7管30mm转管机炮。 - - //Goalkeeper CIWS - #loc_BDArmory_part_bahaGoalKeeper_title = “守门员”近防系统 - #loc_BDArmory_part_bahaGoalKeeper_description = 可以全向旋转的7管30mm转管机炮炮塔。30mm的高爆弹药会在预设距离上爆炸,但该武器并没有自动设置定时引信的功能。其自带的探测和追踪雷达只在短距离上有效,不能用来取代大型探测雷达。 - - //GoalkeeperMk1 CIWS - #loc_BDArmory_part_GoalKeeperBDAcMk1_title = “守门员”Mk 1近防系统 - #loc_BDArmory_part_GoalKeeperBDAcMk1_description = 可以全向旋转的7管30mm转管机炮炮塔。Mk 1版本是从一大片泥地里的一块帐篷布底下发现的,因为价格便宜,特别适合预算有限的军事组织和政府采购。该版本没有探测和追踪用的雷达,需要第三方提供目标信息(让人去观察和指示目标不太好使,因为这玩意发射时的噪音实在是有点大)。其发射的30mm的高爆弹药等飞累了就会自行爆炸,该武器并没有自动设置定时引信的功能。 - - //Goalkeeper MK2 CIWS - #loc_BDArmory_part_BDAcGKmk2_title = “守门员”Mk 2近防系统 - #loc_BDArmory_part_BDAcGKmk2_description = 可以全向旋转的7管30mm转管机炮炮塔。Mk 2版本则是在旧KSC的机库里面,从一大堆空喷漆罐子下面挖出来的,其开发思路基于Mk 1,核心思想是减轻早期观测手受到的听力减退困扰。Mk 2比起Mk 1来说有几项优势,其装备了红外瞄准系统和雷达信号接收机。其发射的30mm的高爆弹药只比Mk 1的稍微改进了一点,飞累了自行爆炸所需的时间要稍微长一些,这样击中目标的概率就大了一点,但是该武器自始至终也还是没有自动设置定时引信的功能。 - - //TWS Locking Radar - #loc_BDArmory_part_scanLockRadar1_title = 边扫描边跟踪雷达 - #loc_BDArmory_part_scanLockRadar1_description = 该设备由一台中距探测雷达与一台内建的追踪雷达组成。该雷达可以锁定目标,且在追踪已锁定目标的同时还可以继续扫描(即TWS - Track While Scan)。适用于防空搜索与追踪,但并不适合探测和追踪地面目标。 - - //Large Detection Radar - #loc_BDArmory_part_scanLargeRadar_title = 大型探测雷达 - #loc_BDArmory_part_scanLargeRadar_description = 具有长距离发现目标能力的大型雷达。该雷达无法追踪和锁定目标,适用于防空警戒,难以探测地面目标。 - - //Hydra-70 Rocket Pod - #loc_BDArmory_part_bahaH70Launcher_title = 火蛇70火箭弹发射器 - //Holds and fires 19 unguided Hydra-70 rockets. - #loc_BDArmory_part_bahaH70Launcher_description = 内装19枚火蛇70火箭弹的发射器。 - - //Hydra-70 Rocket Turret - #loc_BDArmory_part_bahaH70Turret_title = 火蛇70火箭弹炮塔 - //Turret pod that holds and fires 32 unguided Hydra-70 rockets. - #loc_BDArmory_part_bahaH70Turret_description = 内装32枚火蛇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,高爆攻击弹药)是一款使用RCS和推力矢量控制机动的雷达制导导弹,也就是说其可以在真空中转向攻击目标。 - - //AGM-114 Hellfire Missile - #loc_BDArmory_part_bahaAGM-114_title = AGM-114“地狱火”导弹 - //Small, quick, laser guided homing missile. - #loc_BDArmory_part_bahaAGM-114_description = 小型高速激光制导导弹。 - - //Vulcan (Hidden) - #loc_BDArmory_part_bahaHiddenVulcan_title = “火神”机炮(隐身型) - //A 6 barrel 20x102mm rotary cannon. 20x102Ammo - #loc_BDArmory_part_bahaHiddenVulcan_description = 六管20x102mm转管机炮。(发射20x102Ammo) - - //Mk83 JDAM Bomb - #loc_BDArmory_part_bahaJdamMk83_title = Mk 83联合直接攻击弹药 - //1000lb GPS-guided bomb. - #loc_BDArmory_part_bahaJdamMk83_description = 1000磅GPS制导炸弹。 - - //M102 Howitzer (Radial) - #loc_BDArmory_part_bahaM102Howitzer_title = M102榴弹炮(侧面安装版) - //A radially mounted 105mm gun. CannonShells - #loc_BDArmory_part_bahaM102Howitzer_description = 105mm侧面安装火炮。(发射CannonShells) - - //M1 Abrams Cannon - #loc_BDArmory_part_bahaM1Abrams_title = M1“艾布拉姆斯”主炮 - //A 120mm cannon on an armored turret. CannonShells - #loc_BDArmory_part_bahaM1Abrams_description = 安装在装甲炮塔上的120mm火炮。(发射CannonShells) - - //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弹链供弹机炮是一款发射30x173mm高爆弹药的单管自动武器,常安装于武装直升机上。 - - //AGM-65 Maverick Missile - #loc_BDArmory_part_bahaAGM-65_title = AGM-65“小牛”导弹 - //Medium yield laser guided air-to-ground missile. - #loc_BDArmory_part_bahaAGM-65_description = 中等当量激光制导空对地导弹。 - - //Jernas Missile Turret - #loc_BDArmory_part_missileTurretTest_title = “贾纳斯”旋转导弹架 - //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 = Mk 82炸弹 - //500lb unguided bomb. - #loc_BDArmory_part_bahaMk82Bomb_description = 500磅无制导炸弹。 - - //Mk82 SnakeEye Bomb - #loc_BDArmory_part_bahaMk82BombBrake_title = Mk 82“蛇眼”炸弹 - //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 = 发射定时爆炸高爆弹的炮塔,适合短距离防空任务。炮口处有测量每发炮弹初速度、并自动校准引信的装置,使炮弹能准确地在抵近目标的预设距离上爆炸。(发射30x173Ammo) - - //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 = “爱国者”导弹发射器 - //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枚(每个隔间4枚)PAC-3导弹的导弹发射器。如果上面安装了不是导弹的任何东西则保修失效。在武器管理器里选择所安装的导弹即可启动发射器。 - - //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内置型机载雷达 - //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 = 带气动保护罩的前视雷达,可以在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 = 长距多平台高速巡航导弹,附带助推器。 - - //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“响尾蛇”导弹 - //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 = 用于瞄准和侦察目标的吊舱。带有高清摄像头和垂直稳定系统,可感知红外线,使你能迅速发现和锁定供导弹攻击的地面目标。 - - //Tow Launcher - #loc_BDArmory_part_towLauncherTurret_title = “陶”式发射器 - //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枚“陶”式导弹的旋转发射器。如果安装了除“陶”式以外的其他导弹则保修失效。在武器管理器里选择所安装的导弹即可启动发射器。 - - //BGM-71 Tow Missile - #loc_BDArmory_part_bahaTowMissile_title = BGM-71“陶”式反坦克导弹 - //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 Mk 1声纳吊舱 - //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 = BDA Mk 1声纳吊舱适合完全浸没在水下工作,可以用于探测水面和潜水的目标。如果装在水面以上的部分,其探测距离和灵敏度将大打折扣。 - - //StingRayBDATorpedo - #loc_BDArmory_part_StingRayBDATorpedo_title = “魟鱼”BDA轻型鱼雷 - //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. - #loc_BDArmory_part_StingRayBDATorpedo_description = “魟鱼”轻型鱼雷由舰船和直升机发射,请勿用作潜艇鱼雷。顺便有个好玩的事,你可以在“爱国者”发射器里面塞进16枚鱼雷,但是这样非常危险,未经训练进行操作可能导致你事后得哭着写上万字的检讨。 - - //SaturnAL31 - #loc_BDArmory_part_SaturnAL31_title = Saturn AL-31FM1 Afterburning Jet Engine - //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集团立即将其用于新的MkIII测试无人机。 - } -} +// Notes: +// - The "_tags" entries are simply common search terms for the part filter in the SPH/VAB. Instead of being the same in all languages, they should be whatever a user in that language would search for. + +Localization +{ + zh-cn + { + #loc_BDArmory_modname = BD Armory(全称:BahamutoD's Armory,简称BDA) + + #loc_BDArmory_agent_title = 巴哈姆特动力 + #loc_BDArmory_agent_description = 军用武器与弹药业界的领军品牌。该公司同事也和太空计划有长期合作关系,为其提供独特的工程解决方案。 + #loc_BDArmory_part_manufacturer = 巴哈姆特动力 + + #loc_BDArmory_agent2_title = 双冠航空航天工业公司 + + #loc_BDArmory_part_bahaGatlingGun_title = “火神”炮塔 + #loc_BDArmory_part_bahaGatlingGun_description = 六管20x102mm旋转机炮炮塔。 + #loc_BDArmory_part_bahaGatlingGun_tags = BDA 火炮 武器 旋转炮塔 20毫米 加特林 + + #loc_BDArmory_part_bahaTurret_title = .50口径机枪塔 + #loc_BDArmory_part_bahaTurret_description = 双管.50口径机枪。 + #loc_BDArmory_part_bahaTurret_tags = BDA 火炮炮塔 .50口径 武器 + + #loc_BDArmory_part_bahaABL_title = 美国空军空基激光器 + #loc_BDArmory_part_bahaABL_description = 用于点燃目标的高能激光器。每秒耗电350单位。 + #loc_BDArmory_part_bahaABL_tags = BDA 激光炮塔 激光束 反制 武器 + + #loc_BDArmory_part_bahaAdjustableRail_title = 可调节导弹挂架 + #loc_BDArmory_part_bahaAdjustableRail_description = 用于安装导弹的导轨挂架。 + #loc_BDArmory_part_bahaAdjustableRail_tags = BDA 导轨挂载点 导弹发射架 + + #loc_BDArmory_part_bahaAgm86B_title = AGM-86C巡航导弹 + #loc_BDArmory_part_bahaAgm86B_description = 长射程亚音速空射GPS制导巡航导弹。该导弹没有助推器,所以必须在巡航速度以上从空中发射。 + #loc_BDArmory_part_bahaAgm86B_tags = BDA 导弹 GPS 巡航制导 武器 + + #loc_BDArmory_part_bahaAim120_title = AIM-120先进中程空对空导弹 + #loc_BDArmory_part_bahaAim120_description = 中距雷达制导导弹。 + #loc_BDArmory_part_bahaAim120_tags = BDA 导弹 雷达寻的 空对空 对空 武器 + + #loc_BDArmory_part_bahaEMP120_title = AIM-120 先进中程空对空EMP导弹 + #loc_BDArmory_part_bahaEMP120_description = 配备最新微型电磁脉冲弹头的中程雷达制导导弹。 虽然脉冲半径不大(100 米),但却相当有效。导弹对结构造成的破坏极小,但会使爆炸半径内的所有电子设备失效。 + #loc_BDArmory_part_bahaEMP120_tags = BDA导弹,雷达寻的,空对空,空对地,电磁脉冲,军械,武器 + + #loc_BDArmory_part_bdPilotAI_title = AI自动驾驶飞控计算机 + #loc_BDArmory_part_bdPilotAI_description = 在空战和巡逻等飞行任务中解放你的双手!请根据你的飞机实际性能调节各项飞行参数,手动启动引擎。在警戒模式下可以与武器管理器(需要另外安装和设置)协同运作。(实验功能) + #loc_BDArmory_part_bdpilotAI_tags = BDA 飞行员 人工智能 控制 + + #loc_BDArmory_part_bdDriverAI_title = AI 自动驾驶计算机 + #loc_BDArmory_part_bdDriverAI_description = 在驾驶车辆/坦克/水面舰艇作战和巡逻时解放你的双手!请根据你的载具实际性能调节各项驾驶参数,手动启动引擎。在警戒模式下可以与武器管理器(需要另外安装和设置)协同运作。(实验功能) + #loc_BDArmory_part_bdDriverAI_tags = BDA驾驶员,人工智能控制,车辆,地面 + + #loc_BDArmory_part_bdVTOLAI_title = AI 垂直起降飞控计算机 + #loc_BDArmory_part_bdVTOLAI_desc = 在驾驶垂直起降(VTOL)载具(如直升机,垂直起降战机,飞艇等)作战和巡逻时解放你的双手!请根据你的载具实际性能调节各项飞行参数,手动启动引擎。在警戒模式下可以与武器管理器(需要另外安装和设置)协同运作。(实验功能) + #loc_BDArmory_part_bdVTOLAI_tags = BDA飞行员,人工智能控制,直升机,旋翼机,垂直起降 + + #loc_BDArmory_part_bdOrbitalAI_title = AI 轨道飞控计算机 + #loc_BDArmory_part_bdOrbitalAI_desc = 在太空战和巡逻等航天任务中解放你的双手!请根据你的飞船实际性能调节各项飞行参数,手动启动引擎。在警戒模式下可以与武器管理器(需要另外安装和设置)协同运作。(实验功能) + #loc_BDArmory_part_bdOrbitalAI_tags = BDA飞行员,人工智能控制,太空,航天器,轨道飞行器,轨道 + + #loc_BDArmory_part_baha20mmAmmo_title = 20mm弹药箱 + #loc_BDArmory_part_baha20mmAmmo_description = 装有650发20x102mm弹药的弹药箱。 + #loc_BDArmory_part_baha20mmAmmo_tags = BDA 弹药箱 20毫米 弹药 子弹 + + #loc_BDArmory_part_baha25mmAmmo_title = 25mm 弹药箱 + #loc_BDArmory_part_baha25mmAmmo_description = 装有625发25x137mm弹药的弹药箱。 + #loc_BDArmory_part_baha25mmAmmo_tags = BDA弹药箱,25毫米弹药,子弹 + + #loc_BDArmory_part_baha30mmAmmo_title = 30mm弹药箱 + #loc_BDArmory_part_baha30mmAmmo_description = 装有600发30x173mm弹药的弹药箱。 + #loc_BDArmory_part_baha30mmAmmo_tags = BDA 弹药箱 30毫米 弹药 子弹 + + #loc_BDArmory_part_rocket70mmAmmo_title = 70mm 火箭弹药箱 + #loc_BDArmory_part_rocket70mmAmmo_description = 装有48发70mm火箭弹的弹药箱。 + #loc_BDArmory_part_bahaRocketAmmo_tags = BDA弹药箱,25毫米火箭弹,FFAR + + #loc_BDArmory_part_baha50CalAmmo_title = .50弹药箱 + #loc_BDArmory_part_baha50CalAmmo_description = 装有1200发.50口径弹药的弹药箱。 + #loc_BDArmory_part_baha50calAmmo_tags = BDA 弹药箱 .50 50cal 12.7毫米 弹药 子弹 + + #loc_BDArmory_part_UniversalAmmoBoxBDA_title = 通用弹药箱(遗产) + #loc_BDArmory_part_UniversalAmmoBoxBDA_description = (已废弃——请勿使用——需要Fire Spitter mod)这是一个可扩展的弹药箱,可以容纳您想要放入的任何弹药类型。它可以容纳KSP中目前使用的所有弹药类型,每种弹药的最大容量为16.1英寸。与BDAc的额外弹药类型可以根据请求添加(请勿添加虚构的弹药类型)。注意:此部件仍需要Fire Spitter,并且仅用于向后兼容。请在未来使用新的通用弹药箱。 + + #loc_BDArmory_part_BDAcUniversalAmmoBox_title = 通用弹药箱 + #loc_BDArmory_part_BDAcUniversalAmmoBox_description = 可以装入任何所需弹药而且尺寸可调的弹药箱,容量可变内容可选,最大可以装入16.1英寸口径弹药。支持的弹药包括现今用于BDAc的所有自带以及附加种类,需要添加新种类请与我们联系(不存在的弹药不行谢谢)。 + #loc_BDArmory_part_bahaUABAmmo_tags = BDA 弹药箱 + + #loc_BDArmory_part_bahaCannonShellBox_title = 火炮弹药箱 + #loc_BDArmory_part_bahaCannonShellBox_description = 装有10发火炮弹药(CannonShells)的弹药箱。 + #loc_BDArmory_part_bahaCannonAmmo_tags = BDA 弹药箱 炮弹 坦克炮 坦克 + + #loc_BDArmory_part_BD1x1slopeArmor_title = BD 1x1斜面装甲 + #loc_BDArmory_part_bahaArmor_tags = BDA 防护装甲板 装甲舰 装甲车辆 面板 + #loc_BDArmory_part_BD1x1slopeArmor_description = 这是一种坚固的1x1斜坡装甲板,非常适合用于建造各种东西。注意:它不会浮在水上。 + + #loc_BDArmory_part_BD2x1slopeArmor_title = BD 2x1斜面装甲 + #loc_BDArmory_part_BD2x1slopeArmor_description = 这是一种坚固的2×1斜坡装甲板,非常适合用于建造各种东西。注意:它不会浮在水上。 + + #loc_BDArmory_part_BD1x1panelArmor_title = BD 1x1装甲板 + #loc_BDArmory_part_BD1x1panelArmor_description = 这是一种坚固的1×1装甲板,非常适合用于建造各种东西。注意:它不会浮在水上。 + + #loc_BDArmory_part_BD2x1panelArmor_title = BD 2x1装甲板 + #loc_BDArmory_part_BD2x1panelArmor_description = 这是一种坚固的2x1装甲板,非常适合用于建造各种东西。注意:它不会浮在水上。 + + #loc_BDArmory_part_BD3x1panelArmor_title = BD 3x1装甲板 + #loc_BDArmory_part_BD3x1panelArmor_description = 这是一种坚固的3x1装甲板,非常适合用于建造各种东西。注意:它不会浮在水上。 + + #loc_BDArmory_part_BD4x1panelArmor_title = BD 4x1装甲板 + #loc_BDArmory_part_BD4x1panelArmor_description = 这是一种坚固的4x1装甲板,非常适合用于建造各种东西。注意:它不会浮在水上。 + + #loc_BDArmory_part_awacsRadar_title = AWACS探测雷达 + #loc_BDArmory_part_awacsRadar_description = 一款能探测远距离目标的大型雷达。该雷达没有追踪或锁定目标的能力。 + #loc_BDArmory_part_awacsRadar_tags = BDA 雷达 AWACS 跟踪 探测 + + #loc_BDArmory_part_bdammGuidanceModule_title = 导弹制导模块(实验性功能) + #loc_BDArmory_part_bdammGuidanceModule_description = 一台导弹制导计算机。请根据你的导弹实际性能调节各项飞行参数,设置制导模式,锁定目标启用制导,手动控制启动引擎和分级顺序即可。(实验性功能) + #loc_BDArmory_part_bdammGuidanceModule_tags = BDA 导弹 毫米波制导 弹药 + + #loc_BDArmory_part_bahaBrowningAnm2_title = 勃朗宁.50口径AN/M3机枪 + #loc_BDArmory_part_bahaBrowningAnm2_description = 旧式.50口径固定机枪。 + #loc_BDArmory_part_bahaBrowningAnm2_tags = BDA 枪械 .50口径 50口径 武器 + + #loc_BDArmory_part_bahaClusterBomb_title = CBU-87集束炸弹 + #loc_BDArmory_part_bahaClusterBomb_description = 该炸弹会在预设高度炸开并投放大量子弹药。 + #loc_BDArmory_part_bahaClusterBomb_tags = BDA 炸弹 弹药 集束 无制导炸弹 地对地 空对地 武器 + + #loc_BDArmory_part_bahaChaffPod_title = 箔条发射器 + #loc_BDArmory_part_bahaChaffPod_description = 能投放用于迷惑和解除雷达锁定的干扰箔条。 + #loc_BDArmory_part_bahaChaffPod_tags = BDA 反制措施 电子对抗 箔条干扰 + + #loc_BDArmory_part_bahaCmPod_title = 热诱弹发射器 + #loc_BDArmory_part_bahaCmPod_description = 能投放用于迷惑红外制导导弹的热诱弹。 + #loc_BDArmory_part_bahaCmPod_tags = BDA 反制措施 电子对抗 红外诱饵 + + #loc_BDArmory_part_bahaDecoyPod_title = 声学诱饵发射器 + #loc_BDArmory_part_bahaDecoyPod_description = 发射声学诱饵,用于迷惑被动声纳鱼雷。 + #loc_BDArmory_part_bahaDecoyPod_tags = BDA 反制措施 诱饵 + + #loc_BDArmory_part_bahaSBTPod_title = 气幕弹发射器 + #loc_BDArmory_part_bahaSBTPod_description = 发射气幕弹,用于降低敌方主动声呐的效能。 + + #loc_BDArmory_part_bahaECMJammer_title = AN/ALQ-131电子对抗吊舱 + #loc_BDArmory_part_bahaECMJammer_description = 该电子设备可以使雷达更难以锁定你的载具,并增大机动脱锁概率。 + #loc_BDArmory_part_bahaECMJammer_tags = BDA 电子对抗干扰器 反制措施 + + #loc_BDArmory_part_bahaGau-8_title = GAU-8 30x173mm机炮 + #loc_BDArmory_part_bahaGau-8_description = 7管30mm转管机炮。 + #loc_BDArmory_part_bahaGau-8_tags = BDA 30毫米加特林机炮 GAU系列 快速旋转炮 武器 + + #loc_BDArmory_part_bahaGoalKeeper_title = “守门员”近程防御武器系统 + #loc_BDArmory_part_bahaGoalKeeper_description = 可以全向旋转的7管30mm转管机炮炮塔。30mm的高爆弹药会在预设距离上爆炸,但该武器并没有自动设置定时引信的功能。其自带的探测和追踪雷达只在短距离上有效,不能用来取代大型探测雷达。 + #loc_BDArmory_part_bahaGoalKeeper_tags = BDA 30毫米加特林机炮 GAU系列 快速旋转炮 近防系统 武器 + + #loc_BDArmory_part_GoalKeeperBDAcMk1_title = “守门员”Mk1近程防御武器系统 + #loc_BDArmory_part_GoalKeeperBDAcMk1_description = 可以全向旋转的7管30mm转管机炮炮塔。Mk 1版本是从一大片泥地里的一块帐篷布底下发现的,因为价格便宜,特别适合预算有限的军事组织和政府采购。该版本没有探测和追踪用的雷达,需要第三方提供目标信息(让人去观察和指示目标不太好使,因为这玩意发射时的噪音实在是有点大)。其发射的30mm的高爆弹药等飞累了就会自行爆炸,该武器并没有自动设置定时引信的功能。 + + #loc_BDArmory_part_BDAcGKmk2_title = “守门员”Mk2近程防御武器系统 + #loc_BDArmory_part_BDAcGKmk2_description = 可以全向旋转的7管30mm转管机炮炮塔。Mk 2版本则是在旧KSC的机库里面,从一大堆空喷漆罐子下面挖出来的,其开发思路基于Mk 1,核心思想是减轻早期观测手受到的听力减退困扰。Mk 2比起Mk 1来说有几项优势,其装备了红外瞄准系统和雷达信号接收机。其发射的30mm的高爆弹药只比Mk 1的稍微改进了一点,飞累了自行爆炸所需的时间要稍微长一些,这样击中目标的概率就大了一点,但是该武器自始至终也还是没有自动设置定时引信的功能。 + + #loc_BDArmory_part_scanLockRadar1_title = TWS(边扫描边跟踪雷达)锁定雷达 + #loc_BDArmory_part_scanLockRadar1_description = 该设备由一台中距探测雷达与一台内建的追踪雷达组成。该雷达可以锁定目标,且在追踪已锁定目标的同时还可以继续扫描(即TWS - Track While Scan)。适用于防空搜索与追踪,但并不适合探测和追踪地面目标。 + #loc_BDArmory_part_scanLockRadar1_tags = BDA 雷达 探测 跟踪 锁定 扫描 搜索 火控系统 + + #loc_BDArmory_part_scanLargeRadar_title = 大型探测雷达 + #loc_BDArmory_part_scanLargeRadar_description = 具有长距离发现目标能力的大型雷达。该雷达无法追踪和锁定目标,适用于防空警戒,难以探测地面目标。 + #loc_BDArmory_part_scanLargeRadar_tags = BDA 雷达 探测 扫描 搜索 + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = F-86火箭吊舱 + #loc_BDArmory_part_F86RL_description = 专为空对空作战设计的内置火箭发射器,可容纳24枚太空飞鼠火箭(FFAR)。空弹后可从弹药箱中重新装填。 + #loc_BDArmory_part_F86RL_tags = BDA火箭吊舱发射器,空对空,高爆弹药,可重新装填,武器 + + #loc_BDArmory_part_bahaH70Launcher_title = Hydra-70火箭吊舱(九头蛇70航空火箭弹吊舱) + #loc_BDArmory_part_bahaH70Launcher_description = 可容纳并发射19枚无制导的Hydra-70火箭。 + #loc_BDArmory_part_bahaH70Launcher_tags = BDA 火箭发射器 火箭吊舱 空对地 武器 + + #loc_BDArmory_part_bahaH70Turret_title = Hydra-70火箭炮塔 + #loc_BDArmory_part_bahaH70Turret_description = Hydra-70火箭炮塔吊舱是一种可旋转的火箭吊舱,能够容纳并发射32枚无制导的Hydra-70火箭 + #loc_BDArmory_part_bahaH70Turret_tags = BDA 火箭吊舱发射器 空对地 炮塔 武器 + + #loc_BDArmory_part_bahaHarm_title = AGM-88高速反辐射(HARM)导弹 + #loc_BDArmory_part_bahaHarm_description = 高速反辐射导弹。该导弹会自动追踪攻击由雷达警告接收器发现的雷达信号源。 + #loc_BDArmory_part_bahaHarm_tags = BDA 反辐射导弹 导弹 自导 空对地 地对地 弹药 武器 + + //HEKV Missile + #loc_BDArmory_part_bahaHEKV1_title = HE-KV-1导弹 + #loc_BDArmory_part_bahaHEKV1_description = HE-KV-1(High explosive kill vehicle,高爆攻击弹药)是一款使用RCS和推力矢量控制机动的雷达制导导弹,也就是说其可以在真空中转向攻击目标。秒3千米 ΔV。 + #loc_BDArmory_part_bahaHEKV1_tags = BDA missile radar homing ata a2a ordnance rcs space weap + + //KKV Missile + #loc_BDArmory_part_bahaKKV_title = 动能杀伤飞行器(KKV) + #loc_BDArmory_part_bahaKKV_description = 动能杀伤飞行器(KKV)是一种红外制导的寻的导弹,它使用反作用控制推进器和控制力矩陀螺仪进行机动。它能够在真空中转向目标,并且在大气层中具有较高的阻力。动能杀伤飞行器依靠动能来摧毁目标,不携带任何爆炸物。其最大速度变化(Delta-V)为6公里/秒。 + #loc_BDArmory_part_bahaKKV_tags = BDA 雷达制导导弹 空对空 空对地 弹药 RCS 空间 武器 + + #loc_BDArmory_part_bahaAGM-114_title = AGM-114“地狱火”导弹 + #loc_BDArmory_part_bahaAGM-114_description = 小型高速激光制导导弹。 + #loc_BDArmory_part_bahaAGM-114_tags = BDA 激光制导导弹 地对地 空对地 弹药 武器 + + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R “地狱火”II EMP导弹 + #loc_BDArmory_part_bahaAGM-114_EMP_description = 小型高速激光制导导弹,配备最新微型电磁脉冲弹头。 虽然脉冲半径很小(50 米),但却相当有效。 导弹对结构造成的破坏极小,但会使爆炸半径内的所有电子设备失效。 + #loc_BDArmory_part_bahaAGM-114_EMP_tags = BDA 激光制导导弹 地对地 空对地 弹药 电磁脉冲 武器 + + #loc_BDArmory_part_bahaHiddenVulcan_title = “火神”机炮(隐身型) + #loc_BDArmory_part_bahaHiddenVulcan_description = 六管20x102mm转管机炮。(发射20x102Ammo) + #loc_BDArmory_part_bahaHiddenVulcan_tags = BDA 20毫米加特林机炮 武器 + + #loc_BDArmory_part_bahaJdamMk83_title = Mk 83联合直接攻击弹药 + #loc_BDArmory_part_bahaJdamMk83_description = 1000磅GPS制导炸弹。 + #loc_BDArmory_part_bahaJdamMk83_tags = BDA GPS制导炸弹 地对地 导引 武器 + + #loc_BDArmory_part_bahaM102Howitzer_title = M102榴弹炮(侧面安装版) + #loc_BDArmory_part_bahaM102Howitzer_description = 105mm侧面安装火炮。(发射CannonShells) + #loc_BDArmory_part_bahaM102Howitzer_tags = BDA 火炮 炮塔 炮弹 榴弹炮 武器 + + #loc_BDArmory_part_bahaM1Abrams_title = M1“艾布拉姆斯”主炮 + #loc_BDArmory_part_bahaM1Abrams_description = 安装在装甲炮塔上的120mm火炮。(发射CannonShells) + #loc_BDArmory_part_bahaM1Abrams_tags = BDA 坦克炮 炮塔 炮弹 坦克 武器 + + #loc_BDArmory_part_bahaM230ChainGun_title = M230弹链供弹机炮 + #loc_BDArmory_part_bahaM230ChainGun_description = M230弹链供弹机炮是一款发射30x173mm高爆弹药的单管自动武器,常安装于武装直升机上。 + #loc_BDArmory_part_bahaM230ChainGun_tags = BDA 30毫米直升机炮 炮塔 + + #loc_BDArmory_part_bahaAGM-65_title = AGM-65“小牛”导弹 + #loc_BDArmory_part_bahaAGM-65_description = 中等当量激光制导空对地导弹。 + #loc_BDArmory_part_bahaAGM-65_tags = 武器BDA 激光制导导弹 地对地 导引 武器 + + #loc_BDArmory_part_missileTurretTest_title = “贾纳斯”旋转导弹架 + #loc_BDArmory_part_missileTurretTest_description = 可以安装并发射最多8枚小型至中型导弹的导弹架,内置探测追踪雷达。如果上面安装了不是导弹的任何东西则保修失效。在武器管理器里选择所安装的导弹即可启动导弹架。 + #loc_BDArmory_part_missileTurretTest_tags = BDA 导弹炮塔 发射导轨 安装 挂载点 雷达锁定 + + #loc_BDArmory_part_bahaMk82Bomb_title = Mk 82炸弹 + #loc_BDArmory_part_bahaMk82Bomb_description = 500磅无制导炸弹。 + #loc_BDArmory_part_bahaMk82Bomb_tags = BDA 炸弹 无制导炸弹 地对地 武器 + + #loc_BDArmory_part_bahaMk82BombBrake_title = Mk 82“蛇眼”炸弹 + #loc_BDArmory_part_bahaMk82BombBrake_description = 带减速板的500磅无制导炸弹,用于低高度轰炸。 + + #loc_BDArmory_part_bahaOMillennium_title = 厄利孔“千禧年”机炮 + #loc_BDArmory_part_bahaOMillennium_description = 发射定时爆炸高爆弹的炮塔,适合短距离防空任务。炮口处有测量每发炮弹初速度、并自动校准引信的装置,使炮弹能准确地在抵近目标的预设距离上爆炸。(发射30x173Ammo) + #loc_BDArmory_part_bahaOMillennium_tags = BDA 火炮炮塔 近防系统 高射炮 自动炮 奥利康 近防系统 30毫米 武器 + + #loc_BDArmory_part_bahaPac-3_title = PAC-3“爱国者”拦截导弹 + #loc_BDArmory_part_bahaPac-3_description = 中距高速雷达制导地对空导弹。 + #loc_BDArmory_part_bahaPac-3_tags = BDA 导弹 雷达 弹药 空对空 空对地 自导 制导 半主动雷达制导 地空导弹 武器 + + #loc_BDArmory_part_patriotLauncherTurret_title = “爱国者”导弹发射器 + #loc_BDArmory_part_patriotLauncherTurret_description = 可以安装并发射最多16枚(每个隔间4枚)PAC-3导弹的导弹发射器。如果上面安装了不是导弹的任何东西则保修失效。在武器管理器里选择所安装的导弹即可启动发射器。 + #loc_BDArmory_part_patriotLauncherTurret_tags = BDA 导弹 炮塔 发射 导轨 安装 挂载点 地空导弹 + + #loc_BDArmory_part_radarDataReceiver_title = 雷达数据接收器 + #loc_BDArmory_part_radarDataReceiver_description = 该模块可以经由数据链接收雷达信息,并通过链接的其他雷达锁定目标,但是不能自行扫描或锁定目标。适合用于隐蔽的导弹炮台等单位。 + #loc_BDArmory_part_radarDataReceiver_tags = BDA 雷达 探测 数据链 数据锁定 + + //AN/APG-63 Variants + #loc_BDArmory_part_bdRadome_variantPitot = 带天线雷达罩 + #loc_BDArmory_part_bdRadome_variantNoPitot = 不带天线雷达罩 + #loc_BDArmory_part_bdRadome1_title = AN/APG-63机头雷达 + #loc_BDArmory_part_bdRadome1_description = 带气动保护罩的前视雷达,可以在120度视场内扫描并锁定目标。其专为空战优化,难以锁定地面目标。 + #loc_BDArmory_part_bdRadome1_tags = BDA 雷达 雷达罩 探测 锁定 跟踪 扫描 + #loc_BDArmory_part_bdRadome1inline_title = AN/APG-63内置型机载雷达 + #loc_BDArmory_part_bdRadome1inline_description = 带气动保护罩的前视雷达,可以在120度视场内扫描并锁定目标。安装时请注意黑色箭头务必指向前方,其专为空战优化,难以锁定地面目标。 + #loc_BDArmory_part_bdRadome1snub_title = AN/APG-63对地型机头雷达 + #loc_BDArmory_part_bdRadome1snub_description = 带气动保护罩的前视雷达,可以在120度视场内扫描并锁定目标。该型号为对地攻击做了特别优化,攻击地面目标效能大大增强,但其对空作战能力因此受限。 + + #loc_BDArmory_part_bahaRBS-15Cruise_title = RBS-15巡航导弹 + #loc_BDArmory_part_bahaRBS-15Cruise_description = 长距多平台高速巡航导弹,附带助推器。 + #loc_BDArmory_part_bahaRBS-15Cruise_tags = BDA 导弹 弹药 巡航 GPS 制导 自导 武器 + + #loc_BDArmory_part_bahaRBS-15ALCruise_title = RBS-15 空射巡航导弹 + #loc_BDArmory_part_bahaRBS-15ALCruise_description = 长距多平台高速巡航导弹,空射版本不附带助推器。 + + #loc_BDArmory_part_bdRotBombBay_title = 可调旋转弹架 + #loc_BDArmory_part_bdRotBombBay_description = 可调节的旋转弹架。黄色箭头指向武器投放方向,仅限安装导弹和炸弹,一根支架限一枚。 + #loc_BDArmory_part_bdRotBombBay_tags = BDA 导弹 导轨 发射 安装 旋转 架 + #loc_BDArmory_part_bahaS-8Launcher_title = S-8KOM火箭弹发射器 + #loc_BDArmory_part_bahaS-8Launcher_description = 内装23枚S-8KOM无制导火箭弹的发射器,附带气动头锥。 + + #loc_BDArmory_part_bahaAim9_title = AIM-9“响尾蛇”导弹 + #loc_BDArmory_part_bahaAim9_description = 短距红外制导导弹。 + #loc_BDArmory_part_bahaAim9_tags = BDA 导弹 弹药 加热器 热追踪 空对空 空对空导弹 武器 + + #loc_BDArmory_part_bdWarheadSmall_title = 小型高爆弹头 + #loc_BDArmory_part_bdWarheadSmall_description = 一个内含炸药的导弹头锥。 + #loc_BDArmory_part_bdWarheadSmall_tags = BDA 导弹 炸弹 弹药 爆炸 武器 + + #loc_BDArmory_part_bahaSmokeCmPod_title = 烟雾对抗措施吊舱 + #loc_BDArmory_part_bahaSmokeCmPod_description = 可发射用于遮蔽激光瞄准系统视野的防御烟幕。 + #loc_BDArmory_part_bahaSmokeCmPod_tags = BDA 反制措施 烟雾 + + #loc_BDArmory_part_bahaFlirBall_title = FLIR球形瞄准摄像头 + #loc_BDArmory_part_bahaFlirBall_description = 用于瞄准和侦察目标的球形摄像头。带有高清摄像头和垂直稳定系统,可感知红外线,使你能迅速发现和锁定供导弹攻击的地面目标。 + + #loc_BDArmory_part_bahaCamPod_tags = BDA 探测 激光 GPS 摄像机 红外热像仪 目标 + #loc_BDArmory_part_bahaCamPod_title = AN/AAQ-28瞄准吊舱 + #loc_BDArmory_part_bahaCamPod_description = 用于瞄准和侦察目标的吊舱。带有高清摄像头和垂直稳定系统,可感知红外线,使你能迅速发现和锁定供导弹攻击的地面目标。 + + #loc_BDArmory_part_bahaIRSTPod_title = AN/AAQ-42 IRST 吊舱 + #loc_BDArmory_part_bahaIRSTPod_description = 红外搜索与跟踪系统(IRST)安装在一个空气动力学吊舱中,面向前方。它可以扫描和探测 120 度视场范围内的红外信号。该系统专为空对空使用而优化,难以探测地面目标。 + #loc_BDArmory_part_bahaIRSTPod_tags = BDA热成像红外探测吊舱,用于探测、扫描、搜索和跟踪目标,具备红外(IR)和热成像(Therm)功能 + + #loc_BDArmory_part_towLauncherTurret_title = “陶”式发射器 + #loc_BDArmory_part_towLauncherTurret_description = 能安装并发射最多4枚“陶”式导弹的旋转发射器。如果安装了除“陶”式以外的其他导弹则保修失效。在武器管理器里选择所安装的导弹即可启动发射器。 + #loc_BDArmory_part_towLauncherTurret_tags = BDA 导弹 炮塔 导轨 安装 发射 拖曳 + + #loc_BDArmory_part_bahaTowMissile_title = BGM-71“陶”式反坦克导弹 + #loc_BDArmory_part_bahaTowMissile_description = 短程无线激光驾束制导反坦克导弹。 + #loc_BDArmory_part_bahaTowMissile_tags = BDA 导弹 弹药 激光 AGM 地对地 武器 + + #loc_BDArmory_part_missileController_title = 武器管理器 + #loc_BDArmory_part_missileController_description = 一键管理切换各种武器和发射功能。 + #loc_BDArmory_part_missileController_tags = BDA 武器管理 人工智能 武器 + + #loc_BDArmory_part_BDAsonarPod1A_title = BDA Mk1声纳吊舱 + #loc_BDArmory_part_BDAsonarPod1A_description = BDA Mk1声纳吊舱适合完全浸没在水下工作,可以用于探测水面和潜水的目标。如果装在水面以上的部分,其探测距离和灵敏度将大打折扣。 + #loc_BDArmory_part_BDAsonarPod1A_tags = BDA 探测 声呐 舰船 + + #loc_BDArmory_part_StingRayBDATorpedo_title = “魟鱼”BDA轻型鱼雷 + #loc_BDArmory_part_StingRayBDATorpedo_description = “魟鱼”轻型鱼雷由舰船和直升机发射,请勿用作潜艇鱼雷。顺便有个好玩的事,你可以在“爱国者”发射器里面塞进16枚鱼雷,但是这样非常危险,未经训练进行操作可能导致你事后得哭着写上万字的检讨。 + #loc_BDArmory_part_StingRayBDATorpedo_tags = BDA 导弹 弹药 反舰导弹 鱼雷 舰船 声呐 自导 制导 舰对舰 舰对舰 + + #loc_BDArmory_part_EJ200_title = TFJ-EJ200“台风”加力涡扇发动机 + #loc_BDArmory_part_EJ200_description = 据说这款发动机是国际合作的成果,具有卓越的性能和潜力。 + + #loc_BDArmory_part_SaturnAL31_title = “土星” AL-31FM1 后燃室涡扇引擎 + #loc_BDArmory_part_SaturnAL31_description = 这是一款高性能喷气发动机,带有可变几何形状的推力矢量喷嘴和用于额外推力的加力燃烧室。 基于备受欢迎的J-404发动机,KTech工程师看到了(高度)将商用变型改造成军事用途的强大动力装置的潜力。 在看到发动机的潜力后,BDAc集团立即将其用于新的MkIII测试无人机。 + #loc_BDArmory_part_SaturnAL31_tags = 战斗机 后燃器 发动机 喷气式 飞机 萨特恩 推进 AL + + //GravityGun + #loc_BDArmory_part_GravGun_title = 重力枪(零点能量场操纵器) + #loc_BDArmory_part_GravGun_description = 一种将重力武器化的高科技枪支。该武器似乎由科技魔法和外星技术混合驱动,能够对目标施加非牛顿力,并影响其表面质量。巴哈姆特动力对该武器造成的任何损失(对个人、财产或现实结构)概不负责。 + #loc_BDArmory_part_GravGun_tags = BDA重力枪,激光武器,重力,武器 + + #loc_BDArmory_part_genie_title = AIR-2 “妖怪”空对空火箭弹 + #loc_BDArmory_part_genie_description = 1.5千吨当量核战斗部对空火箭弹。 + #loc_BDArmory_part_genie_tags = BDA 导弹 核武器 空对空 空对空导弹 + + #loc_BDArmory_part_GAU22_title = GAU-22/A “平衡者”25x137mm 机炮 + #loc_BDArmory_part_GAU22_description = 一种四管25mm转管机炮 + #loc_BDArmory_part_GAU22_tags = BDA 25毫米加特林机枪,武器,GAU系列 + + #loc_BDArmory_part_sidam_title = “西达姆”防空炮 + #loc_BDArmory_part_sidam_description = 四联装 25 mm防空炮。 + #loc_BDArmory_part_sidam_tags = BDA 25毫米炮塔式防空机关炮,武器 + + #loc_BDArmory_part_REA_title = BD 1x0.5 反应装甲 + #loc_BDArmory_part_REA_Panel_description = 一段尺寸为1米×0.5米的反应式装甲板块。非常适合在现有装甲的基础上增加额外的一点防护。 + #loc_BDArmory_part_REA_Panel_tags = BDA反应式装甲板块,装甲,反应式,防护 + + #loc_BDArmory_part_Panel_title = BD 装甲板 + #loc_BDArmory_part_Panel_description = 坚固的通用结构面板,可以配置成各种尺寸,使用各种材料,是建造或加固各种物品的完美选择。 + + #loc_BDArmory_part_TriPanel_title = BD 直角三角装甲板 + #loc_BDArmory_part_TriIsoPanel_title = BD 三角装甲板 + #loc_BDArmory_part_Tripanel_description = 坚固的通用结构面板,可以配置成各种尺寸,使用各种材料,是建造或加固各种物品的完美选择。这块是三角形的。 + + #loc_BDArmory_part_BombBay_title = 弹舱 + #loc_BDArmory_part_BombBay_description = 带有可展开弹药架的弹舱。弹药在展开之前不会受到气流的影响。 + #loc_BDArmory_part_BombBay_tags = BDA导弹弹药舱,导弹,弹药,轨道,部署,弹架 + + #loc_BDArmory_part_combatSeat_title = EAS-2 外部战斗座椅 + #loc_BDArmory_part_combatSeat_description = EAS-2 外部战斗座椅包含集成的AI自动驾驶飞控计算机和武器管理器,可让坐在座位上的坎巴拉人控制飞行器,在执行空中巡逻作战任务时解放双手。请根据飞机的独特飞行特性调整数值。 请手动启动引擎。 + #loc_BDArmory_part_combatSeat_tags = BDA人工智能武器管理系统,飞行员座椅 + + //AN/APG-77v1 ATG Radar + #loc_BDArmory_part_bdRadome1snub_ground_title = APG-77 对地型机头雷达 + #loc_BDArmory_part_bdRadome1inline_ground_title = APG-77v1 内置型对地型雷达 + #loc_BDArmory_part_bdRadome1_ground_title = APG-77v1 对地型机头雷达 + #loc_BDArmory_part_bdRadome1_Gnd_desc = AN/APG-77v1是一种前视,带气动头锥,使用固态电子器件的有源相控阵(AESA)雷达。具有强大空对地效能,最大工作距离为 40 千米,可在 120 度视场范围内打击静止和移动目标。其专为对地攻击优化,难以锁定空中目标。 + #loc_BDArmory_part_bdRadome1_Gnd_tags = BDA雷达探测,扫描,地面跟踪 + + #loc_BDArmory_part_AWACS_Legged = 有支架 + #loc_BDArmory_part_AWACS_Legless = 无支架 + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/part_deformatter.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/part_deformatter.cfg index 06a7bd79f..6956b0387 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/part_deformatter.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/part_deformatter.cfg @@ -1,5 +1,5 @@ //Replacing all title/manufacturers/descriptions with localization tags -@PART[bahaGatlingGun]:FINAL +@PART[bahaGatlingGun]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaGatlingGun_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -7,7 +7,7 @@ } -@PART[bahaTurret]:FINAL +@PART[bahaTurret]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaTurret_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -15,7 +15,7 @@ } -@PART[bahaABL]:FINAL +@PART[bahaABL]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaABL_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -23,7 +23,7 @@ } -@PART[bahaAdjustableRail]:FINAL +@PART[bahaAdjustableRail]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaAdjustableRail_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -31,7 +31,7 @@ } -@PART[bahaAgm86B]:FINAL +@PART[bahaAgm86B]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaAgm86B_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -39,7 +39,7 @@ } -@PART[bahaAim120]:FINAL +@PART[bahaAim120]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaAim120_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -47,7 +47,7 @@ } -@PART[bdPilotAI]:FINAL +@PART[bdPilotAI]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdPilotAI_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -55,7 +55,7 @@ } -@PART[baha20mmAmmo]:FINAL +@PART[baha20mmAmmo]:AFTER[BDArmory] { @title = #loc_BDArmory_part_baha20mmAmmo_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -63,7 +63,7 @@ } -@PART[baha30mmAmmo]:FINAL +@PART[baha30mmAmmo]:AFTER[BDArmory] { @title = #loc_BDArmory_part_baha30mmAmmo_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -71,7 +71,7 @@ } -@PART[baha50CalAmmo]:FINAL +@PART[baha50CalAmmo]:AFTER[BDArmory] { @title = #loc_BDArmory_part_baha50CalAmmo_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -79,7 +79,7 @@ } -@PART[UniversalAmmoBoxBDA]:FINAL +@PART[UniversalAmmoBoxBDA]:AFTER[BDArmory] { @title = #loc_BDArmory_part_UniversalAmmoBoxBDA_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -87,7 +87,7 @@ } -@PART[bahaCannonShellBox]:FINAL +@PART[bahaCannonShellBox]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaCannonShellBox_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -95,7 +95,7 @@ } -@PART[BD1x1slopeArmor]:FINAL +@PART[BD1x1slopeArmor]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BD1x1slopeArmor_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -103,7 +103,7 @@ } -@PART[BD2x1slopeArmor]:FINAL +@PART[BD2x1slopeArmor]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BD2x1slopeArmor_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -111,7 +111,7 @@ } -@PART[BD1x1panelArmor]:FINAL +@PART[BD1x1panelArmor]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BD1x1panelArmor_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -119,7 +119,7 @@ } -@PART[BD2x1panelArmor]:FINAL +@PART[BD2x1panelArmor]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BD2x1panelArmor_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -127,7 +127,7 @@ } -@PART[BD3x1panelArmor]:FINAL +@PART[BD3x1panelArmor]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BD3x1panelArmor_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -135,7 +135,7 @@ } -@PART[BD4x1panelArmor]:FINAL +@PART[BD4x1panelArmor]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BD4x1panelArmor_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -143,7 +143,7 @@ } -@PART[awacsRadar]:FINAL +@PART[awacsRadar]:AFTER[BDArmory] { @title = #loc_BDArmory_part_awacsRadar_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -151,7 +151,7 @@ } -@PART[bdammGuidanceModule]:FINAL +@PART[bdammGuidanceModule]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdammGuidanceModule_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -159,7 +159,7 @@ } -@PART[bahaBrowningAnm2]:FINAL +@PART[bahaBrowningAnm2]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaBrowningAnm2_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -167,7 +167,7 @@ } -@PART[bahaClusterBomb]:FINAL +@PART[bahaClusterBomb]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaClusterBomb_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -175,7 +175,7 @@ } -@PART[bahaChaffPod]:FINAL +@PART[bahaChaffPod]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaChaffPod_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -183,7 +183,7 @@ } -@PART[bahaCmPod]:FINAL +@PART[bahaCmPod]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaCmPod_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -191,7 +191,7 @@ } -@PART[bahaECMJammer]:FINAL +@PART[bahaECMJammer]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaECMJammer_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -199,7 +199,7 @@ } -@PART[bahaGau-8]:FINAL +@PART[bahaGau-8]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaGau-8_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -207,7 +207,7 @@ } -@PART[bahaGoalKeeper]:FINAL +@PART[bahaGoalKeeper]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaGoalKeeper_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -215,7 +215,7 @@ } -@PART[GoalKeeperBDAcMk1]:FINAL +@PART[GoalKeeperBDAcMk1]:AFTER[BDArmory] { @title = #loc_BDArmory_part_GoalKeeperBDAcMk1_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -223,7 +223,7 @@ } -@PART[BDAcGKmk2]:FINAL +@PART[BDAcGKmk2]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BDAcGKmk2_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -231,7 +231,7 @@ } -@PART[scanLockRadar1]:FINAL +@PART[scanLockRadar1]:AFTER[BDArmory] { @title = #loc_BDArmory_part_scanLockRadar1_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -239,7 +239,7 @@ } -@PART[scanLargeRadar]:FINAL +@PART[scanLargeRadar]:AFTER[BDArmory] { @title = #loc_BDArmory_part_scanLargeRadar_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -247,7 +247,7 @@ } -@PART[bahaH70Launcher]:FINAL +@PART[bahaH70Launcher]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaH70Launcher_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -255,7 +255,7 @@ } -@PART[bahaH70Turret]:FINAL +@PART[bahaH70Turret]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaH70Turret_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -263,7 +263,7 @@ } -@PART[bahaHarm]:FINAL +@PART[bahaHarm]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaHarm_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -271,7 +271,7 @@ } -@PART[bahaHEKV1]:FINAL +@PART[bahaHEKV1]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaHEKV1_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -279,7 +279,7 @@ } -@PART[bahaAGM-114]:FINAL +@PART[bahaAGM-114]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaAGM-114_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -287,7 +287,7 @@ } -@PART[bahaHiddenVulcan]:FINAL +@PART[bahaHiddenVulcan]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaHiddenVulcan_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -295,7 +295,7 @@ } -@PART[bahaJdamMk83]:FINAL +@PART[bahaJdamMk83]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaJdamMk83_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -303,7 +303,7 @@ } -@PART[bahaM102Howitzer]:FINAL +@PART[bahaM102Howitzer]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaM102Howitzer_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -311,7 +311,7 @@ } -@PART[bahaM1Abrams]:FINAL +@PART[bahaM1Abrams]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaM1Abrams_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -319,7 +319,7 @@ } -@PART[bahaM230ChainGun]:FINAL +@PART[bahaM230ChainGun]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaM230ChainGun_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -327,7 +327,7 @@ } -@PART[bahaAGM-65]:FINAL +@PART[bahaAGM-65]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaAGM-65_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -335,7 +335,7 @@ } -@PART[missileTurretTest]:FINAL +@PART[missileTurretTest]:AFTER[BDArmory] { @title = #loc_BDArmory_part_missileTurretTest_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -343,7 +343,7 @@ } -@PART[bahaMk82Bomb]:FINAL +@PART[bahaMk82Bomb]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaMk82Bomb_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -351,7 +351,7 @@ } -@PART[bahaMk82BombBrake]:FINAL +@PART[bahaMk82BombBrake]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaMk82BombBrake_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -359,7 +359,7 @@ } -@PART[bahaOMillennium]:FINAL +@PART[bahaOMillennium]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaOMillennium_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -367,7 +367,7 @@ } -@PART[bahaPac-3]:FINAL +@PART[bahaPac-3]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaPac-3_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -375,7 +375,7 @@ } -@PART[patriotLauncherTurret]:FINAL +@PART[patriotLauncherTurret]:AFTER[BDArmory] { @title = #loc_BDArmory_part_patriotLauncherTurret_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -383,7 +383,7 @@ } -@PART[radarDataReceiver]:FINAL +@PART[radarDataReceiver]:AFTER[BDArmory] { @title = #loc_BDArmory_part_radarDataReceiver_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -391,7 +391,7 @@ } -@PART[bdRadome1]:FINAL +@PART[bdRadome1]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdRadome1_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -399,7 +399,7 @@ } -@PART[bdRadome1inline]:FINAL +@PART[bdRadome1inline]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdRadome1inline_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -407,7 +407,7 @@ } -@PART[bdRadome1snub]:FINAL +@PART[bdRadome1snub]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdRadome1snub_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -415,7 +415,7 @@ } -@PART[bahaRBS-15Cruise]:FINAL +@PART[bahaRBS-15Cruise]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaRBS-15Cruise_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -423,7 +423,7 @@ } -@PART[bdRotBombBay]:FINAL +@PART[bdRotBombBay]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdRotBombBay_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -431,7 +431,7 @@ } -@PART[bahaS-8Launcher]:FINAL +@PART[bahaS-8Launcher]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaS-8Launcher_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -439,7 +439,7 @@ } -@PART[bahaAim9]:FINAL +@PART[bahaAim9]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaAim9_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -447,7 +447,7 @@ } -@PART[bdWarheadSmall]:FINAL +@PART[bdWarheadSmall]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bdWarheadSmall_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -455,7 +455,7 @@ } -@PART[bahaSmokeCmPod]:FINAL +@PART[bahaSmokeCmPod]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaSmokeCmPod_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -463,7 +463,7 @@ } -@PART[bahaFlirBall]:FINAL +@PART[bahaFlirBall]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaFlirBall_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -471,7 +471,7 @@ } -@PART[bahaCamPod]:FINAL +@PART[bahaCamPod]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaCamPod_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -479,7 +479,7 @@ } -@PART[towLauncherTurret]:FINAL +@PART[towLauncherTurret]:AFTER[BDArmory] { @title = #loc_BDArmory_part_towLauncherTurret_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -487,7 +487,7 @@ } -@PART[bahaTowMissile]:FINAL +@PART[bahaTowMissile]:AFTER[BDArmory] { @title = #loc_BDArmory_part_bahaTowMissile_title @manufacturer = #loc_BDArmory_part_manufacturer @@ -495,21 +495,21 @@ } -@PART[missileController]:FINAL +@PART[missileController]:AFTER[BDArmory] { @title = #loc_BDArmory_part_missileController_title @manufacturer = #loc_BDArmory_part_manufacturer @description = #loc_BDArmory_part_missileController_description } -@PART[BDAsonarPod1A]:FINAL +@PART[BDAsonarPod1A]:AFTER[BDArmory] { @title = #loc_BDArmory_part_BDAsonarPod1A_title @manufacturer = #loc_BDArmory_part_manufacturer @description = #loc_BDArmory_part_BDAsonarPod1A_description } -@PART[StingRayBDATorpedo]:FINAL +@PART[StingRayBDATorpedo]:AFTER[BDArmory] { @title = #loc_BDArmory_part_StingRayBDATorpedo_title @manufacturer = #loc_BDArmory_part_manufacturer diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg index 6c610ba81..3ccdb8b32 100644 --- a/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg @@ -1,3 +1,138 @@ +/////////////////////////////////////////// +//Fixes for AoA Technologies Parts +/////////////////////////////////////////// + +@PART[AoAero.ADVfin] //Angled Fin +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 500 //Buff, original was 100 + ExplodeMode = Never + } +} + +@PART[AoAero.TESTFin] //High Efficiency Fin +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 500 //Buff, original was 100 + ExplodeMode = Never + } +} + +@PART[AoA.BroncoCpit] //Combat Dragon II +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1400 //Nerf, made inline with other cokpits, original was 2600 + ExplodeMode = Never + } +} + +@PART[AoA.falkenTwo] //Mk2 Falken Drone +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1600 //Nerf, made inline with other cokpits, original was 2700 + ExplodeMode = Never + } +} + +@PART[AoA.falkenDroneS] //Mk2 Falken Drone RCS +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1800 //Nerf, made inline with other cokpits, original was 2200 + ExplodeMode = Never + } +} + +@PART[AoA.droneTwo] //MK2 "Kerbin Hawk" Drone +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1700 //Nerf, made inline with other cokpits, original was 2200 + ExplodeMode = Never + } +} + +@PART[AoA.droneThree] //MK1-2 "Kerbin Hawk" Drone FLIR +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1700 //Buff, made inline with other cokpits, original was 300 + ExplodeMode = Never + } +} + +@PART[AoA.droneTwo] //MK2 "Kerbin Hawk" Drone +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1700 //Nerf, made inline with other cokpits, original was 2200 + ExplodeMode = Never + } +} + +@PART[AoA.droneTwo] //MK2 "Kerbin Hawk" Drone +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1700 //Nerf, made inline with other cokpits, original was 2200 + ExplodeMode = Never + } +} + +@PART[AoA.Rafale] //Rafale +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 2100 //Nerf, made inline with other cokpits, original was 4500 + ExplodeMode = Never + } +} + +@PART[AoA.SkuCockpit] //KU-34 Cockpit +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1500 //Nerf, made inline with other cokpits, original was 3200 + ExplodeMode = Never + } +} + +@PART[AoA.TucanCpit] //Super Tucano +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1500 //Nerf, made inline with other cokpits, original was 3600 + ExplodeMode = Never + } +} + + +@PART[AoA.Hornet] //Mk1 Cockpit +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1400 //Buff, made inline with other cokpits, original was 1200 + ExplodeMode = Never + } +} + /////////////////////////////////////////// //Fixes for Breaking Ground and Infernal Robotics /////////////////////////////////////////// @@ -450,6 +585,16 @@ //Fixes for Mk2 Expansion and Mk3 Expansion Parts /////////////////////////////////////////// +@PART[M2X_TunaCockpit] //Mk2 Tail Connector +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 2000 //Buff, original was 300 + ExplodeMode = Never + } +} + @PART[M2X_Tailboom] //Mk2 Tail Connector { %MODULE[HitpointTracker] @@ -459,7 +604,61 @@ 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 //clamping from 52k + ExplodeMode = Never + } +} +@PART[M3X_HeavyVTOL] //Elephant +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 3500 //was 100 for some reason? + ExplodeMode = Never + } +} +@PART[M2X_LiftFan] //Banshee Fan +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 900 //was 100 for some reason? + ExplodeMode = Never + } +} +@PART[M2X_FuselageLiftFan] //Fuselage banshee Fan +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 900 //was 100 for some reason? + ExplodeMode = Never + } +} +@PART[M2X_HeavyVTOL] //J. Edgar hover engine +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 2000 //was 100 for some reason? + ExplodeMode = Never + } +} @PART[M2X_EngineShroud] //Mk2 Engine Shroud { %MODULE[HitpointTracker] @@ -595,7 +794,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 } } @@ -869,6 +1068,7 @@ ArmorThickness = 10 maxHitPoints = 1000 //Buff, original was 500, scaled to other S2 buffs ExplodeMode = Never + armorVolume = 11.2 } } @@ -879,6 +1079,16 @@ ArmorThickness = 10 maxHitPoints = 1000 //Buff, original was 100, scaled to other S2 buffs ExplodeMode = Never + armorVolume = 16.8 + } +} +@PART[s2CargoRamp] //Structural Hull S2 +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + ExplodeMode = Never + armorVolume = 28.7 } } @@ -889,6 +1099,7 @@ ArmorThickness = 10 maxHitPoints = 1200 //Buff, original was 100, scaled to other S2 buffs ExplodeMode = Never + armorVolume = 22 } } @@ -909,6 +1120,7 @@ ArmorThickness = 10 maxHitPoints = 1000 //Buff, original was 200, scaled to other S2 buffs ExplodeMode = Never + armorVolume = 17.8 } } @@ -1010,6 +1222,7 @@ ArmorThickness = 10 maxHitPoints = 600 //Buff, original was 100, changed to be similar to the MK1 Junior Fuel ExplodeMode = Never + armorVolume = 4 } } @@ -1020,6 +1233,7 @@ ArmorThickness = 10 maxHitPoints = 600 //Buff, original was 100, buffed in step with other Mk2h parts ExplodeMode = Never + armorVolume = 12 } } @@ -1030,6 +1244,7 @@ ArmorThickness = 10 maxHitPoints = 700 //Buff, original was 100, buffed in step with other Mk2h parts ExplodeMode = Never + armorVolume = 12.25 } } @@ -1040,6 +1255,7 @@ ArmorThickness = 10 maxHitPoints = 600 //Buff, original was 100, changed to be half of Mk2 normal ExplodeMode = Never + armorVolume = 13.5 } } @@ -1050,6 +1266,7 @@ ArmorThickness = 10 maxHitPoints = 900 //Buff, original was 200, changed to be inbetween mk2/h and mk2 ExplodeMode = Never + armorVolume = 15 } } @@ -1090,6 +1307,7 @@ ArmorThickness = 10 maxHitPoints = 650 //Buff, original was 100, changed be similar to structural fuselage ExplodeMode = Never + armorVolume = 8.5 } } @@ -1100,6 +1318,7 @@ ArmorThickness = 10 maxHitPoints = 550 //Buff, original was 100, changed be similar to structural fuselage ExplodeMode = Never + armorVolume = 4 } } @@ -1110,6 +1329,7 @@ ArmorThickness = 10 maxHitPoints = 550 //Buff, original was 100, changed be similar to structural fuselage ExplodeMode = Never + armorVolume = 4 } } @@ -2327,6 +2547,94 @@ ExplodeMode = Never } } +/////////////////////////////////////////// +//Fixes for Stock parts +/////////////////////////////////////////// + +@PART[cupola] //PPD-12 cupola +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + ExplodeMode = Never + armorVolume = 16.8 + } +} +@PART[mk2Cockpit_Standard] //Mk2 Cockpit +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + ExplodeMode = Never + armorVolume = 21 + } +} +@PART[Mk1FuselageStructural] //structural tube +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + ExplodeMode = Never + armorVolume = 8.5 + } +} +@PART[flagPart*] +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 1 + ExplodeMode = Never + maxSupportedArmor = 1 + } +} +@PART[mk2CargoBayL] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 23.5 + } +} +@PART[mk2CargoBayS] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 11.75 + } +} +@PART[mk3CargoBayL] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 135 + } +} +@PART[mk3CargoBayM] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 67.5 + } +} +@PART[mk3CargoBayS] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 33.75 + } +} +@PART[parachuteSingle] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + maxHitPoints = 150 + } +} /////////////////////////////////////////// //Fixes for AP+ Drones and SAS @@ -2558,6 +2866,7 @@ ArmorThickness = 10 maxHitPoints = 350 //Buff, original was 100, scaled to other wing connectors ExplodeMode = Never + armorVolume = 3.6 } } @@ -2568,6 +2877,7 @@ ArmorThickness = 10 maxHitPoints = 200 //Buff, original was 100, scaled to other wing connectors ExplodeMode = Never + armorVolume = 2 } } @@ -2578,9 +2888,18 @@ ArmorThickness = 10 maxHitPoints = 200 //Buff, original was 100, scaled to other wing connectors ExplodeMode = Never + armorVolume = 2 } } +@PART[smallwingConnectortip] //Wing Connector Tip +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 1 + } +} @PART[migfin] //Delta-Advanced Winglet { %MODULE[HitpointTracker] @@ -2588,6 +2907,7 @@ ArmorThickness = 10 maxHitPoints = 400 //Buff, original was 100, brought in line with other tails ExplodeMode = Never + armorVolume = 8.6 } } @@ -2598,6 +2918,7 @@ ArmorThickness = 10 maxHitPoints = 300 //Buff, original was 100, kept lower as it is pre modern ExplodeMode = Never + armorVolume = 10 } } @@ -2608,6 +2929,7 @@ ArmorThickness = 10 maxHitPoints = 300 //Buff, original was 100, kept lower as it is pre modern ExplodeMode = Never + armorVolume = 3.4 } } @@ -2648,6 +2970,25 @@ ArmorThickness = 10 maxHitPoints = 300 //Buff, original was 100, kept low as it carries fuel ExplodeMode = Never + armorVolume = 5.2 + } +} +@PART[fatwing5] //FAT-F Wing Connector +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + ExplodeMode = Never + armorVolume = 3.3 + } +} +@PART[fatwing6] //FAT-GWing Connector +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + ExplodeMode = Never + armorVolume = 3.3 } } @@ -2656,8 +2997,9 @@ %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 } } @@ -2901,7 +3243,7 @@ // Nobody knows what this next thing is HA // So sad makes me want to cry oh well -@PART[B9.Aero.Wing.Procedural.TypeB] //defunct or non-stock?? +@PART[B9.Aero.Wing.Procedural.TypeB] //defunct or non-stock?? //neither, this is the proc crtl surface { %MODULE[HitpointTracker] { @@ -2918,6 +3260,7 @@ ArmorThickness = 10 maxHitPoints = 400 //nerf ExplodeMode = Never + armorVolume = 5.9 } } @@ -2928,6 +3271,7 @@ ArmorThickness = 10 maxHitPoints = 450 //nerf ExplodeMode = Never + armorVolume = 6.25 } } @@ -2938,6 +3282,7 @@ ArmorThickness = 10 maxHitPoints = 400 //nerf ExplodeMode = Never + armorVolume = 6.4 } } @@ -2948,6 +3293,7 @@ ArmorThickness = 10 maxHitPoints = 400 //nerf ExplodeMode = Never + armorVolume = 5.15 } } @@ -2968,6 +3314,15 @@ ArmorThickness = 10 maxHitPoints = 3000 //nerf ExplodeMode = Never + armorVolume = 40.65 + } +} +@PART[wingShuttleElevon1] //Big-S Elevon 1 +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 5.28 } } @@ -2978,6 +3333,7 @@ ArmorThickness = 10 maxHitPoints = 1100 //buff, scale Big-S Elevon 1 ExplodeMode = Never + armorVolume = 11.9 } } @@ -2988,6 +3344,7 @@ ArmorThickness = 10 maxHitPoints = 2400 //buff, scale FAT-455 Aeroplane Tail Fin ExplodeMode = Never + armorVolume = 32 } } @@ -2998,6 +3355,7 @@ ArmorThickness = 10 maxHitPoints = 700 //buff, scale Wing Connector Type C ExplodeMode = Never + armorVolume = 12.1 } } @@ -3008,6 +3366,7 @@ ArmorThickness = 10 maxHitPoints = 1400 //buff, match Wing Connector type A ExplodeMode = Never + armorVolume = 16 } } @@ -3018,6 +3377,34 @@ ArmorThickness = 10 maxHitPoints = 400 //buff, scale Wing Connector Type C ExplodeMode = Never + armorVolume = 4 + } +} + +@PART[elevon2] //Elevon 2 +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 2.5 + } +} + +@PART[elevon3] //Elevon 3 +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 3.4 + } +} + +@PART[elevon5] //Elevon 5 +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + armorVolume = 2.8 } } @@ -3028,6 +3415,7 @@ ArmorThickness = 10 maxHitPoints = 700 //buff, match Wing Connector type C ExplodeMode = Never + armorVolume = 9 } } @@ -3038,6 +3426,7 @@ ArmorThickness = 10 maxHitPoints = 700 //buff, match Wing Connector type C ExplodeMode = Never + armorVolume = 9 } } @@ -3048,6 +3437,7 @@ ArmorThickness = 10 maxHitPoints = 400 //buff, match Wing Connector type D ExplodeMode = Never + armorVolume = 4.5 } } @@ -3058,6 +3448,17 @@ ArmorThickness = 10 maxHitPoints = 200 //buff, scale Wing Connector type D ExplodeMode = Never + armorVolume = 2.5 + } +} +@PART[sweptWing] //Swept Wings +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 1000 + ExplodeMode = Never + armorVolume = 12.1 } } @@ -3068,6 +3469,7 @@ ArmorThickness = 10 maxHitPoints = 700 //buff, match Wing Connector type C ExplodeMode = Never + armorVolume = 9.16 } } @@ -3076,8 +3478,9 @@ %MODULE[HitpointTracker] { ArmorThickness = 10 - maxHitPoints = 1400 //buff, match Wing Connector type A + maxHitPoints = 1600 //buff, match Wing Connector type A ExplodeMode = Never + armorVolume = 16.3 } } @@ -3088,6 +3491,7 @@ ArmorThickness = 10 maxHitPoints = 350 //buff, match Wing Connector type D ExplodeMode = Never + armorVolume = 5 } } @@ -3098,6 +3502,7 @@ ArmorThickness = 10 maxHitPoints = 200 //buff ExplodeMode = Never + armorVolume = 2.8 } } @@ -3108,6 +3513,7 @@ ArmorThickness = 10 maxHitPoints = 200 //buff ExplodeMode = Never + armorVolume = 2.2 } } @@ -3116,8 +3522,17 @@ %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 + } +} +@PART[airlinerTailFin] //FAT-455 Aeroplane tailfin +{ + %MODULE[HitpointTracker] + { ExplodeMode = Never + armorVolume = 39 } } @@ -3488,6 +3903,21 @@ /////////////////////////////////////////// //Fixes for BDA MISC /////////////////////////////////////////// +@PART[seatExternalCmdweaponized] //combat seat +{ + %MODULE[HitpointTracker] + { + maxHitPoints = 500 //this way, this doesn't die before the kerbal in it does + } +} + +@PART[bahaM1Abrams] //Abrams Turret +{ + %MODULE[HitpointTracker] + { + %maxSupportedArmor = 100 //Abrams turret only getting 10 armor is silly + } +} @PART[baha75mmShellBox] //AmmoBox { @@ -3578,3 +4008,55 @@ 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 + } +} +@PART[StingRayBDATorpedo] //Stingray Torpedo +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 5 + maxHitPoints = 50 + } +} +@PART[BahaF86Launcher] //FFAR launcher +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 400 //buff, original was 200 + } +} +@PART[bdRadome1,bdRadome1GA] //1.25m Radome long +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 600 //nerf, original was 1950 for some reason + } +} + 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_Armor.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_Armor.cfg deleted file mode 100644 index 6093d2004..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_Armor.cfg +++ /dev/null @@ -1,17 +0,0 @@ -@PART[BD*Armor] -{ - %MODULE[HitpointTracker] - { - maxHitPoints = 8000 - ArmorThickness = 150 - } -} - -@PART[BD*Armor]:NEEDS[TweakScale] -{ - MODULE - { - name = TweakScale - type = free_square - } -} 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 9df9455de..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,11 +7,50 @@ } } -@PART[awacsRadar] +@PART[awacsRadar]:NEEDS[TweakScale] { %MODULE[TweakScale] { %name = TweakScale %type = free } -} \ No newline at end of file +} + +@PART[BD*Armor]:NEEDS[TweakScale] +{ + MODULE + { + name = TweakScale + type = free_square + } +} + +@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 new file mode 100644 index 000000000..a393cb106 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_battledamage.cfg @@ -0,0 +1,75 @@ +@PART[BDAcUniversalAmmoBox] +{ + %MODULE[ModuleCASE] + { + %CASELevel = 0 + } +} +@PART[bahaM102Howitzer] +{ + %MODULE[ModuleCASE] + { + %CASELevel = 1 + } +} +@PART[bahaS-8Launcher|bahaM1Abrams|bahaH70Turret|bahaH70Launcher] +{ + %MODULE[ModuleCASE] + { + %CASELevel = 2 + } +} + +@PART[*]:HAS[@RESOURCE[LiquidFuel]] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} +@PART[B9_Aero_Wing_Procedural_TypeA] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} +@PART[proceduralTankLiquid] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} + +@PART[*]:HAS[@MODULE[ModuleEngines]] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} +@PART[*]:HAS[@MODULE[ModuleEnginesFX]] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} +@PART[*]:HAS[@MODULE[ModuleCommand]] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} +@PART[*]:HAS[@MODULE[ModuleB9PartSwitch]] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} + +//Mod configs +//AviatorArsenal +@PART[7mmBox|50calBox|13mmBox|15mmBox|20mmBox|23mmBox|30mmBox|40mmBox|75mmBox] +{ + %MODULE[ModuleCASE] + { + %CASELevel = 0 + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BreakingGround_Turrets.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BreakingGround_Turrets.cfg new file mode 100644 index 000000000..639572dcb --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BreakingGround_Turrets.cfg @@ -0,0 +1,8 @@ +@PART[*]:HAS[@MODULE[ModuleRoboticServoHinge]] +{ + %MODULE[ModuleCustomTurret]{} +} +@PART[*]:HAS[@MODULE[ModuleRoboticRotationServo]] +{ + %MODULE[ModuleCustomTurret]{} +} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/SimpleRepaint.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/SimpleRepaint.cfg new file mode 100644 index 000000000..d50aa6fb7 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/SimpleRepaint.cfg @@ -0,0 +1,4 @@ +@PART[bahaKKV]:BEFORE[zzzzzzSimpleRepaint] +{ + %SR_Ignore = true +} 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/MMPatches/VABOrganiser.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/VABOrganiser.cfg new file mode 100644 index 000000000..8c290cc2d --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/VABOrganiser.cfg @@ -0,0 +1,776 @@ +/// Subcategory assignments for BD parts + +ORGANIZERSUBCATEGORY +{ + name = BDCore + Label = Core BD + Priority = 5 + CategoryPriority = 5 +} + +@PART[BDA_EJ200|SaturnAL31]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = jetEngines + } +} + +@PART[seatExternalCmdweaponized]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = seats + } +} + +@PART[mk1opencockpit_RP_type2]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = cockpits + } +} + +/// Ammo +/// ------------- + + +ORGANIZERSUBCATEGORY +{ + name = ordnanceMag + Label = Ordnance Magazines + Priority = 5 + CategoryPriority = 5 +} + +ORGANIZERSUBCATEGORY +{ + name = ammoBox + Label = Boxed Ammunition + Priority = 5 + CategoryPriority = 5 +} +@PART[baha50CalAmmo|baha30mmAmmo|baha25mmAmmo|baha20mmAmmo|UniversalAmmoBoxBDA|bahaCannonShellBox|BDAcUniversalAmmoBox|bahaRocketBox]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = ammoBox + } +} + +ORGANIZERSUBCATEGORY +{ + name = ammoDrum + Label = Drum Ammunition + Priority = 5 + CategoryPriority = 5 +} + +ORGANIZERSUBCATEGORY +{ + name = warhead + Label = Warheads + Priority = 5 + CategoryPriority = 5 +} +@PART[bdWarheadSmall]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = warhead + } +} + +/// Fixed +/// ------------- + +@PART[BahaF86Launcher|bahaH70Launcher|bahaH70Turret|bahaS-8Launcher]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = rocketPods + } +} +ORGANIZERSUBCATEGORY +{ + name = rocketPods + Label = Rocket Pods + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaBrowningAnm2|bahaHiddenVulcan|bahaGau-8|bahaGau-22]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = fixedGuns + } +} +ORGANIZERSUBCATEGORY +{ + name = fixedGuns + Label = Fixed Guns + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaM102Howitzer]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Artillery + } +} +ORGANIZERSUBCATEGORY +{ + name = Artillery + Label = Artillery Guns + Priority = 5 + CategoryPriority = 5 +} + +/// Directed Energy +/// ------------- + + +ORGANIZERSUBCATEGORY +{ + name = electroLaser + Label = Electrical Weaponry + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaABL]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = laser + } +} +ORGANIZERSUBCATEGORY +{ + name = laser + Label = Laser Weaponry + Priority = 5 + CategoryPriority = 5 +} + +@PART[bdImpulseGun]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = forceGun + } +} +ORGANIZERSUBCATEGORY +{ + name = forceGun + Label = Force Weaponry + Priority = 5 + CategoryPriority = 5 +} + +ORGANIZERSUBCATEGORY +{ + name = plasma + Label = Plasma Weaponry + Priority = 5 + CategoryPriority = 5 +} + +/// Turrets +/// ------------- + +@PART[baha130mmTurret|baha100mmTurret|baha76mmTurret|baha57mmTurret]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = navalGun + } +} +ORGANIZERSUBCATEGORY +{ + name = navalGun + Label = Naval Turrets + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaM1Abrams]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = tankGun + } +} +ORGANIZERSUBCATEGORY +{ + name = tankGun + Label = Tank Guns + Priority = 5 + CategoryPriority = 5 +} + +ORGANIZERSUBCATEGORY +{ + name = ifv + Label = IFV Turrets + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaTurret]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = RWS + } +} +ORGANIZERSUBCATEGORY +{ + name = RWS + Label = Remote Weapon Stations + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaGoalKeeper|GoalKeeperBDAcMk1|BDAcGKmk2|bahaSidamTurret|bahaOMillennium]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = CIWS + } +} +ORGANIZERSUBCATEGORY +{ + name = CIWS + Label = Close-In Weapons + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaGatlingGun|bahaM230ChainGun]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Chin + } +} +ORGANIZERSUBCATEGORY +{ + name = Chin + Label = Chinguns + Priority = 5 + CategoryPriority = 5 +} + +/// Missiles +/// ------------- + +@PART[bahaAim9|bahaAim120|AMRAAM_EMP]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = A2A + } +} +ORGANIZERSUBCATEGORY +{ + name = A2A + Label = Air-to-Air + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaAGM-65|HellfireEMP|bahaAGM-114|bahaHarm]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = A2G + } +} +ORGANIZERSUBCATEGORY +{ + name = A2G + Label = Air-to-Ground + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaTowMissile]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = S2S + } +} +ORGANIZERSUBCATEGORY +{ + name = S2S + Label = Surface-to-Surface + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaPac-3]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = S2A + } +} +ORGANIZERSUBCATEGORY +{ + name = S2A + Label = Surface-to-Air + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaRBS-15ALCruise|bahaAgm86B|bahaRBS-15Cruise]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Cruise + } +} +ORGANIZERSUBCATEGORY +{ + name = Cruise + Label = Cruise Missile + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaKKV|bahaHEKV1]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = EKV + } +} +ORGANIZERSUBCATEGORY +{ + name = EKV + Label = Exoatmospheric + Priority = 5 + CategoryPriority = 5 +} + +@PART[BahaClusterMissile|bahaCLS_Long|bahaCLS_Short]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = CLS + } +} +ORGANIZERSUBCATEGORY +{ + name = CLS + Label = Missile Cluster + Priority = 5 + CategoryPriority = 5 +} + +/// Launchers +/// ------------- + +@PART[bahaAdjustableRail|bdRotBombBay|bdMissileBay]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Rail + } +} +ORGANIZERSUBCATEGORY +{ + name = Rail + Label = Missile Rails + Priority = 5 + CategoryPriority = 5 +} + + +ORGANIZERSUBCATEGORY +{ + name = Tube + Label = Launch Tubes + Priority = 5 + CategoryPriority = 5 +} + + +@PART[missileTurretTest|patriotLauncherTurret|towLauncherTurret]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = MTurret + } +} +ORGANIZERSUBCATEGORY +{ + name = MTurret + Label = Missile Turrets + Priority = 5 + CategoryPriority = 5 +} + + +/// Bombs +/// ------------- +@PART[bahaMk82Bomb|bahaMk82BombBrake|bahaClusterBomb]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = DumbBomb + } +} +ORGANIZERSUBCATEGORY +{ + name = DumbBomb + Label = Dumb Bombs + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaJdamMk83]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = GuidedBomb + } +} +ORGANIZERSUBCATEGORY +{ + name = GuidedBomb + Label = Guided Bombs + Priority = 5 + CategoryPriority = 5 +} + + +ORGANIZERSUBCATEGORY +{ + name = GlideBomb + Label = Glide Bombs + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaBombletDispenser]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Dispenser + } +} +ORGANIZERSUBCATEGORY +{ + name = Dispenser + Label = Bomblet Dispensers + Priority = 5 + CategoryPriority = 5 +} + + +/// Torpedos +/// ------------- + +ORGANIZERSUBCATEGORY +{ + name = MissileTorp + Label = Missile-Torpedoes + Priority = 5 + CategoryPriority = 5 +} + +@PART[StingRayBDATorpedo]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Torp + } +} +ORGANIZERSUBCATEGORY +{ + name = Torp + Label = Torpedoes + Priority = 5 + CategoryPriority = 5 +} + +/// Radar +/// ------------- + +@PART[radarDataReceiver]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Datalink + } +} +ORGANIZERSUBCATEGORY +{ + name = Datalink + Label = Data-Link Recievers + Priority = 5 + CategoryPriority = 5 +} + +@PART[bdRadome1|bdRadome1snub|bdRadome1inline]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = RadomeA + } +} +ORGANIZERSUBCATEGORY +{ + name = RadomeA + Label = Air Detection Radomes + Priority = 5 + CategoryPriority = 5 +} + +@PART[bdRadome1GA|bdRadome1snubGA|bdRadome1inlineGA]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = RadomeG + } +} +ORGANIZERSUBCATEGORY +{ + name = RadomeG + Label = Ground Detection Radomes + Priority = 5 + CategoryPriority = 5 +} + + +ORGANIZERSUBCATEGORY +{ + name = FixedRadar + Label = Fixed Panels + Priority = 5 + CategoryPriority = 5 +} + +@PART[BDAsonarPod1A]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Sonar + } +} +ORGANIZERSUBCATEGORY +{ + name = Sonar + Label = Sonar Pods + Priority = 5 + CategoryPriority = 5 +} + +@PART[scanLargeRadar]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Detection + } +} +ORGANIZERSUBCATEGORY +{ + name = Detection + Label = Detection Radar + Priority = 5 + CategoryPriority = 5 +} + +@PART[scanLockRadar1]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Locking + } +} +ORGANIZERSUBCATEGORY +{ + name = Locking + Label = Locking Radar + Priority = 5 + CategoryPriority = 5 +} + +@PART[awacsRadar]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = AWACS + } +} +ORGANIZERSUBCATEGORY +{ + name = AWACS + Label = AWACS Radar + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaIRSTpod]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = IRST + } +} +ORGANIZERSUBCATEGORY +{ + name = IRST + Label = Infared Detector + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaCamPod|bahaFlirBall]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = TargetingPods + } +} +ORGANIZERSUBCATEGORY +{ + name = TargetingPods + Label = Targeting Pods + Priority = 5 + CategoryPriority = 5 +} + +/// Countermeasures +/// ------------- + +ORGANIZERSUBCATEGORY +{ + name = APS + Label = Active Protection + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaECMJammer]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = EW + } +} +ORGANIZERSUBCATEGORY +{ + name = EW + Label = Electronic Warfare + Priority = 5 + CategoryPriority = 5 +} + +@PART[bahaSmokeCmPod|bahaDecoyPod|bahaBubblePod|bahaChaffPod|bahaCmPod]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Counters + } +} +ORGANIZERSUBCATEGORY +{ + name = Counters + Label = Deployable Countermeasures + Priority = 5 + CategoryPriority = 5 +} + +/// Misc +/// ------------- + +@PART[BD_PanelArmorIsoTri|BD_PanelArmorTri|BD_PanelArmor]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Armor + } +} +ORGANIZERSUBCATEGORY +{ + name = Armor + Label = Armor + Priority = 5 + CategoryPriority = 5 +} + +@PART[BD1x0.5ReactiveArmor]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = ERA + } +} +ORGANIZERSUBCATEGORY +{ + name = ERA + Label = Reactive Armor + Priority = 5 + CategoryPriority = 5 +} + + +/// Command +/// ------------- + +@PART[missileController|bdammGuidanceModule]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Combat + } +} +ORGANIZERSUBCATEGORY +{ + name = Combat + Label = Combat Modules + Priority = 5 + CategoryPriority = 5 +} + + +@PART[bdShipAI|bdPilotAI|bdOrbitalAI|bdVTOLAI]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = AI + } +} +ORGANIZERSUBCATEGORY +{ + name = AI + Label = AI Modules + Priority = 5 + CategoryPriority = 5 +} + +/// Misc +/// ------------- + +@PART[bahaAIR-2]:FOR[VABOrganizer] +{ + %VABORGANIZER + { + %organizerSubcategory = Nuclear + } +} +ORGANIZERSUBCATEGORY +{ + name = Nuclear + Label = Nuclear Arms + Priority = 5 + CategoryPriority = 5 +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/CMBubble/cmSmokeModel.mu b/BDArmory/Distribution/GameData/BDArmory/Models/CMBubble/cmSmokeModel.mu new file mode 100644 index 000000000..a1d178d18 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/CMBubble/cmSmokeModel.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/CMBubble/smokeCm.png b/BDArmory/Distribution/GameData/BDArmory/Models/CMBubble/smokeCm.png new file mode 100644 index 000000000..d8d74bd57 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/CMBubble/smokeCm.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/Decoy.png b/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/Decoy.png new file mode 100644 index 000000000..c2a6e1b99 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/Decoy.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/foam.png b/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/foam.png new file mode 100644 index 000000000..c814b82cc Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/foam.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/model.mu b/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/model.mu new file mode 100644 index 000000000..5cf075e71 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/CMDecoy/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/NukeCore.mu b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/NukeCore.mu new file mode 100644 index 000000000..3b1097997 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/NukeCore.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/NukeCore.png b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/NukeCore.png new file mode 100644 index 000000000..a55dd61fa Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/NukeCore.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/Skull.png b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/Skull.png new file mode 100644 index 000000000..35be60782 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/Skull.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/Vengence.mu b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/Vengence.mu new file mode 100644 index 000000000..dddf24b8a Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/Vengence.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/rocketplume2.png b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/rocketplume2.png new file mode 100644 index 000000000..561461620 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/Mutators/rocketplume2.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/Ring.mu b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/Ring.mu new file mode 100644 index 000000000..34a621d29 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/Ring.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/Torii.mu b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/Torii.mu new file mode 100644 index 000000000..7e6ce0552 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/Torii.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/torii_torii_BaseColor.png b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/torii_torii_BaseColor.png new file mode 100644 index 000000000..e87c44183 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/torii_torii_BaseColor.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/waypoint.png b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/waypoint.png new file mode 100644 index 000000000..8d57e4cd4 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/WayPoint/waypoint.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/boresight/boresight.mu b/BDArmory/Distribution/GameData/BDArmory/Models/boresight/boresight.mu new file mode 100644 index 000000000..8a0cf7223 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/boresight/boresight.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/boresight/smokeCm.png b/BDArmory/Distribution/GameData/BDArmory/Models/boresight/smokeCm.png new file mode 100644 index 000000000..b32789d34 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/boresight/smokeCm.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal3.mu b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal3.mu index 9474652d5..835ef7b6c 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal3.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal3.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal4.mu b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal4.mu index da69583f8..fc0fa2cd5 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal4.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal4.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal5.mu b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal5.mu index 6d37c7733..c734a492c 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal5.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/BulletDecal5.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH1.png b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH1.png index 0f93f8c67..a0e9e5425 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH1.png and b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH1.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH2.png b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH2.png index b8d5a3f38..0003e2778 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH2.png and b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH2.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH3.png b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH3.png index cb5429938..352478824 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH3.png and b/BDArmory/Distribution/GameData/BDArmory/Models/bulletDecal/PH3.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/jetExhaustPrefab.mu b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/jetExhaustPrefab.mu new file mode 100644 index 000000000..3cadcbfab Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/jetExhaustPrefab.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/mediumExhaustPrefabII.mu b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/mediumExhaustPrefabII.mu new file mode 100644 index 000000000..6a1c2aee2 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/mediumExhaustPrefabII.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/smallExhaustPrefabII.mu b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/smallExhaustPrefabII.mu new file mode 100644 index 000000000..55883ed2c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/smallExhaustPrefabII.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/sustainerExhaustPrefab.mu b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/sustainerExhaustPrefab.mu new file mode 100644 index 000000000..2559a2f72 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/sustainerExhaustPrefab.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/tex_smokeExhaust.png b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/tex_smokeExhaust.png deleted file mode 100644 index 310a0c89f..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/tex_smokeExhaust.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/tex_sphere.png b/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/tex_sphere.png deleted file mode 100644 index 82370c2ec..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/exhaust/tex_sphere.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/CASEexplosion.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/CASEexplosion.mu new file mode 100644 index 000000000..4032be037 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/CASEexplosion.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/bigsmoke.png b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/bigsmoke.png new file mode 100644 index 000000000..e14256aca Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/bigsmoke.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/detHemisphere.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/detHemisphere.mu new file mode 100644 index 000000000..03b345696 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/detHemisphere.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/flakSmoke.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/flakSmoke.mu new file mode 100644 index 000000000..b9d90200e Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/flakSmoke.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Flare.png b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Flare.png new file mode 100644 index 000000000..6bd4e553e Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Flare.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Plume.png b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Plume.png new file mode 100644 index 000000000..0524f5b87 Binary files /dev/null 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 new file mode 100644 index 000000000..fc8c43761 Binary files /dev/null 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 new file mode 100644 index 000000000..4aac63e5d Binary files /dev/null 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/nukeFlash.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeFlash.mu new file mode 100644 index 000000000..e677a610f Binary files /dev/null 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 new file mode 100644 index 000000000..287e70c7f Binary files /dev/null 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 new file mode 100644 index 000000000..d2717ce0d Binary files /dev/null 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 new file mode 100644 index 000000000..667dcc678 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeShock.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/shockRing.png b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/shockRing.png new file mode 100644 index 000000000..a12fa3003 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/shockRing.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/laser/laserCone.mu b/BDArmory/Distribution/GameData/BDArmory/Models/laser/laserCone.mu new file mode 100644 index 000000000..120f1f19f Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/laser/laserCone.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/laser/laserTex.png b/BDArmory/Distribution/GameData/BDArmory/Models/laser/laserTex.png new file mode 100644 index 000000000..32564c328 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/laser/laserTex.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg index 9d505f2a0..ab857a55f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg @@ -20,8 +20,8 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 2500 + cost = 1900 category = none bdacategory = Gun turrets subcategory = 0 @@ -35,7 +35,7 @@ PART // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - + tags = #loc_BDArmory_part_bahaGatlingGun_tags // --- standard part parameters --- mass = 0.2 dragModelType = default @@ -78,13 +78,15 @@ PART fireAnimName = fireAnimation spinDownAnimation = true + SpoolUpTime = 0.15 + roundsPerMinute = 5500 - maxDeviation = 0.175 + maxDeviation = 0.401 //~9mrad, 80% maxEffectiveDistance = 2500 maxTargetingRange = 5000 ammoName = 20x102Ammo - bulletType = 20x102mmHEBullet + bulletType = 20x102mmHEBullet; 20x102mmBullet requestResourceAmount = 1 hasRecoil = true @@ -94,20 +96,12 @@ PART weaponType = ballistic - projectileColor = 255, 15, 0, 128//RGBA 0-255 - startColor = 255, 90, 0, 32 - fadeColor = false - shellScale = 0.66 - - tracerStartWidth = 0.18 - tracerEndWidth = 0.18 tracerLength = 0 //test tracerDeltaFactor = 2.75 tracerInterval = 3 - nonTracerWidth = 0.035 maxHeat = 3600 heatPerShot = 36 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/50CalTurret/50cal.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/50CalTurret/50cal.cfg index 6ff1e1110..e9bdaf4fb 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/50CalTurret/50cal.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/50CalTurret/50cal.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 950 + cost = 400 category = none bdacategory = Gun turrets subcategory = 0 bulkheadProfiles = srf - title = .50cal Turret - manufacturer = Bahamuto Dynamics - description = A dual barrel .50 cal machine gun. + title = #loc_BDArmory_part_bahaTurret_title //.50cal Turret + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaTurret_description //A dual barrel .50 cal machine gun. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - + tags = #loc_BDArmory_part_bahaTurret_tags // --- standard part parameters --- mass = 0.15 dragModelType = default @@ -75,12 +75,12 @@ PART fireAnimName = fireAnimation roundsPerMinute = 450 - maxDeviation = 0.65 + maxDeviation = 0.262 //~6mrad maxEffectiveDistance = 2500 weaponType = ballistic ammoName = 50CalAmmo - bulletType = 12.7mmBullet + bulletType = 12.7mmBullet; 12.7mmAPIBullet requestResourceAmount = 1 shellScale = 0.463 bulletDmgMult = 1.3 @@ -89,10 +89,6 @@ PART onlyFireInRange = true bulletDrop = true - projectileColor = 255, 90, 0, 128 //RGBA 0-255 - startColor = 255, 105, 0, 70 - tracerStartWidth = 0.15 - tracerEndWidth = 0.05 tracerLength = 0 maxHeat = 3600 @@ -101,7 +97,8 @@ PART fireSoundPath = BDArmory/Parts/50CalTurret/sounds/shot overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat - + explModelPath = BDArmory/Models/explosion/30mmExplosion + explSoundPath = BDArmory/Sounds/subExplode } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg index 15a51ab68..59998eae9 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg @@ -20,17 +20,18 @@ node_stack_bottom = 0.0, -0.573, 0, 0, -1, 0, 1 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 7600 +entryCost = 25000 +cost = 10000 category = none bdacategory = Laser turrets subcategory = 0 -bulkheadProfiles = srf -title = USAF Airborne Laser -manufacturer = Bahamuto Dynamics -description = A high powered laser for setting things on fire. Uses 350 electric charge per second. +bulkheadProfiles = size1, srf +title = #loc_BDArmory_part_bahaABL_title //USAF Airborne Laser +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaABL_description //A high powered laser for setting things on fire. Uses 350 electric charge per second. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 +tags = #loc_BDArmory_part_bahaABL_tags // --- standard part parameters --- mass = 0.8 @@ -85,6 +86,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 +100,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 b43adba34..b012fc4ce 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AGM86-17/AGM86-17.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AGM86-17/AGM86-17.cfg @@ -21,17 +21,18 @@ node_stack_base = 0.0, 0.0, -2.407, 0, 0, -1, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 2400 +entryCost = 10000 +cost = 5000 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf -title = AGM-86C Cruise Missile -manufacturer = Bahamuto Dynamics -description = 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. 2017 overhaul version. +title = #loc_BDArmory_part_bahaAgm86B_title //AGM-86C Cruise Missile +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaAgm86B_description //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. 2017 overhaul version. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 +tags = #loc_BDArmory_part_bahaAgm86B_tags // --- standard part parameters --- mass = 1.15 @@ -72,7 +73,7 @@ MODULE homingType = Cruise targetingType = gps - terminalManeuvering = false + terminalGuidanceShouldActivate = false maxOffBoresight = 65 lockedSensorFOV = 6 @@ -108,6 +109,8 @@ MODULE { name = BDExplosivePart tntMass = 1300 + caliber = 620 + warheadType = ShapedCharge } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/AIM4FalconGAR2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/AIM4FalconGAR2.cfg deleted file mode 100644 index 176406726..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/AIM4FalconGAR2.cfg +++ /dev/null @@ -1,100 +0,0 @@ -PART -{ - // --- general parameters --- - name = AIM4FalconGAR2 - module = Part - author = Kurgan - - // --- asset parameters --- - MODEL - { - model = BDArmory/Parts/maverick/model - texture = texture, BDArmory/Parts/AIM4Falcon/texture2 - scale = 0.6, 0.6, 1 - } - rescaleFactor = 1 - - // --- node definitions --- - node_attach = 0.0, 0.15, -0.5, 0, 1, 0, 0 - node_stack_top = 0.0, 0.089, -0.28, 0.0, 1.0, 0.0, 0, 1 - - // --- editor parameters --- - TechRequired = precisionEngineering - entryCost = 2100 - cost = 400 - category = none - subcategory = 0 - title = AIM-4 Falcon GAR-2 - manufacturer = Bahamuto Dynamics - description = This old heat-seeking missile was designed to shoot down slow bombers with limited maneuverability, it was ineffective against more maneuverable fighters. Lacking proximity fusing, the missile would only detonate if a direct hit was scored. - // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision - attachRules = 1,1,0,0,1 - - // --- standard part parameters --- - mass = 0.061 - dragModelType = default - maximum_drag = 0.01 - minimum_drag = 0.01 - angularDrag = 2 - crashTolerance = 5 - maxTemp = 3600 - - - MODULE - { - name = MissileLauncher - - shortName = AIM-4b - - thrust = 15 //KN thrust during boost phase - cruiseThrust = 0 //thrust during cruise phase - dropTime = 0 //how many seconds after release until engine ignites - boostTime = 9 //seconds of boost phase - cruiseTime = 0 //seconds of cruise phase - - guidanceActive = true //missile has guidanceActive - maxTurnRateDPS = 6 //degrees per second - - decoupleSpeed = 10 - decoupleForward = true - - missileType = missile - homingType = AAM - targetingType = heat - heatThreshold = 30 - maxOffBoresight = 65 - lockedSensorFOV = 2 - optimumAirspeed = 1080 - DetonationDistance = 0.05 - - maxAoA = 30 - - aero = true - liftArea = 0.0015 - steerMult = 0.4 - maxTorque = 12 - torqueRampUp = 50 - aeroSteerDamping = 5 - - minStaticLaunchRange = 200 - maxStaticLaunchRange = 9700 - - audioClipPath = BDArmory/Sounds/rocketLoop - boostClipPath = BDArmory/Sounds/rocketLoop - exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust - boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust - boostTransformName = boostTransform - boostExhaustTransformName = boostTransform - - engageAir = true - engageMissile = false - engageGround = false - engageSLW = false - - } - MODULE - { - name = BDExplosivePart - tntMass = 3.4 - } -} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/AIM4FalconMK1.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/AIM4FalconMK1.cfg deleted file mode 100644 index 435b9def0..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/AIM4FalconMK1.cfg +++ /dev/null @@ -1,101 +0,0 @@ -PART -{ - // --- general parameters --- - name = AIM4FalconMK1 - module = Part - author = Kurgan - - // --- asset parameters --- - MODEL - { - model = BDArmory/Parts/maverick/model - texture = texture, BDArmory/Parts/AIM4Falcon/texture - scale = 0.6, 0.6, 1 - } - rescaleFactor = 1 - - // --- node definitions --- - node_attach = 0.0, 0.15, -0.5, 0, 1, 0, 0 - node_stack_top = 0.0, 0.089, -0.28, 0.0, 1.0, 0.0, 0, 1 - - // --- editor parameters --- - TechRequired = precisionEngineering - entryCost = 2100 - cost = 400 - category = none - subcategory = 0 - title = AIM-4 Falcon GAR-1 - manufacturer = Bahamuto Dynamics - description = This old radar-guided missile was designed to shoot down slow bombers with limited maneuverability, it was ineffective against more maneuverable fighters. Lacking proximity fusing, the missile would only detonate if a direct hit was scored. - // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision - attachRules = 1,1,0,0,1 - - // --- standard part parameters --- - mass = 0.061 - dragModelType = default - maximum_drag = 0.01 - minimum_drag = 0.01 - angularDrag = 2 - crashTolerance = 5 - maxTemp = 3600 - - - MODULE - { - name = MissileLauncher - - shortName = AIM-4a - - thrust = 15 //KN thrust during boost phase - cruiseThrust = 0 //thrust during cruise phase - dropTime = 0 //how many seconds after release until engine ignites - boostTime = 9 //seconds of boost phase - cruiseTime = 0 //seconds of cruise phase - - guidanceActive = true //missile has guidanceActive - maxTurnRateDPS = 6 //degrees per second - - decoupleSpeed = 10 - decoupleForward = true - - missileType = missile - homingType = AAM - targetingType = radar - activeRadarRange = 9700 - radarLOAL = true - maxOffBoresight = 45 - lockedSensorFOV = 2 - optimumAirspeed = 1080 - DetonationDistance = 0.05 - - maxAoA = 30 - - aero = true - liftArea = 0.0015 - steerMult = 0.4 - maxTorque = 12 - torqueRampUp = 50 - aeroSteerDamping = 5 - - minStaticLaunchRange = 200 - maxStaticLaunchRange = 9700 - - audioClipPath = BDArmory/Sounds/rocketLoop - boostClipPath = BDArmory/Sounds/rocketLoop - exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust - boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust - boostTransformName = boostTransform - boostExhaustTransformName = boostTransform - - engageAir = true - engageMissile = false - engageGround = false - engageSLW = false - - } - MODULE - { - name = BDExplosivePart - tntMass = 3.4 - } -} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/texture.png b/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/texture.png deleted file mode 100644 index 73f328632..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/texture.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/texture2.png b/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/texture2.png deleted file mode 100644 index 57d44efbc..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/AIM4Falcon/texture2.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/AIR-2 Genie.png b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/AIR-2 Genie.png new file mode 100644 index 000000000..ed2e9ff03 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/AIR-2 Genie.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/Model.mu new file mode 100644 index 000000000..068c55b0d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/Model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg new file mode 100644 index 000000000..8471b4d59 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg @@ -0,0 +1,116 @@ +PART +{ + // Kerbal Space Program - Part Config + // + // + + // --- general parameters --- + name = bahaAIR-2 + module = Part + author = SuicidalInsanity + + // --- asset parameters --- + mesh = Model.mu + rescaleFactor = 1 + + + // --- node definitions --- + node_attach = 0.0, 0.17, -0.5, 0, 1, 0, 0 + node_stack_top = 0.0, 0.17, -0.1, 0, 1, 0, 0 + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 2100 + cost = 4000 + category = none + bdacategory = Missiles + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_genie_title //AIR-2 Genie Air-To-Air Rocket + 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 + tags = #loc_BDArmory_part_genie_tags + // --- standard part parameters --- + mass = 0.3729 + dragModelType = default + maximum_drag = 0.02 + minimum_drag = 0.02 + angularDrag = 2 + crashTolerance = 5 + maxTemp = 3600 + + + MODULE + { + name = MissileLauncher + + shortName = AIR-2 + + thrust = 162 //KN thrust during boost phase + cruiseThrust = 0//thrust during cruise phase + dropTime = 0 //how many seconds after release until engine ignites + boostTime = 2 //seconds of boost phase + cruiseTime = 0//seconds of cruise phase + guidanceActive = true //missile has guidanceActive + maxTurnRateDPS = 1 //degrees per second + + audioClipPath = BDArmory/Sounds/rocketLoop + exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust + boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust + boostExhaustTransformName = exhaustTransform + boostTransformName = exhaustTransform + DetonationDistance = 300 + aero = true + liftArea = 0.004 + steerMult = 4 + maxTorque = 10 + maxAoA = 5 + //aeroSteerDamping = 4.5 + torqueRampUp = 5 + + decoupleSpeed = 5 + decoupleForward = true + rotationTransformName = rotationTransform + deployAnimationName = GenieDeploy + deployedDrag = 0 + deployTime = 0.35 + + homingType = aam + missileType = missile + targetingType = none + heatThreshold = 50 + maxOffBoresight = 10 + allAspect = false + minStaticLaunchRange = 1500 + maxStaticLaunchRange = 15000 + + engageAir = true + engageMissile = false + engageGround = True + engageSLW = false + + } + MODULE + { + name = BDModuleNuke + thermalRadius = 900 //clamps AoE to a max of this distance + yield = 1.5// yield, in kilotons + fluence = 1.5 //thermal bloom modifier, reduce for less toasty nukes + isEMP = true // does this generate an EMP + reportingName = W-25 Warhead //weapon name that appears in competition log + //initial flash + flashModelPath = BDArmory/Models/explosion/nuke/nukeFlash + //shockwave + shockModelPath = BDArmory/Models/explosion/nuke/nukeShock + //fireball + blastModelPath = BDArmory/Models/explosion/nuke/nukeBlast + //mushroom cloud stalk + plumeModelPath = BDArmory/Models/explosion/nuke/nukePlume + //ground scatter + debrisModelPath = BDArmory/Models/explosion/nuke/nukeScatter + //sound + blastSoundPath = BDArmory/Models/explosion/nuke/nukeBoom + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/20mm.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/20mm.cfg index 8b5db6fae..6283b63a9 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/20mm.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/20mm.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 + entryCost = 750 cost = 600 category = none bdacategory = Ammo subcategory = 0 bulkheadProfiles = srf - title = 20mm Ammunition Box - manufacturer = Bahamuto Dynamics - description = Ammo box containing 650 20x102mm rounds. + title = #loc_BDArmory_part_baha20mmAmmo_title //20mm Ammunition Box + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_baha20mmAmmo_description //Ammo box containing 650 20x102mm rounds. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,1,1 - + tags = #loc_BDArmory_part_baha20mmAmmo_tags // --- standard part parameters --- mass = 0.01 dragModelType = default @@ -47,7 +47,11 @@ PART amount = 650 maxAmount = 650 } - + MODULE + { + name = ModuleCASE + CASELevel = 0 + } MODULE { name = CFEnable diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/25mm.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/25mm.cfg new file mode 100644 index 000000000..72d830935 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/25mm.cfg @@ -0,0 +1,70 @@ +PART +{ + // Kerbal Space Program - Part Config + // + // + + // --- general parameters --- + name = baha25mmAmmo + module = Part + author = Legodlak + + // --- asset parameters --- + //mesh = model.mu + rescaleFactor = 1.1 + + + // --- node definitions --- + node_attach = 0.0, -0.1129, 0, 0, -1, 0, 0 + + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 800 + cost = 850 + category = none + bdacategory = Ammo + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_baha25mmAmmo_title //25mm Ammunition Box + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_baha25mmAmmo_description //Ammo box containing 625 25x137mm rounds. + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,1,1 + tags = #loc_BDArmory_part_baha25mmAmmo_tags + // --- standard part parameters --- + mass = 0.01 + dragModelType = default + maximum_drag = 0.2 + minimum_drag = 0.2 + angularDrag = 2 + crashTolerance = 7 + maxTemp = 3600 + + RESOURCE + { + name = 25x137Ammo + amount = 625 + maxAmount = 625 + } + MODULE + { + name = ModuleCASE + CASELevel = 0 + } + MODULE + { + name = CFEnable + } + + MODEL + { + model = BDArmory/Parts/AmmoBox/model + texture = texture, BDArmory/Parts/AmmoBox/texture25mm + } + DRAG_CUBE +{ + cube = Default,0.231,0.46985,0.1428,0.231,0.46985,0.1428,0.4151,0.49205,0.1111,0.4151,0.4834,0.3177,0.15,0.466,0.1399,0.15,0.46615,0.1399, 0,0.01306,-4.669E-09, 0.5282,0.297,0.7912 +} + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/30mm.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/30mm.cfg index 4b3981a61..dbc9c71c8 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/30mm.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/30mm.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 + entryCost = 1200 cost = 1000 category = none bdacategory = Ammo subcategory = 0 bulkheadProfiles = srf - title = 30mm Ammunition Box - manufacturer = Bahamuto Dynamics - description = Ammo box containing 600 30x173mm rounds. + title = #loc_BDArmory_part_baha30mmAmmo_title //30mm Ammunition Box + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_baha30mmAmmo_description //Ammo box containing 600 30x173mm rounds. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,1,1 - + tags = #loc_BDArmory_part_baha30mmAmmo_tags // --- standard part parameters --- mass = 0.01 dragModelType = default @@ -47,7 +47,11 @@ PART amount = 600 maxAmount = 600 } - + MODULE + { + name = ModuleCASE + CASELevel = 0 + } MODULE { name = CFEnable diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/50cal.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/50cal.cfg index a478b7a32..35141e48e 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/50cal.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/50cal.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.1129, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 500 +cost = 400 category = none bdacategory = Ammo subcategory = 0 bulkheadProfiles = srf -title = 50cal Ammunition Box -manufacturer = Bahamuto Dynamics -description = Ammo box containing 1200 .50 cal rounds. +title = #loc_BDArmory_part_baha50CalAmmo_title //50cal Ammunition Box +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_baha50CalAmmo_description //Ammo box containing 1200 .50 cal rounds. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,1,1 - +tags = #loc_BDArmory_part_baha50calAmmo_tags // --- standard part parameters --- mass = 0.01 dragModelType = default @@ -47,7 +47,11 @@ RESOURCE amount = 1200 maxAmount = 1200 } - +MODULE +{ + name = ModuleCASE + CASELevel = 0 +} MODULE { name = CFEnable diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg index e60dfe312..2aba7314c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg @@ -22,17 +22,17 @@ node_attach = 0.0, -0.1129, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering entryCost = 2100 -cost = 600 +cost = 2000 category = none bdacategory = Ammo subcategory = 0 bulkheadProfiles = srf -title = Universal Ammo Box -manufacturer = Bahamuto Dynamics -description = Scalable Ammo box containing whatever ammo you want to put in it. holds 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) +title = #loc_BDArmory_part_BDAcUniversalAmmoBox_title //Universal Ammo Box +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_BDAcUniversalAmmoBox_description //Scalable Ammo box containing whatever ammo you want to put in it. holds 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) // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,1,1 - +tags = #loc_BDArmory_part_bahaUABAmmo_tags // --- standard part parameters --- mass = 0.1 @@ -49,25 +49,19 @@ 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 - basePartMass = 0.1 - showInfo = true - displayCurrentTankCost = true + name = ModuleCASE + CASELevel = 0 } 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 + 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 } - 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 50fe3fc85..e20c75031 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/UniversalAmmoBoxBDA.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/UniversalAmmoBoxBDA.cfg @@ -20,19 +20,19 @@ node_attach = 0.0, -0.1129, 0, 0, -1, 0, 0 // --- editor parameters --- -TechRequired = precisionEngineering +TechRequired = Unresearcheable //precisionEngineering entryCost = 2100 -cost = 600 +cost = 2000 category = none bdacategory = Ammo subcategory = 0 bulkheadProfiles = srf -title = Universal Ammo Box (Legacy) -manufacturer = Bahamuto Dynamics -description = (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. +title = #loc_BDArmory_part_UniversalAmmoBoxBDA_title //Universal Ammo Box (Legacy) +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_UniversalAmmoBoxBDA_description //(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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,1,1 - +tags = #loc_BDArmory_part_bahaUABAmmo_tags // --- standard part parameters --- mass = 0.1 @@ -43,31 +43,25 @@ angularDrag = 2 crashTolerance = 7 maxTemp = 3600 + MODULE + { + name = ModuleCASE + CASELevel = 0 + } MODULE { name = CFEnable } 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/AmmoBox/cannonShell.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/cannonShell.cfg index d5f793a60..f0596c23f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/cannonShell.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/cannonShell.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.1129, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 1150 +cost = 1000 category = none bdacategory = Ammo subcategory = 0 bulkheadProfiles = srf -title = Cannon Ammunition Box -manufacturer = Bahamuto Dynamics -description = Ammo box containing 10 cannon shells. +title = #loc_BDArmory_part_bahaCannonShellBox_title //Cannon Ammunition Box +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaCannonShellBox_description //Ammo box containing 10 cannon shells. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,1,1 - +tags = #loc_BDArmory_part_bahaCannonAmmo_tags // --- standard part parameters --- mass = 0.015 dragModelType = default @@ -47,7 +47,11 @@ RESOURCE amount = 10 maxAmount = 10 } - +MODULE +{ + name = ModuleCASE + CASELevel = 0 +} MODULE { name = CFEnable diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/rockets.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/rockets.cfg new file mode 100644 index 000000000..905b69f7b --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/rockets.cfg @@ -0,0 +1,67 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaRocketBox +module = Part +author = BahamutoD + +// --- asset parameters --- +rescaleFactor = 1 +// --- node definitions --- +node_attach = 0.0, -0.1129, 0, 0, -1, 0, 0 + + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 1750 +cost = 1500 +category = none +bdacategory = Ammo +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_rocket70mmAmmo_title +manufacturer = #loc_BDArmory_agent_title +description = #loc_BDArmory_part_rocket70mmAmmo_description +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 0,1,0,1,1 +tags = #loc_BDArmory_part_bahaRocketAmmo_tags +// --- standard part parameters --- +mass = 0.015 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 7 +maxTemp = 3600 + +RESOURCE +{ + name = Rockets + amount = 48 + maxAmount = 48 +} +MODULE +{ + name = ModuleCASE + CASELevel = 0 +} +MODULE +{ + name = CFEnable +} + + MODEL + { + model = BDArmory/Parts/AmmoBox/model + texture = texture, BDArmory/Parts/AmmoBox/textureRocket + scale = 1.3, 1, 1.8 + } +DRAG_CUBE +{ + cube = Default,0.231,0.46985,0.1428,0.231,0.46985,0.1428,0.4151,0.49205,0.1111,0.4151,0.4834,0.3177,0.15,0.466,0.1399,0.15,0.46615,0.1399, 0,0.01306,-4.669E-09, 0.5282,0.297,0.7912 +} +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/texture25mm.png b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/texture25mm.png new file mode 100644 index 000000000..1cff85cb9 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/texture25mm.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/textureRocket.png b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/textureRocket.png new file mode 100644 index 000000000..231e16dc1 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/textureRocket.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1panelArmor.cfg deleted file mode 100644 index e0fce08aa..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1panelArmor.cfg +++ /dev/null @@ -1,416 +0,0 @@ -PART -{ -name = BD1x1slopeArmor -module = Part -author = SpannerMonkey -buoyancy = -1 -rescaleFactor = 1 - - MODEL - { - model = BDArmory/Parts/ArmorPlate/BD1x1slopeArmor - scale = 1.0, 1.0, 1.0 - } - - NODE - { - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT - } - NODE - { - - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = Node3 - size = 0 - method = FIXED_JOINT - } -node_attach = -0.5, 0, 0, 0, 0, 1, 0 - -TechRequired = composites -entryCost = 7200 -cost = 100 -category = Structural -bdacategory = Armor -subcategory = 0 -bulkheadProfiles = srf -title = BD 1x1 slope Armor -manufacturer = Bahamuto Dynamics -description = A sturdy 1x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float -attachRules = 1,1,1,1,1 - -// --- standard part parameters --- -mass = 0.0375 -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 - - - - -} -//////////////////////////////////////////////////////////////////////// -PART -{ -name = BD2x1slopeArmor -module = Part -author = SpannerMonkey -buoyancy = -1 - MODEL - { - model = BDArmory/Parts/ArmorPlate/BD2x1slopeArmor - scale = 1.0, 1.0, 1.0 - } -rescaleFactor = 1 - NODE - { - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = Node3 - size = 0 - method = FIXED_JOINT - } -node_attach = -0.0, 0, 0.36, 0, 0, 1, 0 - -TechRequired = composites -entryCost = 7200 -cost = 100 -category = Structural -bdacategory = Armor -subcategory = 0 -bulkheadProfiles = srf -title = BD 2x1 slope Armor -manufacturer = Bahamuto Dynamics -description = A sturdy 2x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float -attachRules = 1,1,1,1,1 - -// --- standard part parameters --- -mass = 0.075 -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 - - - -} -///////////////////////////////////////////////////////////////////////////////// -PART -{ -name = BD1x1panelArmor -module = Part -author = SpannerMonkey -buoyancy = -1 - MODEL - { - model = BDArmory/Parts/ArmorPlate/BD1x1panelArmor - scale = 1.0, 1.0, 1.0 - } -rescaleFactor = 1 - NODE - { - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = Node3 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node4 - transform = Node4 - size = 0 - method = FIXED_JOINT - } -node_attach = -0.0, 0, 0.5, 0, 0, 1, 0 - -TechRequired = composites -entryCost = 7200 -cost = 100 -category = Structural -bdacategory = Armor -subcategory = 0 -bulkheadProfiles = srf -title = BD 1x1 panel Armor -manufacturer = Bahamuto Dynamics -description = A sturdy 1x1 Armor plate, perfect for constructing all sorts of things. PS does not float -attachRules = 1,1,1,1,1 - -// --- standard part parameters --- -mass = 0.075 -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 - - - -} - -///////////////////////////////////////////////////////////////////////////////// -PART -{ -name = BD2x1panelArmor -module = Part -author = SpannerMonkey -buoyancy = -1 - MODEL - { - model = BDArmory/Parts/ArmorPlate/BD2x1panelArmor - scale = 1.0, 1.0, 1.0 - } -rescaleFactor = 1 - NODE - { - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = Node3 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node4 - transform = Node4 - size = 0 - method = FIXED_JOINT - } -node_attach = -0.0, 0, 0.5, 0, 0, 1, 0 - -TechRequired = composites -entryCost = 7200 -cost = 100 -category = Structural -bdacategory = Armor -subcategory = 0 -bulkheadProfiles = srf -title = BD 2x1 panel Armor -manufacturer = Bahamuto Dynamics -description = A sturdy 2x1 Armor plate, perfect for constructing all sorts of things. PS does not float -attachRules = 1,1,1,1,1 - -// --- standard part parameters --- -mass = 0.15 -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 - - - -} -///////////////////// -PART -{ -name = BD3x1panelArmor -module = Part -author = SpannerMonkey -buoyancy = -1 - MODEL - { - model = BDArmory/Parts/ArmorPlate/BD3x1panelArmor - scale = 1.0, 1.0, 1.0 - } -rescaleFactor = 1 - NODE - { - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = Node3 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node4 - transform = Node4 - size = 0 - method = FIXED_JOINT - } -node_attach = -0.0, 0, 0.5, 0, 0, 1, 0 - -TechRequired = composites -entryCost = 7200 -cost = 100 -category = Structural -bdacategory = Armor -subcategory = 0 -bulkheadProfiles = srf -title = BD 3x1 panel Armor -manufacturer = Bahamuto Dynamics -description = A sturdy 3x1 Armor plate, perfect for constructing all sorts of things. PS does not float -attachRules = 1,1,1,1,1 - -// --- standard part parameters --- -mass = 0.225 -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 - - - -} -/////////// -PART -{ -name = BD4x1panelArmor -module = Part -author = Spanner -buoyancy = -1 - MODEL - { - model = BDArmory/Parts/ArmorPlate/BD4x1panelArmor - scale = 1.0, 1.0, 1.0 - } -rescaleFactor = 1 - NODE - { - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = Node3 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node4 - transform = Node4 - size = 0 - method = FIXED_JOINT - } -node_attach = -0.0, 0, 0.5, 0, 0, 1, 0 - -TechRequired = composites -entryCost = 7200 -cost = 100 -category = Structural -bdacategory = Armor -subcategory = 0 -bulkheadProfiles = srf -title = BD 4x1 panel Armor -manufacturer = Bahamuto Dynamics -description = A sturdy 4x1 Armor plate, perfect for constructing all sorts of things. PS does not float -attachRules = 1,1,1,1,1 - -// --- standard part parameters --- -mass = 0.3 -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 - - - -} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/CarrierDeck.png b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/CarrierDeck.png new file mode 100644 index 000000000..4efb2fb1c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/CarrierDeck.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/CarrierDeckNRM.png b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/CarrierDeckNRM.png new file mode 100644 index 000000000..1a941c775 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/CarrierDeckNRM.png differ 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..9d1d76e07 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTripanelArmor.cfg @@ -0,0 +1,97 @@ +PART +{ +name = BD_PanelArmorIsoTri +module = Part +author = SuicidalInsanity +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/IsoTriPanel + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + 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 +tags = #loc_BDArmory_part_bahaArmor_tags +// --- 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 + + 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 = BDArmory/Parts/ArmorPlate/armorpanelNRM + _BumpMap = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/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 new file mode 100644 index 000000000..b4cc16401 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5panelArmor.cfg @@ -0,0 +1,106 @@ +PART +{ +name = BD0.5x0.5panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 0.5 + NODE + { + name = Node1 + transform = Node1 + size = 0 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 0 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 0 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 0 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 0 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 5 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 0.5x0.5 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 0.5x0.5 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.01875 +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 = 125 + armorVolume = 0.25 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5slopeArmor.cfg new file mode 100644 index 000000000..e84e31371 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5slopeArmor.cfg @@ -0,0 +1,102 @@ +PART +{ +name = BD0.5x0.5slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 +rescaleFactor = 0.5 + + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } + + NODE + { + name = Node1 + transform = Node1 + size = 1 + method = FIXED_JOINT + } + NODE + { + + name = Node2 + transform = Node2 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 1 + method = FIXED_JOINT + } +node_attach = -0.5, 0, 0, 0, 0, 1, 1 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 2.5 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 0.5x0.5 slope Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 0.5x0.5 slope Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.009375 +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 = 125 + armorVolume = 0.125 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1.5x0.5panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1.5x0.5panelArmor.cfg new file mode 100644 index 000000000..e910fed71 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1.5x0.5panelArmor.cfg @@ -0,0 +1,106 @@ +PART +{ +name = BD1.5x0.5panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 0.5 + NODE + { + name = Node1 + transform = Node1 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 1 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 1 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 15 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 1.5x0.5 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 1.5x0.5 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.05625 +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 = 125 + armorVolume = 0.75 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD12x4panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD12x4panelArmor.cfg new file mode 100644 index 000000000..c29d6d9d8 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD12x4panelArmor.cfg @@ -0,0 +1,104 @@ +PART +{ +name = BD12x4panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 4 + NODE + { + name = Node1 + transform = Node1 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 8 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 7 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 960 +category = none +subcategory = 0 +title = BD 12x4 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 12x4 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 3.6 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 2000 +breakingTorque = 2000 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 300 + armorVolume = 48 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD16x4panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD16x4panelArmor.cfg new file mode 100644 index 000000000..0d1c60be2 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD16x4panelArmor.cfg @@ -0,0 +1,102 @@ +PART +{ +name = BD16x4panelArmor +module = Part +author = Spanner +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 4 + NODE + { + name = Node1 + transform = Node1 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 8 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 7 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 1280 +category = none +subcategory = 0 +title = BD 16x4 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 16x4 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 4.8 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 2000 +breakingTorque = 2000 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 300 + armorVolume = 64 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x0.5panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x0.5panelArmor.cfg new file mode 100644 index 000000000..77e3c15b7 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x0.5panelArmor.cfg @@ -0,0 +1,106 @@ +PART +{ +name = BD1x0.5panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 0.5 + NODE + { + name = Node1 + transform = Node1 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 1 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 1 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 10 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 1x0.5 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 1x0.5 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.0375 +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 = 125 + armorVolume = 0.5 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x0.5slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x0.5slopeArmor.cfg new file mode 100644 index 000000000..725722fe1 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x0.5slopeArmor.cfg @@ -0,0 +1,99 @@ +PART +{ +name = BD1x0.5slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 0.5 + NODE + { + name = Node1 + transform = Node1 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 1 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 1 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.36, 0, 0, 1, 1 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 5 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 1x0.5 slope Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 1x0.5 slope Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.01875 +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 = 125 + armorVolume = 0.25 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor.cfg new file mode 100644 index 000000000..f20830a85 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor.cfg @@ -0,0 +1,106 @@ +PART +{ +name = BD1x1panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 1 + NODE + { + name = Node1 + transform = Node1 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 2 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 2 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 20 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 1x1 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 1x1 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.075 +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 = 150 + armorVolume = 1 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1panelArmor.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor.mu similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1panelArmor.mu rename to BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor.mu diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor.cfg new file mode 100644 index 000000000..c39170bdd --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor.cfg @@ -0,0 +1,102 @@ +PART +{ +name = BD1x1slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 +rescaleFactor = 1 + + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } + + NODE + { + name = Node1 + transform = Node1 + size = 2 + method = FIXED_JOINT + } + NODE + { + + name = Node2 + transform = Node2 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 2 + method = FIXED_JOINT + } +node_attach = -0.5, 0, 0, 0, 0, 1, 0 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 10 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 1x1 slope Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 1x1 slope Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.0375 +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 = 150 + armorVolume = 0.5 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1slopeArmor.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor.mu similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD1x1slopeArmor.mu rename to BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor.mu diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x0.5panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x0.5panelArmor.cfg new file mode 100644 index 000000000..b312a0ad0 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x0.5panelArmor.cfg @@ -0,0 +1,105 @@ +PART +{ +name = BD2x0.5panelArmor +module = Part +author = Spanner +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 0.5 + NODE + { + name = Node1 + transform = Node1 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 2 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 0 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 20 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 2x0.5 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 2x0.5 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.075 +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 + maxSupportedArmor = 125 + ArmorThickness = 25 + armorVolume = 1 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor.cfg new file mode 100644 index 000000000..6f5f4db02 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor.cfg @@ -0,0 +1,106 @@ +PART +{ +name = BD2x1panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 1 + NODE + { + name = Node1 + transform = Node1 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 2 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 3 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 40 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 2x1 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 2x1 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.15 +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 = 150 + armorVolume = 2 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD2x1panelArmor.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor.mu similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD2x1panelArmor.mu rename to BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor.mu diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor.cfg new file mode 100644 index 000000000..2c4677a73 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor.cfg @@ -0,0 +1,99 @@ +PART +{ +name = BD2x1slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 1 + NODE + { + name = Node1 + transform = Node1 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 2 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 2 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.36, 0, 0, 1, 3 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 20 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 2x1 slope Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 2x1 slope Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.075 +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 = 150 + armorVolume = 1 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD2x1slopeArmor.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor.mu similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD2x1slopeArmor.mu rename to BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor.mu diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x2panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x2panelArmor.cfg new file mode 100644 index 000000000..52a5f7b02 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x2panelArmor.cfg @@ -0,0 +1,104 @@ +PART +{ +name = BD2x2panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 2 + NODE + { + name = Node1 + transform = Node1 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 3 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 3 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 80 +category = none +subcategory = 0 +title = BD 2x2 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 2x2 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.3 +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 = 200 + armorVolume = 4 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x2slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x2slopeArmor.cfg new file mode 100644 index 000000000..9463a529e --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD2x2slopeArmor.cfg @@ -0,0 +1,101 @@ +PART +{ +name = BD2x2slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 +rescaleFactor = 2 + + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } + + NODE + { + name = Node1 + transform = Node1 + size = 3 + method = FIXED_JOINT + } + NODE + { + + name = Node2 + transform = Node2 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 3 + method = FIXED_JOINT + } +node_attach = -0.5, 0, 0, 0, 0, 1, 3 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 40 +category = none +subcategory = 0 +title = BD 2x2 sloped plate +manufacturer = Bahamuto Dynamics +description = A sturdy 2x2 sloped plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.15 +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 = 200 + armorVolume = 2 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor.cfg new file mode 100644 index 000000000..6eb544973 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor.cfg @@ -0,0 +1,105 @@ +PART +{ +name = BD3x1panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 1 + NODE + { + name = Node1 + transform = Node1 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 3 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 3 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 60 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 3x1 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 3x1 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.225 +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 = 150 + armorVolume = 3 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD3x1panelArmor.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor.mu similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD3x1panelArmor.mu rename to BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor.mu diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor.cfg new file mode 100644 index 000000000..1f2699a76 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor.cfg @@ -0,0 +1,105 @@ +PART +{ +name = BD4x1panelArmor +module = Part +author = Spanner +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 1 + NODE + { + name = Node1 + transform = Node1 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 3 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 3 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 3 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 80 +category = none +subcategory = 0 +bulkheadProfiles = srf +title = BD 4x1 panel Armor +manufacturer = Bahamuto Dynamics +description = A sturdy 4x1 Armor plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.3 +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 = 150 + armorVolume = 4 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD4x1panelArmor.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor.mu similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/BD4x1panelArmor.mu rename to BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor.mu diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x2panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x2panelArmor.cfg new file mode 100644 index 000000000..ce2d34e60 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x2panelArmor.cfg @@ -0,0 +1,105 @@ +PART +{ +name = BD4x2panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 2 + NODE + { + name = Node1 + transform = Node1 + size = 4 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 4 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 4 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 4 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 4 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 160 +category = none +subcategory = 0 +title = BD 4x2 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 4x2 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.6 +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 = 200 + armorVolume = 8 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x2slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x2slopeArmor.cfg new file mode 100644 index 000000000..00a8b3a87 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x2slopeArmor.cfg @@ -0,0 +1,97 @@ +PART +{ +name = BD4x2slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 2 + NODE + { + name = Node1 + transform = Node1 + size = 4 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 4 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 4 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.36, 0, 0, 1, 4 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 80 +category = none +subcategory = 0 +title = BD 4x2 sloped plate +manufacturer = Bahamuto Dynamics +description = A sturdy 4x2 sloped plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.3 +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 = 200 + armorVolume = 4 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x4panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x4panelArmor.cfg new file mode 100644 index 000000000..5d3415edc --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x4panelArmor.cfg @@ -0,0 +1,104 @@ +PART +{ +name = BD4x4panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 4 + NODE + { + name = Node1 + transform = Node1 + size = 5 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 5 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 5 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 5 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 5 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 320 +category = none +subcategory = 0 +title = BD 4x4 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 4x4 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 1.2 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 2000 +breakingTorque = 2000 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 300 + armorVolume = 16 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x4slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x4slopeArmor.cfg new file mode 100644 index 000000000..5dd8733d1 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD4x4slopeArmor.cfg @@ -0,0 +1,101 @@ +PART +{ +name = BD4x4slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 +rescaleFactor = 4 + + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD1x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } + + NODE + { + name = Node1 + transform = Node1 + size = 6 + method = FIXED_JOINT + } + NODE + { + + name = Node2 + transform = Node2 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 6 + method = FIXED_JOINT + } +node_attach = -0.5, 0, 0, 0, 0, 1, 6 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 160 +category = none +subcategory = 0 +title = BD 4x4 sloped plate +manufacturer = Bahamuto Dynamics +description = A sturdy 4x4 sloped plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.6 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 2000 +breakingTorque = 2000 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 300 + armorVolume = 8 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD6x2panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD6x2panelArmor.cfg new file mode 100644 index 000000000..929ab7433 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD6x2panelArmor.cfg @@ -0,0 +1,104 @@ +PART +{ +name = BD6x2panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD3x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 2 + NODE + { + name = Node1 + transform = Node1 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 6 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 6 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 240 +category = none +subcategory = 0 +title = BD 6x2 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 6x2 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.9 +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 = 200 + armorVolume = 12 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x2panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x2panelArmor.cfg new file mode 100644 index 000000000..8a408762c --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x2panelArmor.cfg @@ -0,0 +1,104 @@ +PART +{ +name = BD8x2panelArmor +module = Part +author = Spanner +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD4x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 2 + NODE + { + name = Node1 + transform = Node1 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 6 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 6 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 320 +category = none +subcategory = 0 +title = BD 8x2 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 8x2 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 1.2 +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 = 200 + armorVolume = 16 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x4panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x4panelArmor.cfg new file mode 100644 index 000000000..397139a90 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x4panelArmor.cfg @@ -0,0 +1,104 @@ +PART +{ +name = BD8x4panelArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1panelArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 4 + NODE + { + name = Node1 + transform = Node1 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 6 + method = FIXED_JOINT + } + NODE + { + name = Node4 + transform = Node4 + size = 6 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.5, 0, 0, 1, 6 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 640 +category = none +subcategory = 0 +title = BD 8x4 plate +manufacturer = Bahamuto Dynamics +description = A sturdy 8x4 plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 2.4 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 2000 +breakingTorque = 2000 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 300 + armorVolume = 32 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x4slopeArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x4slopeArmor.cfg new file mode 100644 index 000000000..cd5406ece --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD8x4slopeArmor.cfg @@ -0,0 +1,97 @@ +PART +{ +name = BD8x4slopeArmor +module = Part +author = SpannerMonkey +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Legacy/BD2x1slopeArmor + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 4 + NODE + { + name = Node1 + transform = Node1 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node2 + transform = Node2 + size = 8 + method = FIXED_JOINT + } + NODE + { + name = Node3 + transform = Node3 + size = 8 + method = FIXED_JOINT + } +node_attach = -0.0, 0, 0.36, 0, 0, 1, 6 + +TechRequired = Unresearcheable +entryCost = 7200 +cost = 320 +category = none +subcategory = 0 +title = BD 8x4 sloped plate +manufacturer = Bahamuto Dynamics +description = A sturdy 8x4 sloped plate, perfect for constructing all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 1.2 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 2000 +breakingTorque = 2000 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 300 + armorVolume = 16 + } + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BDGrey_NRM.dds b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BDGrey_NRM.dds new file mode 100644 index 000000000..42ffb312d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BDGrey_NRM.dds differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/EYEGrey.dds b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/EYEGrey.dds new file mode 100644 index 000000000..42ffb312d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/EYEGrey.dds differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Panel.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Panel.mu new file mode 100644 index 000000000..8b67f2196 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Panel.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TriPanel.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TriPanel.mu new file mode 100644 index 000000000..7b90b9e6f Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TriPanel.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg new file mode 100644 index 000000000..e7fdc864a --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg @@ -0,0 +1,97 @@ +PART +{ +name = BD_PanelArmorTri +module = Part +author = SuicidalInsanity +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/TriPanel + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + 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_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 +cost = 5 +category = Structural +bdacategory = Armor +subcategory = 0 +bulkheadProfiles = srf +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 +tags = #loc_BDArmory_part_bahaArmor_tags +// --- 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 + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 500 + armorVolume = 0.5 + isTriangularPanel = true + } + MODULE + { + name = BDAdjustableArmor + ArmorTransformName = ArmorTransform + 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 + { + 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 = BDArmory/Parts/ArmorPlate/armorpanelNRM + _BumpMap = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg new file mode 100644 index 000000000..ff785389f --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg @@ -0,0 +1,91 @@ +PART +{ +name = BD_PanelArmor +module = Part +author = SuicidalInsanity +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/Panel + texture = EYEGrey, BDArmory/Parts/ArmorPlate/EYEGrey + texture = BDGrey_NRM, BDArmory/Parts/ArmorPlate/BDGrey_NRM + 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.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 + +stackSymmetry = 1 +TechRequired = composites +entryCost = 7200 +cost = 5 +category = Structural +bdacategory = Armor +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_Panel_title //BD Armor Panel +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #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. +attachRules = 1,1,1,1,1 +tags = #loc_BDArmory_part_bahaArmor_tags +// --- standard part parameters --- +mass = 0.01 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 200 +breakingTorque = 200 +maxTemp = 2000 +fuelCrossFeed = True + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 500 + armorVolume = 1 + } + MODULE + { + name = BDAdjustableArmor + ArmorTransformName = ArmorTransform + 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 + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/armorpanelNRM + _BumpMap = BDArmory/Parts/ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + mainTextureURL = BDArmory/Parts/ArmorPlate/CarrierDeck + _BumpMap = BDArmory/Parts/ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/Jet_Engines_Stardust.dds b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/Jet_Engines_Stardust.dds new file mode 100644 index 000000000..634890995 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/Jet_Engines_Stardust.dds differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg new file mode 100644 index 000000000..9a986de68 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg @@ -0,0 +1,628 @@ +PART +{ + name = BDA_EJ200 + module = Part + author = Porkjet, modified by Stardust + //mesh = turboJet.mu + MODEL + { + model = Squad/Parts/Engine/jetEngines/turboJet + texture = Jet Engines, BDArmory/Parts/EJ200/Jet_Engines_Stardust + scale = 0.5, 0.52, 0.5 + } + //MODEL + //{ + // model = Squad/Parts/Engine/jetEngines/turbineInside + // + rescaleFactor = 1 + node_stack_top = 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 + CoMOffset = 0.0, 1.5, 0.0 + TechRequired = supersonicFlight + entryCost = 4500 + cost = 2000 + category = Engine + subcategory = 0 + title = #loc_BDArmory_part_EJ200_title //TFJ-EJ200 "Typhoon" Afterburning Turbofan + manufacturer = #loc_BDArmory_agent2_title //Twin Crown Aerospace Industries + description = #loc_BDArmory_part_EJ200_description //Word is that this engine was the result of international cooperation, which produced an engine with exceptional performance and potential. + attachRules = 1,0,1,0,0 + tags = bda after aircraft burner engine fighter jet (typhoon plane propuls + mass = 0.65 + // heatConductivity = 0.06 // half default + skinInternalConductionMult = 4.0 + emissiveConstant = 0.8 // engine nozzles are good at radiating. + //emissiveConstant = 3.2 // engine nozzles are good at radiating. + dragModelType = default + maximum_drag = 0.2 + minimum_drag = 0.2 + angularDrag = 2 + crashTolerance = 25 + maxTemp = 2000 // = 3600 + bulkheadProfiles = size0 + MODULE + { + name = MultiModeEngine + primaryEngineID = Dry + secondaryEngineID = Wet + carryOverThrottle = True + autoSwitchAvailable = False + primaryEngineModeDisplayName = #autoLOC_6001896 //#autoLOC_6001896 = Dry + secondaryEngineModeDisplayName = #autoLOC_6001895 //#autoLOC_6001895 = Wet + } + MODULE + { + name = ModuleEnginesFX + engineID = Dry + thrustVectorTransformName = thrustTransform + exhaustDamage = True + ignitionThreshold = 0.1 + minThrust = 0 + maxThrust = 60 + heatProduction = 15 + useEngineResponseTime = True + engineAccelerationSpeed = 0.5 + engineDecelerationSpeed = 0.5 + useVelocityCurve = False + flameoutEffectName = flameout + powerEffectName = power_dry + //runningEffectName = running_thrust + engageEffectName = engage + disengageEffectName = disengage + spoolEffectName = running_dry + engineSpoolIdle = 0.05 + engineSpoolTime = 2.0 + EngineType = Turbine + exhaustDamageMultiplier = 5 + clampPropReceived = True + fxOffset = 0, 0, 0.3 + PROPELLANT + { + name = IntakeAir + ignoreForIsp = True + ratio = 40 + } + PROPELLANT + { + name = LiquidFuel + resourceFlowMode = STAGE_STACK_FLOW_BALANCE + ratio = 1 + DrawGauge = True + } + atmosphereCurve + { + key = 0 10000 0 0 + } + // Jet params + atmChangeFlow = True + useVelCurve = True + useAtmCurve = True + //flowMultCap = 1.1 + machLimit = 1.4 + machHeatMult = 20.0 + velCurve + { + key = 0 1 -0.1714286 -0.1714286 + key = 0.35 0.94 0.008035712 0.008035712 + key = 0.67 1 0.4014425 0.4014425 + key = 0.8 1.08 0.7826924 0.7826924 + key = 1 1.27 0.5964285 0.5964285 + key = 1.7 1.44 -0.1785716 -0.1785716 + key = 2 1.26 -1.5 -1.5 + key = 2.25 0.66 -2.4 -2.4 + } + atmCurve + { + key = 0 0 -0.009380879 0.02675156 + key = 0.1475 0 0 0 + key = 0.21 0.75 10.11998 9.912155 + key = 0.2653 1 2.007882 1.716759 + key = 1.013 1 0.0005999365 -0.0007497668 + } + } + MODULE + { + name = ModuleEnginesFX + engineID = Wet + thrustVectorTransformName = thrustTransform + exhaustDamage = True + ignitionThreshold = 0.1 + minThrust = 0 + maxThrust = 92 + heatProduction = 75 + useEngineResponseTime = True + engineAccelerationSpeed = 0.8 + engineDecelerationSpeed = 0.8 + useVelocityCurve = False + flameoutEffectName = flameout + //powerEffectName = running_wet + runningEffectName = power_wet + engageEffectName = engage + disengageEffectName = disengage + spoolEffectName = running_wet + engineSpoolIdle = 0.05 + engineSpoolTime = 2.0 + EngineType = Turbine + exhaustDamageMultiplier = 20 + clampPropReceived = True + fxOffset = 0, 0, 0.3 + PROPELLANT + { + name = IntakeAir + ignoreForIsp = True + ratio = 12 + } + PROPELLANT + { + name = LiquidFuel + resourceFlowMode = STAGE_STACK_FLOW_BALANCE + ratio = 1 + DrawGauge = True + } + atmosphereCurve + { + key = 0 5000 0 0 + } + // Jet params + atmChangeFlow = True + useVelCurve = True + useAtmCurve = True + //flowMultCap = 1.1 + machLimit = 2.75 + machHeatMult = 20.0 + + velCurve + { + key = 0 1 -0.1499999 -0.1499999 + key = 0.2 0.97 0 0 + key = 0.4 1 0.2249998 0.2249998 + key = 0.5 1.03 0.425 0.425 + key = 0.7 1.14 0.7083334 0.7083334 + key = 1 1.4 1.183333 1.183333 + key = 1.5 2.15 1.92 1.92 + key = 2 3.32 1.15 1.15 + key = 2.25 3.31 -2.226667 -2.226667 + key = 3 0 -4.413333 -4.413333 + + } + atmCurve + { + // less linear because AB has a big ram effect at high speed at high alt. + key = 0 0 -0.009380879 0.02675156 + key = 0.1475 0 0 0 + key = 0.21 0.75 10.11998 9.912155 + key = 0.2653 1 2.007882 1.716759 + key = 1.013 1 0.0005999365 -0.0007497668 + } + } + + MODULE + { + name = FXModuleAnimateThrottle + animationName = TurboJetNozzleDry + responseSpeed = 0.05 + layer = 1 + dependOnEngineState = True + dependOnThrottle = True + engineName = Dry + weightOnOperational = True + } + MODULE + { + name = FXModuleAnimateThrottle + animationName = TurboJetNozzleWet + responseSpeed = 0.08 + layer = 2 + dependOnEngineState = True + dependOnThrottle = True + engineName = Wet + weightOnOperational = True + } + MODULE + { + name = FXModuleAnimateThrottle + animationName = TurboJetHeat + responseSpeed = 0.0005 + layer = 3 + dependOnEngineState = True + engineName = Wet + } + //MODULE + { + name = ModuleGimbal + gimbalTransformName = Gimbal + gimbalRange = 10 + gimbalResponseSpeed = 8 + //gimbalRangeYP = 0 + //gimbalRangeXP = 10 + useGimbalResponseSpeed = true + } + + MODULE + { + name = FXModuleConstrainPosition + matchRotation = false + matchPosition = true + CONSTRAINFX + { + targetName = NozzlePoint + moversName = Nozzle + } + } + + MODULE + { + name = ModuleAlternator + engineName = Wet + outputName = #autoLOC_6001892 //#autoLOC_6001892 = Alternator (Wet) + RESOURCE + { + name = ElectricCharge + rate = 7.5 + } + } + MODULE + { + name = ModuleAlternator + engineName = Dry + outputName = #autoLOC_6001893 //#autoLOC_6001893 = Alternator (Dry) + RESOURCE + { + name = ElectricCharge + rate = 6.0 + } + } + MODULE + { + name = ModuleSurfaceFX + thrustProviderModuleIndex = 1 + fxMax = 0.6 + maxDistance = 25 + falloff = 2 + thrustTransformName = thrustTransform + } + MODULE + { + name = ModuleSurfaceFX + thrustProviderModuleIndex = 2 + fxMax = 0.6 + maxDistance = 25 + falloff = 2 + thrustTransformName = thrustTransform + } + EFFECTS + { + + running_dry + { + PREFAB_PARTICLE + { + prefabName = fx_smokeTrail_light + transformName = smokePoint + emission = 0.0 0.0 + emission = 0.05 0.0 + emission = 0.075 0.25 + emission = 1.0 1.25 + speed = 0.0 0.25 + speed = 1.0 1.0 + localOffset = 0, 0, 1 + localRotation = 1, 0, 0, -90 + } + AUDIO + { + channel = Ship + clip = sound_jet_low + volume = 0.0 0.0 + volume = 0.05 0.2 + volume = 1.0 0.45 + pitch = 0.0 0.2 + pitch = 0.05 0.4 + pitch = 0.33 0.7 + pitch = 1.0 1.3 + loop = true + } + } + + power_dry + { + AUDIO + { + channel = Ship + clip = sound_jet_deep + volume = 0.0 0.0 + volume = 0.05 0.5 + volume = 1.0 1.4 + pitch = 0.0 0.3 + pitch = 1.0 0.6 + loop = true + } + } + running_wet + { + PREFAB_PARTICLE + { + prefabName = fx_smokeTrail_light + transformName = smokePoint + emission = 0.0 0.0 + emission = 0.05 0.0 + emission = 0.075 0.5 + emission = 1.0 1.25 + speed = 0.0 0.25 + speed = 1.0 1.0 + localOffset = 0, 0, 1 + localRotation = 1, 0, 0, -90 + } + AUDIO + { + channel = Ship + clip = sound_jet_low + volume = 0.0 0.0 + volume = 0.05 0.3 + volume = 1.0 0.45 + pitch = 0.0 0.2 + pitch = 0.05 0.4 + pitch = 0.33 0.7 + pitch = 1.0 0.9 + loop = true + } + } + power_wet + { + + AUDIO + { + channel = Ship + clip = sound_rocket_spurts + volume = 0.0 0.0 + volume = 0.1 0.4 + volume = 1.0 0.6 + pitch = 0.0 0.1 + pitch = 0.33 0.2 + pitch = 1.0 3.0 + loop = true + } + MODEL_MULTI_PARTICLE + { + modelName = Squad/FX/afterburner_shock + transformName = smokePoint + emission = 0.0 0.0 + emission = 0.05 0.05 + emission = 0.33 0.1 + emission = 1.0 1.0 + speed = 0.0 0.0 + speed = 0.05 0.05 + speed = 0.33 0.16 + speed = 1.0 1.0 + energy = 0.0 1.5 + energy = 0.33 1.0 + energy = 1.0 1.0 + localOffset = 0, 0, 2 + localScale = 0.5, 0.5, 0.5 + } + } + engage + { + AUDIO + { + channel = Ship + clip = sound_vent_medium + volume = 1.0 + pitch = 2.2 + loop = false + } + } + disengage + { + AUDIO + { + channel = Ship + clip = sound_vent_soft + volume = 1.0 + pitch = 2.0 + loop = false + } + } + flameout + { + PREFAB_PARTICLE + { + prefabName = fx_exhaustSparks_flameout_2 + transformName = smokePoint + oneShot = true + } + AUDIO + { + channel = Ship + clip = sound_explosion_low + volume = 1.0 + pitch = 2.0 + loop = false + } + } + } + MODULE + { + name = ModuleTestSubject + useStaging = True + useEvent = True + situationMask = 15 + CONSTRAINT + { + type = OXYGEN + value = True + } + CONSTRAINT + { + type = SPEEDENV + test = LT + value = 200 + prestige = Trivial + } + CONSTRAINT + { + type = SPEEDENV + test = GT + value = 100 + prestige = Trivial + } + CONSTRAINT + { + type = SPEEDENV + test = LT + value = 100 + prestige = Significant + } + CONSTRAINT + { + type = SPEEDENV + test = GT + value = 50 + prestige = Significant + } + CONSTRAINT + { + type = SPEEDENV + test = LT + value = 50 + prestige = Exceptional + } + CONSTRAINT + { + type = SPEEDENV + test = GT + value = 20 + prestige = Exceptional + } + CONSTRAINT + { + type = SPEED + test = LT + value = 450 + situationMask = 8 + prestige = Trivial + } + CONSTRAINT + { + type = SPEED + test = GT + value = 150 + situationMask = 8 + prestige = Trivial + } + CONSTRAINT + { + type = SPEED + test = LT + value = 650 + situationMask = 8 + prestige = Significant + } + CONSTRAINT + { + type = SPEED + test = GT + value = 250 + situationMask = 8 + prestige = Significant + } + CONSTRAINT + { + type = SPEED + test = LT + value = 900 + situationMask = 8 + prestige = Exceptional + } + CONSTRAINT + { + type = SPEED + test = GT + value = 350 + situationMask = 8 + prestige = Exceptional + } + CONSTRAINT + { + type = DENSITY + test = GT + value = 0.2 + situationMask = 8 + prestige = Trivial + } + CONSTRAINT + { + type = DENSITY + test = GT + value = 0.1 + situationMask = 8 + prestige = Significant + } + CONSTRAINT + { + type = DENSITY + test = GT + value = 0.05 + situationMask = 8 + prestige = Exceptional + } + CONSTRAINT + { + type = ALTITUDEENV + test = GT + value = 4000 + prestige = Trivial + } + CONSTRAINT + { + type = ALTITUDEENV + test = LT + value = 8000 + prestige = Trivial + } + CONSTRAINT + { + type = ALTITUDEENV + test = GT + value = 1000 + prestige = Significant + } + CONSTRAINT + { + type = ALTITUDEENV + test = LT + value = 2000 + prestige = Significant + } + CONSTRAINT + { + type = ALTITUDEENV + test = GT + value = 500 + prestige = Exceptional + } + CONSTRAINT + { + type = ALTITUDEENV + test = LT + value = 1000 + prestige = Exceptional + } + CONSTRAINT + { + type = REPEATABILITY + value = ALWAYS + prestige = Trivial + } + CONSTRAINT + { + type = REPEATABILITY + value = BODYANDSITUATION + prestige = Significant + } + CONSTRAINT + { + type = REPEATABILITY + value = ONCEPERPART + prestige = Exceptional + } + } +} 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/F-86Launcher/F86Launcher.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher.cfg new file mode 100644 index 000000000..0de264d72 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher.cfg @@ -0,0 +1,101 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = BahaF86Launcher +module = Part +author = SuicidalInsanity + +// --- asset parameters --- +mesh = model.mu +rescaleFactor = 1 + + +// --- node definitions --- +node_attach = 0.0, 0.0, 0.05, 0, 1, 0, 0 + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 800 +cost = 400 +category = none +bdacategory = Rocket pods +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_F86RL_title // FFAR Reloadable Rocket Pod +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_F86RL_description +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 1,1,0,0,1 +tags = #loc_BDArmory_part_F86RL_tags +// --- standard part parameters --- +mass = 0.1 +dragModelType = default +maximum_drag = 0.01 +minimum_drag = 0.01 +angularDrag = 2 +crashTolerance = 37 +maxTemp = 3600 + + +MODULE + { + name = ModuleWeapon + shortName = FFAR Launcher + + fireTransformName = rockets + + hasDeployAnim = true //if true and reloadAnim false, will play retract/deploy sequence //when reloading if BeltFed = false + deployAnimName = F86Unpack + + hasReloadAnim = false //if true, will play this anim when reloading, with anim length clamped to RelaodTime + //reloadAnimName = reloadAnim + + hasFireAnimation = false + + roundsPerMinute = 650 + maxEffectiveDistance = 2500 + maxTargetingRange = 4000 + + weaponType = rocket + bulletType = FFAR70; H70M247 //ammos ued by the launcher + ammoName = Rockets + + requestResourceAmount = 1 + rocketPod = true //has rocket submodels + externalAmmo = true //supply rocket from external bin, not internal supply + + BeltFed = false //is supplied from a limited magazine instead of an ammo bin + RoundsPerMag = 24 //rounds fired before reload period + ReloadTime = 10 + + onlyFireInRange = true + + autoProxyTrackRange = 1200 + + fireSoundPath = BDArmory/Sounds/launch + oneShotSound = true + + explModelPath = BDArmory/Models/explosion/explosion + explSoundPath = BDArmory/Sounds/explode1 + } + + +RESOURCE +{ + name = Rockets + amount = 24 + maxAmount = 24 +} + + +MODULE +{ + name = ModuleCASE + CASELevel = 2 +} + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher.png b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher.png new file mode 100644 index 000000000..f6371f89d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher_N.png b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher_N.png new file mode 100644 index 000000000..6d305ff89 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86Launcher_N.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86RLHousing.png b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86RLHousing.png new file mode 100644 index 000000000..f0e3febaf Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86RLHousing.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86RLHousing_N.png b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86RLHousing_N.png new file mode 100644 index 000000000..03ab201d1 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/F86RLHousing_N.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/model.mu new file mode 100644 index 000000000..c391fe082 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/F-86Launcher/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/GAU22A.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/GAU22A.png new file mode 100644 index 000000000..601c32a80 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/GAU22A.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/Gau22loop.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/Gau22loop.ogg new file mode 100644 index 000000000..0de9f7ab9 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/Gau22loop.ogg differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/Model.mu new file mode 100644 index 000000000..231c22920 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/Model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/muzzle.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/muzzle.png new file mode 100644 index 000000000..7e5cc3ccf Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/muzzle.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg new file mode 100644 index 000000000..f1df6fffa --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg @@ -0,0 +1,94 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaGau-22 +module = Part +author = SuicidalInsanity + +// --- asset parameters --- +mesh = model.mu +rescaleFactor = 1.1 + +// --- node definitions --- +node_attach = 0.0, 0.0, 0, 0, -1, 0, 1 + + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 2500 +cost = 2100 +category = none +bdacategory = Guns +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_GAU22_title //GAU-22/A 25x137mm Cannon +manufacturer = loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_GAU22_description //A 4 barrel 25mm rotary cannon. 25x137mmAmmo. +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 0,1,0,0,1 +tags = #loc_BDArmory_part_GAU22_tags +// --- standard part parameters --- +mass = 0.3 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 60 +maxTemp = 3600 + +stagingIcon = SOLID_BOOSTER + +MODULE +{ + name = ModuleWeapon + + fireTransformName = fireTransform + shortName = GAU-22 + hasDeployAnim = true + deployAnimName = GauUnpack + + hasFireAnimation = true + fireAnimName = GauFireAnim + spinDownAnimation = true + SpoolUpTime = 0.2 + roundsPerMinute = 3300 + maxDeviation = 0.223 //5mrad, 80% + maxEffectiveDistance = 3000 + maxTargetingRange = 5000 + + ammoName = 25x137Ammo + bulletType = 25x137mmAPEXBullet + requestResourceAmount = 1 + + hasRecoil = true + onlyFireInRange = false + bulletDrop = true + useRippleFire = false + + weaponType = ballistic + + tracerLength = 0 + tracerDeltaFactor = 2.75 + tracerInterval = 4 + + //oneShotWorldParticles = true + + maxHeat = 3600 + heatPerShot = 48 + heatLoss = 820 + + autoProxyTrackRange = 1200 + + fireSoundPath = BDArmory/Parts/Gau-22A/Gau22loop + overheatSoundPath = BDArmory/Parts/20mmVulcan/sounds/VulcanEnd + oneShotSound = false + + explModelPath = BDArmory/Models/explosion/30mmExplosion + explSoundPath = BDArmory/Sounds/subExplode +} + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/smoke.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/smoke.png new file mode 100644 index 000000000..ceb6fa6e5 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/smoke.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg index 433979966..77459d299 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg @@ -23,18 +23,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 1700 - cost = 750 + entryCost = 9000 + cost = 7500 category = none bdacategory = Gun turrets subcategory = 0 - bulkheadProfiles = srf - title = GoalkeeperMk1 CIWS - manufacturer = Bahamuto Dynamics - description = 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. 30x173 + bulkheadProfiles = size2, srf + title = #loc_BDArmory_part_GoalKeeperBDAcMk1_title //GoalkeeperMk1 CIWS + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_GoalKeeperBDAcMk1_description //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. 30x173 // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaGoalKeeper_tags // --- standard part parameters --- mass = 4 dragModelType = default @@ -83,9 +83,9 @@ PART hasFireAnimation = true fireAnimName = GKBDAcFire spinDownAnimation = true - + SpoolUpTime = 0.3 roundsPerMinute = 4200 - maxDeviation = 0.50 + maxDeviation = 0.262 maxEffectiveDistance = 4000 maxTargetingRange = 5000 @@ -98,17 +98,13 @@ PART bulletDrop = true weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true - projectileColor = 255, 20, 0, 160//RGBA 0-255 - startColor = 255, 30, 0, 24 - fadeColor = true - - tracerStartWidth = 0.18 - tracerEndWidth = 0.18 tracerLength = 0 tracerDeltaFactor = 2.75 tracerInterval = 2 - nonTracerWidth = 0.065 maxHeat = 3600 heatPerShot = 36 @@ -121,8 +117,6 @@ PART oneShotSound = false //explosion - airDetonation = true - airDetonationTiming = false explModelPath = BDArmory/Models/explosion/30mmExplosion explSoundPath = BDArmory/Sounds/subExplode diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg index 5d2b0ce26..ee96709a5 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg @@ -25,18 +25,18 @@ node_stack_bottom = 0.0, -0.0, 0, 0, -1, 0, 2 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 950 +entryCost = 10000 +cost = 8000 category = none bdacategory = Gun turrets subcategory = 0 -bulkheadProfiles = srf -title = Goalkeeper MK2 CIWS -manufacturer = Bahamuto Dynamics -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. +bulkheadProfiles = size2, 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 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 - +tags = #loc_BDArmory_part_bahaGoalKeeper_tags // --- standard part parameters --- mass = 4.4 dragModelType = default @@ -85,9 +85,9 @@ MODULE hasFireAnimation = true fireAnimName = BDAcGKmk2 spinDownAnimation = true - + SpoolUpTime = 0.3 roundsPerMinute = 4200 - maxDeviation = 0.40 + maxDeviation = 0.241 //5.5mrad maxEffectiveDistance = 4000 maxTargetingRange = 5000 @@ -100,17 +100,13 @@ MODULE bulletDrop = true weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true - projectileColor = 255, 20, 0, 160//RGBA 0-255 - startColor = 255, 30, 0, 24 - fadeColor = true - - tracerStartWidth = 0.18 - tracerEndWidth = 0.18 tracerLength = 0 tracerDeltaFactor = 2.75 tracerInterval = 2 - nonTracerWidth = 0.065 maxHeat = 3600 heatPerShot = 36 @@ -123,12 +119,8 @@ MODULE oneShotSound = false //explosion - airDetonation = true - airDetonationTiming = false explModelPath = BDArmory/Models/explosion/30mmExplosion explSoundPath = BDArmory/Sounds/subExplode - - } @@ -172,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/Bolt.png b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/Bolt.png new file mode 100644 index 000000000..504e181e4 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/Bolt.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/CD_GravGun_UV.png b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/CD_GravGun_UV.png new file mode 100644 index 000000000..293991956 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/CD_GravGun_UV.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/Model.mu new file mode 100644 index 000000000..645a09753 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/Model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/RepulsorFire.wav b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/RepulsorFire.wav new file mode 100644 index 000000000..dbb859c38 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/RepulsorFire.wav differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/gravgunemission2.png b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/gravgunemission2.png new file mode 100644 index 000000000..746a0077c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/gravgunemission2.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg new file mode 100644 index 000000000..895e75277 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg @@ -0,0 +1,92 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bdImpulseGun +module = Part +author = Concodroid, SuicidalInsanity + +// --- asset parameters --- +mesh = model.mu +rescaleFactor = 1.4 + + +// --- node definitions --- +node_attach = 0.0, 0.0, 0.00, 0, 1, 0, 0 + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 25000 +cost = 10000 +category = none +bdacategory = Lasers +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_GravGun_title +manufacturer = #loc_BDArmory_part_manufacturer +description = #loc_BDArmory_part_GravGun_description +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 0,1,0,0,1 +tags = #loc_BDArmory_part_GravGun_tags +// --- standard part parameters --- +mass = 0.1 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 60 +maxTemp = 3600 + +stagingIcon = SOLID_BOOSTER + + MODULE + { + name = ModuleWeapon + shortName = Repulsor Cannon + + fireTransformName = fireTransform + + hasDeployAnim = false + hasFireAnimation = true + fireAnimName = GravAnim + spinDownAnimation = false + + roundsPerMinute = 500 + maxDeviation = 0.15 + maxEffectiveDistance = 2500 + maxTargetingRange = 4000 + + weaponType = laser + pulseLaser = true //change this to false if beamlasers preferred + 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.25 // mass change, in tons + ammoName = ElectricCharge + requestResourceAmount = 50 + + projectileColor = 56, 83, 255, 240//RGBA 0-255 + startColor = 56, 83, 255, 240 + fadeColor = false + tracerStartWidth = 0.4 + tracerEndWidth = 0.4 + tracerLength = 0 + + autoProxyTrackRange = 1200 + + fireSoundPath = BDArmory/Parts/GravityGun/RepulsorFire + overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat + oneShotSound = true + laserTexturePath = BDArmory/Parts/GravityGun/Bolt + maxHeat = 3600 + heatPerShot = 115 + heatLoss = 825 + + } +} + + 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..8ef10ee1d --- /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. + tags = #loc_BDArmory_part_bahaIRSTPod_tags + 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 new file mode 100644 index 000000000..ec53ed541 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/BombBay.cfg @@ -0,0 +1,66 @@ +PART +{ + name = bdMissileBay + module = Part + author = SuicidalInsanity + + // --- asset parameters --- + mesh = Model.mu + rescaleFactor = 1 + NODE + { + name = rail_1 + transform = rail_Node + size = 0 + method = FIXED_JOINT + } + NODE + { + name = rail_2 + transform = rail_Node2 + size = 0 + method = FIXED_JOINT + } + NODE + { + name = rail_3 + transform = rail_Node3 + size = 0 + method = FIXED_JOINT + } + + //Attachnode transforms require a 'rail_' prefix + node_stack_top = 0.0, 0.286, 0, 0, 1, 0, 0 + TechRequired = precisionEngineering + entryCost = 1000 + cost = 500 + category = none + bdacategory = Missile turrets + subcategory = 0 + bulkheadProfiles = size1 + title = #loc_BDArmory_part_BombBay_title //Ordnance Bay + manufacturer = #loc_BDArmory_agent_title + description = #loc_BDArmory_part_BombBay_description //A weapons bay with deployable rails for launching ordnance. + tags = #loc_BDArmory_part_BombBay_tags + attachRules = 1,1,1,1,0 + mass = 0.2 + dragModelType = none + maximum_drag = 0.1 + minimum_drag = 0.1 + angularDrag = 0.5 + crashTolerance = 30 + maxTemp = 2000 + fuelCrossFeed = True + thermalMassModifier = 6.0 + emissiveConstant = 0.95 + + MODULE + { + name = BDDeployableRail + deployAnimName= weaponsBay //anim name of rail + rotationDelay = 0.08 //wait time after anim finished to fire missile + deployTransformName = deployTransform //deployment transform name + hideMissiles = false //for flat conformal deployable rails if you want the missile model to be hidden while retracted + } + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/Model.mu new file mode 100644 index 000000000..2f50678f9 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/Model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/ordnanceBay.png b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/ordnanceBay.png new file mode 100644 index 000000000..ea0a7d4d3 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/ordnanceBay.png differ 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/mk1opencockpit_RP_Type2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/mk1opencockpit_RP_Type2.cfg index 31dc18670..c764b1376 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/mk1opencockpit_RP_Type2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/mk1opencockpit_RP_Type2.cfg @@ -1,4 +1,4 @@ -PART +PART { name = mk1opencockpit_RP_type2 module = Part @@ -33,7 +33,7 @@ maxTemp = 2000 // = 3000 fuelCrossFeed = True bulkheadProfiles = size1, srf - tags = aircraft airplane cockpit hollow pipe plane tube open + tags = bda cockpit chair batt sas aircraft airplane cockpit hollow pipe plane tube open DRAG_CUBE { cube = Default, 2.418281,0.7749683,0.6994118, 2.418281,0.7749683,0.6994118, 1.213026,0.9717144,0.1341177, 1.213026,0.9717144,0.1341177, 2.418281,0.7723047,0.6994124, 2.418281,0.7723256,0.6994124, 0,0,0, 1.25,1.937501,1.250001 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 d125a4114..92491c5d6 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/weaponizedcommandseat.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/weaponizedcommandseat.cfg @@ -1,26 +1,27 @@ -PART +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 = 200 + 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. + tags = #loc_BDArmory_part_combatSeat_tags mass = 0.05 dragModelType = default maximum_drag = 0.05 @@ -33,7 +34,6 @@ vesselType = Plane CrewCapacity = 1 bulkheadProfiles = srf - tags = command Seat MODULE { name = KerbalSeat 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 7f327395e..333ecef8a 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 @@ -20,18 +20,18 @@ PART node_stack_base = 0.0, 0.0, -2.179, 0, 0, -1, 0 // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 2000 + entryCost = 6000 + cost = 3000 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = RBS-15 Cruise Missile - manufacturer = Bahamuto Dynamics - description = Long distance, multi-platform high-speed cruise missile with boosters. + title = #loc_BDArmory_part_bahaRBS-15Cruise_title //RBS-15 Cruise Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaRBS-15Cruise_description //Long distance, multi-platform high-speed cruise missile with boosters. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaRBS-15Cruise_tags // --- standard part parameters --- mass = 1.15 dragModelType = default @@ -82,7 +82,7 @@ PART steerMult = 5 maxTorque = 23 torqueRampUp = 25 - terminalManeuvering = false + terminalGuidanceShouldActivate = false //set true to enable terminal guidance boosterMass = 0.25 boosterDecoupleSpeed = 7 @@ -103,7 +103,9 @@ 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 6ffe9c817..d06917fe5 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 @@ -20,18 +20,18 @@ PART node_stack_base = 0.0, 0.0, -2.179, 0, 0, -1, 0 // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 2000 + entryCost = 7000 + cost = 3500 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = RBS-15 Air launched Cruise Missile - manufacturer = Bahamuto Dynamics - description = Long distance, multi-platform high-speed cruise missile Air launched variant without external boosters + title = #loc_BDArmory_part_bahaRBS-15ALCruise_title //RBS-15 Air launched Cruise Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaRBS-15ALCruise_description //Long distance, multi-platform high-speed cruise missile Air launched variant without external boosters // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaRBS-15Cruise_tags // --- standard part parameters --- mass = 1.15 dragModelType = default @@ -82,7 +82,7 @@ PART steerMult = 8 maxTorque = 65 torqueRampUp = 50 - terminalManeuvering = false + terminalGuidanceShouldActivate = false //set true to enable terminal guidance minStaticLaunchRange = 700 maxStaticLaunchRange = 40000 @@ -97,7 +97,9 @@ PART MODULE { name = BDExplosivePart - tntMass = 300 + tntMass = 200 + caliber = 500 + warheadType = ShapedCharge } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ERA.png b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ERA.png new file mode 100644 index 000000000..edf3a1cea Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ERA.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/Model.mu new file mode 100644 index 000000000..14f085edb Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/Model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg new file mode 100644 index 000000000..c1e11c064 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg @@ -0,0 +1,60 @@ +PART +{ +name = BD1x0.5ReactiveArmor +module = Part +author = SuicidalInsanity +rescaleFactor = 1 +node_attach = 0.0, 0, 0.0, 0, 0, 1, 1 + +TechRequired = composites +entryCost = 7200 +cost = 400 +category = Structural +bdacategory = Armor +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_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 +tags = #loc_BDArmory_part_REA_Panel_tags +// --- standard part parameters --- +mass = 0.01 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 30 +maxTemp = 2000 +fuelCrossFeed = false + + MODULE + { + name = HitpointTracker + ArmorThickness = 16 + armorVolume = 0.5 + } + MODULE + { + name = ModuleReactiveArmor + sectionTransformName = sections //name of ERA sections transform in model + armorName = Reactive Armor //reporting name if anything is damaged by ERA detonation + NXRA = false //Non-Explosive Reactive Armor? + SectionHP = 300 //HP per section; total HP is sectionHP * number of sections + sensitivity = 30 //minimum caliber of incoming round to trigger explosive Reactive Armor + armorModifier = 1.5 //this times 300 is the equivalent protection of the armor against + //shaped charges for hits at 68 degrees. For NXRA this multiplies + // the plates' armor thickness + ERAflyerPlateHalfDimension = 0.25 //half of the average length/width of the flyer plate + ERAgurneyConstant = 2700 //Gurney specific energy of the ERA, equal to sqrt(2E) (in m/s) + ERArelativeEffectiveness = 1.72 //tnt RE of the ERA explosive + ERAexplosiveMass = 15 //ERA explosive mass (in kg) + ERAexplosiveDensity = 1650 //ERA explosive density (in kg/m^3) + ERAbackingPlate = true //symmetrical sandwich plate ? + ERAspacing = 0.25 //spacing between back plate and armor + ERAdetonationDelay = 50 //detonation delay (in microseconds) + ERAplateThickness = 16 //plate thickness (in mm) + ERAplateMaterial = Mild Steel //plate material, from the names in BulletDefs/BD_Armors.cfg + } + +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnAL31.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnAL31.cfg index 0151abacf..b44767221 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnAL31.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnAL31.cfg @@ -1,4 +1,4 @@ -PART +PART { name = SaturnAL31 module = Part @@ -12,15 +12,16 @@ node_stack_top = 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 CoMOffset = 0.0, 1.2, 0.0 TechRequired = supersonicFlight - entryCost = 9000 - cost = 2000 + entryCost = 7500 + cost = 3000 category = Engine subcategory = 0 bulkheadProfiles = size1 - title = Saturn AL-31FM1 Afterburning Jet Engine - manufacturer = KTech - description = 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. + title = #loc_BDArmory_part_SaturnAL31_title //Saturn AL-31FM1 Afterburning Jet Engine + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics//KTech + description = #loc_BDArmory_part_SaturnAL31_description //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. attachRules = 1,0,1,0,0 + tags = #loc_BDArmory_part_SaturnAL31_tags mass = 1.05 skinInternalConductionMult = 4.0 emissiveConstant = 0.8 // engine nozzles are good at radiating. @@ -30,7 +31,6 @@ angularDrag = 2 crashTolerance = 7 maxTemp = 2600 - tags = fighter jet MODULE { name = MultiModeEngine diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnWaterfall.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnWaterfall.cfg new file mode 100644 index 000000000..df52b7b5d --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/SaturnAL31/SaturnWaterfall.cfg @@ -0,0 +1,199 @@ +@PART[SaturnAL31]:NEEDS[Waterfall]:FOR[StockWaterfallEffects] +{ + // Removes the stock effect block, and replace it with one that has no particles + !EFFECTS {} + EFFECTS + { + fx-saturn-dry-spool + { + AUDIO + { + name = soundDry1 + channel = Ship + clip = sound_jet_low + volume = 0.0 0.0 + volume = 0.0001 1.12 + volume = 1.0 1.32 + pitch = 0.0 0.3 + pitch = 1.0 1.0 + loop = true + } + AUDIO + { + name = soundDry2 + channel = Ship + clip = sound_jet_deep + volume = 0.1 0.0 + volume = 0.3 1.12 + volume = 1.0 1.25 + pitch = 0.0 0.3 + pitch = 1.0 1.0 + loop = true + } + } + + fx-saturn-dry-power + { + + } + + fx-saturn-wet-spool + { + AUDIO + { + name = soundWet1 + channel = Ship + clip = sound_jet_low + volume = 0.0 0.0 + volume = 0.0001 0.65 + volume = 1.0 0.8 + pitch = 0.0 0.3 + pitch = 1.0 1.5 + loop = true + } + AUDIO + { + name = soundWet2 + channel = Ship + clip = sound_jet_deep + volume = 0.1 0.0 + volume = 0.3 2 + volume = 1.0 3 + pitch = 0.0 0.3 + pitch = 1.0 1.3 + loop = true + } + } + + fx-saturn-wet-running + { + AUDIO + { + name = soundWet3 + channel = Ship + clip = sound_jet_deep + volume = 0.1 0.0 + volume = 0.3 1.12 + volume = 1.0 1.25 + pitch = 0.0 0.4 + pitch = 1.0 1.0 + loop = true + } + } + engage + { + AUDIO + { + channel = Ship + clip = sound_vent_soft + volume = 1.0 + pitch = 2.2 + loop = false + } + } + disengage + { + AUDIO + { + channel = Ship + clip = sound_vent_soft + volume = 1 + pitch = 1.8 + loop = false + } + } + flameout + { + PREFAB_PARTICLE + { + prefabName = fx_exhaustSparks_flameout_2 + transformName = smokePoint + oneShot = true + } + AUDIO + { + channel = Ship + clip = sound_explosion_low + volume = 1 + pitch = 2.0 + loop = false + } + } + } + +@MODULE[ModuleEngines*],0 + { + %powerEffectName = fx-saturn-dry-power + %spoolEffectName = fx-saturn-dry-spool + } +@MODULE[ModuleEngines*],1 + { + %runningEffectName = fx-saturn-wet-running + %spoolEffectName = fx-saturn-wet-spool + } + + MODULE + { + name = ModuleWaterfallFX + moduleID = saturnDry + CONTROLLER + { + name = atmosphereDepth + linkedTo = atmosphere_density + } + CONTROLLER + { + name = throttle + linkedTo = throttle + engineID = Dry + responseRateUp = 0.01 + responseRateDown = 0.01 + } + CONTROLLER + { + name = mach + linkedTo = mach + } + TEMPLATE + { + templateName = stock-kerozine-turbofan-2 + overrideParentTransform = thrustTransform + position = 0,0,0 + rotation = 0, 0, 0 + scale = 1, 1, 1 + } + } + +MODULE + { + name = ModuleWaterfallFX + moduleID = saturnWet + + CONTROLLER + { + name = atmosphereDepth + linkedTo = atmosphere_density + } + CONTROLLER + { + name = throttle + linkedTo = throttle + engineID = Wet + responseRateUp = 0.001 + responseRateDown = 0.016 + } + CONTROLLER + { + name = mach + linkedTo = mach + } + TEMPLATE + { + templateName = stock-kerozine-afterburner + overrideParentTransform = thrustTransform + position = 0,0,0 + rotation = 0, 0, 0 + scale = 1, 1, 1 + } + } +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/Model.mu new file mode 100644 index 000000000..2748a0c67 Binary files /dev/null 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 new file mode 100644 index 000000000..faad4fe22 Binary files /dev/null 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/muzzle.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/muzzle.png new file mode 100644 index 000000000..865a9e937 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/muzzle.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg new file mode 100644 index 000000000..655ec07c6 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg @@ -0,0 +1,137 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaSidamTurret +module = Part +author = Suicidalinsanity + +// --- asset parameters --- +mesh = Model.mu +rescaleFactor = 1 + + +// --- node definitions --- +node_attach = 0.0, 0.0, 0, 0, -1, 0, 1 +node_stack_bottom = 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 2 + + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 8500 +cost = 4000 +category = none +bdacategory = Gun turrets +subcategory = 0 +title = #loc_BDArmory_part_sidam_title //Sidam Anti-Air gun +manufacturer = #loc_BDArmory_agent_title +description = #loc_BDArmory_part_sidam_description //A salvo-firing quad 25mm anti-air gun. +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 1,1,0,0,1 +bulkheadProfiles = size1p5, srf +tags = #loc_BDArmory_part_sidam_tags +// --- standard part parameters --- +mass = 1 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 60 +maxTemp = 3600 + +stagingIcon = SOLID_BOOSTER + +MODULE +{ + name = ModuleTurret + + yawTransformName = Turret + pitchTransformName = GunMounts + + pitchSpeedDPS = 150 + yawSpeedDPS = 150 + + minPitch = -10 + maxPitch = 85 + yawRange = 360 + + smoothRotation = true + smoothMultiplier = 10 +} + +MODULE +{ + name = ModuleWeapon + + shortName = SIDAM AA + + fireTransformName = fireTransform + muzzleTransformName = muzzleTransform1; muzzleTransform2; muzzleTransform3; muzzleTransform4 + hasDeployAnim = true + deployAnimName = DeployAnim + hasFireAnimation = true + fireAnimName = SUFireAnim; PLFireAnim; PUFireAnim; SLFireAnim + spinDownAnimation = false + + roundsPerMinute = 1220 + maxDeviation = 0.342 + maxEffectiveDistance = 2500 + maxTargetingRange = 5000 + + ammoName = 25x137Ammo + bulletType = 25x137mmBullet + requestResourceAmount = 1 + + hasRecoil = true + onlyFireInRange = true + bulletDrop = true + + weaponType = ballistic + shellScale = 1 + oneShotWorldParticles = true + + tracerLength = 60 + tracerDeltaFactor = 2.75 + + maxHeat = 3600 + heatPerShot = 65 + heatLoss = 750 + + fireSoundPath = BDArmory/Parts/Sidam/shot + overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat + oneShotSound = true + + //explosion + explModelPath = BDArmory/Models/explosion/30mmExplosion + explSoundPath = BDArmory/Sounds/subExplode + + +} +MODULE + { + name = ModulePartVariants + primaryColor = #ffffff + secondaryColor = #000000 + baseDisplayName = #autoLOC_8007122 + baseThemeName = Dark + useMultipleDragCubes = false + 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/shot.wav b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/shot.wav new file mode 100644 index 000000000..96197c702 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/shot.wav differ 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/SonarPod/BDAsonarPod1A.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg index 1d6874bb1..33ae0f120 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg @@ -1,4 +1,4 @@ -PART +PART { // Kerbal Space Program - Part Config // @@ -26,17 +26,17 @@ node_attach = 0.0, -0.0, 0, 0, 0, -1 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 2200 +cost = 1000 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = srf -title = BDA MK1 Sonar Pod -manufacturer = SM Armory -description = 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. - +title = #loc_BDArmory_part_BDAsonarPod1A_title //BDA MK1 Sonar Pod +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics //manufactured by SM Armory +description = #loc_BDArmory_part_BDAsonarPod1A_description //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. +tags = #loc_BDArmory_part_BDAsonarPod1A_tags // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 @@ -67,32 +67,33 @@ MODULE resourceDrain = 1.0 // change to higher values for more capable radars, e.g AESA // -- Section: Capabilities -- - omnidirectional = true // false: boresight scan radar - directionalFieldOfView = 90 // for omni and boresight + omnidirectional = false // false: boresight scan radar + directionalFieldOfView = 270 // for omni and boresight //boresightFOV = 10 // for boresight only scanRotationSpeed = 90 // degress per second lockRotationSpeed = 90 // only relevant if canLock //lockRotationAngle = 4 showDirectionWhileScan = true // can show target direction on radar screen. False: radar echos displayed as block only (no direction) - //multiLockFOV = 30 // only relevant if canLock + multiLockFOV = 10 // only relevant if canLock //lockAttemptFOV = 2 // only relevant if canLock maxLocks = 1 //how many targets can be locked/tracked simultaneously. only relevant if canLock 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! - radarGroundClutterFactor = 0.4 // how much is the radar efficiency reduced to by ground clutter/look-down? + radarGroundClutterFactor = 1.0 // how much is the radar 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.25 // Ground targets, especially ships, already have a massively larger RCS than fighters, hence // any ground clutter factor >0.25 is to be considered very good, making an efficient surface/horizon search radar. // values >1.0 are possible, meaning the radar is MORE efficient during look down than vs air targets. + sonarType = 1 //Active Sonar; 0 = Radar; 2 = passive sonar radarDetectionCurve { @@ -102,7 +103,7 @@ MODULE // of their rcs. This is achieved by using a minrcs value of zero, thus detecting everything. // key = distance rcs key = 0.0 0 - key = 5 0 //between 0 and 5 km the min cross section is 0, thus assured detection of everything + key = 3 0 //between 0 and 5 km the min cross section is 0, thus assured detection of everything key = 11 10 key = 20 55 //at 20km range a rcs of 55 m^2 can be detected key = 40 150 @@ -114,7 +115,7 @@ MODULE // ATTENTION: DO NOT USE an "assured locking range" here, as this would render lock-breaking // ECM-jammers & chaff completely ineffective!! key = 0.0 0 - key = 5 5 //needs higher rcs to lock at comparable range + key = 3 5 //needs higher rcs to lock at comparable range key = 11 20 key = 20 75 key = 40 200 // at max range only very large ("noisy") targets can be tracked diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg index b84672b35..447be7390 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg @@ -1,45 +1,47 @@ -PART +PART { name = StingRayBDATorpedo module = Part - mesh = model.mu author = Spanner + MODEL + { + model = BDArmory/Parts/StingRayBDA/StingRayBDATorpedo + scale = 0.75, 0.75, 1.0 + } rescaleFactor = 1 -NODE -{ - name = Node1 - transform = Node1 - size = 0 - method = FIXED_JOINT //FIXED_JOINT, HINGE_JOINT, LOCKED_JOINT, MERGED_PHYSICS or NO_PHYSICS -} -NODE -{ - name = Node2 - transform = Node2 - size = 0 - method = FIXED_JOINT //FIXED_JOINT, HINGE_JOINT, LOCKED_JOINT, MERGED_PHYSICS or NO_PHYSICS -} - - buoyancy = 0.2 + NODE + { + name = Node1 + transform = Node1 + size = 0 + method = FIXED_JOINT //FIXED_JOINT, HINGE_JOINT, LOCKED_JOINT, MERGED_PHYSICS or NO_PHYSICS + } + NODE + { + name = Node2 + transform = Node2 + size = 0 + method = FIXED_JOINT //FIXED_JOINT, HINGE_JOINT, LOCKED_JOINT, MERGED_PHYSICS or NO_PHYSICS + } + + buoyancy = 1.5 CoMOffset = 0.0, -0.0, 0.4 // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 470 + entryCost = 4000 + cost = 2000 category = none bdacategory = Torpedoes subcategory = 0 bulkheadProfiles = srf - - title = Sting Ray BDA LightWeight Torpedo - manufacturer = BD Armory // manufactured by SM Armory - - 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 wriiten + title = #loc_BDArmory_part_StingRayBDATorpedo_title //Sting Ray BDA LightWeight Torpedo + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics // manufactured by SM Armory + description = #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 // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,0,0,0,0 - + tags = #loc_BDArmory_part_StingRayBDATorpedo_tags // --- standard part parameters --- mass = 0.2655 dragModelType = default @@ -50,72 +52,65 @@ NODE breakingForce = 5000 breakingTorque = 5000 maxTemp = 3200 - tags = torpedo - - -MODULE -{ - name = MissileLauncher - - shortName = SRayBDA - - thrust = 47.8 //KN thrust during boost phase - cruiseThrust = 22.2 //thrust during cruise phase - cruiseDelay = 0 /// delay between boost ending and cruise starting, only used for large ship launched and air drop versions - dropTime = 8 //how many seconds after release until engine ignites extended drop time allows torpedo to sink to depth, too short a drop time will turn it into a very wayward missile - boostTime = 2 //seconds of boost phase - cruiseTime = 480 //seconds of cruise phase - //spoolEngine = true // N/A special cases only - - guidanceActive = true //missile has guidanceActive - - decoupleSpeed = 1.5 //f 0.1 steps max value 10 - decoupleForward = true // throws the torpedo out of the tube - isTubeLoaded = true - - optimumAirspeed = 45 - torpedo = true - waterImpactTolerance = 100 - - aero = true - liftArea = 0.0037 - steerMult = 2 - maxTorque = 45 - maxAoA = 30 - aeroSteerDamping = 5 - - missileType = torpedo // used by code to determine characteristics - homingType = AAM - targetingType = radar - - radarLOAL = true - activeRadarRange = 8000 - maxOffBoresight = 270 - lockedSensorFOV = 7 - maxTurnRateDPS = 40 //degrees per second - - proxyDetonate = false - DetonationDistance = 0 - - audioClipPath = BDArmory/Sounds/TorpPropFX - exhaustPrefabPath = BDArmory/FX/TorpWake - boostExhaustPrefabPath = BDArmory/FX/jetdriveWake - boostTransformName = boostTransform - boostExhaustTransformName = boostTransform - radarLOAL = true - minStaticLaunchRange = 200 - maxStaticLaunchRange = 8000 - - engageAir = false - engageMissile = false - engageGround = true - engageSLW = true -} - -MODULE -{ - name = BDExplosivePart - tntMass = 120 -} -} + MODULE + { + name = MissileLauncher + + shortName = Stingray + + thrust = 38 + cruiseThrust = 9 + dropTime = 5 + boostTime = 2 + cruiseTime = 240 + + guidanceActive = true + maxTurnRateDPS = 28 + + decoupleSpeed = 2 + decoupleForward = true + + missileType = torpedo + torpedo = true + homingType = SLW + targetingType = radar + activeRadarRange = 8000 + //chaffEffectivity = 2 + radarLOAL = true + seekerTimeout = 10 //search for targets for 10 sec + maxOffBoresight = 100 + lockedSensorFOV = 5 + DetonationDistance = 0 + optimumAirspeed = 35 + waterImpactTolerance = 110 + + maxAoA = 30 + + aero = true + liftArea = 0.0037 + steerMult = 2 + maxTorque = 25 + aeroSteerDamping = 10 + + minStaticLaunchRange = 200 + maxStaticLaunchRange = 8000 + + audioClipPath = BDArmory/Sounds/TorpPropFX + exhaustPrefabPath = BDArmory/FX/TorpWake + boostExhaustPrefabPath = BDArmory/FX/jetdriveWake + boostTransformName = boostTransform + boostExhaustTransformName = boostTransform + + engageAir = false + engageMissile = false + engageGround = true + engageSLW = true + } + + MODULE + { + name = BDExplosivePart + tntMass = 108 + } +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/adjustableRail/adjustableRail.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/adjustableRail/adjustableRail.cfg index 4b8a4208d..a5df345bf 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/adjustableRail/adjustableRail.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/adjustableRail/adjustableRail.cfg @@ -23,18 +23,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 100 + cost = 50 category = none bdacategory = Missile turrets subcategory = 0 bulkheadProfiles = srf - title = Adjustable Missile Rail - manufacturer = Bahamuto Dynamics - description = A rail for mounting missiles. + title = #loc_BDArmory_part_bahaAdjustableRail_title //Adjustable Missile Rail + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaAdjustableRail_description //A rail for mounting missiles. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,1,1,1 - + tags = #loc_BDArmory_part_bahaAdjustableRail_tags // --- standard part parameters --- mass = 0.01 dragModelType = default diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/OAI1.png b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/OAI1.png new file mode 100644 index 000000000..a28542075 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/OAI1.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/VAI1.png b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/VAI1.png new file mode 100644 index 000000000..bbfa103ea Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/VAI1.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/orbital_ai.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/orbital_ai.cfg new file mode 100644 index 000000000..c0ffd07ea --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/orbital_ai.cfg @@ -0,0 +1,59 @@ +PART +{ + // Kerbal Space Program - Part Config + // + // + + // --- general parameters --- + name = bdOrbitalAI + module = Part + author = Josue + MODEL + { + model = BDArmory/Parts/aiPilot/model + scale = 1.0, 1.0, 1.0 + texture = texture, BDArmory/Parts/aiPilot/OAI1 + } + rescaleFactor = 1 + + + // --- node definitions --- + node_attach = 0.0, -0.02069652, 0, 0, -1, 0, 0 + + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 2100 + cost = 0 // 600 + category = none + bdacategory = Control + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_bdOrbitalAI_title //AI Orbital Pilot + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdOrbitalAI_desc //Pilots spacecraft in orbit on combat missions without using your hands. Tune the values based on your ship's unique characteristics. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 + tags = #loc_BDArmory_part_bdOrbitalAI_tags + // --- standard part parameters --- + mass = 0 + dragModelType = none + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 60 + maxTemp = 3600 + + PhysicsSignificance = 1 + + + MODULE + { + name = BDModuleOrbitalAI + } + + DRAG_CUBE + { + none = True + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/part.cfg index 39b2131c0..0df641c4a 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/part.cfg @@ -1,52 +1,56 @@ PART { -// Kerbal Space Program - Part Config -// -// - -// --- general parameters --- -name = bdPilotAI -module = Part -author = BahamutoD - -// --- asset parameters --- -mesh = model.mu -rescaleFactor = 1 - - -// --- node definitions --- -node_attach = 0.0, -0.02069652, 0, 0, -1, 0, 0 - - -// --- editor parameters --- -TechRequired = precisionEngineering -entryCost = 2100 -cost = 0 // 600 -category = none -bdacategory = Control -subcategory = 0 -bulkheadProfiles = srf -title = AI Pilot Flight Computer -manufacturer = Bahamuto Dynamics -description = 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) -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - -// --- standard part parameters --- -mass = 0.001 -dragModelType = default -maximum_drag = 0.02 -minimum_drag = 0.02 -angularDrag = 2 -crashTolerance = 60 -maxTemp = 3600 - -PhysicsSignificance = 1 - - -MODULE -{ - name = BDModulePilotAI -} - + // Kerbal Space Program - Part Config + // + // + + // --- general parameters --- + name = bdPilotAI + module = Part + author = BahamutoD + + // --- asset parameters --- + mesh = model.mu + rescaleFactor = 1 + + + // --- node definitions --- + node_attach = 0.0, -0.02069652, 0, 0, -1, 0, 0 + + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 2100 + cost = 0 // 600 + category = none + bdacategory = Control + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_bdPilotAI_title //AI Pilot Flight Computer + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdPilotAI_description //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) + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 + tags = #loc_BDArmory_part_bdpilotAI_tags + // --- standard part parameters --- + mass = 0 + dragModelType = none + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 60 + maxTemp = 3600 + + PhysicsSignificance = 1 + + + MODULE + { + name = BDModulePilotAI + } + + DRAG_CUBE + { + none = True + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/surface_ai.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/surface_ai.cfg index cd5031da2..7d2f517ca 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/surface_ai.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/surface_ai.cfg @@ -1,55 +1,59 @@ PART { -// Kerbal Space Program - Part Config -// -// - -// --- general parameters --- -name = bdShipAI -module = Part -author = Spanner -MODEL -{ - model = BDArmory/Parts/aiPilot/model - scale = 1.0, 1.0, 1.0 - texture = texture, BDArmory/Parts/aiPilot/SAI1 -} -rescaleFactor = 1 - - -// --- node definitions --- -node_attach = 0.0, -0.02069652, 0, 0, -1, 0, 0 - - -// --- editor parameters --- -TechRequired = precisionEngineering -entryCost = 2100 -cost = 0 // 600 -category = none -bdacategory = Control -subcategory = 0 -bulkheadProfiles = srf -title = AI Surface Operation Driver -manufacturer = Bahamuto Dynamics -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) -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - -// --- standard part parameters --- -mass = 0.001 -dragModelType = default -maximum_drag = 0.02 -minimum_drag = 0.02 -angularDrag = 2 -crashTolerance = 60 -maxTemp = 3600 - -PhysicsSignificance = 1 - - -MODULE -{ - name = BDModuleSurfaceAI -} - + // Kerbal Space Program - Part Config + // + // + + // --- general parameters --- + name = bdShipAI + module = Part + author = Spanner + MODEL + { + model = BDArmory/Parts/aiPilot/model + scale = 1.0, 1.0, 1.0 + texture = texture, BDArmory/Parts/aiPilot/SAI1 + } + rescaleFactor = 1 + + + // --- node definitions --- + node_attach = 0.0, -0.02069652, 0, 0, -1, 0, 0 + + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 2100 + cost = 0 // 600 + category = none + bdacategory = Control + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_bdDriverAI_title //AI Surface Operation Driver + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #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) + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 + tags = #loc_BDArmory_part_bdDriverAI_tags + // --- standard part parameters --- + mass = 0 + dragModelType = none + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 60 + maxTemp = 3600 + + PhysicsSignificance = 1 + + + MODULE + { + name = BDModuleSurfaceAI + } + + DRAG_CUBE + { + none = True + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/vtol_ai.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/vtol_ai.cfg new file mode 100644 index 000000000..fdf8a6de6 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aiPilot/vtol_ai.cfg @@ -0,0 +1,59 @@ +PART +{ + // Kerbal Space Program - Part Config + // + // + + // --- general parameters --- + name = bdVTOLAI + module = Part + author = Josue + MODEL + { + model = BDArmory/Parts/aiPilot/model + scale = 1.0, 1.0, 1.0 + texture = texture, BDArmory/Parts/aiPilot/VAI1 + } + rescaleFactor = 1 + + + // --- node definitions --- + node_attach = 0.0, -0.02069652, 0, 0, -1, 0, 0 + + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 2100 + cost = 0 // 600 + category = none + bdacategory = Control + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_bdVTOLAI_title //AI Vertical Takeoff and Landing Pilot + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdVTOLAI_desc //Drives your VTOL craft (i.e. helicopters, VTOL jets, airships) on combat and patrol missions 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) + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 + tags = #loc_BDArmory_part_bdVTOLAI_tags + // --- standard part parameters --- + mass = 0 + dragModelType = none + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 60 + maxTemp = 3600 + + PhysicsSignificance = 1 + + + MODULE + { + name = BDModuleVTOLAI + } + + DRAG_CUBE + { + none = True + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg index f159bd513..db715c465 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg @@ -21,18 +21,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 2000 + entryCost = 5000 + cost = 2500 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = AIM-120 AMRAAM Missile - manufacturer = Bahamuto Dynamics - description = Medium range radar guided homing missile. + title = #loc_BDArmory_part_bahaAim120_title //AIM-120 AMRAAM Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaAim120_description //Medium range radar guided homing missile. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaAim120_tags // --- standard part parameters --- mass = 0.152 dragModelType = default @@ -55,10 +55,13 @@ PART boostTime = 2.2 //seconds of boost phase cruiseTime = 120 //seconds of cruise phase guidanceActive = true //missile has guidanceActive - maxTurnRateDPS = 35 //degrees per second + maxTurnRateDPS = 35 //thrust+maxTurnRateDPS is the value used by the AI to prioritize missiles over each other. maxTurnRateDPS, in degrees per second, ONLY affects flight of RCS missiles like the HEKV1 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,26 +69,43 @@ PART boostTransformName = boostTransform boostExhaustTransformName = boostTransform - optimumAirspeed = 1372 + 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 - liftArea = 0.0020 - steerMult = 8 - maxTorque = 60 - maxAoA = 30 + aero = true //Missile has aerodynamics + liftArea = 0.0020 //increases lift which helps with manuevering and turning, but also increases drag + // dragArea = 0.0020 // Option to set reference area for drag independently from the lift area, otherwise defaults to liftArea + steerMult = 8 //big number = steer harder... + maxTorque = 60 //ammount of torque that will be applied to the missile for turning + 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 - targetingType = radar - activeRadarRange = 6000 - maxOffBoresight = 120 - lockedSensorFOV = 7 + homingType = aam //air to air missile different types are AAM, AGM, Cruise, Ballistic, ProNav, and AugProNav + + terminalHoming = true + terminalHomingType = ProNav //switches to ProNav at final engagement for a more accurate homing solution at expense of energy expenditure + pronavGain = 3 // ProNav gain constant, only used with ProNav and AugProNav homing types (default is 3) + terminalHomingRange = 8000 + + targetingType = inertial + gpsUpdates = 1 // How often GPS or inertial guidance should update the target's position (in seconds) - minStaticLaunchRange = 500 - maxStaticLaunchRange = 25000 + terminalManeuvering = true + terminalGuidanceShouldActivate = true // Should the missile try to active it's terminalGuidanceType? + terminalGuidanceType = radar // Guidance type to use when the missile approaches the target + terminalGuidanceDistance = 8000 // Range at which the missile should switch to terminalGuidanceType + activeRadarRange = 9000 // Active radar range (this should be greater than terminalGuidanceDistance to ensure the target is spotted) - radarLOAL = true + 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 + seekerTimeout = 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 @@ -96,7 +116,7 @@ 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 } - - } 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 b230701f5..61b7dc801 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 @@ -24,18 +25,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 2000 + entryCost = 16000 + cost = 8000 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = AMRAAM EMP Missile - manufacturer = Bahamuto Dynamics - 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. + title = #loc_BDArmory_part_bahaEMP120_title //AMRAAM EMP Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaEMP120_tags // --- standard part parameters --- mass = 0.152 dragModelType = default @@ -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 @@ -79,8 +80,20 @@ PART missileType = missile homingType = aam - targetingType = radar - activeRadarRange = 6000 + + terminalHoming = true + terminalHomingType = ProNav + pronavGain = 3 + terminalHomingRange = 8000 + + targetingType = inertial + gpsUpdates = 1 + + terminalManeuvering = true + terminalGuidanceShouldActivate = true + terminalGuidanceType = radar + terminalGuidanceDistance = 8000 + activeRadarRange = 9000 maxOffBoresight = 120 lockedSensorFOV = 7 @@ -88,7 +101,7 @@ PART maxStaticLaunchRange = 25000 radarLOAL = true - + seekerTimeout = 5 engageAir = true engageMissile = false engageGround = true @@ -105,5 +118,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 905b3f4db..86e4d7a34 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/awacsRadar.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/awacsRadar.cfg @@ -10,7 +10,7 @@ module = Part author = BahamutoD // --- asset parameters --- -mesh = radar2.mu +mesh = model.mu rescaleFactor = 1 @@ -20,18 +20,18 @@ node_attach = 0.0, 0, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 25000 +cost = 10000 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = srf -title = AWACS Detection Radar -manufacturer = Bahamuto Dynamics -description = A large radar capable of detecting objects from a long distance. This radar does NOT have the capability of tracking or locking targets. +title = #loc_BDArmory_part_awacsRadar_title //AWACS Detection Radar +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_awacsRadar_description //A large radar capable of detecting objects from a long distance. This radar does NOT have the capability of tracking or locking targets. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_awacsRadar_tags // --- standard part parameters --- mass = 1.45 dragModelType = default @@ -42,6 +42,47 @@ 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 + { + colliderLeft = true + colliderRight = true + legs = true + Dish = true + DishCollider = true + LeglessDish = false + LeglessDishCollider = false + } + } + VARIANT + { + name = Legless + displayName = #loc_BDArmory_part_AWACS_Legless + primaryColor = #ffffff + secondaryColor = #ffffff + GAMEOBJECTS + { + colliderLeft = false + colliderRight = false + legs = false + Dish = false + DishCollider = false + LeglessDish = true + LeglessDishCollider = true + } + } +} + MODULE { name = ModuleRadar @@ -74,7 +115,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..28e7e331f 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/bammGuidance/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/bammGuidance/part.cfg index a037c3605..b50abd65d 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/bammGuidance/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/bammGuidance/part.cfg @@ -1,71 +1,73 @@ PART { - - -// --- general parameters --- -name = bdammGuidanceModule -module = Part -author = BahamutoD - -// --- asset parameters --- -mesh = model.mu -rescaleFactor = 1 - -// --- node definitions --- -// definition format is Position X, Position Y, Position Z, Up X, Up Y, Up Z - -node_attach = 0.0, -0.0146915, 0, 0, -1, 0, 0 - - - -// --- FX definitions --- - - -// --- editor parameters --- -TechRequired = advAerodynamics -entryCost = 6800 -cost = 180 -category = none -bdacategory = Control -subcategory = 0 -bulkheadProfiles = srf -title = Modular Missile Guidance (EXPERIMENTAL) -manufacturer = Bahamuto Dynamics -description = 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) - -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - -// --- standard part parameters --- -mass = 0.001 -dragModelType = default -maximum_drag = 0.02 -minimum_drag = 0.02 -angularDrag = .25 -crashTolerance = 60 -maxTemp = 3400 - -PhysicsSignificance = 1 - -MODULE -{ - name = BDModularGuidance - ForwardTransform = ForwardNegative - UpTransform = RightPositive -} - - -RESOURCE -{ - name = ElectricCharge - amount = 10 - maxAmount = 10 -} - -MODULE -{ - name = ModuleSAS - SASServiceLevel = 3 -} - + // --- general parameters --- + name = bdammGuidanceModule + module = Part + author = BahamutoD + + // --- asset parameters --- + mesh = model.mu + rescaleFactor = 1 + + // --- node definitions --- + // definition format is Position X, Position Y, Position Z, Up X, Up Y, Up Z + + node_attach = 0.0, -0.0146915, 0, 0, -1, 0, 0 + + + + // --- FX definitions --- + + + // --- editor parameters --- + TechRequired = advAerodynamics + entryCost = 1000 + cost = 0 + category = none + bdacategory = Control + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_bdammGuidanceModule_title //Modular Missile Guidance (EXPERIMENTAL) + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdammGuidanceModule_description //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) + + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 + tags = #loc_BDArmory_part_bdammGuidanceModule_tags + // --- standard part parameters --- + mass = 0 + dragModelType = none + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 60 + maxTemp = 3400 + + PhysicsSignificance = 1 + + MODULE + { + name = BDModularGuidance + ForwardTransform = ForwardNegative + UpTransform = RightPositive + } + + + RESOURCE + { + name = ElectricCharge + amount = 10 + maxAmount = 10 + } + + MODULE + { + name = ModuleSAS + SASServiceLevel = 3 + } + + DRAG_CUBE + { + none = True + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg index 5e62d8fdf..c1d459323 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.06105912, 0.05663621, 0, -1, 0, 1 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 950 +entryCost = 350 +cost = 200 category = none bdacategory = Guns subcategory = 0 bulkheadProfiles = srf -title = Browning Heavy Machine Gun (AN/M3) -manufacturer = Bahamuto Dynamics -description = An old fixed .50 cal machine gun 50cal ammo +title = #loc_BDArmory_part_bahaBrowningAnm2_title //Browning Heavy Machine Gun (AN/M3) +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaBrowningAnm2_description //An old fixed .50 cal machine gun 50cal ammo // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaBrowningAnm2_tags // --- standard part parameters --- mass = 0.04 dragModelType = default @@ -54,12 +54,12 @@ MODULE hasFireAnimation = false roundsPerMinute = 1150 - maxDeviation = 0.22 + maxDeviation = 0.177 //~4mrad maxEffectiveDistance = 2800 maxTargetingRange = 4000 weaponType = ballistic - bulletType = 12.7mmBullet + bulletType = 12.7mmBullet; 12.7mmAPIBullet ammoName = 50CalAmmo requestResourceAmount = 1 @@ -68,19 +68,14 @@ MODULE hasRecoil = true onlyFireInRange = true bulletDrop = true - - projectileColor = 255, 50, 0, 160 //RGBA 0-255 - startColor = 255, 105, 25, 120 - fadeColor = true - tracerStartWidth = 0.18 - tracerEndWidth = 0.16 + tracerLength = 0 tracerDeltaFactor = 2.75 - tracerInterval = 30 - nonTracerWidth = 0.035 + tracerInterval = 5 + autoProxyTrackRange = 1200 - fireSoundPath = BDArmory/Parts/browninganm2/Sounds/fire + fireSoundPath = BDArmory/Parts/browninganm2/sounds/fire overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat oneShotSound = true @@ -89,8 +84,6 @@ MODULE heatLoss = 825 //explosion - airDetonation = false - airDetonationTiming = false explModelPath = BDArmory/Models/explosion/30mmExplosion explSoundPath = BDArmory/Sounds/subExplode } 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/clusterBomb/clusterBomb.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/clusterBomb/clusterBomb.cfg index f61e11b6f..6b2245845 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/clusterBomb/clusterBomb.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/clusterBomb/clusterBomb.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.25, 0, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 150 +entryCost = 350 +cost = 200 category = none bdacategory = Bombs subcategory = 0 bulkheadProfiles = srf -title = CBU-87 Cluster Bomb -manufacturer = Bahamuto Dynamics -description = This bomb splits open and deploys many small bomblets at a certain altitude. +title = #loc_BDArmory_part_bahaClusterBomb_title //CBU-87 Cluster Bomb +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaClusterBomb_description //This bomb splits open and deploys many small bomblets at a certain altitude. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaClusterBomb_tags // --- standard part parameters --- mass = 0.467 dragModelType = none diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/chaffDispenser.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/chaffDispenser.cfg index cca9bbc18..4a62cfa8c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/chaffDispenser.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/chaffDispenser.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.11, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 +entryCost = 800 cost = 600 category = none bdacategory = Countermeasures subcategory = 0 bulkheadProfiles = srf -title = Chaff Dispenser -manufacturer = Bahamuto Dynamics -description = Drops chaff for confusing or breaking radar locks. +title = #loc_BDArmory_part_bahaChaffPod_title //Chaff Dispenser +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaChaffPod_description //Drops chaff for confusing or breaking radar locks. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaChaffPod_tags // --- standard part parameters --- mass = 0.001 dragModelType = default diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/flareDispenser.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/flareDispenser.cfg index d64185f89..c6d375b6d 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/flareDispenser.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/cmDropper/flareDispenser.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.11, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 +entryCost = 800 cost = 600 category = none bdacategory = Countermeasures subcategory = 0 bulkheadProfiles = srf -title = Flare Dispenser -manufacturer = Bahamuto Dynamics -description = Drops flares for confusing heat-seeking missiles. +title = #loc_BDArmory_part_bahaCmPod_title //Flare Dispenser +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaCmPod_description //Drops flares for confusing heat-seeking missiles. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaCmPod_tags // --- standard part parameters --- mass = 0.001 dragModelType = default @@ -46,6 +46,7 @@ MODULE { name = CMDropper countermeasureType = flare + ejectTransformName = cmTransform ejectVelocity = 40 } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/Bubbler.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/Bubbler.cfg new file mode 100644 index 000000000..ec7e73560 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/Bubbler.cfg @@ -0,0 +1,67 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaBubblePod +module = Part +author = BahamutoD + +// --- asset parameters --- +MODEL +{ + model = BDArmory/Parts/decoyDropper/model + texture = CMBubbleScreen, BDArmory/Parts/decoyDropper/CMBubbleScreen +} + +rescaleFactor = 1 + + +// --- node definitions --- +node_attach = 0.0, -0.11, 0, 0, -1, 0, 0 + + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 800 +cost = 600 +category = none +bdacategory = Countermeasures +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_bahaSBTPod_title //Decoy Launcher +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaSBTPod_description //Launches bubble curtain countermeausres to degrade enemy active sonar. +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 0,1,0,0,1 +tags = #loc_BDArmory_part_bahaDecoyPod_tags +// --- standard part parameters --- +mass = 0.001 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 7 +maxTemp = 3600 + + +MODULE +{ + name = CMDropper + countermeasureType = bubble + ejectVelocity = 10 +} + +RESOURCE +{ + name = CMBubbleCurtain + amount = 25 + maxAmount = 25 +} +DRAG_CUBE +{ + cube = Default,0.06035,0.47415,0.1406,0.06035,0.47415,0.1406,0.06429,0.48925,0.1149,0.06429,0.42815,0.2935,0.05487,0.4389,0.1835,0.05487,0.4394,0.1835, 0,-0.1847,3.198E-08, 0.2372,0.2509,0.2774 +} +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/CMAcousticDecoy.png b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/CMAcousticDecoy.png new file mode 100644 index 000000000..1027d268c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/CMAcousticDecoy.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/CMBubbleScreen.png b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/CMBubbleScreen.png new file mode 100644 index 000000000..591baca93 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/CMBubbleScreen.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/Spoofer.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/Spoofer.cfg new file mode 100644 index 000000000..cb72a28a7 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/Spoofer.cfg @@ -0,0 +1,70 @@ + +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaDecoyPod +module = Part +author = BahamutoD + +// --- asset parameters --- + +MODEL +{ + model = BDArmory/Parts/decoyDropper/model + texture = CMAcousticDecoy, BDArmory/Parts/decoyDropper/CMAcousticDecoy +} + + +rescaleFactor = 1 + + +// --- node definitions --- +node_attach = 0.0, -0.11, 0, 0, -1, 0, 0 + + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 800 +cost = 600 +category = none +bdacategory = Countermeasures +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_bahaDecoyPod_title //Decoy Launcher +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaDecoyPod_description //Drops flares for confusing heat-seeking missiles. +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 0,1,0,0,1 +tags = #loc_BDArmory_part_bahaDecoyPod_tags +// --- standard part parameters --- +mass = 0.001 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 7 +maxTemp = 3600 + + +MODULE +{ + name = CMDropper + countermeasureType = decoy + ejectVelocity = 10 +} + +RESOURCE +{ + name = CMDecoy + amount = 25 + maxAmount = 25 +} +DRAG_CUBE +{ + cube = Default,0.06035,0.47415,0.1406,0.06035,0.47415,0.1406,0.06429,0.48925,0.1149,0.06429,0.42815,0.2935,0.05487,0.4389,0.1835,0.05487,0.4394,0.1835, 0,-0.1847,3.198E-08, 0.2372,0.2509,0.2774 +} +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/model.mu new file mode 100644 index 000000000..aa46fba99 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/part.cfg new file mode 100644 index 000000000..3c79bb3c5 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/decoyDropper/part.cfg @@ -0,0 +1,71 @@ +RESOURCE_DEFINITION +{ + name = CMDecoy + density = .002 + flowMode = ALL_VESSEL + transfer = PUMP + isTweakable = true +} + +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaDecoyPod +module = Part +author = BahamutoD + +// --- asset parameters --- +mesh = model.mu +rescaleFactor = 1 + + +// --- node definitions --- +node_attach = 0.0, -0.11, 0, 0, -1, 0, 0 + + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 800 +cost = 600 +category = none +bdacategory = Countermeasures +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_bahaDecoyPod_title //Decoy Launcher +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaDecoyPod_description //Drops flares for confusing heat-seeking missiles. +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 0,1,0,0,1 +tags = #loc_BDArmory_part_bahaDecoyPod_tags +// --- standard part parameters --- +mass = 0.001 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 2 +crashTolerance = 7 +maxTemp = 3600 + + +MODULE +{ + name = CMDropper + countermeasureType = decoy + ejectVelocity = 10 +} + +RESOURCE +{ + name = CMDecoy + amount = 25 + maxAmount = 25 +} +DRAG_CUBE +{ + cube = Default,0.06035,0.47415,0.1406,0.06035,0.47415,0.1406,0.06429,0.48925,0.1149,0.06429,0.42815,0.2935,0.05487,0.4389,0.1835,0.05487,0.4394,0.1835, 0,-0.1847,3.198E-08, 0.2372,0.2509,0.2774 +} +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg index a016241b9..19abe7bc4 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.2892764, -0.1, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 3500 +cost = 1800 category = none bdacategory = Countermeasures subcategory = 0 bulkheadProfiles = srf -title = AN/ALQ-131 ECM Jammer -manufacturer = Bahamuto Dynamics -description = This electronic device makes it harder for radars to lock onto your vehicle, and increases your chances of breaking the lock. It drastically increases your detectability when turned on, though. +title = #loc_BDArmory_part_bahaECMJammer_title //AN/ALQ-131 ECM Jammer +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaECMJammer_description //This electronic device makes it harder for radars to lock onto your vehicle, and increases your chances of breaking the lock. It drastically increases your detectability when turned on, though. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaECMJammer_tags // --- standard part parameters --- mass = 0.3 dragModelType = default @@ -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 = 500 // factor (in relation to a vessels base radar cross section) how strong the lockbreaking effect is + 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 41d4e16e0..98cd7951f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/gau-8/gau8.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/gau-8/gau8.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 4500 + cost = 4000 category = none bdacategory = Guns subcategory = 0 bulkheadProfiles = srf - title = GAU-8 30x173mm Cannon - manufacturer = Bahamuto Dynamics - description = A 7 barrel 30mm rotary cannon. + title = #loc_BDArmory_part_bahaGau-8_title //GAU-8 30x173mm Cannon + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaGau-8_description //A 7 barrel 30mm rotary cannon. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - + tags = #loc_BDArmory_part_bahaGau-8_tags // --- standard part parameters --- mass = 0.55 dragModelType = default @@ -55,15 +55,15 @@ PART hasFireAnimation = true fireAnimName = fireAnim spinDownAnimation = true - + SpoolUpTime = 0.3 roundsPerMinute = 3900 - maxDeviation = 0.45 + maxDeviation = 0.223 //5mrad, 80% maxEffectiveDistance = 4000 maxTargetingRange = 5000 weaponType = ballistic ammoName = 30x173Ammo - bulletType = 30x173HEBullet + bulletType = 30x173HEBullet; 30x173Bullet requestResourceAmount = 1 @@ -72,16 +72,9 @@ PART bulletDrop = true useRippleFire = false - projectileColor = 255, 70, 0, 128//RGBA 0-255 - startColor = 255, 90, 0, 32 - fadeColor = true - - tracerStartWidth = 0.10 - tracerEndWidth = 0.10 tracerLength = 0 tracerDeltaFactor = 2.75 tracerInterval = 3 - nonTracerWidth = 0.035 maxHeat = 3600 heatPerShot = 56 @@ -92,8 +85,6 @@ PART oneShotSound = false //explosion - airDetonation = false - airDetonationTiming = false explModelPath = BDArmory/Models/explosion/30mmExplosion explSoundPath = BDArmory/Sounds/subExplode diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg index 407396c03..da05b6e85 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 11000 + cost = 9000 category = none bdacategory = Gun turrets subcategory = 0 - bulkheadProfiles = srf - title = Goalkeeper CIWS - manufacturer = Bahamuto Dynamics - description = 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. + bulkheadProfiles = size2, srf + title = #loc_BDArmory_part_bahaGoalKeeper_title //Goalkeeper CIWS + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaGoalKeeper_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaGoalKeeper_tags // --- standard part parameters --- mass = 4 dragModelType = default @@ -82,9 +82,9 @@ PART hasFireAnimation = true fireAnimName = fireAnimation2 spinDownAnimation = true - + SpoolUpTime = 0.3 roundsPerMinute = 4200 - maxDeviation = 0.50 + maxDeviation = 0.262 //~6mrad maxEffectiveDistance = 4000 maxTargetingRange = 5000 @@ -97,17 +97,13 @@ PART bulletDrop = true weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true - projectileColor = 255, 20, 0, 160//RGBA 0-255 - startColor = 255, 30, 0, 24 - fadeColor = true - - tracerStartWidth = 0.18 - tracerEndWidth = 0.18 tracerLength = 0 tracerDeltaFactor = 2.75 tracerInterval = 2 - nonTracerWidth = 0.065 maxHeat = 3600 heatPerShot = 36 @@ -120,8 +116,6 @@ PART oneShotSound = false //explosion - airDetonation = true - airDetonationTiming = false explModelPath = BDArmory/Models/explosion/30mmExplosion explSoundPath = BDArmory/Sounds/subExplode @@ -160,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 5f09c95bb..0cafceb45 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar1.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, 0, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 8000 +cost = 4000 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = srf -title = TWS Locking Radar -manufacturer = Bahamuto Dynamics -description = 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. +title = #loc_BDArmory_part_scanLockRadar1_title //TWS Locking Radar +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_scanLockRadar1_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_scanLockRadar1_tags // --- standard part parameters --- mass = 1 dragModelType = default @@ -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! @@ -86,7 +86,10 @@ MODULE // Ground targets, especially ships, already have a massively larger RCS than fighters, hence // any ground clutter factor >0.25 is to be considered very good, making an efficient surface/horizon search radar. // values >1.0 are possible, meaning the radar is MORE efficient during look down than vs air targets. - + radarChaffClutterFactor = 1.0 //Factor defining how effective the radar is at compensating for enemy chaff, relevant for guiding SARH missiles and radar-targeted gun/rocket/laser turrets + // 0.0 = radar completely spoofed by chaff, chaff 100% effective + // 1.0 = no decrease in signal position/strength, chaff completely filtered out + // Default if unset: 1 (no change from legacy radar behavior). Values above 1 will be clamped to 1. radarDetectionCurve { // floatcurve to define at what range (km) which minimum cross section (m^2) can be detected. diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg index 7c38add75..0391ced36 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, 0, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 12000 +cost = 5000 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = srf -title = Large Detection Radar -manufacturer = Bahamuto Dynamics -description = 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. +title = #loc_BDArmory_part_scanLargeRadar_title //Large Detection Radar +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_scanLargeRadar_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_scanLargeRadar_tags // --- standard part parameters --- mass = 1.45 dragModelType = default @@ -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/h70Launcher/h70Launcher.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Launcher.cfg index ce144eab3..6c75bb2fb 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Launcher.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Launcher.cfg @@ -20,20 +20,20 @@ node_stack_top = 0.0, 0.1917292, 0, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 650 +entryCost = 500 +cost = 300 category = none bdacategory = Rocket pods subcategory = 0 bulkheadProfiles = srf -title = Hydra-70 Rocket Pod -manufacturer = Bahamuto Dynamics -description = Holds and fires 19 unguided Hydra-70 rockets. +title = #loc_BDArmory_part_bahaH70Launcher_title //Hydra-70 Rocket Pod +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaH70Launcher_description //Holds and fires 19 unguided Hydra-70 rockets. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tag = #loc_BDArmory_part_bahaH70Launcher_tags // --- standard part parameters --- -mass = 0.016 +mass = 0.036 dragModelType = default maximum_drag = 0.01 minimum_drag = 0.01 @@ -57,11 +57,12 @@ MODULE maxTargetingRange = 8000 weaponType = rocket - bulletType = Hydra70 // ; Scylla70 <- FIXME this one isn't defined + bulletType = Hydra70; H70M247; H70Mk67; ammoName = Hydra70Rocket requestResourceAmount = 1 rocketPod = true + externalAmmo = false onlyFireInRange = true @@ -83,6 +84,10 @@ RESOURCE } - +MODULE +{ + name = ModuleCASE + CASELevel = 2 +} } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Rocket/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Rocket/model.mu index 1ffc36f46..a37b6f6b1 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Rocket/model.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/h70Launcher/h70Rocket/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg index 0b6ca6572..51d20280e 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 650 + entryCost = 1200 + cost = 600 category = none bdacategory = Rocket turrets subcategory = 0 bulkheadProfiles = srf - title = Hydra-70 Rocket Turret - manufacturer = Bahamuto Dynamics - description = Turret pod that holds and fires 32 unguided Hydra-70 rockets. + title = #loc_BDArmory_part_bahaH70Turret_title //Hydra-70 Rocket Turret + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaH70Turret_description //Turret pod that holds and fires 32 unguided Hydra-70 rockets. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaH70Turret_tags // --- standard part parameters --- mass = 0.416 dragModelType = default @@ -58,11 +58,12 @@ PART maxTargetingRange = 8000 weaponType = rocket - bulletType = Hydra70 + bulletType = Hydra70; H70M247; H70Mk67; ammoName = Hydra70Rocket requestResourceAmount = 1 rocketPod = true + externalAmmo = false onlyFireInRange = true @@ -104,18 +105,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 @@ -124,6 +124,10 @@ PART amount = 32 maxAmount = 32 } - + MODULE + { + name = ModuleCASE + CASELevel = 2 + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/harm/harm.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/harm/harm.cfg index cf8baa64a..5742cbcf3 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/harm/harm.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/harm/harm.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 568 + entryCost = 5000 + cost = 2500 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = AGM-88 HARM Missile - manufacturer = Bahamuto Dynamics - description = High-speed anti-radiation missile. This missile will home in on radar sources detected by the Radar Warning Receiver. + title = #loc_BDArmory_part_bahaHarm_title //AGM-88 HARM Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaHarm_description //High-speed anti-radiation missile. This missile will home in on radar sources detected by the Radar Warning Receiver. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaHarm_tags // --- standard part parameters --- mass = 0.355 dragModelType = default @@ -43,65 +43,74 @@ PART MODULE -{ - name = MissileLauncher - shortName = HARM - - thrust = 60 - cruiseThrust = 15 - dropTime = 0.5 - boostTime = 2.5 - cruiseTime = 60 - - guidanceActive = true - maxTurnRateDPS = 35 - - CruiseSpeed = 633.33 - - DetonationDistance = 0 - - decoupleSpeed = 5 - decoupleForward = false - - audioClipPath = BDArmory/Sounds/rocketLoop - exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust - boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust - - boostTransformName = boostTransform - boostExhaustTransformName = boostTransform - optimumAirspeed = 633.33 - - homingType = AGM - targetingType = antirad - terminalManeuvering = false - terminalGuidanceType = antirad - terminalGuidanceDistance = 30000 - - maxOffBoresight = 65 - lockedSensorFOV = 7 - - maxAoA = 40 - - aero = true - liftArea = 0.003 - steerMult = 8 - maxTorque = 55 - maxAoA = 40 - agmDescentRatio = 1.25 - - minStaticLaunchRange = 800 - maxStaticLaunchRange = 30000 + { + name = MissileLauncher + shortName = HARM + + thrust = 60 + cruiseThrust = 15 + dropTime = 0.5 + boostTime = 2.5 + cruiseTime = 60 + + guidanceActive = true + maxTurnRateDPS = 35 + + CruiseSpeed = 633.33 + + DetonationDistance = 0 + + decoupleSpeed = 5 + decoupleForward = false + + audioClipPath = BDArmory/Sounds/rocketLoop + exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust + boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust + + boostTransformName = boostTransform + boostExhaustTransformName = boostTransform + optimumAirspeed = 633.33 + + homingType = AGM + targetingType = antirad + terminalGuidanceShouldActivate = false + terminalGuidanceType = antirad + terminalGuidanceDistance = 30000 + + antiradTargetTypes = 0,5 // Set to all the radar rwrThreatTypes this missile should target (separated by commas), will default to 0,5 if not set + // RWR Threat Types: + // 0 = SAM site radar + // 1 = Fighter radar (airborne) + // 2 = AWACS radar (airborne) + // 3, 4 = ACTIVE MISSILE (DO NOT USE UNLESS YOU KNOW WHAT YOU'RE DOING!) + // 5 = Detection radar (ground/ship based) + // 6 = SONAR (ship/submarine based) + // 7, 8 = ACTIVE TORPEDO (DO NOT USE UNLESS YOU KNOW WHAT YOU'RE DOING!) + + maxOffBoresight = 65 + lockedSensorFOV = 7 + + maxAoA = 40 + + aero = true + liftArea = 0.003 + steerMult = 8 + maxTorque = 55 + maxAoA = 40 + agmDescentRatio = 1.25 + + minStaticLaunchRange = 800 + maxStaticLaunchRange = 30000 + + engageAir = false + engageMissile = false + engageGround = true + engageSLW = false + } - engageAir = false - engageMissile = false - engageGround = true - engageSLW = false -} MODULE { name = BDExplosivePart tntMass = 70 } - - } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/hekv1/hekv1.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/hekv1/hekv1.cfg index c140792f2..0a98ea3c7 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/hekv1/hekv1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/hekv1/hekv1.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.1232686, 0.0, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 +entryCost = 8000 cost = 4000 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf -title = HE-KV-1 Missile -manufacturer = Bahamuto Dynamics -description = 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. +title = #loc_BDArmory_part_bahaHEKV1_title //HE-KV-1 Missile +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaHEKV1_description //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. 3 km/s delta-V. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaHEKV1_tags // --- standard part parameters --- mass = 0.14 dragModelType = default @@ -47,35 +47,42 @@ MODULE name = MissileLauncher shortName = HEKV - thrust = 12 - cruiseThrust = 0 + thrust = 10.8 + cruiseThrust = 5.2 dropTime = 1 - boostTime = 42 - cruiseTime = 0 + boostTime = 12 + cruiseTime = 30 + + useFuel = true // mass decreases as fuel burns, missile acceleration is non-constant + boosterMass = 0 // no independent booster, empty mass is 0.058 (including warhead ~40kg high explosive equivalent to 80kg TNT) + boosterFuelMass = 0.037 + cruiseFuelMass = 0.045 - maxTurnRateDPS = 25 //degrees per second + maxTurnRateDPS = 19 //degrees per second decoupleSpeed = 5 decoupleForward = true hasRCS = true - rcsThrust = 20 - + rcsThrust = 5.5 + + dragArea = 0.0010 // very low drag, but missile is still affected by drag in-atmo + audioClipPath = BDArmory/Sounds/jet guidanceActive = true //missile has guidanceActive - homingType = RCS + homingType = orbital // Replaces homingType = RCS targetingType = radar missileType = missile DetonationDistance = 1 activeRadarRange = 40000 - maxOffBoresight = 360 + maxOffBoresight = 180 lockedSensorFOV = 15 radarLOAL = true minStaticLaunchRange = 500 - maxStaticLaunchRange = 100000 + maxStaticLaunchRange = 60000 engageAir = true engageMissile = true @@ -86,7 +93,7 @@ MODULE MODULE { name = BDExplosivePart - tntMass = 120 + tntMass = 80 } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg index 1d68eaf8c..ec69795cf 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 220 + entryCost = 2500 + cost = 1200 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = AGM-114R Hellfire II - manufacturer = Bahamuto Dynamics - description = Small, quick, laser guided homing missile. + title = #loc_BDArmory_part_bahaAGM-114_title //AGM-114R Hellfire II + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaAGM-114_description //Small, quick, laser guided homing missile. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaAGM-114_tags // --- standard part parameters --- mass = 0.050 dragModelType = default @@ -46,12 +46,12 @@ PART { name = MissileLauncher - shortName = AGM-114R + shortName = Hellfire thrust = 10 //KN thrust during boost phase cruiseThrust = 0 //thrust during cruise phase dropTime = 0.4 //how many seconds after release until engine ignites - boostTime = 3.1 //seconds of boost phase + boostTime = 3.5 //seconds of boost phase cruiseTime = 0 //seconds of cruise phase guidanceActive = true //missile has guidanceActive @@ -60,23 +60,31 @@ PART decoupleSpeed = 15 decoupleForward = true - missileType = missile - homingType = AGM + + homingType = kappa + + LoftMaxAltitude = 16000 + LoftRangeOverride = 1200 + LoftVertVelComp = 0.5 + LoftTermAngle = 30 + LoftAngle = 40 + LoftRangeFac = 1.5 + kappaAngle = 60 + targetingType = laser - maxOffBoresight = 65 - lockedSensorFOV = 2 - optimumAirspeed = 450 - DetonationDistance = 0.1 - agmDescentRatio = 1.1 + maxOffBoresight = 100 + lockedSensorFOV = 4 + optimumAirspeed = 550 + DetonationDistance = 0 maxAoA = 45 aero = true - liftArea = 0.0016 + liftArea = 0.0012 steerMult = 0.9 - maxTorque = 15 - torqueRampUp = 50 + maxTorque = 14 + torqueRampUp = 20 aeroSteerDamping = 5 minStaticLaunchRange = 500 @@ -95,7 +103,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 3f579c014..43b721c87 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 @@ -23,18 +24,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 220 + entryCost = 16000 + cost = 6000 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = AGM-114R Hellfire II EMP - manufacturer = Bahamuto Dynamics - description = 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. + title = #loc_BDArmory_part_bahaAGM-114_EMP_title //AGM-114R Hellfire II EMP + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaAGM-114_EMP_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaAGM-114_EMP_tags // --- standard part parameters --- mass = 0.050 dragModelType = default @@ -49,14 +50,13 @@ PART { name = MissileLauncher - shortName = AGM-114R + shortName = Hellfire thrust = 10 //KN thrust during boost phase cruiseThrust = 0 //thrust during cruise phase dropTime = 0.4 //how many seconds after release until engine ignites - boostTime = 3.1 //seconds of boost phase + boostTime = 3.5 //seconds of boost phase cruiseTime = 0 //seconds of cruise phase - DetonationDistance = 10 guidanceActive = true //missile has guidanceActive maxTurnRateDPS = 32 //degrees per second @@ -64,23 +64,31 @@ PART decoupleSpeed = 15 decoupleForward = true - missileType = missile - homingType = AGM + + homingType = kappa + + LoftMaxAltitude = 16000 + LoftRangeOverride = 1200 + LoftVertVelComp = 0.5 + LoftTermAngle = 30 + LoftAngle = 40 + LoftRangeFac = 1.5 + kappaAngle = 60 + targetingType = laser - maxOffBoresight = 65 - lockedSensorFOV = 7 - optimumAirspeed = 450 - DetonationDistance = 0.1 - agmDescentRatio = 1.1 + maxOffBoresight = 100 + lockedSensorFOV = 4 + optimumAirspeed = 550 + DetonationDistance = 0 maxAoA = 45 aero = true - liftArea = 0.0016 + liftArea = 0.0012 steerMult = 0.9 - maxTorque = 15 - torqueRampUp = 50 + maxTorque = 14 + torqueRampUp = 20 aeroSteerDamping = 5 minStaticLaunchRange = 500 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 44e0e2f5c..aa2708ad7 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/hiddenVulcan/hiddenVulcan.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/hiddenVulcan/hiddenVulcan.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.01, 0, 0, -1, 0, 1 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 +entryCost = 1200 cost = 950 category = none bdacategory = Guns subcategory = 0 bulkheadProfiles = srf -title = Vulcan (Hidden) -manufacturer = Bahamuto Dynamics -description = A 6 barrel 20x102mm rotary cannon. 20x102Ammo +title = #loc_BDArmory_part_bahaHiddenVulcan_title //Vulcan (Hidden) +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaHiddenVulcan_description //A 6 barrel 20x102mm rotary cannon. 20x102Ammo // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaHiddenVulcan_tags // --- standard part parameters --- mass = 0.1 dragModelType = default @@ -52,13 +52,15 @@ MODULE hasDeployAnim = false hasFireAnimation = false + SpoolUpTime = 0.15 + roundsPerMinute = 5500 - maxDeviation = 0.125 + maxDeviation = 0.3567 //8 mrad maxEffectiveDistance = 2500 maxTargetingRange = 5000 ammoName = 20x102Ammo - bulletType = 20x102mmHEBullet + bulletType = 20x102mmHEBullet; 20x102mmBullet requestResourceAmount = 1 hasRecoil = true @@ -67,17 +69,10 @@ MODULE useRippleFire = false weaponType = ballistic - - projectileColor = 255, 60, 0, 128 //RGBA 0-255 - startColor = 255, 105, 0, 64 - fadeColor = true - - tracerStartWidth = 0.12 - tracerEndWidth = 0.12 + tracerLength = 0 tracerDeltaFactor = 2.75 tracerInterval = 3 - nonTracerWidth = 0.035 //oneShotWorldParticles = true diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg index c8d963035..a377c3d55 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.1779008, 0.2834791, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 200 +entryCost = 1800 +cost = 600 category = none bdacategory = Bombs subcategory = 0 bulkheadProfiles = srf -title = Mk83 JDAM Bomb -manufacturer = Bahamuto Dynamics -description = 1000lb GPS-guided bomb. +title = #loc_BDArmory_part_bahaJdamMk83_title //Mk83 JDAM Bomb +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaJdamMk83_description //1000lb GPS-guided bomb. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaJdamMk83_tags // --- standard part parameters --- mass = 0.460 dragModelType = default @@ -61,13 +61,14 @@ MODULE missileType = bomb DetonationDistance = 0 targetingType = gps + gpsUpdates = 0.5 homingType = AGMBallistic optimumAirspeed = 300 aero = true - liftArea = 0.0009 - steerMult = .3 + liftArea = 0.004 + steerMult = 4 maxTorque = 8 engageAir = false diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/KKV_TCA.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/KKV_TCA.mu new file mode 100644 index 000000000..67d4478f5 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/KKV_TCA.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/TEX_TCA_KKV.dds b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/TEX_TCA_KKV.dds new file mode 100644 index 000000000..91d6c375f Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/TEX_TCA_KKV.dds differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/kkv.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/kkv.cfg new file mode 100644 index 000000000..cd9b15351 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/kkv.cfg @@ -0,0 +1,102 @@ +PART +{ +// Kerbal Space Program - Part Config +// +// + +// --- general parameters --- +name = bahaKKV +module = Part +author = BahamutoD, Josue, Stardust + +// --- asset parameters --- +mesh = KKV_TCA.mu +rescaleFactor = 0.33 // ~1.25m x 0.2m, 0.25m^3 volume + +// --- node definitions --- +//node_attach = 0.0, 0.1232686, -0.3764487, 0, 1, 0, 0 +//node_stack_top = 0.0, 0.1232686, 0.0, 0, 1, 0, 0 + +NODE +{ + name = node0 + transform = node0 + size = 0 + method = FIXED_JOINT //FIXED_JOINT, HINGE_JOINT, LOCKED_JOINT, MERGED_PHYSICS or NO_PHYSICS +} + +// --- editor parameters --- +TechRequired = precisionEngineering +entryCost = 8000 +cost = 2000 +category = none +bdacategory = Missiles +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_bahaKKV_title //Kinetic Kill Vehicle +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaKKV_description //The KKV (kinetic kill vehicle) is a IR-guided homing missile that uses reaction control thrusters and a control moment gyroscope to maneuver. It is capable of steering towards targets in a vacuum and has high drag in atmosphere. The KKV relies on kinetic energy to destroy its target and carries no explosives. 6 km/s delta-V. +// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision +attachRules = 1,1,0,0,1 +tags = #loc_BDArmory_part_bahaKKV_tags +// --- standard part parameters --- +mass = 0.045 +dragModelType = default +maximum_drag = 0.1 +minimum_drag = 0.1 +angularDrag = 2 +crashTolerance = 5 +maxTemp = 3600 + + +MODULE +{ + name = MissileLauncher + shortName = KKV + + thrust = 2.65 + cruiseThrust = 0 + dropTime = 0 + boostTime = 45 + cruiseTime = 0 + + maxTurnRateDPS = 6 //degrees per second + + decoupleSpeed = 10 + decoupleForward = true + + hasRCS = true + rcsThrust = 1.4 + + audioClipPath = BDArmory/Sounds/jet + guidanceActive = true //missile has guidanceActive + homingType = orbital // Replaces homingType = RCS + missileType = missile + + DetonationDistance = 0 + + targetingType = heat + heatThreshold = 0.1 + maxOffBoresight = 180 + lockedSensorFOV = 12.5 + uncagedLock = true + targetCoM = true // advanced IR targeting, target CoM instead of hottest part + flareEffectivity = 0 // advanced IR, flares ineffective, use point defense + + minStaticLaunchRange = 500 + maxStaticLaunchRange = 100000 + + engageAir = true + engageMissile = true + engageGround = false + engageSLW = false + + useFuel = true // mass decreases as fuel burns, missile acceleration is non-constant + boosterMass = 0 // no independent booster, empty mass is 0.006 + boosterFuelMass = 0.039 // no cruise, just boost + + dragArea = 0.0025 // drag will still be applied, missile is not meant for use in-atmo + +} +// no explosive modules, kinetic warhead +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/muzzle.dds b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/muzzle.dds new file mode 100644 index 000000000..dfc85160c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/kkv/muzzle.dds differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102.cfg index 7be8a9dce..6a0565aae 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102.cfg @@ -20,18 +20,18 @@ node_attach = 0, 0, 0.7742968, 0, 0, -1, 1 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 3500 +entryCost = 4500 +cost = 2500 category = none bdacategory = Gun turrets subcategory = 0 bulkheadProfiles = srf -title = M102 Howitzer (Radial) -manufacturer = Bahamuto Dynamics -description = A radially mounted 105mm gun. CannonShells +title = #loc_BDArmory_part_bahaM102Howitzer_title //M102 Howitzer (Radial) +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaM102Howitzer_description //A radially mounted 105mm gun. CannonShells // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaM102Howitzer_tags // --- standard part parameters --- mass = 1 dragModelType = default @@ -75,12 +75,12 @@ MODULE spinDownAnimation = false roundsPerMinute = 13 - maxDeviation = 0.25 + maxDeviation = 0.2 maxTargetingRange = 8000 maxEffectiveDistance = 8000 ammoName = CannonShells - bulletType = 105mmBullet + bulletType = 105mmBullet; 105mmHEBullet requestResourceAmount = 1 hasRecoil = true @@ -89,10 +89,6 @@ MODULE weaponType = ballistic - projectileColor = 255, 90, 0, 128 - - tracerStartWidth = 0.27 - tracerEndWidth = 0.2 tracerLength = 0 tracerDeltaFactor = 3.75 @@ -105,7 +101,6 @@ MODULE oneShotSound = true showReloadMeter = true reloadAudioPath = BDArmory/Parts/m1Abrams/sounds/reload - } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102HE.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102HE.cfg deleted file mode 100644 index e5552a918..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/m102HE.cfg +++ /dev/null @@ -1,124 +0,0 @@ -PART -{ -// Kerbal Space Program - Part Config -// -// - -// --- general parameters --- -name = bahaM102HowitzerHE -module = Part -author = BahamutoD - -// --- asset parameters --- -MODEL - { - model = BDArmory/Parts/m102Howitzer/model - texture = tex_m102, BDArmory/Parts/m102Howitzer/tex_m102B2 - } - rescaleFactor = 1 - - -// --- node definitions --- -node_attach = 0, 0, 0.7742968, 0, 0, -1, 1 - - -// --- editor parameters --- -TechRequired = precisionEngineering -entryCost = 2100 -cost = 3500 -category = none -bdacategory = Gun turrets -subcategory = 0 -bulkheadProfiles = srf -title = M102 Howitzer (Radial) HE -manufacturer = Bahamuto Dynamics -description = A radially mounted 105mm gun. HE CannonShells -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - -// --- standard part parameters --- -mass = 1 -dragModelType = default -maximum_drag = 0.2 -minimum_drag = 0.2 -angularDrag = 2 -crashTolerance = 60 -maxTemp = 3600 - -stagingIcon = SOLID_BOOSTER - -MODULE -{ - name = ModuleTurret - - yawTransformName = aimRotate - pitchTransformName = aimPitch - - pitchSpeedDPS = 80 - yawSpeedDPS = 80 - - minPitch = -15 - maxPitch = 15 - yawRange = 30 - - smoothRotation = true - smoothMultiplier = 10 -} - -MODULE -{ - name = ModuleWeapon - shortName = M102 Howitzer HE - - fireTransformName = fireTransform - - hasDeployAnim = false - - hasFireAnimation = true - fireAnimName = fireAnim - spinDownAnimation = false - - roundsPerMinute = 13 - maxDeviation = 0.25 - maxTargetingRange = 8000 - maxEffectiveDistance = 8000 - - ammoName = CannonShells - bulletType = 105mmHEBullet - requestResourceAmount = 1 - - hasRecoil = true - onlyFireInRange = true - bulletDrop = true - - weaponType = ballistic - - projectileColor = 255, 90, 0, 128 - - tracerStartWidth = 0.27 - tracerEndWidth = 0.2 - tracerLength = 0 - tracerDeltaFactor = 3.75 - - maxHeat = 3600 - heatPerShot = 60 - heatLoss = 740 - - fireSoundPath = BDArmory/Parts/m1Abrams/sounds/shot - overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat - oneShotSound = true - showReloadMeter = true - reloadAudioPath = BDArmory/Parts/m1Abrams/sounds/reload - -} - - - -RESOURCE -{ - name = CannonShells - amount = 20 - maxAmount = 20 -} - -} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/tex_m102B2.png b/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/tex_m102B2.png deleted file mode 100644 index 9d374bf3f..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/m102Howitzer/tex_m102B2.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg index bb98e9744..b8457b33c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg @@ -16,21 +16,22 @@ rescaleFactor = 1 // --- node definitions --- node_attach = 0.0, -0.178, 0, 0, -1, 0, 0 +node_stack_bottom = 0.0, -0.185, 0.0, 0.0, -1.0, 0.0, 2 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 3500 +entryCost = 12000 +cost = 4000 category = none bdacategory = Gun turrets subcategory = 0 -bulkheadProfiles = srf -title = M1 Abrams Cannon -manufacturer = Bahamuto Dynamics -description = A 120mm cannon on an armored turret. CannonShells +bulkheadProfiles = size2, srf +title = #loc_BDArmory_part_bahaM1Abrams_title //M1 Abrams Cannon +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaM1Abrams_description //A 120mm cannon on an armored turret. CannonShells // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - +attachRules = 1,1,0,0,1 +tags = #loc_BDArmory_part_bahaM1Abrams_tags // --- standard part parameters --- mass = 2 dragModelType = default @@ -78,24 +79,22 @@ MODULE spinDownAnimation = false roundsPerMinute = 10 - maxDeviation = 0.2 + maxDeviation = 0.05 maxTargetingRange = 8000 maxEffectiveDistance = 8000 ammoName = CannonShells - bulletType = 120mmBullet + bulletType = 120mmBullet; 120mmBulletHE; 120mmBulletCannister + //bulletType = 120mmBulletSabot; 120mmBulletHEAT; 120mmBulletCannister // Delete above line and uncomment this to use realistic projectiles. requestResourceAmount = 1 + canHotSwap = true hasRecoil = true onlyFireInRange = true bulletDrop = true weaponType = ballistic - - projectileColor = 255, 90, 0, 190 - - tracerStartWidth = 0.27 - tracerEndWidth = 0.20 + tracerLength = 0 tracerDeltaFactor = 3.75 tracerLuminance = 2 @@ -121,5 +120,9 @@ RESOURCE amount = 20 maxAmount = 20 } - +MODULE +{ + name = ModuleCASE + CASELevel = 2 +} } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1AbramsHE.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1AbramsHE.cfg deleted file mode 100644 index eeed72011..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1AbramsHE.cfg +++ /dev/null @@ -1,129 +0,0 @@ -PART -{ -// Kerbal Space Program - Part Config -// -// - -// --- general parameters --- -name = bahaM1AbramsHE -module = Part -author = BahamutoD - -// --- asset parameters --- -MODEL - { - model = BDArmory/Parts/m1Abrams/model - texture = tex_abrams, BDArmory/Parts/m1Abrams/tex_abramsB2 - } - rescaleFactor = 1 - - -// --- node definitions --- -node_attach = 0.0, -0.178, 0, 0, -1, 0, 0 - -// --- editor parameters --- -TechRequired = precisionEngineering -entryCost = 2100 -cost = 3500 -category = none -bdacategory = Gun turrets -subcategory = 0 -bulkheadProfiles = srf -title = M1 Abrams Cannon HE -manufacturer = Bahamuto Dynamics -description = A 120mm cannon on an armored turret. HE CannonShells. Rotation is limited. -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - -// --- standard part parameters --- -mass = 2 -dragModelType = default -maximum_drag = 0.2 -minimum_drag = 0.2 -angularDrag = 2 -crashTolerance = 125 -maxTemp = 3600 - -stagingIcon = SOLID_BOOSTER - -MODULE -{ - name = ModuleTurret - - yawTransformName = aimRotate - pitchTransformName = aimPitch - - pitchSpeedDPS = 60 - yawSpeedDPS = 40 - - minPitch = -4 - maxPitch = 27 - yawRange = 30 - - smoothRotation = true - smoothMultiplier = 10 - - audioPath = BDArmory/Sounds/hydraulicLoop - maxAudioPitch = 0.42 - minAudioPitch = 0.15 - maxVolume = 0.60 -} - -MODULE -{ - name = ModuleWeapon - - fireTransformName = fireTransform - - hasDeployAnim = false - - hasFireAnimation = true - fireAnimName = fireAnim - spinDownAnimation = false - - roundsPerMinute = 10 - maxDeviation = 0.2 - maxTargetingRange = 8000 - maxEffectiveDistance = 8000 - - ammoName = CannonShells - bulletType = 120mmBulletHE - requestResourceAmount = 1 - - hasRecoil = true - onlyFireInRange = true - bulletDrop = true - - weaponType = ballistic - - projectileColor = 255, 90, 0, 190 - - tracerStartWidth = 0.27 - tracerEndWidth = 0.20 - tracerLength = 0 - tracerDeltaFactor = 3.75 - tracerLuminance = 2 - - maxHeat = 3600 - heatPerShot = 60 - heatLoss = 740 - - fireSoundPath = BDArmory/Parts/m1Abrams/sounds/shot - overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat - oneShotSound = true - showReloadMeter = true - reloadAudioPath = BDArmory/Parts/m1Abrams/sounds/reload - -} - - - - -RESOURCE -{ - name = CannonShells - amount = 20 - maxAmount = 20 -} - -} 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/m1Abrams/tex_abramsB2.png b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/tex_abramsB2.png deleted file mode 100644 index 6f4069da1..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/tex_abramsB2.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg index 12ed1a8f1..3df1b7fa4 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.06, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 950 +entryCost = 2300 +cost = 1500 category = none bdacategory = Gun turrets subcategory = 0 bulkheadProfiles = srf -title = M230 Chain Gun Turret -manufacturer = Bahamuto Dynamics -description = The M230 Chain Gun is a single-barrel automatic cannon firing 30x173 Ammo high explosive rounds. It is commonly used on attack helicopters. +title = #loc_BDArmory_part_bahaM230ChainGun_title //M230 Chain Gun Turret +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaM230ChainGun_description //The M230 Chain Gun is a single-barrel automatic cannon firing 30x173 Ammo high explosive rounds. It is commonly used on attack helicopters. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaM230ChainGun_tags // --- standard part parameters --- mass = 0.1 dragModelType = default @@ -76,12 +76,13 @@ MODULE spinDownAnimation = false roundsPerMinute = 625 - maxDeviation = 0.4 + isChaingun = true + maxDeviation = 0.35 maxEffectiveDistance = 2500 maxTargetingRange = 5000 ammoName = 30x173Ammo - bulletType = 30x173HEBullet + bulletType = 30x173HEBullet; 30x173Bullet requestResourceAmount = 1 shellScale = 0.66 @@ -91,59 +92,48 @@ MODULE weaponType = ballistic - projectileColor = 255, 90, 0, 128 //RGBA 0-255 - startColor = 255, 105, 0, 90 - tracerStartWidth = 0.16 - tracerEndWidth = 0.16 tracerLength = 0 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 explModelPath = BDArmory/Models/explosion/30mmExplosion explSoundPath = BDArmory/Sounds/subExplode - } 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 e414cb7cd..321ec796c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/maverick/maverickMissile.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/maverick/maverickMissile.cfg @@ -20,18 +20,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 400 + entryCost = 5000 + cost = 2500 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = AGM-65 Maverick Missile - manufacturer = Bahamuto Dynamics - description = Medium yield laser guided air-to-ground missile. + title = #loc_BDArmory_part_bahaAGM-65_title //AGM-65 Maverick Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaAGM-65_description //Medium yield laser guided air-to-ground missile. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaAGM-65_tags // --- standard part parameters --- mass = 0.27 dragModelType = default @@ -54,7 +54,7 @@ PART boostTime = 0.575 //seconds of boost phase cruiseTime = 3.495 //seconds of cruise phase guidanceActive = true //missile has guidanceActive - maxTurnRateDPS = 9 //degrees per second + maxTurnRateDPS = 12 //degrees per second audioClipPath = BDArmory/Sounds/rocketLoop exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust @@ -63,8 +63,8 @@ PART boostTransformName = boostTransform homingType = AGM - agmDescentRatio = 1.85 - optimumAirspeed = 350 + agmDescentRatio = 1.6 + optimumAirspeed = 210 missileType = missile targetingType = laser @@ -72,12 +72,17 @@ PART DetonationDistance = 0 aero = true - liftArea = 0.0048 - steerMult = .75 - maxTorque = 15 + liftArea = 0.0032 + dragArea = 0.0015 + steerMult = 3 + maxTorque = 10 + aeroSteerDamping = 4 + + gLimit = 8 + maxAoA = 18 - minStaticLaunchRange = 800 - maxStaticLaunchRange = 5500 + minStaticLaunchRange = 1600 + maxStaticLaunchRange = 8000 engageAir = false engageMissile = false @@ -89,7 +94,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 1a128a05b..a9a8dd735 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/missileTurret/missileTurret.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/missileTurret/missileTurret.cfg @@ -27,18 +27,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 3000 + cost = 1000 category = none bdacategory = Missile turrets subcategory = 0 - bulkheadProfiles = srf - title = Jernas Missile Turret - manufacturer = Bahamuto Dynamics - description = 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. + bulkheadProfiles = size1, srf + title = #loc_BDArmory_part_missileTurretTest_title //Jernas Missile Turret + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_missileTurretTest_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,1,0,1 - + tags = #loc_BDArmory_part_missileTurretTest_tags // --- standard part parameters --- mass = 0.75 dragModelType = default @@ -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/mk82Bomb/mk82Bomb.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/mk82Bomb/mk82Bomb.cfg index a21cb46a7..2838e7b53 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/mk82Bomb/mk82Bomb.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/mk82Bomb/mk82Bomb.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.1365, 0.15, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 50 +entryCost = 250 +cost = 100 category = none bdacategory = Bombs subcategory = 0 bulkheadProfiles = srf -title = Mk82 Bomb -manufacturer = Bahamuto Dynamics -description = 500lb unguided bomb. +title = #loc_BDArmory_part_bahaMk82Bomb_title //Mk82 Bomb +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaMk82Bomb_description //500lb unguided bomb. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaMk82Bomb_tags // --- standard part parameters --- mass = 0.227 dragModelType = default @@ -62,7 +62,7 @@ MODULE simpleCoD = 0,0,-2 simpleStableTorque = 5 rndAngVel = 2 - + liftArea = 0 missileType = bomb homingType = none diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/mk82BombBrake/mk82BombBrake.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/mk82BombBrake/mk82BombBrake.cfg index d961605bc..c50be2ee2 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/mk82BombBrake/mk82BombBrake.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/mk82BombBrake/mk82BombBrake.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.1365, 0.15, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 75 +entryCost = 300 +cost = 125 category = none bdacategory = Bombs subcategory = 0 bulkheadProfiles = srf -title = Mk82 SnakeEye Bomb -manufacturer = Bahamuto Dynamics -description = 500lb unguided bomb with airbrakes. Use for low altitude bombing. +title = #loc_BDArmory_part_bahaMk82BombBrake_title //Mk82 SnakeEye Bomb +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaMk82BombBrake_description //500lb unguided bomb with airbrakes. Use for low altitude bombing. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaMk82Bomb_tags // --- standard part parameters --- mass = 0.227 dragModelType = default @@ -70,7 +70,7 @@ MODULE simpleCoD = 0,0,-2 simpleStableTorque = 5 rndAngVel = 2 - + liftArea = 0 missileType = bomb homingType = none diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg index f9be2b0b5..9593343b6 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg @@ -11,29 +11,30 @@ PART // --- asset parameters --- mesh = model.mu - rescaleFactor = 1 + rescaleFactor = 0.9 // --- node definitions --- node_attach = 0.0, -0.6017585, 0, 0, -1, 0, 1 + node_stack_bottom = 0.0, -0.6017585, 0.0, 0.0, -1.0, 0.0, 2 // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 + entryCost = 7000 cost = 3500 category = none bdacategory = Gun turrets subcategory = 0 - bulkheadProfiles = srf - title = Oerlikon Millennium Cannon - manufacturer = Bahamuto Dynamics - description = 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 + bulkheadProfiles = size1p5, srf + title = #loc_BDArmory_part_bahaOMillennium_title //Oerlikon Millennium Cannon + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaOMillennium_description //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 // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision - attachRules = 0,1,0,0,1 - + attachRules = 1,1,0,0,1 + tags = #loc_BDArmory_part_bahaOMillennium_tags // --- standard part parameters --- - mass = 2 + mass = 1 dragModelType = default maximum_drag = 0.2 minimum_drag = 0.2 @@ -83,33 +84,35 @@ PART spinDownAnimation = false roundsPerMinute = 850 - maxDeviation = 0.47 + maxDeviation = 0.18 maxTargetingRange = 3800 airDetonation = true + detonationRange = 150 maxEffectiveDistance = 3800 weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true + ammoName = 30x173Ammo - bulletType = 30x173HEBullet + bulletType = 35x228AHEADBullet; 35x228HEBullet requestResourceAmount = 1 + bulletDmgMult = 2.5 //so submunitions do more than scratch damage hasRecoil = true onlyFireInRange = true bulletDrop = true - projectileColor = 255, 110, 0, 128 - - tracerStartWidth = 0.22 - tracerEndWidth = 0.18 tracerLength = 0 maxHeat = 3600 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 c69a7af2e..9da134f89 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/pac-3/pac3.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/pac-3/pac3.cfg @@ -21,18 +21,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 + entryCost = 8000 cost = 4000 bdacategory = Missiles category = none subcategory = 0 bulkheadProfiles = srf - title = MIM-104F PATRIOT PAC-3 - manufacturer = Bahamuto Dynamics - description = Medium range, high speed, radar-guided surface to air missile. + title = #loc_BDArmory_part_bahaPac-3_title //MIM-104F PATRIOT PAC-3 + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaPac-3_description //Medium range, high speed, radar-guided surface to air missile. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaPac-3_tags // --- standard part parameters --- mass = 0.312 dragModelType = default @@ -62,7 +62,7 @@ PART exhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust - optimumAirspeed = 1250 + optimumAirspeed = 1280 aero = true liftArea = 0.005 @@ -89,6 +89,7 @@ PART { name = BDExplosivePart tntMass = 73 + warheadType = ContinuousRod } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg index c1f22a0cf..dcf9670c8 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg @@ -44,18 +44,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 7500 + cost = 2500 category = none bdacategory = Missile turrets subcategory = 0 - bulkheadProfiles = srf - title = Patriot Launcher Turret - manufacturer = Bahamuto Dynamics - description = 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. + bulkheadProfiles = size1p5, srf + title = #loc_BDArmory_part_patriotLauncherTurret_title //Patriot Launcher Turret + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_patriotLauncherTurret_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,1,0,1 - + tags = #loc_BDArmory_part_patriotLauncherTurret_tags // --- standard part parameters --- mass = 1.75 dragModelType = default @@ -106,20 +106,16 @@ 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 + } } - - - } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg index fe2084cae..47e34bcd1 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg @@ -21,17 +21,17 @@ node_attach = 0.0, 0, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering entryCost = 2100 -cost = 600 +cost = 500 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = srf -title = Radar Data Receiver -manufacturer = Bahamuto Dynamics -description = 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. +title = #loc_BDArmory_part_radarDataReceiver_title //Radar Data Receiver +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_radarDataReceiver_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_radarDataReceiver_tags // --- standard part parameters --- mass = 0.05 dragModelType = default @@ -41,7 +41,7 @@ angularDrag = 2 crashTolerance = 7 maxTemp = 3600 -physicsSignificance = 1 +PhysicsSignificance = 1 MODULE @@ -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 b32dd3f6b..8e46fcc83 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.cfg @@ -7,15 +7,16 @@ PART 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 = 6200 - cost = 320 + entryCost = 5500 + cost = 2000 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = size1 - title = AN/APG-63V2 Radome - manufacturer = Bahamuto Dynamics - description = 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. + title = #loc_BDArmory_part_bdRadome1_title //AN/APG-63V2 Radome + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdRadome1_description //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. + tags = #loc_BDArmory_part_bdRadome1_tags attachRules = 1,0,1,1,0 stackSymmetry = 2 mass = 0.375 @@ -31,45 +32,46 @@ PART MODEL { model = BDArmory/Parts/radome125/radome1 + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125 } -MODULE -{ - name = ModuleRadar + MODULE + { + name = ModuleRadar - // -- Section: General Configuration -- - radarName = AN/APG-63V2 Radome // if left empty part.title is used, but advised to set this to a nice printable text - rwrThreatType = 1 // IMPORTANT, please set correctly: - // 0 = SAM site radar - // 1 = Fighter radar (airborne) - // 2 = AWACS radar (airborne) - // 3, 4 = ACTIVE MISSILE (DO NOT USE UNLESS YOU KNOW WHAT YOU'RE DOING! - // 5 = Detection radar (ground/ship based) - // 6 = SONAR (ship/submarine based) - rotationTransformName = scanRotation - //turretID = 0 // if needed - resourceDrain = 0.825 // change to higher values for more capable radars, e.g AESA + // -- Section: General Configuration -- + radarName = AN/APG-63V2 Radome // if left empty part.title is used, but advised to set this to a nice printable text + rwrThreatType = 1 // IMPORTANT, please set correctly: + // 0 = SAM site radar + // 1 = Fighter radar (airborne) + // 2 = AWACS radar (airborne) + // 3, 4 = ACTIVE MISSILE (DO NOT USE UNLESS YOU KNOW WHAT YOU'RE DOING! + // 5 = Detection radar (ground/ship based) + // 6 = SONAR (ship/submarine based) + rotationTransformName = scanRotation + //turretID = 0 // if needed + resourceDrain = 0.825 // change to higher values for more capable radars, e.g AESA - // -- Section: Capabilities -- - omnidirectional = false // false: boresight scan radar - directionalFieldOfView = 120 // for omni and boresight - //boresightFOV = 10 // for boresight only - //scanRotationSpeed = 240 // degress per second - //lockRotationSpeed = 120 // only relevant if canLock - lockRotationAngle = 4 - showDirectionWhileScan = true // can show target direction on radar screen. False: radar echos displayed as block only (no direction) - multiLockFOV = 40 // only relevant if canLock - //lockAttemptFOV = 2 // only relevant if canLock - maxLocks = 3 //how many targets can be locked/tracked simultaneously. only relevant if canLock + // -- Section: Capabilities -- + omnidirectional = false // false: boresight scan radar + directionalFieldOfView = 120 // for omni and boresight + //boresightFOV = 10 // for boresight only + //scanRotationSpeed = 240 // degress per second + //lockRotationSpeed = 120 // only relevant if canLock + lockRotationAngle = 4 + showDirectionWhileScan = true // can show target direction on radar screen. False: radar echos displayed as block only (no direction) + multiLockFOV = 40 // only relevant if canLock + //lockAttemptFOV = 2 // only relevant if canLock + maxLocks = 3 //how many targets can be locked/tracked simultaneously. only relevant if canLock - 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]) + canScan = true // scanning/detecting targets (volume search) + canLock = true // locking/tracking targets (fire control) + canTrackWhileScan = true // continue scanning while tracking a locked target + 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! + minSignalThreshold = 80 // DEPRECATED, NO LONGER USED! use detection float curve! + minLockedSignalThreshold = 100 // DEPRECATED, NO LONGER USED! use locktrack float curve! radarGroundClutterFactor = 0.1 // how much is the radar efficiency reduced to by ground clutter/look-down? // 0.0 = reduced to 0% (=IMPOSSIBLE to detect ground targets) @@ -79,33 +81,63 @@ MODULE // any ground clutter factor >0.25 is to be considered very good, making an efficient surface/horizon search radar. // values >1.0 are possible, meaning the radar is MORE efficient during look down than vs air targets. - radarDetectionCurve - { - // floatcurve to define at what range (km) which minimum cross section (m^2) can be detected. - // this defines both min/max range of the radar, and sensitivity/efficiency - // it is recommended to define an "assured detection range", at which all craft are detected regardless - // of their rcs. This is achieved by using a minrcs value of zero, thus detecting everything. - // key = distance rcs - key = 0.0 0 - key = 5 0 //between 0 and 5 km the min cross section is 0, thus assured detection of everything - key = 10 5 // - key = 20 15 // - key = 35 25 //maxrange of 35km - } + radarDetectionCurve + { + // floatcurve to define at what range (km) which minimum cross section (m^2) can be detected. + // this defines both min/max range of the radar, and sensitivity/efficiency + // it is recommended to define an "assured detection range", at which all craft are detected regardless + // of their rcs. This is achieved by using a minrcs value of zero, thus detecting everything. + // key = distance rcs + key = 0.0 0 + key = 5 0 //between 0 and 5 km the min cross section is 0, thus assured detection of everything + key = 10 5 // + key = 20 15 // + key = 35 25 //maxrange of 35km + } - radarLockTrackCurve - { - // same as detectionCurve, just for locking/tracking purpose - // ATTENTION: DO NOT USE an "assured locking range" here, as this would render lock-breaking - // ECM-jammers & chaff completely ineffective!! - // key = distance rcs - key = 0.0 0 - key = 5 5 // - key = 10 7 // - key = 20 20 // - key = 35 35 //maxrange of 35km - } -} + radarLockTrackCurve + { + // same as detectionCurve, just for locking/tracking purpose + // ATTENTION: DO NOT USE an "assured locking range" here, as this would render lock-breaking + // ECM-jammers & chaff completely ineffective!! + // key = distance rcs + key = 0.0 0 + key = 5 5 // + key = 10 7 // + key = 20 20 // + key = 35 35 //maxrange of 35km + } + } + MODULE + { + name = ModulePartVariants + primaryColor = #ffffff + secondaryColor = #000000 + baseVariant = Pitot + useMultipleDragCubes = false + VARIANT + { + name = Pitot + displayName = #loc_BDArmory_part_bdRadome_variantPitot //Pitot Tube + primaryColor = #ffffff + secondaryColor = #000000 + GAMEOBJECTS + { + fighterRadomePilot = true + } + } + VARIANT + { + name = Cone + displayName = #loc_BDArmory_part_bdRadome_variantNoPitot //No Pitot Tube + primaryColor = #ffffff + secondaryColor = #000000 + GAMEOBJECTS + { + fighterRadomePilot = false + } + } + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.mu index ae3abc0a9..497d10257 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.mu differ 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..2bfeef894 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1GA.cfg @@ -0,0 +1,116 @@ +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. + tags = #loc_BDArmory_part_bdRadome1_Gnd_tags + attachRules = 1,0,1,1,0 + 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 + } + + 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 + } + } + + MODULE + { + name = ModulePartVariants + primaryColor = #ffffff + secondaryColor = #000000 + baseVariant = Pitot + useMultipleDragCubes = false + VARIANT + { + name = Pitot + displayName = Pitot Tube + primaryColor = #ffffff + secondaryColor = #000000 + GAMEOBJECTS + { + fighterRadomePilot = true + } + } + VARIANT + { + name = Cone + displayName = No Pitot Tube + primaryColor = #ffffff + secondaryColor = #000000 + GAMEOBJECTS + { + fighterRadomePilot = false + } + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg index 95c40a1d6..f7246aa98 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg @@ -8,15 +8,16 @@ PART 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 = 6200 - cost = 320 + entryCost = 5500 + cost = 2000 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = size1 - title = AN/APG-63V2 Inline Radome - manufacturer = Bahamuto Dynamics - description = 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. + title = #loc_BDArmory_part_bdRadome1inline_title //AN/APG-63V2 Inline Radome + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdRadome1inline_description //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. + tags = #loc_BDArmory_part_bdRadome1_tags attachRules = 1,1,1,1,0 stackSymmetry = 2 mass = 0.375 @@ -32,6 +33,7 @@ PART MODEL { model = BDArmory/Parts/radome125/radome1inline + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125 } @@ -67,7 +69,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/radome1inline.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.mu index 8d64a3b35..388db0931 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.mu differ 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..870387d04 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inlineGA.cfg @@ -0,0 +1,84 @@ +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. + tags = #loc_BDArmory_part_bdRadome1_Gnd_tags + attachRules = 1,1,1,1,0 + 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_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 + } + + 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 + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg index 7d0378c7d..c2c3d8c8f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg @@ -7,15 +7,16 @@ PART 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 = 6200 - cost = 320 + entryCost = 5500 + cost = 2250 category = none bdacategory = Radars subcategory = 0 bulkheadProfiles = size1 - title = AN/APG-63V1 Radome - manufacturer = Bahamuto Dynamics - 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. + title = #loc_BDArmory_part_bdRadome1snub_title //AN/APG-63V1 Radome + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #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. + tags = #loc_BDArmory_part_bdRadome1_tags attachRules = 1,0,1,1,0 stackSymmetry = 2 mass = 0.375 @@ -31,6 +32,7 @@ PART MODEL { model = BDArmory/Parts/radome125/radome1snub + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125 } @@ -66,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/radome1snub.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.mu index 05111e80c..c4077c878 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.mu differ 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..18ddfd6d5 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snubGA.cfg @@ -0,0 +1,79 @@ +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 = #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. + tags = #loc_BDArmory_part_bdRadome1_Gnd_tags + attachRules = 1,0,1,1,0 + 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 + } + 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 + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125.png b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125.png index 5ca97aad3..db6c739c5 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125.png and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125.png differ 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..d37c3f90d 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/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/rotaryBombBay/rotaryBombBay.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/rotaryBombBay.cfg index 5d7438ad7..bee812cd1 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/rotaryBombBay.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/rotaryBombBay.cfg @@ -13,15 +13,16 @@ PART node_attach = 0.0, -1.640427, 0.0, 0.0, -1.0, 0.0, 1 TechRequired = precisionEngineering - entryCost = 6200 - cost = 320 + entryCost = 1000 + cost = 500 category = none bdacategory = Missile turrets subcategory = 0 bulkheadProfiles = size1 - title = Adjustable Rotary Bomb Rack - manufacturer = Bahamuto Dynamics - description = 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. + title = #loc_BDArmory_part_bdRotBombBay_title //Adjustable Rotary Bomb Rack + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdRotBombBay_description //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. + tags = #loc_BDArmory_part_bdRotBombBay_tags attachRules = 1,1,1,1,0 mass = 0.375 dragModelType = default diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Launcher.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Launcher.cfg index 764bc02a8..6db65fd71 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Launcher.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Launcher.cfg @@ -20,20 +20,20 @@ node_stack_top = 0.0, 0.3988, 0, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 650 +entryCost = 5500 +cost = 3500 category = none bdacategory = Rocket pods subcategory = 0 bulkheadProfiles = srf -title = S-8KOM Rocket Pod -manufacturer = Bahamuto Dynamics -description = Holds and fires 23 unguided S-8KOM rockets. It has an aerodynamic nose cone. +title = #loc_BDArmory_part_bahaS-8Launcher_title //S-8KOM Rocket Pod +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaS-8Launcher_description //Holds and fires 23 unguided S-8KOM rockets. It has an aerodynamic nose cone. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaH70Launcher_tags // --- standard part parameters --- -mass = 0.016 +mass = 0.04 dragModelType = default maximum_drag = 0.01 minimum_drag = 0.01 @@ -54,14 +54,15 @@ MODULE roundsPerMinute = 1000 maxEffectiveDistance = 4000 - maxTargetingRange = 8000 + maxTargetingRange = 5000 weaponType = rocket - bulletType = 8KOMS + bulletType = 8KOMS; 8DMS ammoName = S-8KOMRocket requestResourceAmount = 1 rocketPod = true + externalAmmo = false onlyFireInRange = true @@ -84,6 +85,10 @@ RESOURCE } - +MODULE + { + name = ModuleCASE + CASELevel = 2 + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Rocket/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Rocket/model.mu index 4e8f29099..206baf1a8 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Rocket/model.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/s-8Launcher/s-8Rocket/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg index e9b4e5b40..6886669e5 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg @@ -1,96 +1,158 @@ PART { - // Kerbal Space Program - Part Config - - // --- general parameters --- - name = bahaAim9 - module = Part - author = BahamutoD - - // --- asset parameters --- - mesh = model.mu - rescaleFactor = 1 - - // --- node definitions --- - node_attach = 0.0, 0.06188124, 0, 0, 1, 0, 0 - node_stack_top = 0.0, 0.06188124, 0, 0, 1, 0, 0 - - // --- editor parameters --- - TechRequired = precisionEngineering - entryCost = 2100 - cost = 1200 - category = none - bdacategory = Missiles - subcategory = 0 - bulkheadProfiles = srf - title = AIM-9 Sidewinder Missile - manufacturer = Bahamuto Dynamics - description = Short range heat seeking missile. - // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision - attachRules = 1,1,0,0,1 - - // --- standard part parameters --- - mass = 0.085 - dragModelType = default - maximum_drag = 0.01 - minimum_drag = 0.01 - angularDrag = 2 - crashTolerance = 5 - maxTemp = 3600 - - - MODULE - { - name = MissileLauncher - - shortName = AIM-9 - - thrust = 22 //KN thrust during boost phase - cruiseThrust = 12 //thrust during cruise phase - dropTime = 0.1 //how many seconds after release until engine ignites - boostTime = 3.5 //seconds of boost phase - cruiseTime = 30 //seconds of cruise phase - guidanceActive = true //missile has guidanceActive - maxTurnRateDPS = 45 //degrees per second - decoupleSpeed = 5 - decoupleForward = false - - audioClipPath = BDArmory/Sounds/rocketLoop - exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust - boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust - boostExhaustTransformName = boostTransform - boostTransformName = boostTransform - - optimumAirspeed = 894 - - aero = true - liftArea = 0.002 - steerMult = 4 - maxTorque = 35 - maxAoA = 55 - //aeroSteerDamping = 4.5 - torqueRampUp = 50 - - homingType = aam - missileType = missile - targetingType = heat - heatThreshold = 50 - maxOffBoresight = 90 - lockedSensorFOV = 6 - - minStaticLaunchRange = 200 - maxStaticLaunchRange = 15000 - - engageAir = true - engageMissile = false - engageGround = false - engageSLW = false - - } - MODULE - { - name = BDExplosivePart - tntMass = 15 - } - + // Kerbal Space Program - Part Config + + // --- general parameters --- + name = bahaAim9 + module = Part + author = BahamutoD + + // --- asset parameters --- + mesh = model.mu + rescaleFactor = 1 + + // --- node definitions --- + node_attach = 0.0, 0.06188124, 0, 0, 1, 0, 0 + node_stack_top = 0.0, 0.06188124, 0, 0, 1, 0, 0 + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 1400 + cost = 1200 + category = none + bdacategory = Missiles + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_bahaAim9_title //AIM-9 Sidewinder Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaAim9_description //Short range heat seeking missile with limited flare rejection capabilities. + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 1,1,0,0,1 + tags = #loc_BDArmory_part_bahaAim9_tags + // --- standard part parameters --- + mass = 0.085 + dragModelType = default + maximum_drag = 0.01 + minimum_drag = 0.01 + angularDrag = 2 + crashTolerance = 5 + maxTemp = 3600 + + + MODULE + { + name = MissileLauncher + + shortName = AIM-9 + + thrust = 22 //KN thrust during boost phase + cruiseThrust = 12 //thrust during cruise phase + dropTime = 0.1 //how many seconds after release until engine ignites + boostTime = 3.5 //seconds of boost phase + cruiseTime = 30 //seconds of cruise phase + guidanceActive = true //missile has guidanceActive + maxTurnRateDPS = 45 //degrees per second + decoupleSpeed = 5 + decoupleForward = false + + audioClipPath = BDArmory/Sounds/rocketLoop + exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust + boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust + boostExhaustTransformName = boostTransform + boostTransformName = boostTransform + + optimumAirspeed = 894 + + aero = true + liftArea = 0.002 + steerMult = 4 + maxTorque = 35 + maxAoA = 55 + //aeroSteerDamping = 4.5 + torqueRampUp = 50 + gLimit = 30 + + 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 + terminalHoming = true + terminalHomingType = ProNav + terminalHomingRange = 3000 + pronavGain = 7 + + missileType = missile + targetingType = heat + seekerTimeout = 1 //timelimit without a detected target before heat-seeking guidance fails (default is 1). + heatThreshold = 50 // Distance-adjusted heat of target in order to be detected by missile, lower value results in better detection range + 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 + targetCoM = false // if true: target CoM instead of hottest part + lockedSensorFOV = 6 // How much the seeker can see in the direction it is looking (in terms of angle cone). + flareEffectivity = 1 // Modifies how the missile targeting is affected by flares, 1 is fully affected (normal behavior), lower values mean less affected (0 is ignores flares), higher values means more affected. This is a simple multiplier for flares, use the locked sensor bias curves for more sophisticated flare rejection behavior. + lockedSensorFOVBias + { + // floatcurve to define how the missile weights targets and flares off the center of where the seeker is looking + // is only active once the missile has locked onto a target + // should be set from 0 (seeker is looking directly at target/flare) to at least lockedSensorFOV/2 (target/flare is on edge of sensor FOV) + // it is recommended that the value at 0 be set to 1.0 and max value set to 0. Here is an example of an effective flare rejection curve: + // key = angle off seeker centerline weighting bias + // key = 0.0 1 // Highest weighing on centerline + // key = 3 0.83 // 17% lower weighting at edge of sensor FOV + // key = 10 0.25 // Beyond Sidewinder FOV, provided for example purposes for large sensor FOV missiles + // key = 30 0.1 // + // key = 90 0.0 // Beyond 90 flare/target is behind missile + // This set up will maxmize flare effectiviness: + // key = 0.0 1 + // key = 90 1 + // This is the current default curve (same as 1.4.0.7 BDAc Sidewinder): + key = 0 1 0 0 + key = 0.6 0.993333333333333 -0.0222222222222222 -0.0222222222222222 + key = 1.2 0.973333333333333 -0.0444444444444444 -0.0444444444444444 + key = 1.8 0.94 -0.0666666666666667 -0.0666666666666667 + key = 2.4 0.893333333333333 -0.0888888888888889 -0.0888888888888889 + key = 3 0.833333333333333 -0.111111111111111 -0.111111111111111 + // For other missiles it will scale from 0 to lockedSensorFOV/2 instead of 0 to 3 (3 = lockedSensorFOV/2 for Sidewinder) + } + lockedSensorVelocityBias + { + // floatcurve to define how the missile weights targets and flares based on the angle between their velocity vectors + // is only active once the missile has locked onto a target + // should be set from 0 (tracked target and flare/alternate target have aligned velocity vectors) + // to an angle less than 180 (tracked target and flare/alternate target travelling in opposite directions) + // it is recommended that the value at 0 be set to 1.0 and max key value be 0. Here is an example of an effective flare rejection curve: + // key = angle between velocities weighting bias + // key = 0.0 1 // Highest weighing for target/flare travelling in same direction as tracked target + // key = 3.0 0.83 // + // key = 10 0.25 // + // key = 30 0.1 // + // key = 90 0.0 // ignore flares travelling perpendicular to target or in opposite direction + // This set up will maxmize flare effectiviness: + // key = 0.0 1 + // key = 180 1 + // This is the current default curve (same as 1.4.0.7 BDAc Sidewinder): + key = 0.0 1 + key = 180 1 + } + + minStaticLaunchRange = 200 + maxStaticLaunchRange = 15000 + + engageAir = true + engageMissile = false + engageGround = false + engageSLW = false + + } + MODULE + { + name = BDExplosivePart + tntMass = 15 + warheadType = ContinuousRod + 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/smallWarhead/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg index 8ef0ffaed..e9a3e8465 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg @@ -30,10 +30,10 @@ category = none bdacategory = Warheads subcategory = 0 bulkheadProfiles = size0 -title = Small High Explosive Warhead -manufacturer = Bahamuto Dynamics -description = A missile nose cone packed with explosives. - +title = #loc_BDArmory_part_bdWarheadSmall_title //Small High Explosive Warhead +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bdWarheadSmall_description //A missile nose cone packed with explosives. +tags = #loc_BDArmory_part_bdWarheadSmall_tags // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,1,0,0 stackSymmetry = 2 @@ -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/smokeCm/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/smokeCm/part.cfg index 846737b5a..c42fc8eb9 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/smokeCm/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/smokeCm/part.cfg @@ -20,18 +20,18 @@ node_attach = 0.0, -0.05, 0, 0, -1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 +entryCost = 800 cost = 600 category = none bdacategory = Countermeasures subcategory = 0 bulkheadProfiles = srf -title = Smoke Countermeasure Pod -manufacturer = Bahamuto Dynamics -description = Fires smoke-screen countermeasures for occluding laser points. +title = #loc_BDArmory_part_bahaSmokeCmPod_title //Smoke Countermeasure Pod +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaSmokeCmPod_description //Fires smoke-screen countermeasures for occluding laser points. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 0,1,0,0,1 - +tags = #loc_BDArmory_part_bahaSmokeCmPod_tags // --- standard part parameters --- mass = 0.003 dragModelType = default diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/flirBall.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/flirBall.cfg index 3fdc1a091..c0f718195 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/flirBall.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/flirBall.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0, -0.07813086, 0, 0, -1, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 -cost = 600 +entryCost = 2400 +cost = 1200 category = none bdacategory = Targeting subcategory = 0 bulkheadProfiles = srf -title = FLIR Targeting Ball -manufacturer = Bahamuto Dynamics -description = 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. +title = #loc_BDArmory_part_bahaFlirBall_title //FLIR Targeting Ball +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_bahaFlirBall_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaCamPod_tags // --- standard part parameters --- mass = 0.08 dragModelType = default @@ -54,9 +54,10 @@ MODULE name = ModuleTargetingCamera cameraTransformName = camTransform eyeHolderTransformName = eyeHolderTransform - zoomFOVs = 40,20,4,1.5 + zoomFOVs = 40,15.6,7.96,4,2,1.001,0.751,0.3756 gimbalLimit = 120 rollCameraModel = false + traverseRate = 180 } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/targetingCam.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/targetingCam.cfg index 0a35d0a6f..bcee18728 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/targetingCam.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/targetingCam/targetingCam.cfg @@ -20,18 +20,18 @@ node_stack_top = 0.0, 0.2792059, -0.1272891, 0, 1, 0, 0 // --- editor parameters --- TechRequired = precisionEngineering -entryCost = 2100 +entryCost = 750 cost = 600 category = none bdacategory = Targeting subcategory = 0 bulkheadProfiles = srf -title = AN/AAQ-28 Targeting Pod -manufacturer = Bahamuto Dynamics -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. +title = #loc_BDArmory_part_bahaCamPod_title //AN/AAQ-28 Targeting Pod +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - +tags = #loc_BDArmory_part_bahaCamPod_tags // --- standard part parameters --- mass = 0.2 dragModelType = default @@ -52,8 +52,10 @@ MODULE name = ModuleTargetingCamera cameraTransformName = camTransform eyeHolderTransformName = eyeHolderTransform - zoomFOVs = 40,15,3,1 + zoomFOVs = 40,15.6,7.96,4,2,1.001,0.751,0.3756,0.18779 gimbalLimit = 120 + maxRayDistance = 30000 + traverseRate = 90 } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/towLauncher/towLauncher.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/towLauncher/towLauncher.cfg index 8c0fb7590..6069f7b1a 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/towLauncher/towLauncher.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/towLauncher/towLauncher.cfg @@ -23,18 +23,18 @@ PART // --- editor parameters --- TechRequired = precisionEngineering - entryCost = 2100 - cost = 950 + entryCost = 1800 + cost = 600 category = none bdacategory = Missile turrets subcategory = 0 bulkheadProfiles = srf - title = Tow Launcher - manufacturer = Bahamuto Dynamics - description = 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. + title = #loc_BDArmory_part_towLauncherTurret_title //Tow Launcher + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_towLauncherTurret_description //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. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,1,0,1 - + tags = #loc_BDArmory_part_towLauncherTurret_tags // --- standard part parameters --- mass = 0.25 dragModelType = default diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg index f16b95f19..1783837b2 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg @@ -21,17 +21,17 @@ PART // --- editor parameters --- TechRequired = precisionEngineering entryCost = 2100 - cost = 120 + cost = 600 category = none bdacategory = Missiles subcategory = 0 bulkheadProfiles = srf - title = BGM-71 Tow Missile - manufacturer = Bahamuto Dynamics - description = Short distance, laser beam-riding, wireless anti-tank missile. + title = #loc_BDArmory_part_bahaTowMissile_title //BGM-71 Tow Missile + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaTowMissile_description //Short distance, laser beam-riding, wireless anti-tank missile. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 - + tags = #loc_BDArmory_part_bahaTowMissile_tags // --- standard part parameters --- mass = 0.021 dragModelType = default @@ -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,10 @@ 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 21744864c..3d13dcfea 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/weaponManager/weaponManager.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/weaponManager/weaponManager.cfg @@ -1,62 +1,68 @@ PART { -// Kerbal Space Program - Part Config -// -// - -// --- general parameters --- -name = missileController -module = Part -author = BahamutoD - -// --- asset parameters --- -mesh = model.mu -rescaleFactor = 1 - - -// --- node definitions --- -node_attach = 0.0, 0.036, 0, 0, -1, 0, 0 - - -// --- editor parameters --- -TechRequired = precisionEngineering -entryCost = 2100 -cost = 0 // 600 -category = none -bdacategory = Control -subcategory = 0 -bulkheadProfiles = srf -title = Weapon Manager -manufacturer = Bahamuto Dynamics -description = Cycle through missiles/bombs and fire them with a single button. -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 - -// --- standard part parameters --- -mass = 0.001 -dragModelType = default -maximum_drag = 0.2 -minimum_drag = 0.2 -angularDrag = 2 -crashTolerance = 25 -maxTemp = 3600 - -PhysicsSignificance = 1 - - -MODULE -{ - name = MissileFire -} + // Kerbal Space Program - Part Config + // + // -MODULE -{ - name = RadarWarningReceiver -} + // --- general parameters --- + name = missileController + module = Part + author = BahamutoD -MODULE -{ - name = ModuleWingCommander -} + // --- asset parameters --- + mesh = model.mu + rescaleFactor = 1 + + + // --- node definitions --- + node_attach = 0.0, 0.036, 0, 0, -1, 0, 0 + + + // --- editor parameters --- + TechRequired = precisionEngineering + entryCost = 2100 + cost = 0 // 600 + category = none + bdacategory = Control + subcategory = 0 + bulkheadProfiles = srf + title = #loc_BDArmory_part_missileController_title //Weapon Manager + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_missileController_description //Cycle through missiles/bombs and fire them with a single button. + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 + tags = #loc_BDArmory_part_missileController_tags + // --- standard part parameters --- + mass = 0 + dragModelType = none + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 25 + maxTemp = 3600 + + PhysicsSignificance = 1 + + + MODULE + { + name = MissileFire + } + + MODULE + { + name = RadarWarningReceiver + omniDetection = true //if RWR can detect everything, or only radar missiles + fieldOfView = 360 //FoV of RWR, default 360 deg. + } + + MODULE + { + name = ModuleWingCommander + } + DRAG_CUBE + { + none = True + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg b/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg index 86b065c25..ffc5ec38f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg @@ -5,7 +5,7 @@ RESOURCE_DEFINITION name = LaserBolt title = Laser Plasma density = .000168 // 1k 268kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -15,7 +15,7 @@ RESOURCE_DEFINITION name = 7.62x39Ammo title = 7.62x39 mm Ammo density = .000268 // 1k 268kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -26,7 +26,7 @@ RESOURCE_DEFINITION title = 7.7x56 mm Ammo abbreviation = 7.7/56 density = .000259 //1k 259kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -36,7 +36,7 @@ RESOURCE_DEFINITION title = 7.92x57mm Mau Ammo abbreviation = 7.92Mau density = .0001 //1k 100kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -46,7 +46,7 @@ RESOURCE_DEFINITION title = 9x19 mm Parabellum Ammo abbreviation = 9x19Para density = 0.000011838 //1k 188.85g x - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -55,7 +55,7 @@ RESOURCE_DEFINITION { name = 50CalAmmo density = .000116942 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -67,7 +67,7 @@ name = 20x21Ammo title 20x21 mm Ammo abbreviation = 20/21 density = 0.00011 //1k 110kg -flowMode = ALL_VESSEL +flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -76,7 +76,7 @@ RESOURCE_DEFINITION { name = 20x102Ammo density = .000259 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -87,7 +87,7 @@ RESOURCE_DEFINITION title = 20x163 mm Ammo abbreviation = 20/163 density = .0007 //1k 700kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -98,7 +98,7 @@ RESOURCE_DEFINITION title = 23x115 mm Ammo abbreviation = 23/115 density = 0.000498 //1k 498kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -108,7 +108,7 @@ RESOURCE_DEFINITION title = 23x152 mm Ammo abbreviation = 23/152 density = 0.000511 //1k 511kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -119,7 +119,7 @@ RESOURCE_DEFINITION abbreviation = 25/137 density = 0.000528 //1k 528kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -130,7 +130,7 @@ RESOURCE_DEFINITION title = 30x165 mm Ammo abbreviation = 30long density = 0.000727 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -140,7 +140,7 @@ RESOURCE_DEFINITION title = 30x173 mm Ammo abbreviation = 30AP density = 0.000741 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -150,7 +150,7 @@ RESOURCE_DEFINITION title = 30x173HE mm Ammo abbreviation = 30HE density = 0.000741 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -160,7 +160,7 @@ RESOURCE_DEFINITION title = 37 mm FlaK Ammo abbreviation = 37FlaK density = 0.0021 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -170,7 +170,7 @@ RESOURCE_DEFINITION title = 40x53 mm Ammo abbreviation = 40short density = 0.001690 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -180,7 +180,7 @@ RESOURCE_DEFINITION title = 40x53 mm HE Ammo abbreviation = 40HEshort density = 0.001690 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -190,7 +190,7 @@ RESOURCE_DEFINITION title = 40x311 mm Ammo abbreviation = 40/311 density = 0.00211 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -199,7 +199,7 @@ RESOURCE_DEFINITION name = 54cmMortarShells title = 54 cm Mortar Shell density = 1 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -208,8 +208,8 @@ RESOURCE_DEFINITION name = 57x438Ammo title = 57x438 mm Ammo abbreviation = 57/438 -density = .00151 -flowMode = ALL_VESSEL +density = .005675 +flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -220,7 +220,7 @@ RESOURCE_DEFINITION title = Tungsten Shell abbreviation = Tung density = .015968 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -231,7 +231,7 @@ name = 75x714Ammo title = 75x714 mm Ammo abbreviation = 75/714 density = .00151 -flowMode = ALL_VESSEL +flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -243,7 +243,7 @@ name = 76x636Ammo title = 76x636mm Ammo abbreviation = 76/636 density = 0.0125 -flowMode = ALL_VESSEL +flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -254,7 +254,7 @@ RESOURCE_DEFINITION title = 76.2 mm (3") Shell abbreviation = 3" density = 0.0056 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -265,7 +265,7 @@ RESOURCE_DEFINITION title = 90 mm (3.5") Shell abbreviation = 90mm density = 0.0147 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -276,7 +276,7 @@ RESOURCE_DEFINITION title = 100 mm (4") Shell abbreviation = 100mm density = 0.0187 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -284,7 +284,7 @@ RESOURCE_DEFINITION { name = CannonShells density = 0.0186 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -294,7 +294,7 @@ RESOURCE_DEFINITION title = 105mm Shell abbreviation = 105mm density = 0.0186 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -304,7 +304,7 @@ RESOURCE_DEFINITION title = 105mm HE Shell abbreviation = 105mmHE density = 0.0186 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -325,7 +325,7 @@ RESOURCE_DEFINITION title = 120 mm Shell abbreviation = 120mm density = 0.0187 //18.7kg - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -355,7 +355,7 @@ RESOURCE_DEFINITION title = 5 inch/62 Shell abbreviation = 5" //127 density = .03268 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -366,7 +366,7 @@ RESOURCE_DEFINITION title = 5inch1/4 Shell abbreviation = 5.25" density = .03568 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -397,7 +397,7 @@ RESOURCE_DEFINITION title = 155mm (6.1") Shell abbreviation = 6.1-155 density = 0.04562 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -417,7 +417,7 @@ RESOURCE_DEFINITION title = 203mm (8") Shell abbreviation = 8" density = 0.06602 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -467,7 +467,7 @@ RESOURCE_DEFINITION name = M65ShellAmmo title = M65 Shell density = .8522 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -520,7 +520,7 @@ RESOURCE_DEFINITION title = Type 4 AA Rocket abbreviation = Ty4AAR density = 0.022 //10 220 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = false } @@ -530,17 +530,17 @@ RESOURCE_DEFINITION title = Anti-Tank Rocket abbreviation = ATRa density = .008212 //10 8.2? - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } RESOURCE_DEFINITION { - name = Hades122rocket + name = Hades122Rocket title = Hades 122mm UG Rocket abbreviation = Had122 density = .0322 //10 322 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -550,7 +550,7 @@ RESOURCE_DEFINITION title = Hydra-70 rockets abbreviation = Hyd70 density = .0122 - flowMode = ALL_VESSEL + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true } @@ -559,9 +559,19 @@ RESOURCE_DEFINITION { name = S-8KOMRocket title = S-8KOM rockets - abbreviation = S8KOM - density = .0036 - flowMode = ALL_VESSEL + abbreviation = S8 + density = .0113 + flowMode = STACK_PRIORITY_SEARCH transfer = PUMP isTweakable = true -} \ No newline at end of file +} +RESOURCE_DEFINITION +{ + name = Rockets + title = Rockets + abbreviation = Rockets + density = .01 + flowMode = STACK_PRIORITY_SEARCH + transfer = PUMP + isTweakable = true +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Resources/Countermeasures.cfg b/BDArmory/Distribution/GameData/BDArmory/Resources/Countermeasures.cfg index ee0a31475..e3995b193 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Resources/Countermeasures.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Resources/Countermeasures.cfg @@ -23,4 +23,22 @@ RESOURCE_DEFINITION flowMode = ALL_VESSEL transfer = PUMP isTweakable = true -} \ No newline at end of file +} + +RESOURCE_DEFINITION +{ + name = CMBubbleCurtain + density = .002 + flowMode = ALL_VESSEL + transfer = PUMP + isTweakable = true +} + +RESOURCE_DEFINITION +{ + name = CMDecoy + density = .002 + flowMode = ALL_VESSEL + transfer = PUMP + isTweakable = true +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/BDArmoryMissileManeuverEnvelopePlotter.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/BDArmoryMissileManeuverEnvelopePlotter.py new file mode 100644 index 000000000..180aca290 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/BDArmoryMissileManeuverEnvelopePlotter.py @@ -0,0 +1,936 @@ +import numpy as np +import matplotlib.pyplot as plt +import math +from KSPFloatCurveCalc import FloatCurve +from os import system +import configparser + +## ---------------------------------- Instructions ---------------------------------- ## + +# To run this code, install numpy and matplotlib in your Python install via the +# "py -[version] -m pip install numpy" and "py -[version] -m pip install matplotlib" +# commands. Ensure you are installing these on the right version of Python. For more +# detailed instructions, there are a number of tutorials on YouTube and Google. + +# Edit the settings section to match your missile's parameters and your BD settings, +# then run the code. + +# Advanced users may replace the atmospheric model with their own model. Note that +# the provided model is a simplified one-equation fit of Kerbin's atmosphere, up to +# 37,879.4 m of altitude. + +# The code will output 4 graphs, the achievable gs at the given conditions, AoA limit, +# the required torque at the given conditions and the torque margin (the difference +# between the required torque and the available torque). A 5th graph will be output if +# there are risky conditions in the expected operating envelope of the missile, where +# there is a minimal torque margin and the AoA limit is below 1°, where an excessive +# steerMult may cause the missile to oscillate and lose control. + +## ---------------------------------------------------------------------------------- ## +## ------------------------------------ Settings ------------------------------------ ## +## ---------------------------------------------------------------------------------- ## + +# These are BD Armory global settings, if you don't know what they are, don't change them +GLOBAL_LIFT_MULTIPLIER = 0.25 +GLOBAL_DRAG_MULTIPLIER = 6 + +# Missile parameters +liftArea = 0.0020 +dragArea = 0.0020 +thrust = 25.0 +mass = 0.152 +maxTorque = 80 +maxTorqueAero = 0.0 +maxAoA = 45 +gLimit = 40 +gMargin = 0 + +# maxAltitude can be between 0 and 37879.4 m with the provided atmospheric model +maxAltitude = 35000 +maxAirspeed = 1600 + +# numAlt and numAirspeed are the number of steps that should be used. For altitude, +# it's the steps from 0 m to maxAltitude, and for airspeed it's from 0 to maxAirspeed +# though not inclusive of 0 airspeed (since it provides a trivial answer) +# Using a 1 for either will provide a 2D graph at maxAltitude or maxAirspeed +numAlt = 50 +numAirspeed = 1000 + +## ---------------------------------------------------------------------------------- ## +## -------------------------------- Atmospheric Model ------------------------------- ## +## ---------------------------------------------------------------------------------- ## + +# This function returns the density for a given altitude, it can be replaced with +# any atmospheric model of your choice, however the function must work with numpy +# matrices, returning a matrix of the same shape. +def rhoFromAlt(alt): + return (2.522601349 / (1 + np.exp(0.000200946 * alt + 0.0575766))) + +## ---------------------------------------------------------------------------------- ## +## --------------------------------- Basic Functions -------------------------------- ## +## ---------------------------------------------------------------------------------- ## + +DEG2RAD = np.pi / 180 +RAD2DEG = 180 / np.pi + +liftCurveKeys = [ + np.array([0, 0, 0.04375, 0.04375]), + np.array([8, 0.35, 0.04801136, 0.04801136]), + np.array([30, 1.5]), + np.array([65, 0.6]), + np.array([90, 0.7]) + ] + +liftCurve = FloatCurve(liftCurveKeys) + +dragCurveKeys = [ + np.array([0, 0.00215, 0, 0]), + np.array([5, 0.00285, 0.0002775, 0.0002775]), + np.array([30, 0.01, 0.0002142857, 0.01115385]), + np.array([55, 0.3, 0.008434067, 0.008434067]), + np.array([90, 0.5, 0.005714285, 0.005714285]) + ] + +dragCurve = FloatCurve(dragCurveKeys) + +def calcAeroPerf(q, liftArea, dragArea, thrust, mass, AoA): + lift = q * liftArea * GLOBAL_LIFT_MULTIPLIER * liftCurve.Evaluate(AoA) + drag = q * dragArea * GLOBAL_DRAG_MULTIPLIER * dragCurve.Evaluate(AoA) + + torque = lift * np.cos(AoA * DEG2RAD) + drag * np.sin(AoA * DEG2RAD) + + gAchieved = (lift + thrust * np.sin(AoA * DEG2RAD)) / (mass * 9.80665) + + return [gAchieved, torque] + +## ---------------------------------------------------------------------------------- ## +## ------------------------------------ g Limiter ----------------------------------- ## +## ---------------------------------------------------------------------------------- ## + +TRatioInflec1 = 1.181181181181181 # Thrust to Lift Ratio (at AoA of 30) where the maximum occurs after the 65 degree mark +TRatioInflec2 = 2.242242242242242 # Thrust to Lift Ratio (at AoA of 30) where a local maximum no longer exists, above this every section must be searched + +AoACurveKeys = [ + np.array([0.0000000000, 30.0000000000, 5.577463, 5.577463]), + np.array([0.7107107107, 33.9639639640, 6.24605, 6.24605]), + np.array([1.5315315315, 39.6396396396, 8.396343, 8.396343]), + np.array([1.9419419419, 43.6936936937, 12.36403, 12.36403]), + np.array([2.1421421421, 46.6666666667, 19.63926, 19.63926]), + np.array([2.2122122122, 48.3783783784, 34.71423, 34.71423]), + np.array([2.2422422422, 49.7297297297, 44.99994, 44.99994]) + ] # Floatcurve containing AoA of (local) max acceleration + # for a given thrust to lift (at the max CL of 1.5 at 30 degrees of AoA) ratio. Limited to a max + # of TRatioInflec2 where a local maximum no longer exists + +AoACurve = FloatCurve(AoACurveKeys) + +AoAEqCurveKeys = [ + np.array([1.1911911912, 89.6396396396, -53.40001, -53.40001]), + np.array([1.3413413413, 81.6216216216, -49.69999, -49.69999]), + np.array([1.5215215215, 73.3333333333, -37.62499, -37.62499]), + np.array([1.7217217217, 67.4774774775, -24.31731, -24.31731]), + np.array([1.9819819820, 62.4324324324, -24.09232, -24.09232]), + np.array([2.1821821822, 56.6666666667, -48.1499, -48.1499]), + np.array([2.2422422422, 52.6126126126, -67.49978, -67.49978]) + ] # Floatcurve containing AoA after which the acceleration goes above + # that of the local maximums'. Only exists between TRatioInflec1 and TRatioInflec2. + +AoAEqCurve = FloatCurve(AoAEqCurveKeys) + +gMaxCurveKeys = [ + np.array([0.0000000000, 1.5000000000, 0.8248255, 0.8248255]), + np.array([1.2012012012, 2.4907813293, 0.8942869, 0.8942869]), + np.array([1.9119119119, 3.1757276995, 1.019205, 1.019205]), + np.array([2.2422422422, 3.5307206802, 1.074661, 1.074661]) + ] # Floatcurve containing max acceleration times the mass (total force) + # normalized by q*S*GLOBAL_LIFT_MULTIPLIER for TRatio between 0 and TRatioInflec2. Note that after TRatioInflec1 + # this becomes a local maxima not a global maxima. This is used to narrow down what part of the curve we should + # solve on. + +gMaxCurve = FloatCurve(gMaxCurveKeys) + +# Linearized CL v.s. AoA curve to enable fast solving. Algorithm performs bisection using the fast calculations of the bounds +# and then performs a linear solve +linAoA = [ 0, 10, 24, 30, 38, 57, 65, 90 ] +linCL = [ 0, 0.454444597111092, 1.34596044049850, 1.5, 1.38043381924198, 0.719566180758018, 0.6, 0.7 ] +# Sin at the points +linSin = [ 0, 0.173648177666930, 0.406736643075800, 0.5, 0.615661475325658, 0.838670567945424, 0.906307787036650, 1 ] +# Slope of CL at the intervals +linSlope = [ 0.0454444597111092, 0.0636797030991005, 0.0256732599169169, -0.0149457725947522, -0.0347825072886297, -0.0149457725947522, 0.004 ] +# y-Intercept of line at those intervals +linIntc = [ 0, -0.182352433879912, 0.729802202492494, 1.94837317784257, 2.70216909620991, 1.57147521865889, 0.34 ] + +def getGLimit(q, mass, liftArea, thrust, gLim, margin, maxAoA): + gLimited = False + # Force required to reach g-limit + gLim *= (mass * 9.80665) + + currAoA = maxAoA + + interval = 0 + + # Factor by which to multiply the lift coefficient to get lift, it's the dynamic pressure times the lift area times + # the global lift multiplier + qSk = q * liftArea * GLOBAL_LIFT_MULTIPLIER + + currG = 0 + + # If we're in the post thrust state + if (thrust == 0): + # If the maximum lift achievable is not enough to reach the request accel + # the we turn to the AoA required for max lift + if (gLim > 1.5 * qSk): + currAoA = 30 + else: + # Otherwise, first we calculate the lift in interval 2 (between 24 and 30 AoA) + currG = linCL[2] * qSk; # CL(alpha)*qSk + thrust*sin(alpha) + + # If the resultant g at 24 AoA is < gLim then we're in interval 2 + if (currG < gLim): + interval = 2 + else: + # Otherwise check interval 1 + currG = linCL[1] * qSk + + if (currG > gLim): + # If we're still > gLim then we're in interval 0 + interval = 0 + else: + # Otherwise we're in interval 1 + interval = 1 + + # Calculate AoA for G, since no thrust we can use the faster linear equation + currAoA = calcAoAforGLinear(qSk, gLim, linSlope[interval], linIntc[interval], 0) + + # Are we gLimited? + gLimited = currAoA < maxAoA + return currAoA if gLimited else maxAoA + else: + # If we're under thrust, first calculate the ratio of Thrust to lift at max CL + TRatio = thrust / (1.5 * qSk) + + # Initialize bisection limits + LHS = 0 + RHS = 7 + + if (TRatio < TRatioInflec2): + # If we're below TRatioInflec2 then we know there's a local max + currG = gMaxCurve.Evaluate(TRatio) * qSk + + if (TRatio > TRatioInflec1): + # If we're above TRatioInflec1 then we know it's only a local max + + # First calculate the allowable force margin + # This exists because drag gets very bad above the local max + margin = max(margin, 0) + margin *= mass + + if (currG + margin < gLim): + # If we're within the margin + if (currG > gLim): + # And our local max is > gLim, then we know that + # there is a solution. Calculate the AoAMax + # where the local max occurs + AoAMax = AoACurve.Evaluate(TRatio) + + # And determine our right hand bound based on + # our AoAMax + if (AoAMax > linAoA[4]): + RHS = 5 + elif (AoAMax > linAoA[3]): + RHS = 4 + else: + RHS = 3 + else: + # If our local max is < gLim then we can simply set + # our AoA to be the AoA of the local max + currAoA = AoACurve.Evaluate(TRatio) + gLimited = currAoA < maxAoA + return currAoA if gLimited else maxAoA + else: + # If we're not within the margin then we need to consider + # the high AoA section. First calculate the absolute maximum + # g we can achieve + currG = 0.7 * qSk + thrust + + # If the absolute maximum g we can achieve is not enough, then return + # the local maximum in order to preserve energy + if (currG < gLim): + currAoA = AoACurve.Evaluate(TRatio) + gLimited = currAoA < maxAoA + return currAoA if gLimited else maxAoA + + # If we're within the limit, then find the AoA where the normal force + # once again reaches the local max value + AoAEq = AoAEqCurve.Evaluate(TRatio) + + # And determine the left hand bound from there + if (AoAEq > linAoA[6]): + # If we're in the final section then just calculate it directly + currAoA = calcAoAforGNonLin(qSk, gLim, linSlope[6], linIntc[6], 0) + gLimited = currAoA < maxAoA + return currAoA if gLimited else maxAoA + elif (AoAEq > linAoA[5]): + LHS = 5 + else: + LHS = 4 + else: + # If we're not above TRatioInflec1 then we only have to consider the + # curve up to the local max + AoAMax = AoACurve.Evaluate(TRatio) + + # Determine the right hand bound for calculation + if (gLim < currG): + if (AoAMax > linAoA[3]): + RHS = 4 + else: + RHS = 3 + else: + gLimited = currAoA < maxAoA + return currAoA if gLimited else maxAoA + else: + # If we're above TRatioInflec2 then we have to search the whole thing, but past that ratio + # the function is monotonically increasing so it's OK + + # That being said, first calculate the absolute maximum + # g we can achieve + currG = 0.7 * qSk + thrust + + # If the absolute maximum g we can achieve is not enough, then return + # max AoA + if (currG < gLim): + return maxAoA + + currG = linCL[RHS] * qSk + thrust * linSin[RHS] + if (currG < gLim): + return maxAoA + + # Bisection search + while ( (RHS - LHS) > 1): + interval = math.floor(0.5 * (RHS + LHS)) + + currG = linCL[interval] * qSk + thrust * linSin[interval] + + if (currG < gLim): + LHS = interval + else: + RHS = interval + + if (LHS == 0): + # If we're below 15 (here 10 degrees) then use the linear approximation for sin + currAoA = calcAoAforGLinear(qSk, gLim, linSlope[LHS], linIntc[LHS], thrust) + else: + # Otherwise use the second order approximation centered at pi/2 + currAoA = calcAoAforGNonLin(qSk, gLim, linSlope[LHS], linIntc[LHS], thrust) + + gLimited = currAoA < maxAoA + return currAoA if gLimited else maxAoA + # Pseudocode / logic + # If T = 0 + # We know it's in the first section. If m*gReq > (1.5*q*k*s) then set to min of maxAoA and 30 (margin?). If + # < then we first make linear estimate, then solve by bisection of intervals first -> solve on interval. + # If TRatio < TRatioInflec2 + # First we check the endpoints -> both gMax, and, if TRatio > TRatioInflec1, then 0.7*q*S*k + T (90 degree case). + # If gMax > m*gReq then the answer < AoACurve -> Determine where it is via calculating the pre-calculated points + # then seeing which one has gCalc > m*gReq, using the interval bounded by the point with gCalc > m*gReq on the + # right end. Use bisection -> we know it's bounded at the RHS by the 38 or the 57 section. We can compare the + # AoACurve with 38, if > 38 then use 57 as the bound, otherwise bisection with 38 as the bound. Using this to + # determine which interval we're looking at, we then calc AoACalc. Return the min of maxAoA and AoACalc. + # If gMax < m*gReq, then if TRatio < TRatioInflec1, set to min of AoACurve and maxAoA. If TRatio > TRatioInflec1 + # then we look at the 0.3*q*S*k + T. If < m*gReq then we'll set it to the min of maxAoA and either AoACurve or + # 90, depends on the margin. See below. If > m*gReq then it's in the last two sections, bound by AoAEq on the LHS. + # If AoAEq > 65, then we solve on the last section. If AoAEq < 65 then we check the point at AoA = 65 using the + # pre-calculated values. If > m*gReq then we know that it's in the 57-65 section, otherwise we know it's in the + # 65-90 section. + # Consider adding a margin, if gMax only misses m*gReq by a little we should probably avoid going to the higher + # angles as it adds a lot of drag. Maybe distance based? User settable? + # If TRatio > TRatioInflec2 then we have a continuously monotonically increasing function + # We use the fraction m*gReq/(0.3*q*S*k + T) to determine along which interval we should solve, noting that this + # is an underestimate of the thrust required. (Maybe use arcsin for a more accurate estimate? Costly.) Then simply + # calculate the pre-calculated value at the next point -> bisection and solve on the interval. + + # For all cases, if AoA < 15 then we can use the linear approximation of sin, if an interval includes both AoA < 15 + # and AoA > 15 then try < 15 (interval 2) first, then if > 15 try the non-linear starting from 15. Otherwise we use + # non-linear equation. + +# Calculate AoA for a given g loading, given m*g, the dynamic pressure times the lift area times the lift multiplier, +# the linearized approximation of the AoA curve (in slope, y-intercept form) and the thrust. Linear uses a linear +# small angle approximation for sin and non-linear uses a 2nd order approximation of sin about pi/2 +def calcAoAforGLinear(qSk, mg, CLalpha, CLintc, thrust): + return (mg - CLintc * qSk) / (CLalpha * qSk + thrust * DEG2RAD) + +def calcAoAforGNonLin(qSk, mg, CLalpha, CLintc, thrust): + CLalpha *= qSk + return (2 * CLalpha + np.pi * thrust * DEG2RAD - 2 * np.sqrt(CLalpha * CLalpha + np.pi * thrust * DEG2RAD * CLalpha + 2 * thrust * (CLintc * qSk + thrust - mg) * DEG2RAD * DEG2RAD)) / (2 * thrust * DEG2RAD * DEG2RAD) + +## ---------------------------------------------------------------------------------- ## +## --------------------------------- Torque Limiter --------------------------------- ## +## ---------------------------------------------------------------------------------- ## + +torqueAoAReturnKeys = [ + np.array([2.6496350364963499, 88.7129999999999939, -106.9758, -106.9758]), + np.array([2.73134328358208922, 79.9722000000000008, -70.59726, -70.59726]), + np.array([3.14937759336099621, 65.6675999999999931, -28.9337, -28.9337]), + np.array([3.52488687782805465, 56.7873000000000019, -31.87921, -31.87921]), + np.array([3.69483568075117441, 49.9707000000000008, -61.73428, -61.73428]), + np.array([3.76190476190476275, 44.3798999999999992, -83.35883, -18.59649]), + np.array([3.83091787439613629, 43.0964999999999989, -23.74979, -23.74979]), + np.array([3.92610837438423754, 40.3451999999999984, -28.9031, -28.9031]) + ] + +torqueAoAReturn = FloatCurve(torqueAoAReturnKeys) + +# Note we use linAoA for this as well +linLiftTorque = [ 0, 0.449212170675488687, 1.23071251302548967, 1.29903810567665712, 1.08779669420507852, 0.391903830317496704, 0.253570957044423284, 0 ] +linDragTorque = [ 0, 0.000748453415988048856, 0.00346671023416293559, 0.00499999999999927499, 0.0656669812489726473, 0.26524150275361541, 0.336257675049945692, 0.5 ] + +# Slope of cos * CL at the intervals +linLiftTorqueSlope = [ 0.0449212178074, 0.0558214214286, 0.0113876666667, -0.026405125, -0.0366259562991, -0.0172916091591, -0.0101428382818 ] +# y-Intercept of line at those intervals +linLiftTorqueIntc = [ 0, -0.109002114286, 0.957408, 2.09119175, 2.47958333937, 1.37752555239, 0.91285544536 ] + +# Slope of sin * CD at the intervals +linDragTorqueSlope = [ 0.000166666666667, 0.000166666666667, 0.000166666666667, 0.00691309375, 0.0107346842105, 0.009046, 0.00653472 ] +# y-Intercept of line at those intervals +linDragTorqueIntc = [ 0, 0, 0, -0.2023928125, -0.347613, -0.251358, -0.0881248 ] + +DLRatioInflec1 = 2.63636363636363624 +DLRatioInflec2 = 3.92610837438423754 + +torqueBounds = [-1, 7] +torqueAoABounds = [-1.0, -1.0, -1.0] + +# Algorithm is similar to getGLimit, except in this case we only calculate which sections to search in whenever +# the liftArea and dragArea change. We define this using a set of numbers, torqueAoAReturn, the AoA at which torque goes past the local +# maximum which occurs at around 28° AoA, if this is set to -1, then we ONLY search the lower portion of the plot and torqueMaxLocal which +# gives the non-dim torque (which needs to be pre-multiplied by the SUM of liftArea * liftMult and dragArea * dragMult before being saved) +# which is +ve when there's a local maximum, it is set to the negative of the number when a local maximum does not exist, it is not set to +# -1 instead as it still provides a useful bisection point, with which we can determine the first LHS/RHS index. +#public static float[] linAoA = { 0f, 10f, 24f, 30f, 38f, 57f, 65f, 90f }; +def setupTorqueAoALimit(liftArea, dragArea): + global torqueBounds + global torqueAoABounds + + # Drag / Lift ratio + DL = (GLOBAL_DRAG_MULTIPLIER * dragArea)/(GLOBAL_LIFT_MULTIPLIER * liftArea) + # The % contribution of drag, note that this will error out if there's no drag, + # but that's not supposed to happen. + SkR = DL / (DL + 1) + + # If we're above DLRationInflec2 then we must search the whole range of AoAs + if (DL < DLRatioInflec2): + # If we're below DLRatioInflec1 then we're bounded on the right by 30° + if (DL < DLRatioInflec1): + torqueBounds = [3, -1] + else: + AoARHS = torqueAoAReturn.Evaluate(DL) + if (AoARHS > linAoA[6]): + torqueBounds = [6, 7] + elif (AoARHS > linAoA[5]): + torqueBounds = [5, 7] + else: + torqueBounds = [4, 7] + + torqueAoABounds[2] = AoARHS + # This AoA happens to be a linear function of D/L + torqueAoABounds[0] = 0.0307482 * DL + 28.49333 + # This non-dimensionalized torque happens to be a + # linear function of SkR + torqueAoABounds[1] = -1.30417 * SkR + 1.30879 + + return [torqueBounds, torqueAoABounds] + +def getTorqueAoALimit(q, liftArea, dragArea, maxTorque): + # Technically not required, but in case anyone starts allowing for the CoL to vary + CoLDist = 1 + + # Divide out the dynamic pressure and CoLDist components of torque + maxTorque /= q * CoLDist + maxTorque *= 0.8 # Let's only go up to 80% of maxTorque to leave some leeway + + LHS = 0 + RHS = 7 + interval = 3 + + # Drag and Lift Area multipliers + dragSk = dragArea * GLOBAL_DRAG_MULTIPLIER + liftSk = liftArea * GLOBAL_LIFT_MULTIPLIER + + # Here we store the AoA of local max torque, we set it to 180f as for the case + # where the entire range must be searched, this gives the correct AoA + currAoA = 180 + + if (torqueBounds[0] > 0): + # If we have a left torque bound then we don't need to search the entire range + torqueMaxLocal = torqueAoABounds[0] + currAoA = torqueAoABounds[1] + + if (torqueBounds[1] > 0): + # If we have a right torque bound then we need to determine if we're searching + # in the low AoA or the high AoA section, this is decided by if the maxTorque + # is greater than torqueAoABounds times dragSk + liftSk + if (maxTorque > torqueMaxLocal * (dragSk + liftSk)): + # If maxTorque exceeds the max aerodynamic torque possible, then just return 180f + if (maxTorque > (liftSk * linLiftTorque[7] + dragSk * linDragTorque[7])): + return 180 + + LHS = torqueBounds[0] + RHS = torqueBounds[1] + else: + RHS = torqueBounds[0] + else: + # If we don't have a right torque bound then we're bound only by the low + # AoA section, and hence can return immediately if torque exceeds the localMax + if (maxTorque > torqueMaxLocal * (dragSk + liftSk)): + return 180 + + # Otherwise we just search the low AoA portion + RHS = torqueBounds[0] + else: + # If maxTorque exceeds the max aerodynamic torque possible, then just return 180f + if (maxTorque > (liftSk * linLiftTorque[7] + dragSk * linDragTorque[7])): + return 180 + + currTorque = 0 + + # Bisection search + while ((RHS - LHS) > 1): + interval = math.floor(0.5 * (RHS + LHS)) + + currTorque = liftSk * linLiftTorque[interval] + dragSk * linDragTorque[interval] + + if (currTorque < maxTorque): + LHS = interval + else: + RHS = interval + + currAoA = (maxTorque - (linLiftTorqueIntc[LHS] * liftSk + linDragTorqueIntc[LHS] * dragSk)) / (linLiftTorqueSlope[LHS] * liftSk + linDragTorqueSlope[LHS] * dragSk) + return currAoA + +## ---------------------------------------------------------------------------------- ## +## ----------------------------------- Calculation ---------------------------------- ## +## ---------------------------------------------------------------------------------- ## +def calculate(): + global plotWarn + global gAchievedMat + global AoAMat + global torqueMat + global torqueMarginMat + global warnTorqueMat + global turnRateMat + + gAchievedMat = np.zeros(qMat.shape) + AoAMat = np.zeros(qMat.shape) + torqueMat = np.zeros(qMat.shape) + torqueMarginMat = np.zeros(qMat.shape) + warnTorqueMat = np.zeros(qMat.shape) + + plotWarn = False + + for j in range(numAlt): + for i in range(numAirspeed): + currq = qMat[i, j] + + currMaxTorque = maxTorque + currq * maxTorqueAero + + currAoA = getGLimit(currq, mass, liftArea, thrust, gLimit, gMargin, maxAoA) + + currAoA = min(currAoA, getTorqueAoALimit(currq, liftArea, dragArea, currMaxTorque)) + + [currgAchieved, currTorque] = calcAeroPerf(currq, liftArea, dragArea, thrust, mass, currAoA) + + gAchievedMat[i, j] = currgAchieved + AoAMat[i, j] = currAoA + torqueMat[i, j] = currTorque + torqueMarginMat[i, j] = currMaxTorque - currTorque + + if (currTorque / currMaxTorque > 0.95 and currAoA < 1): + plotWarn = True + warnTorqueMat[i, j] = 1.0 + + turnRateMat = 9.80665 * np.divide(gAchievedMat, speedMat) * RAD2DEG + turnRateMat[speedMat < 100] = 0.0 + +## ---------------------------------------------------------------------------------- ## +## ------------------------------------ Plotting ------------------------------------ ## +## ---------------------------------------------------------------------------------- ## + +def plot(): + if (numAirspeed > 1 and numAlt > 1): + plt.figure(1) + ax = plt.axes(projection = '3d') + ax.plot_surface(altMat, speedMat, gAchievedMat, cmap=plt.cm.jet, + linewidth=0, antialiased=False) + plt.title('Achievable g-loading') + ax.set_xlabel('Altitude (m)') + ax.set_ylabel('Airspeed (m/s)') + ax.set_zlabel('Achievable gs') + + plt.figure(2) + ax = plt.axes(projection = '3d') + ax.plot_surface(altMat, speedMat, turnRateMat, cmap=plt.cm.jet, + linewidth=0, antialiased=False) + plt.title('Instantaneous Turn Rate') + ax.set_xlabel('Altitude (m)') + ax.set_ylabel('Airspeed (m/s)') + ax.set_zlabel('Turn Rate (°/s)') + + plt.figure(3) + ax = plt.axes(projection = '3d') + ax.plot_surface(altMat, speedMat, AoAMat, cmap=plt.cm.jet, + linewidth=0, antialiased=False) + plt.title('AoA Limit') + ax.set_xlabel('Altitude (m)') + ax.set_ylabel('Airspeed (m/s)') + ax.set_zlabel('AoA Limit (°)') + + plt.figure(4) + ax = plt.axes(projection = '3d') + ax.plot_surface(altMat, speedMat, torqueMat, cmap=plt.cm.jet, + linewidth=0, antialiased=False) + plt.title('Required Torque') + ax.set_xlabel('Altitude (m)') + ax.set_ylabel('Airspeed (m/s)') + ax.set_zlabel('Torque (kN-m)') + + plt.figure(5) + ax = plt.axes(projection = '3d') + ax.plot_surface(altMat, speedMat, torqueMarginMat, cmap=plt.cm.jet, + linewidth=0, antialiased=False) + plt.title('Torque Margin') + ax.set_xlabel('Altitude (m)') + ax.set_ylabel('Airspeed (m/s)') + ax.set_zlabel('Torque Margin (kN-m)') + + if (plotWarn): + plt.figure(6) + ax = plt.axes(projection = '3d') + ax.plot_surface(altMat, speedMat, warnTorqueMat, cmap=plt.cm.jet, + linewidth=0, antialiased=False) + plt.title('Warning! Potential Instability With High steerMult!') + ax.set_xlabel('Altitude (m)') + ax.set_ylabel('Airspeed (m/s)') + + elif (numAirspeed > 1): + plt.figure(1) + plt.plot(speedMat, gAchievedMat) + plt.title(f'Achievable g-loading at Altitude {maxAltitude} m') + plt.xlabel('Airspeed (m/s)') + plt.ylabel('Achievable gs') + + plt.figure(2) + plt.plot(speedMat, turnRateMat) + plt.title(f'Instantaneous Turn Rate at Altitude {maxAltitude} m') + plt.xlabel('Airspeed (m/s)') + plt.ylabel('Turn Rate (°/s)') + + plt.figure(3) + plt.plot(speedMat, AoAMat) + plt.title(f'AoA Limit at Altitude {maxAltitude} m') + plt.xlabel('Airspeed (m/s)') + plt.ylabel('AoA Limit (°)') + + plt.figure(4) + plt.plot(speedMat, torqueMat) + plt.title(f'Required Torque at Altitude {maxAltitude} m') + plt.xlabel('Airspeed (m/s)') + plt.ylabel('Torque (kN-m)') + + plt.figure(5) + plt.plot(speedMat, torqueMarginMat) + plt.title(f'Torque Margin at Altitude {maxAltitude} m') + plt.xlabel('Airspeed (m/s)') + plt.ylabel('Torque (kN-m)') + + if (plotWarn): + plt.figure(6) + plt.plot(speedMat, warnTorqueMat) + plt.title(f'Warning! Potential Instability With High steerMult! at Altitude {maxAltitude} m') + plt.xlabel('Airspeed (m/s)') + else: + plt.figure(1) + plt.plot(altMat[0], gAchievedMat[0]) + plt.title(f'Achievable g-loading at Airspeed {maxAirspeed} m/s') + plt.xlabel('Altitude (m)') + plt.ylabel('Achievable gs') + + plt.figure(2) + plt.plot(altMat[0], turnRateMat[0]) + plt.title(f'Instantaneous Turn Rate at Airspeed {maxAirspeed} m/s') + plt.xlabel('Altitude (m)') + plt.ylabel('Turn Rate (°/s)') + + plt.figure(3) + plt.plot(altMat[0], AoAMat[0]) + plt.title(f'AoA Limit at Airspeed {maxAirspeed} m/s') + plt.xlabel('Altitude (m)') + plt.ylabel('AoA Limit (°)') + + plt.figure(4) + plt.plot(altMat[0], torqueMat[0]) + plt.title(f'Required Torque at Airspeed {maxAirspeed} m/s') + plt.xlabel('Altitude (m)') + plt.ylabel('Torque (kN-m)') + + plt.figure(5) + plt.plot(altMat[0], torqueMarginMat[0]) + plt.title(f'Torque Margin at Airspeed {maxAirspeed} m/s') + plt.xlabel('Altitude (m)') + plt.ylabel('Torque (kN-m)') + + if (plotWarn): + plt.figure(6) + plt.plot(altMat[0], warnTorqueMat[0]) + plt.title(f'Warning! Potential Instability With High steerMult! at Airspeed {maxAirspeed} m/s') + plt.xlabel('Altitude (m)') + plt.draw() + plt.pause(0.05) + input('Press Enter to close plots...') + plt.close('all') + +## ---------------------------------------------------------------------------------- ## +## -------------------------------------- Setup ------------------------------------- ## +## ---------------------------------------------------------------------------------- ## + +def setup(): + global altMat + global speedMat + global qMat + global maxAltitude + + if (maxAltitude > 37879.4): + print('Warning! Max altitude > atmospheric model, reducing to 37879.4 m.\n' + 'If you have altered the atmospheric model, please change this check in the setup() function.') + maxAltitude = 37879.4 + + if (numAlt > 1): + altArr = np.linspace(0, maxAltitude, numAlt) + else: + altArr = maxAltitude + + if (numAirspeed > 1): + speedArr = np.linspace(maxAirspeed / numAirspeed, maxAirspeed, numAirspeed) + else: + speedArr = maxAirspeed + + altMat, speedMat = np.meshgrid(altArr, speedArr) + rhoMat = rhoFromAlt(altMat) + qMat = 0.5 * np.multiply(rhoMat, np.square(speedMat)) + + setupTorqueAoALimit(liftArea, dragArea) + +## ---------------------------------------------------------------------------------- ## +## ------------------------------------- Display ------------------------------------ ## +## ---------------------------------------------------------------------------------- ## + +def display(): + system("clear||cls") + print(f'## ---------------------------------------------------------------------------------- ##\n' + f'## --------------------- BDA+ Missile Maneuver Envelope Plotter --------------------- ##\n' + f'## ----------------------------------- by: BillNye ---------------------------------- ##\n' + f'## ---------------------------------------------------------------------------------- ##\n' + f'\n' + f'Current Missile Parameters:\n' + f'liftArea = {liftArea}\n' + f'dragArea = {dragArea}\n' + f'thrust = {thrust}\n' + f'mass = {mass}\n' + f'maxTorque = {maxTorque}\n' + f'maxTorqueAero = {maxTorqueAero}\n' + f'maxAoA = {maxAoA}\n' + f'gLimit = {gLimit}\n' + f'gMargin = {gMargin}\n' + f'\n' + f'Current BDA+ Settings:\n' + f'GLOBAL_LIFT_MULTIPLIER = {GLOBAL_LIFT_MULTIPLIER}\n' + f'GLOBAL_DRAG_MULTIPLIER = {GLOBAL_DRAG_MULTIPLIER}\n' + f'\n' + f'Current Calculation Settings:\n' + f'maxAltitude = {maxAltitude} m\n' + f'maxAirspeed = {maxAirspeed} m/s\n' + f'numAlt = {numAlt}\n' + f'numAirspeed = {numAirspeed}\n' + f'\n' + f'Ready to Plot = {calculationComplete}\n') + +## ---------------------------------------------------------------------------------- ## +## -------------------------------------- Main -------------------------------------- ## +## ---------------------------------------------------------------------------------- ## + +setupComplete = False +calculationComplete = False +config = configparser.ConfigParser() + +while (True): + display() + + if (not setupComplete): + setup() + setupComplete = True + + command = input().lower() + display() + + match command: + case 'calculate' | 'c': + print('Calculation in progress...') + calculate() + calculationComplete = True + case 'plot' | 'p': + if (not calculationComplete): + print('Calculation in progress...') + calculate() + plot() + case 'liftarea' | 'la': + liftArea = float(input('liftArea = ')) + calculationComplete = False + case 'dragarea' | 'da': + dragArea = float(input('dragArea = ')) + calculationComplete = False + case 'thrust' | 't': + thrust = float(input('thrust = ')) + calculationComplete = False + case 'mass' | 'm': + mass = float(input('mass = ')) + calculationComplete = False + case 'maxtorque' | 'mt': + maxTorque = float(input('maxTorque = ')) + calculationComplete = False + case 'maxtorqueaero' | 'mta': + maxTorqueAero = float(input('maxTorqueAero = ')) + calculationComplete = False + case 'maxaoa' | 'aoa': + maxAoA = float(input('maxAoA = ')) + calculationComplete = False + case 'glimit' | 'g': + gLimit = float(input('gLimit = ')) + calculationComplete = False + case 'gmargin' | 'gm': + gMargin = float(input('gMargin = ')) + calculationComplete = False + case 'global_lift_multiplier' | 'glm': + GLOBAL_LIFT_MULTIPLIER = float(input('GLOBAL_LIFT_MULTIPLIER = ')) + calculationComplete = False + case 'global_drag_multiplier' | 'gdm': + GLOBAL_DRAG_MULTIPLIER = float(input('GLOBAL_DRAG_MULTIPLIER = ')) + calculationComplete = False + case 'maxaltitude' | 'alt': + maxAltitude = float(input('maxAltitude = ')) + calculationComplete = False + setupComplete = False + case 'maxairspeed' | 'speed': + maxAirspeed = float(input('maxAirspeed = ')) + calculationComplete = False + setupComplete = False + case 'numalt' | 'nalt': + numAlt = int(input('numAlt = ')) + calculationComplete = False + setupComplete = False + case 'numairspeed' | 'nspeed': + numAirspeed = int(input('numAirspeed = ')) + calculationComplete = False + setupComplete = False + case 'load' | 'l': + filename = input('filename = ') + '.ini' + success = True + try: + config.read(filename) + except: + print(f'file: "{filename}" is formatted incorrectly!') + success = False + input('Press Enter to continue...') + + if (len(config.sections()) == 0): + print(f'file: "{filename}" does not exist!') + success = False + input('Press Enter to continue...') + + if (success): + if config.has_section('Missile'): + calculationComplete = False + for key in config['Missile']: + match key.lower(): + case 'liftarea': + liftArea = float(config['Missile'][key]) + case 'dragarea': + dragArea = float(config['Missile'][key]) + case 'thrust': + thrust = float(config['Missile'][key]) + case 'mass': + mass = float(config['Missile'][key]) + case 'maxtorque': + maxTorque = float(config['Missile'][key]) + case 'maxtorqueaero': + maxTorqueAero = float(config['Missile'][key]) + case 'maxaoa': + maxAoA = float(config['Missile'][key]) + case 'glimit': + gLimit = float(config['Missile'][key]) + case 'gmargin': + gMargin = float(config['Missile'][key]) + + if config.has_section('BDSettings'): + calculationComplete = False + for key in config['BDSettings']: + match key.lower(): + case 'global_lift_multiplier': + GLOBAL_LIFT_MULTIPLIER = float(config['BDSettings'][key]) + case 'global_drag_multiplier': + GLOBAL_DRAG_MULTIPLIER = float(config['BDSettings'][key]) + + if config.has_section('EnvelopeSettings'): + calculationComplete = False + setupComplete = False + for key in config['EnvelopeSettings']: + match key.lower(): + case 'maxaltitude': + maxAltitude = float(config['EnvelopeSettings'][key]) + case 'maxairspeed': + maxAirspeed = float(config['EnvelopeSettings'][key]) + case 'numalt': + numAlt = int(config['EnvelopeSettings'][key]) + case 'numairspeed': + numAirspeed = int(config['EnvelopeSettings'][key]) + case 'save' | 's': + filename = input('filename = ') + config = configparser.ConfigParser() + + config['BDSettings'] = {'GLOBAL_LIFT_MULTIPLIER': GLOBAL_LIFT_MULTIPLIER, + 'GLOBAL_DRAG_MULTIPLIER': GLOBAL_DRAG_MULTIPLIER} + + config['Missile'] = {'liftArea': liftArea, + 'dragArea': dragArea, + 'thrust': thrust, + 'mass': mass, + 'maxTorque': maxTorque, + 'maxTorqueAero': maxTorqueAero, + 'maxAoA': maxAoA, + 'gLimit': gLimit, + 'gMargin': gMargin} + + config['EnvelopeSettings'] = {'maxAltitude': maxAltitude, + 'maxAirspeed': maxAirspeed, + 'numAlt': numAlt, + 'numAirspeed': numAirspeed} + + with open(filename + '.ini', 'w') as configfile: + config.write(configfile) + case 'help' | 'h': + print('Available Commands:\n' + '"Field Name" - allows you to edit the value of the field, E.G. "liftArea".\n' + '"calculate" - performs calculation.\n' + '"plot" - plots data, performs calculation if needed.\n' + '"load" - loads parameters from an ini file, you will be prompted for a filename.\n' + '"save" - saves parameters to an ini file, you will be prompted for a filename.\n' + '"quit" - exits the program.\n' + 'Note shorthands are available, usually the first letter of any command. Fields use abbreviations like "la" for liftArea, and "AoA for maxAoA.') + input("Press Enter to continue...") + case 'quit' | 'exit' | 'x' | 'q': + break + case _: + print('Unknown command. Use "help" to get available commands.') + input("Press Enter to continue...") + +## ---------------------------------------------------------------------------------- ## +## ---------------------------------------------------------------------------------- ## +## ---------------------------------------------------------------------------------- ## \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/KSPFloatCurveCalc.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/KSPFloatCurveCalc.py new file mode 100644 index 000000000..3f89051d6 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/KSPFloatCurveCalc.py @@ -0,0 +1,60 @@ +import numpy as np +import warnings + +class FloatCurve: + """ + Implements KSP FloatCurves in Python. + Initialize via a list of keys in the form of numpy ndarrays (either 2 or 4 long). + Evaluate(x) functions as in KSP + EvaluateArray(arr) will take a list/ndarray of x values and return a ndarray. + """ + def __init__(self, inKeys: list[np.ndarray]): + self.numKeys = len(inKeys) + for i in range(self.numKeys): + # Deal with keys that aren't of size 4 + if (inKeys[i].shape[0] < 4): + if (inKeys[i].shape[0] == 1): + # If only 1 value is given, then use that as the x and default to a value of 0 + inKeys[i] = np.array([inKeys[i][0], 0, 0, 0]) + warnings.warn(f'ERROR! Key: {i} is of length 1, defaulting to a value of 0!') + if (inKeys[i].shape[0] == 3): + # If 3 values are given, default to 0, 0 slopes + warnings.warn(f'ERROR! Key: {i} is of length 3, defaulting to 0, 0 slopes') + # Unity/KSP defaults keys of length 2 to 0, 0 slopes + inKeys[i] = np.array([inKeys[i][0], inKeys[i][1], 0, 0]) + self.keys = np.array(inKeys) + # Sort keys to enable binary search + self.keys[self.keys[:,0].argsort()] + + def Evaluate(self, x): + # Find index where the p1x < x + indexL = np.searchsorted(self.keys[:,0], x, side='right') - 1 + if (indexL == self.numKeys - 1): + # If we've reached the end, then just return the value at the end + return self.keys[-1,1] + return self.__Evaluate(x, + self.keys[indexL,0], self.keys[indexL,1], self.keys[indexL,3], + self.keys[indexL+1,0], self.keys[indexL+1,1], self.keys[indexL+1,2]) + + def EvaluateArray(self, arr): + results = [] + for x in arr: + # Find index where the p1x < x + indexL = np.searchsorted(self.keys[:,0], x, side='right') - 1 + if (indexL == self.numKeys - 1): + # If we've reached the end, then just return the value at the end + results.append(self.keys[-1,1]) + else: + results.append(self.__Evaluate(x, + self.keys[indexL,0], self.keys[indexL,1], self.keys[indexL,3], + self.keys[indexL+1,0], self.keys[indexL+1,1], self.keys[indexL+1,2])) + return np.array(results) + + @staticmethod + def __Evaluate(x, p1x, p1y, tp1, p2x, p2y, tp2): + # Taken from https://discussions.unity.com/t/what-is-the-math-behind-animationcurve-evaluate/72058/4 (because I'm lazy and someone already solved the system) + a = (p1x * tp1 + p1x * tp2 - p2x * tp1 - p2x * tp2 - 2 * p1y + 2 * p2y) / (p1x * p1x * p1x - p2x * p2x * p2x + 3 * p1x * p2x * p2x - 3 * p1x * p1x * p2x) + b = ((-p1x * p1x * tp1 - 2 * p1x * p1x * tp2 + 2 * p2x * p2x * tp1 + p2x * p2x * tp2 - p1x * p2x * tp1 + p1x * p2x * tp2 + 3 * p1x * p1y - 3 * p1x * p2y + 3 * p1y * p2x - 3 * p2x * p2y) / (p1x * p1x * p1x - p2x * p2x * p2x + 3 * p1x * p2x * p2x - 3 * p1x * p1x * p2x)) + c = ((p1x * p1x * p1x * tp2 - p2x * p2x * p2x * tp1 - p1x * p2x * p2x * tp1 - 2 * p1x * p2x * p2x * tp2 + 2 * p1x * p1x * p2x * tp1 + p1x * p1x * p2x * tp2 - 6 * p1x * p1y * p2x + 6 * p1x * p2x * p2y) / (p1x * p1x * p1x - p2x * p2x * p2x + 3 * p1x * p2x * p2x - 3 * p1x * p1x * p2x)) + d = ((p1x * p2x * p2x * p2x * tp1 - p1x * p1x * p2x * p2x * tp1 + p1x * p1x * p2x * p2x * tp2 - p1x * p1x * p1x * p2x * tp2 - p1y * p2x * p2x * p2x + p1x * p1x * p1x * p2y + 3 * p1x * p1y * p2x * p2x - 3 * p1x * p1x * p2x * p2y) / (p1x * p1x * p1x - p2x * p2x * p2x + 3 * p1x * p2x * p2x - 3 * p1x * p1x * p2x)) + return a * x * x * x + b * x * x + c * x + d \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_CS_log_files.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_CS_log_files.py new file mode 100755 index 000000000..b409b8fca --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_CS_log_files.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +# Standard library imports +import argparse +import re +import sys +from pathlib import Path + +VERSION = "5.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, 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="3,1.5,-1,4e-3,1e-4,4e-5,0.01,5e-4,1e-4,4e-5,0.15,2e-3,3e-5,1.5e-5,0.075,0,0,0", help="Score weights.") +parser.add_argument("--show-weights", action='store_true', help="Show the score weights.") +parser.add_argument("-s", "--separately", action='store_true', help="Show the results of each log separately (for multiple logs).") +parser.add_argument("-a", "--all", action='store_true', help="Glob all the cts-* files in the folder as input.") +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() + +log_dir = Path(__file__).parent.parent / "Logs" if len(args.logs) == 0 else Path('.') + +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", "parts lost to asteroids", "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", "Asteroids", "Acc%", "RktAcc%"])} +try: + weights = {field: float(w) for field, w in zip(fields, args.weights.split(','))} + if len(weights) != len(fields): + raise ValueError("Invalid number of weights.") +except: + raise ValueError("Failed to parse input weights") + +if args.show_weights: + field_width = max(len(f) for f in fields) + for f, w in weights.items(): + print(f"{f}:{' '*(field_width - len(f))} {w}") + sys.exit() + +if len(args.logs) == 1 and Path(args.logs[0]).is_dir(): # We were passed a folder instead of log files -> treat it as the log_dir. + log_dir = Path(args.logs[0]) + args.logs=[] +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 not args.all and len(competition_files) > 0: + competition_files = competition_files[-1:] + +data = {} +for filename in competition_files: + with open(filename, "r", encoding="utf-8") as file_data: + data[filename] = {} + Craft_Name = None + for line in file_data: + if not "BDArmory.VesselSpawner" in line: # Identifier for continuous spawn logs. + continue + if " Name:" in line: # Next craft + Craft_Name = line.split(" Name:")[-1].replace("\n", "") + data[filename][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[filename][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 + 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[filename][Craft_Name]: + data[filename][Craft_Name]["killed by"].update(killedby) + else: + data[filename][Craft_Name]["killed by"] = killedby # {death_nr: killer} + elif " GMKILL:" in line: # GM kills + data[filename][Craft_Name]["GM kills"] = {int(nr): killer for nr, killer in (gmKill.split(":") for gmKill in line.split(": GMKILL:")[-1].replace("\n", "").split(", ")) if killer not in ("LandedTooLong", "Asteroids")} + elif " WHOSHOTME:" in line: # Counts up hits + data[filename][Craft_Name]["shot by"] = {int(life): {by: int(hits) for hits, by in (entry.split(":", 1) for entry in hitsby.split(";"))} + for life, hitsby in (entry.split(":", 1) for entry in line.split(": WHOSHOTME:")[-1].replace("\n", "").split(", "))} + elif " WHOSTRUCKMEWITHROCKETS:" in line: # Counts up hits + data[filename][Craft_Name]["rocket strike by"] = {int(life): {by: int(hits) for hits, by in (entry.split(":", 1) for entry in hitsby.split(";"))} + for life, hitsby in (entry.split(":", 1) for entry in line.split(": WHOSTRUCKMEWITHROCKETS:")[-1].replace("\n", "").split(", "))} + elif " WHOSTRUCKMEWITHMISSILES:" in line: # Counts up hits + data[filename][Craft_Name]["missile strike by"] = {int(life): {by: int(hits) for hits, by in (entry.split(":", 1) for entry in hitsby.split(";"))} + for life, hitsby in (entry.split(":", 1) for entry in line.split(": WHOSTRUCKMEWITHMISSILES:")[-1].replace("\n", "").split(", "))} + elif " WHOPARTSHITMEWITHROCKETS:" in line: # Counts up parts hit + data[filename][Craft_Name]["rocket parts hit by"] = {int(life): {by: int(parts) for parts, by in (entry.split(":", 1) for entry in partshitby.split(";"))} + for life, partshitby in (entry.split(":", 1) for entry in line.split(": WHOPARTSHITMEWITHROCKETS:")[-1].replace("\n", "").split(", "))} + elif " WHOPARTSHITMEWITHMISSILES:" in line: # Counts up parts hit + data[filename][Craft_Name]["missile parts hit by"] = {int(life): {by: int(parts) for parts, by in (entry.split(":", 1) for entry in partshitby.split(";"))} + for life, partshitby in (entry.split(":", 1) for entry in line.split(": WHOPARTSHITMEWITHMISSILES:")[-1].replace("\n", "").split(", "))} + elif " WHODAMAGEDMEWITHBULLETS:" in line: # Counts up damage + data[filename][Craft_Name]["bullet damage by"] = {int(life): {by: float(damage) for damage, by in (entry.split(":", 1) for entry in damageby.split(";"))} + for life, damageby in (entry.split(":", 1) for entry in line.split(": WHODAMAGEDMEWITHBULLETS:")[-1].replace("\n", "").split(", "))} + elif " WHODAMAGEDMEWITHROCKETS:" in line: # Counts up damage + data[filename][Craft_Name]["rocket damage by"] = {int(life): {by: float(damage) for damage, by in (entry.split(":", 1) for entry in damageby.split(";"))} + for life, damageby in (entry.split(":", 1) for entry in line.split(": WHODAMAGEDMEWITHROCKETS:")[-1].replace("\n", "").split(", "))} + elif " WHODAMAGEDMEWITHMISSILES:" in line: # Counts up damage + data[filename][Craft_Name]["missile damage by"] = {int(life): {by: float(damage) for damage, by in (entry.split(":", 1) for entry in damageby.split(";"))} + for life, damageby in (entry.split(":", 1) for entry in line.split(": WHODAMAGEDMEWITHMISSILES:")[-1].replace("\n", "").split(", "))} + elif " WHORAMMEDME:" in line: # Counts up rams + data[filename][Craft_Name]["rammed by"] = {int(life): {by: int(parts) for parts, by in (entry.split(":", 1) for entry in partshitby.split(";"))} + for life, partshitby in (entry.split(":", 1) for entry in line.split(": WHORAMMEDME:")[-1].replace("\n", "").split(", "))} + elif " PARTSLOSTTOASTEROIDS:" in line: # Count up parts + data[filename][Craft_Name]["parts lost to asteroids"] = {int(life): int(partsLost) for life, partsLost in (entry.split(":", 1) for entry in line.split(": PARTSLOSTTOASTEROIDS:")[-1].replace("\n", "").split(", "))} + elif " ACCURACY:" in line: + for item in line.split(" ACCURACY:")[-1].replace("\n", "").split(","): + _, hits, shots, rocketStrikes, rocketsFired = re.split('[:/]', item) + data[filename][Craft_Name]["acc hits"] += int(hits) + data[filename][Craft_Name]["shots"] += int(shots) + data[filename][Craft_Name]["acc rocket strikes"] += int(rocketStrikes) + data[filename][Craft_Name]["rockets fired"] += int(rocketsFired) + data[filename][Craft_Name]["accuracy"] = 100 * data[filename][Craft_Name]["acc hits"] / data[filename][Craft_Name]["shots"] if data[filename][Craft_Name]["shots"] > 0 else 0 + data[filename][Craft_Name]["rocket accuracy"] = 100 * data[filename][Craft_Name]["acc rocket strikes"] / data[filename][Craft_Name]["rockets fired"] if data[filename][Craft_Name]["rockets fired"] > 0 else 0 + + for Craft_Name in data[filename]: + data[filename][Craft_Name]["hits"] = sum(hitby[life][Craft_Name] for hitby in (data[filename][other]["shot by"] for other in data[filename] + if other != Craft_Name and "shot by" in data[filename][other]) for life in hitby if Craft_Name in hitby[life]) + data[filename][Craft_Name]["rocket strikes"] = sum(hitby[life][Craft_Name] for hitby in (data[filename][other]["rocket strike by"] for other in data[filename] + if other != Craft_Name and "rocket strike by" in data[filename][other]) for life in hitby if Craft_Name in hitby[life]) + data[filename][Craft_Name]["missile strikes"] = sum(hitby[life][Craft_Name] for hitby in (data[filename][other]["missile strike by"] for other in data[filename] + if other != Craft_Name and "missile strike by" in data[filename][other]) for life in hitby if Craft_Name in hitby[life]) + + data[filename][Craft_Name]["rocket parts hit"] = sum(partshitby[life][Craft_Name] for partshitby in (data[filename][other]["rocket parts hit by"] for other in data[filename] + if other != Craft_Name and "rocket parts hit by" in data[filename][other]) for life in partshitby if Craft_Name in partshitby[life]) + data[filename][Craft_Name]["missile parts hit"] = sum(partshitby[life][Craft_Name] for partshitby in (data[filename][other]["missile parts hit by"] + for other in data[filename] if other != Craft_Name and "missile parts hit by" in data[filename][other]) for life in partshitby if Craft_Name in partshitby[life]) + + data[filename][Craft_Name]["bullet damage"] = sum(damageby[life][Craft_Name] for damageby in (data[filename][other]["bullet damage by"] for other in data[filename] + if other != Craft_Name and "bullet damage by" in data[filename][other]) for life in damageby if Craft_Name in damageby[life]) + data[filename][Craft_Name]["rocket damage"] = sum(damageby[life][Craft_Name] for damageby in (data[filename][other]["rocket damage by"] for other in data[filename] + if other != Craft_Name and "rocket damage by" in data[filename][other]) for life in damageby if Craft_Name in damageby[life]) + data[filename][Craft_Name]["missile damage"] = sum(damageby[life][Craft_Name] for damageby in (data[filename][other]["missile damage by"] for other in data[filename] + if other != Craft_Name and "missile damage by" in data[filename][other]) for life in damageby if Craft_Name in damageby[life]) + + data[filename][Craft_Name]["bullet damage taken"] = sum(damage for damageby in data[filename][Craft_Name]['bullet damage by'].values() for damage in damageby.values()) if 'bullet damage by' in data[filename][Craft_Name] else 0 + data[filename][Craft_Name]["rocket damage taken"] = sum(damage for damageby in data[filename][Craft_Name]['rocket damage by'].values() for damage in damageby.values()) if 'rocket damage by' in data[filename][Craft_Name] else 0 + data[filename][Craft_Name]["missile damage taken"] = sum(damage for damageby in data[filename][Craft_Name]['missile damage by'].values() + for damage in damageby.values()) if 'missile damage by' in data[filename][Craft_Name] else 0 + + data[filename][Craft_Name]["rammed parts"] = sum(partshitby[life][Craft_Name] for partshitby in (data[filename][other]["rammed by"] for other in data[filename] + if other != Craft_Name and "rammed by" in data[filename][other]) for life in partshitby if Craft_Name in partshitby[life]) + + data[filename][Craft_Name]["damage/spawn"] = (data[filename][Craft_Name]["bullet damage"] + data[filename][Craft_Name]["rocket damage"] + + data[filename][Craft_Name]["missile damage"]) / (1 + data[filename][Craft_Name]["deaths"]) + + data[filename][Craft_Name]["kills"] = sum(1 for kill in (data[filename][other]["killed by"] for other in data[filename] if other != + Craft_Name and "killed by" in data[filename][other]) for life in kill if Craft_Name == kill[life]) + data[filename][Craft_Name]["parts lost to asteroids"] = sum(data[filename][Craft_Name]["parts lost to asteroids"].values()) if "parts lost to asteroids" in data[filename][Craft_Name] else 0 + + # Aggregate the damagers for computing assists later. + data[filename][Craft_Name]['damaged by'] = {} + if 'bullet damage by' in data[filename][Craft_Name]: + data[filename][Craft_Name]['damaged by'] = {k: set(v) for k, v in data[filename][Craft_Name]['bullet damage by'].items() if k < data[filename][Craft_Name]['deaths']} + if 'rocket damage by' in data[filename][Craft_Name]: + for k, v in data[filename][Craft_Name]['rocket damage by'].items(): + if k < data[filename][Craft_Name]['deaths']: + if k in data[filename][Craft_Name]['damaged by']: + data[filename][Craft_Name]['damaged by'][k] = data[filename][Craft_Name]['damaged by'][k].union(set(v)) + else: + data[filename][Craft_Name]['damaged by'][k] = set(v) + if 'missile damage by' in data[filename][Craft_Name]: + for k, v in data[filename][Craft_Name]['missile damage by'].items(): + if k < data[filename][Craft_Name]['deaths']: + if k in data[filename][Craft_Name]['damaged by']: + data[filename][Craft_Name]['damaged by'][k] = data[filename][Craft_Name]['damaged by'][k].union(set(v)) + else: + data[filename][Craft_Name]['damaged by'][k] = set(v) + if 'rammed by' in data[filename][Craft_Name]: + for k, v in data[filename][Craft_Name]['rammed by'].items(): + if k < data[filename][Craft_Name]['deaths']: + if k in data[filename][Craft_Name]['damaged by']: + data[filename][Craft_Name]['damaged by'][k] = data[filename][Craft_Name]['damaged by'][k].union(set(v)) + else: + data[filename][Craft_Name]['damaged by'][k] = set(v) + + # Sanity check + if data[filename][Craft_Name]["hits"] != data[filename][Craft_Name]["acc hits"]: + print(f"Warning: inconsistency in hit counting {data[filename][Craft_Name]['hits']} vs {data[filename][Craft_Name]['acc hits']} for log {filename}") + if data[filename][Craft_Name]["rocket strikes"] != data[filename][Craft_Name]["acc rocket strikes"]: + print(f"Warning: inconsistency in rocket strike counting {data[filename][Craft_Name]['rocket strikes']} vs {data[filename][Craft_Name]['acc rocket strikes']} for log {filename}") + + # Compute assists and scores. + for Craft_Name in data[filename]: + data[filename][Craft_Name]["assists"] = sum(1 for other in data[filename] for life, damagedby in data[filename][other]['damaged by'].items() if Craft_Name in damagedby and not ( + 'killed by' in data[filename][other] and life in data[filename][other]['killed by']) and not ('GM kills' in data[filename][other] and life in data[filename][other]['GM kills'])) + + data[filename][Craft_Name]["score"] = sum(weights[field] * data[filename][Craft_Name][field] for field in fields) + +if len(data) > 0: + # Write results to console + if args.separately: + for filename, summary in data.items(): + print(f"Results for {filename}:") + name_length = max([len(craft) for craft in summary]) + field_lengths = {field: max(len(fields_short[field]) + 2, 8) for field in fields} + fields_to_show = [field for field in fields if not all(summary[craft][field] == 0 for craft in summary)] + print(f"Name{' '*(name_length-4)} score" + "".join(f"{fields_short[field]:>{field_lengths[field]}}" for field in fields_to_show)) + for craft in sorted(summary, key=lambda c: summary[c]["score"], reverse=True): + print(f"{craft}{' '*(name_length-len(craft))} {summary[craft]['score']:8.2f}" + + "".join(f"{summary[craft][field]:>{field_lengths[field]}.0f}" if 'accuracy' not in field else f"{summary[craft][field]:>{field_lengths[field]-1}.1f}%" for field in fields_to_show)) + print("") + + if not args.no_file: + # Write results to file + with open(log_dir / f"results-{Path(filename).stem}.csv", "w", encoding="utf-8") as results_data: + results_data.write("Name,Score," + ",".join(fields_to_show) + "\n") + for craft in sorted(summary, key=lambda c: summary[c]["score"], reverse=True): + results_data.write(f"{craft},{summary[craft]['score']:.2f}," + ",".join(f"{summary[craft][field]:.2f}" for field in fields_to_show) + "\n") + else: + # Merge the results from each log into a single summary. + summary = {} + for filename, entry in data.items(): + for craft, results in entry.items(): + if craft not in summary: + summary[craft] = {f: results[f] for f in fields + ['acc hits', 'shots', 'acc rocket strikes', 'rockets fired', 'score']} + else: + for f in fields + ['acc hits', 'shots', 'acc rocket strikes', 'rockets fired', 'score']: + summary[craft][f] = summary[craft].get(f, 0) + results[f] + for craft, entry in summary.items(): + entry['accuracy'] = entry['hits'] / entry['shots'] * 100 if entry['shots'] > 0 else 0 + entry['rocket accuracy'] = entry['rocket strikes'] / entry['rockets fired'] * 100 if entry['rockets fired'] > 0 else 0 + + name_length = max([len(craft) for craft in summary]) + field_lengths = {field: max(len(fields_short[field]) + 2, 8) for field in fields} + fields_to_show = [field for field in fields if not all(summary[craft][field] == 0 for craft in summary)] + print(f"Name{' '*(name_length-4)} score" + "".join(f"{fields_short[field]:>{field_lengths[field]}}" for field in fields_to_show)) + for craft in sorted(summary, key=lambda c: summary[c]["score"], reverse=True): + print(f"{craft}{' '*(name_length-len(craft))} {summary[craft]['score']:8.2f}" + + "".join(f"{summary[craft][field]:>{field_lengths[field]}.0f}" if 'accuracy' not in field else f"{summary[craft][field]:>{field_lengths[field]-1}.1f}%" for field in fields_to_show)) + + if not args.no_file: + # Write results to file + with open(log_dir / f"results.csv", "w", encoding="utf-8") as results_data: + results_data.write("Name,Score," + ",".join(fields_to_show) + "\n") + for craft in sorted(summary, key=lambda c: summary[c]["score"], reverse=True): + results_data.write(f"{craft},{summary[craft]['score']:.2f}," + ",".join(f"{summary[craft][field]:.2f}" for field in fields_to_show) + "\n") +else: + print(f"No valid log files found.") diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_n-choose-k_results.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_n-choose-k_results.py new file mode 100644 index 000000000..2a1764685 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_n-choose-k_results.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +# Standard library imports +import argparse +import json +import sys +from collections import Counter +from pathlib import Path +from typing import Union + +VERSION = "2.2" + +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() + +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.results is None: + logsDir = Path(__file__).parent.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: + 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(): + print(f"File not found: {results_file}") + else: + with open(results_file, 'r', encoding='utf-8') as f: + data = json.load(f) + counts = Counter([(next(iter(heat['result']['teams'].keys())), next(iter(heat['result']['dead teams'].keys()))) for Round in data.values() for heat in Round.values() if heat['result']['result'] == 'Win']) + A = set(k[0] for k in counts.keys()) + B = set(k[1] for k in counts.keys()) + names = sorted(A.union(B)) + name_map = {n: c for c, n in enumerate(names)} + 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 + 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 + 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/Scripts/parse_pvp_scores.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_pvp_scores.py new file mode 100644 index 000000000..07536ea6d --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/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 = "2.2" + +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.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', encoding='utf-8') as f: + summary = json.load(f) + with open(tournamentDir / "results.json", 'r', encoding='utf-8') 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/Scripts/parse_tournament_log_files.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_tournament_log_files.py new file mode 100755 index 000000000..79127aa03 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_tournament_log_files.py @@ -0,0 +1,680 @@ +#!/usr/bin/env python3 + +# Standard library imports +import argparse +import json +import re +import sys +from base64 import b64decode, b64encode +from collections import Counter +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Tuple, Union + +VERSION = "25.2" + +parser = argparse.ArgumentParser(description="Tournament log parser", formatter_class=argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument('tournament', type=str, nargs='*', help="Tournament folder to parse.") +parser.add_argument('-q', '--quiet', action='store_true', help="Don't print results summary to console.") +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.01,0,5e-4,0,1e-4,4e-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. Use -w cfg to read the weights from the score_weights.cfg file.") +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('--average-duplicates', action='store_true', help="Average the values of duplicates in the summary.") +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 + +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.current_dir and len(args.tournament) == 0: + tournamentDirs = [Path('')] +else: + if len(args.tournament) == 0: + tournamentDirs = None + logsDir = Path(__file__).parent.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 + +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: + if args.weights == 'cfg': + with open(Path(__file__).parent.parent / 'PluginData' / 'score_weights.cfg', 'r', encoding='utf-8') as f: + lines = f.readlines() + field_names = { + "Wins": "wins", + "Survived": "survivedCount", + "MIA": "miaCount", + "Deaths": "deathCount", + "Death Order": "deathOrder", + "Death Time": "deathTime", + "Clean Kills": "cleanKills", + "Assists": "assists", + "Hits": "hits", + "Hits Taken": "hitsTaken", + "Bullet Damage": "bulletDamage", + "Bullet Damage Taken": "bulletDamageTaken", + "Rocket Hits": "rocketHits", + "Rocket Hits Taken": "rocketHitsTaken", + "Rocket Parts Hit": "rocketPartsHit", + "Rocket Parts Hit Taken": "rocketPartsHitTaken", + "Rocket Damage": "rocketDamage", + "Rocket Damage Taken": "rocketDamageTaken", + "Missile Hits": "missileHits", + "Missile Hits Taken": "missileHitsTaken", + "Missile Parts Hit": "missilePartsHit", + "Missile Parts Hit Taken": "missilePartsHitTaken", + "Missile Damage": "missileDamage", + "Missile Damage Taken": "missileDamageTaken", + "RamScore": "ramScore", + "RamScore Taken": "ramScoreTaken", + "Battle Damage": "battleDamage", + "Parts Lost To Asteroids": "partsLostToAsteroids", + "HP Remaining": "HPremaining", + "Accuracy": "accuracy", + "Rocket Accuracy": "rocket_accuracy", + "Waypoint Count": "waypointCount", + "Waypoint Time": "waypointTime", + "Waypoint Deviation": "waypointDeviation", + } + found_section=False + tmp_weights = {} + for line in lines: + line = line.strip() + if line == 'ScoreWeights': + found_section = True + continue + if line == f'{{': + continue + if not found_section: + continue + if found_section and line == '}': + break + + try: + field, weight = line.split("=") + tmp_weights[field_names[field.strip()]] = float(weight.strip()) + except: + print("Failed to parse score weights.") + sys.exit(1) + weights = [tmp_weights[field] for field in score_fields] + else: + weights = list(float(w) for w in args.weights.split(',')) + + if args.waypoint_scores: + for i in range(len(weights)-3): + weights[i] = 0 +except: + weights = [] + +if args.show_weights: + field_width = max(len(f) for f in score_fields) + for w, f in zip(weights, score_fields): + print(f"{f}:{' ' * (field_width - len(f))} {w}") + 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 encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: + """ Encode the craft names in base64 to avoid issues with naming. + + Args: + log_lines (List[str]): The log lines. + + Returns: + Tuple[Dict[str, str], List[str]]: The dictionary of encoded names to actual names and the modified log lines. + """ + craft_names = set() + for line in log_lines: + if 'BDArmory.BDACompetitionMode' not in line: + continue + _, line = line.split(' ', 1) + field, entry = line.split(':', 1) + if field not in ('DEAD', 'MIA', 'ALIVE'): + continue + if field == 'DEAD': + order, time, craft = entry.split(':', 2) + craft_names.add(craft) + if field == 'MIA': + craft_names.add(entry) + if field == 'ALIVE': + craft_names.add(entry) + craft_names.update({json.dumps(name, ensure_ascii=False)[1:-1] for name in craft_names}) # Handle manually encoded DEADTEAMS. + 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(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.replace('\\"', '"') for k, v in craft_names.items()} # Reverse the craft name encoding dict and fix the \\ due to JSON encoding. + return encoded_craft_names, log_lines + + +for tournamentNumber, tournamentDir in enumerate(tournamentDirs): + if tournamentNumber > 0 and not args.quiet: + print("") + tournamentData = {} + tournamentMetadata = {} + 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 + tournamentData[round.name] = {} + logFiles = sorted(round.glob("[0-9]*.log")) + if len(logFiles) == 0: + del tournamentData[round.name] + continue + for heat in logFiles if args.N == None else logFiles[:args.N]: + 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) + for line in log_lines: + if 'BDArmory.BDACompetitionMode' not in line: + continue # Ignore irrelevant lines + _, field = line.split(' ', 1) + if field.startswith('Dumping Results'): + duration = float(field[field.find('(') + 4:field.find(')') - 1]) + timestamp = datetime.fromisoformat(field[field.find(' at ') + 4:]) + tournamentData[round.name][heat.name]['duration'] = duration + tournamentMetadata['duration'] = (min(tournamentMetadata['duration'][0], timestamp), max(tournamentMetadata['duration'][1], timestamp + timedelta(seconds=duration)) + ) if 'duration' in tournamentMetadata else (timestamp, timestamp + timedelta(seconds=duration)) + elif field.startswith('ALIVE:'): + state, craft = field.split(':', 1) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]] = {'state': state} + elif field.startswith('DEAD:'): + state, order, time, craft = field.split(':', 3) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]] = {'state': state, 'deathOrder': int(order), 'deathTime': float(time)} + elif field.startswith('MIA:'): + state, craft = field.split(':', 1) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]] = {'state': state} + elif field.startswith('WHOSHOTWHOWITHGUNS:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'hitsBy': {encoded_craft_names[player]: int(hits) for player, hits in zip(data[1::2], data[::2])}}) + elif field.startswith('WHODAMAGEDWHOWITHGUNS:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'bulletDamageBy': {encoded_craft_names[player]: float(damage) for player, damage in zip(data[1::2], data[::2])}}) + elif field.startswith('WHOHITWHOWITHMISSILES:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'missileHitsBy': {encoded_craft_names[player]: int(hits) for player, hits in zip(data[1::2], data[::2])}}) + elif field.startswith('WHOPARTSHITWHOWITHMISSILES:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'missilePartsHitBy': {encoded_craft_names[player]: int(hits) for player, hits in zip(data[1::2], data[::2])}}) + elif field.startswith('WHODAMAGEDWHOWITHMISSILES:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'missileDamageBy': {encoded_craft_names[player]: float(damage) for player, damage in zip(data[1::2], data[::2])}}) + elif field.startswith('WHOHITWHOWITHROCKETS:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'rocketHitsBy': {encoded_craft_names[player]: int(hits) for player, hits in zip(data[1::2], data[::2])}}) + elif field.startswith('WHOPARTSHITWHOWITHROCKETS:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'rocketPartsHitBy': {encoded_craft_names[player]: int(hits) for player, hits in zip(data[1::2], data[::2])}}) + elif field.startswith('WHODAMAGEDWHOWITHROCKETS:'): + _, craft, shooters = field.split(':', 2) + data = shooters.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'rocketDamageBy': {encoded_craft_names[player]: float(damage) for player, damage in zip(data[1::2], data[::2])}}) + elif field.startswith('WHORAMMEDWHO:'): + _, craft, rammers = field.split(':', 2) + data = rammers.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'rammedPartsLostBy': {encoded_craft_names[player]: int(partsLost) for player, partsLost in zip(data[1::2], data[::2])}}) + elif field.startswith('WHODAMAGEDWHOWITHBATTLEDAMAGE'): + _, craft, rammers = field.split(':', 2) + data = rammers.split(':') + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'battleDamageBy': {encoded_craft_names[player]: float(damage) for player, damage in zip(data[1::2], data[::2])}}) + elif field.startswith('CLEANKILLGUNS:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanKillBy': encoded_craft_names[killer]}) + elif field.startswith('CLEANKILLROCKETS:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanRocketKillBy': encoded_craft_names[killer]}) + elif field.startswith('CLEANKILLMISSILES:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanMissileKillBy': encoded_craft_names[killer]}) + elif field.startswith('CLEANKILLRAMMING:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanRamKillBy': encoded_craft_names[killer]}) + elif field.startswith('HEADSHOTGUNS:'): # FIXME make head-shots separate from clean-kills + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanKillBy': encoded_craft_names[killer]}) + elif field.startswith('HEADSHOTROCKETS:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanRocketKillBy': encoded_craft_names[killer]}) + elif field.startswith('HEADSHOTMISSILES:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanMissileKillBy': encoded_craft_names[killer]}) + elif field.startswith('HEADSHOTRAMMING:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanRamKillBy': encoded_craft_names[killer]}) + elif field.startswith('KILLSTEALGUNS:'): # FIXME make kill-steals separate from clean-kills + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanKillBy': encoded_craft_names[killer]}) + elif field.startswith('KILLSTEALROCKETS:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanRocketKillBy': encoded_craft_names[killer]}) + elif field.startswith('KILLSTEALMISSILES:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanMissileKillBy': encoded_craft_names[killer]}) + elif field.startswith('KILLSTEALRAMMING:'): + _, craft, killer = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'cleanRamKillBy': encoded_craft_names[killer]}) + 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)}) + elif field.startswith('ACCURACY:'): + _, craft, accuracy, rocket_accuracy = field.split(':', 3) + hits, shots = accuracy.split('/') + rocket_strikes, rockets_fired = rocket_accuracy.split('/') + accuracy = CalculateAccuracy(int(hits), int(shots)) + rocket_accuracy = CalculateAccuracy(int(rocket_strikes), int(rockets_fired)) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'accuracy': accuracy, 'hits': int(hits), 'shots': int( + shots), 'rocket_accuracy': rocket_accuracy, 'rocket_strikes': int(rocket_strikes), 'rockets_fired': int(rockets_fired)}) + elif field.startswith('RESULT:'): + heat_result = field.split(':', 2) + result_type = heat_result[1] + 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': {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': {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': {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', encoding="utf-8") as outFile: + json.dump(tournamentData, outFile, indent=2, ensure_ascii=False) + + craftNames = sorted(list(set(craft for round in tournamentData.values() for heat in round.values() for craft in heat['craft'].keys()))) + teamWins = Counter([team for round in tournamentData.values() for heat in round.values() if heat['result']['result'] == "Win" for team in heat['result']['teams']]) + teamDraws = Counter([team for round in tournamentData.values() for heat in round.values() if heat['result']['result'] == "Draw" for team in heat['result']['teams']]) + teamDeaths = Counter([team for round in tournamentData.values() for heat in round.values() if 'dead teams' in heat['result'] for team in heat['result']['dead teams']]) + teams = {team: members for round in tournamentData.values() for heat in round.values() if 'teams' in heat['result'] for team, members in heat['result']['teams'].items()} + teams.update({team: members for round in tournamentData.values() for heat in round.values() if 'dead teams' in heat['result'] for team, members in heat['result']['dead teams'].items()}) + summary = { + '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: { + 'wins': len([1 for round in tournamentData.values() for heat in round.values() if heat['result']['result'] == "Win" and craft in next(iter(heat['result']['teams'].values())).split(", ")]), + 'survivedCount': len([1 for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'ALIVE']), + 'miaCount': len([1 for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'MIA']), + 'deathCount': ( + len([1 for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD']), # Total + len([1 for round in tournamentData.values() 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 round in tournamentData.values() 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 round in tournamentData.values() 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 round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and not any(field in heat['craft'][craft] + # Dirty kill + for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy')) and any(field in heat['craft'][craft] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy'))]), + len([1 for round in tournamentData.values() 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 round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft']]), + 'cleanKills': ( + len([1 for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() for data in heat['craft'].values() if 'cleanKillBy' in data and data['cleanKillBy'] == craft]), # Bullets + len([1 for round in tournamentData.values() for heat in round.values() for data in heat['craft'].values() if 'cleanRocketKillBy' in data and data['cleanRocketKillBy'] == craft]), # Rockets + len([1 for round in tournamentData.values() for heat in round.values() for data in heat['craft'].values() if 'cleanMissileKillBy' in data and data['cleanMissileKillBy'] == craft]), # Missiles + len([1 for round in tournamentData.values() for heat in round.values() for data in heat['craft'].values() if 'cleanRamKillBy' in data and data['cleanRamKillBy'] == craft]), # Rams + ), + 'assists': len([1 for round in tournamentData.values() 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 round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'hitsBy' in heat['craft'][craft]]), + 'bulletDamage': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'bulletDamageBy' in heat['craft'][craft]]), + 'rocketHits': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'rocketHitsBy' in heat['craft'][craft]]), + 'rocketPartsHit': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'rocketPartsHitBy' in heat['craft'][craft]]), + 'rocketDamage': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'rocketDamageBy' in heat['craft'][craft]]), + 'missileHits': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'missileHitsBy' in heat['craft'][craft]]), + 'missilePartsHit': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'missilePartsHitBy' in heat['craft'][craft]]), + 'missileDamage': sum([data[field][craft] for round in tournamentData.values() 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 round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'missileDamageBy' in heat['craft'][craft]]), + 'ramScore': sum([data[field][craft] for round in tournamentData.values() 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 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]])), + } + for craft in craftNames + }, + 'team results': { + 'wins': teamWins, + 'draws': teamDraws, + 'deaths': teamDeaths + }, + 'teams': teams + } + if args.average_duplicates: + to_remove = [] + for craft in summary['craft']: + duplicates = [c for c in summary['craft'] if c.startswith(craft) and c[len(craft):].startswith('_') and c[len(craft) + 1:].isdigit()] + if len(duplicates) > 0: + duplicates.append(craft) + summary['craft'][craft] = { + key: + sum(summary['craft'][duplicate][key] for duplicate in duplicates) / len(duplicates) if not isinstance(summary['craft'][craft][key], tuple) else + tuple(sum(summary['craft'][duplicate][key][i] for duplicate in duplicates) / len(duplicates) for i in range(len(summary['craft'][craft][key]))) + for key in summary['craft'][craft] + } + to_remove.extend([duplicate for duplicate in duplicates if duplicate != craft]) + summary['craft'] = {craft: data for craft, data in summary['craft'].items() if craft not in to_remove} + + for craft in summary['craft'].values(): + spawns = craft['survivedCount'] + craft['deathCount'][0] + craft.update({ + 'damage/hit': craft['bulletDamage'] / craft['hits'] if craft['hits'] > 0 else 0, + 'hits/spawn': craft['hits'] / spawns if spawns > 0 else 0, + '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 craftName, summary_data in summary['craft'].items(): + # Treat waypoints separately so we can avoid non-negative scores for waypoints. + 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')) + 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(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', encoding="utf-8") as outFile: + json.dump(summary, outFile, indent=2, ensure_ascii=False) + + if len(summary['craft']) > 0: + if not args.no_files: + headers = (["score", ] if args.score else []) + [k for k in next(iter(summary['craft'].values())).keys() if k not in ('score',)] + 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 + 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( + ",".join(str(int(100 * sf) / 100) for sf in score[h]) if isinstance(score[h], tuple) + else ",".join(str(int(100 * sf) / 100) for sf in score[h].values()) if isinstance(score[h], dict) + 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', 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_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) 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.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]}" + ) # Python <3.12 has issues with line breaks in f-strings. + 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}} + for craft in sorted(summary['craft']): + tmp = summary['craft'][craft] + spawns = tmp['survivedCount'] + tmp['deathCount'][0] + summary_strings.update({ + craft: { + 'Name': craft, + 'Wins': f"{tmp['wins']:.0f}", + 'Survive': f"{tmp['survivedCount']:.0f}", + 'MIA': f"{tmp['miaCount']:.0f}", + 'Deaths (BRMRAS)': f"{tmp['deathCount'][0]:.0f} ({' '.join(f'{s:.0f}' for s in tmp['deathCount'][1:])})", + 'D.Order': f"{tmp['deathOrder']:.3f}", + 'D.Time': f"{tmp['deathTime']:.1f}", + 'Kills (BRMR)': f"{tmp['cleanKills'][0]:.0f} ({' '.join(f'{s:.0f}' for s in tmp['cleanKills'][1:])})", + 'Assists': f"{tmp['assists']:.0f}", + 'Hits': f"{tmp['hits']:.0f}", + 'Damage': f"{tmp['bulletDamage']:.0f}", + 'DmgTaken': f"{tmp['bulletDamageTaken']:.0f}", + 'RocHits': f"{tmp['rocketHits']:.0f}", + 'RocParts': f"{tmp['rocketPartsHit']:.0f}", + 'RocDmg': f"{tmp['rocketDamage']:.0f}", + 'HitByRoc': f"{tmp['rocketHitsTaken']:.0f}", + 'MisHits': f"{tmp['missileHits']:.0f}", + 'MisParts': f"{tmp['missilePartsHit']:.0f}", + 'MisDmg': f"{tmp['missileDamage']:.0f}", + 'HitByMis': f"{tmp['missileHitsTaken']:.0f}", + 'Ram': f"{tmp['ramScore']:.0f}", + 'BD dealt': f"{tmp['battleDamage']:.0f}", + 'BD taken': f"{tmp['battleDamageTaken']:.0f}", + 'Ast.': f"{tmp['partsLostToAsteroids']:.0f}", + 'Acc%': f"{tmp['accuracy']:.3g}", + 'RktAcc%': f"{tmp['rocket_accuracy']:.3g}", + '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']:.0f}", + 'WPtime': f"{tmp['waypointTime']:.1f}", + 'WPdev': f"{tmp['waypointDeviation']:.1f}", + 'WPbestC': f"{tmp['waypointBestCount']:.0f}", + '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:])] + column_widths = {column: max(len(craft[column]) + 2 for craft in summary_strings.values()) for column in headers} + strings.append(''.join(f"{header:{column_widths[header]}s}" for header in columns_to_show)) + for craft in sorted(summary['craft'], key=None if not args.score else lambda craft: summary['craft'][craft]['score'], reverse=False if not args.score else True): + strings.append(''.join(f"{summary_strings[craft][header]:{column_widths[header]}s}" for header in columns_to_show)) + + # Teams summary + if len(teamNames) > 0 and not all(name in default_team_names for name in teamNames): # Don't do teams if they're assigned as 'A', 'B', ... as they won't be consistent between rounds. + name_length = max([len(team) for team in teamNames]) + strings.append(f"\nTeam{' ' * (name_length - 4)}\tWins\tDraws\tDeaths\tVessels") + for team in sorted(teamNames, key=lambda team: teamWins[team], reverse=True): + strings.append(f"{team}{' ' * (name_length - len(team))}\t{teamWins[team]}\t{teamDraws[team]}\t{teamDeaths[team]}\t{summary['teams'][team]}") + + # Per round cumulative score + if args.score and not args.no_cumulative: + name_length = max([len(name) for name in per_round_scores.keys()] + [23]) + strings.append(f"\nName \\ Cumulative Score{' ' * (name_length - 22)}\t" + "\t".join(f"{r:>7d}" for r in range(len(next(iter(per_round_scores.values())))))) + strings.append('\n'.join(f"{craft}:{' ' * (name_length - len(craft))}\t" + "\t".join(f"{s:>7.2f}" for s in cumsum(per_round_scores[craft])) + for craft in sorted(per_round_scores, key=lambda craft: summary['craft'][craft]['score'], reverse=True))) + + # Print stuff to the console. + for string in strings: + print(string) + + # Write teams results to the summary.csv file. + if not args.no_files: + 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(", ", ","))])) + + # Write per round cumulative score results to summary.csv file. + if args.score and not args.no_cumulative: + f.write(f"\n\nName \\ Cumulative Score Per Round," + ",".join(f"{r:>7d}" for r in range(len(next(iter(per_round_scores.values())))))) + for craft in sorted(per_round_scores, key=lambda craft: summary['craft'][craft]['score'], reverse=True): + f.write(f"\n{craft}," + ",".join(f"{s:.2f}" for s in cumsum(per_round_scores[craft]))) + + else: + print(f"No valid log files found in {tournamentDir}.") diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_tournament_state.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_tournament_state.py new file mode 100644 index 000000000..c9a5ad390 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/parse_tournament_state.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +# Standard library imports +import argparse +import gzip +import json +from pathlib import Path + +VERSION = "1.2" + +parser = argparse.ArgumentParser( + description="Tournament state parser", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + epilog="The tournament.state file is recursively encoded JSON instead of proper JSON due to Unity's simplistic JSONUtility functionality. This script decodes it and converts the result to proper JSON, saving it to tournament.json." +) +parser.add_argument("state", type=Path, nargs="?", help="The tournament.state file.") +parser.add_argument("-p", "--print", action="store_true", help="Print the JSON to the console.") +parser.add_argument("-r", "--re-encode", action="store_true", help="Re-encode the tournament.json file back to the tournament.state file.") +args = parser.parse_args() + +if args.state is None: + args.state = Path(__file__).parent.parent / "PluginData" / "tournament.state" +state_file: Path = args.state +json_file: Path = state_file.with_suffix(".json") + +if not args.re_encode: # Decode the tournament.state to pure JSON and optionally print it. + try: # Try compressed gzip first + with gzip.open(args.state, "rb") as f: + state = json.load(f) + except: # Revert to plain UTF-8 + with open(args.state, "r", encoding='utf-8') as f: + state = json.load(f) + + # Various elements are recursively encoded in JSON strings due to Unity's limited JSONUtility functionality. + # We decode and organise them here. + + # Heats (configurations for spawning and teams) + state["heats"] = {f"Heat {i}": json.loads(rnd) for i, rnd in enumerate(state["_heats"])} + for heat in state["heats"].values(): + heat["teams"] = [json.loads(team)["team"] for team in heat["_teams"]] + del heat["_teams"] + del state["_heats"] + + # Scores + _scores = json.loads(state["_scores"]) + del state["_scores"] + _scores["weights"] = {k: v for k, v in zip(_scores["_weightKeys"], _scores["_weightValues"])} + del _scores["_weightKeys"], _scores["_weightValues"] + players = _scores["_players"] + del _scores["_players"] + _scores["scores"] = {p: s for p, s in zip(players, _scores["_scores"])} + del _scores["_scores"] + _scores["files"] = {p: s for p, s in zip(players, _scores["_files"])} + del _scores["_files"] + results = [json.loads(results) for results in _scores["_results"]] + for result in results: + result["survivingTeams"] = [json.loads(team) for team in result["_survivingTeams"]] + del result["_survivingTeams"] + result["deadTeams"] = [json.loads(team) for team in result["_deadTeams"]] + del result["_deadTeams"] + _scores["results"] = results + del _scores["_results"] + scores = _scores["scores"] + _scores["scores"] = { + player: + [ + json.loads(score_data["scoreData"]) | { + field: {other_player: values for other_player, values in zip(players, score_data[field]) if other_player != player} + for field in ("hitCounts", "damageFromGuns", "damageFromRockets", "rocketPartDamageCounts", "rocketStrikeCounts", "rammingPartLossCounts", "damageFromMissiles", "missilePartDamageCounts", "missileHitCounts", "battleDamageFrom") + } | { + "damageTypesTaken": score_data["damageTypesTaken"], + "everyoneWhoDamagedMe": score_data["everyoneWhoDamagedMe"] + } for score_data in [json.loads(rnd) for rnd in json.loads(scores[player])["serializedScoreData"]] + ] for player in players + } + state["scores"] = _scores + + # Team files + state["teamFiles"] = [json.loads(team)["ls"] for team in state["_teamFiles"]] + del state["_teamFiles"] + + with open(json_file, "w") as f: + json.dump(state, f, indent=2) + + if args.print: + print(json.dumps(state, indent=2)) + +else: # Re-encode the tournament.json to a tournament.state file + with open(json_file, "r", encoding='utf-8') as f: + state = json.load(f) + separators = (',', ':') + + # Heats + for heat in state["heats"].values(): + heat["_teams"] = [json.dumps({"team": team}, separators=separators) for team in heat["teams"]] + del heat["teams"] + state["_heats"] = [json.dumps(heat, separators=separators) for heat in state["heats"].values()] + del state["heats"] + + # Scores + scores = state["scores"] + scores["_weightKeys"] = list(scores["weights"].keys()) + scores["_weightValues"] = list(scores["weights"].values()) + scores["_players"] = list(scores["scores"].keys()) + players = scores["_players"] + _scores = [scores["scores"][player] for player in players] + scores["_files"] = [scores["files"][player] for player in players] + results = scores["results"] + for result in results: + result["_survivingTeams"] = [json.dumps(team, separators=separators) for team in result["survivingTeams"]] + result["_deadTeams"] = [json.dumps(team, separators=separators) for team in result["deadTeams"]] + del result["survivingTeams"], result["deadTeams"] + scores["_results"] = [json.dumps(result, separators=separators) for result in results] + _scores = [ + json.dumps({"serializedScoreData": [ + json.dumps({ + "scoreData": json.dumps( + { + field: heat[field] for field in heat if field not in ("hitCounts", "damageFromGuns", "damageFromRockets", "rocketPartDamageCounts", "rocketStrikeCounts", "rammingPartLossCounts", "damageFromMissiles", "missilePartDamageCounts", "missileHitCounts", "battleDamageFrom", "damageTypesTaken", "everyoneWhoDamagedMe") + }, separators=separators) + } | { + field: [heat[field][player] if player in heat[field] else 0 for player in players] for field in ("hitCounts", "damageFromGuns", "damageFromRockets", "rocketPartDamageCounts", "rocketStrikeCounts", "rammingPartLossCounts", "damageFromMissiles", "missilePartDamageCounts", "missileHitCounts", "battleDamageFrom") + } | { + field: heat[field] for field in ("damageTypesTaken", "everyoneWhoDamagedMe") + }, separators=separators) + for heat in playerScores + ]}, separators=separators) for playerScores in _scores + ] + scores["_scores"] = _scores + del scores["weights"], scores["scores"], scores["files"], scores["results"] + state["_scores"] = json.dumps(scores, separators=separators) + del state["scores"] + + # Team files + state["_teamFiles"] = [json.dumps({"ls": team}, separators=separators) for team in state["teamFiles"]] + del state["teamFiles"] + + try: # Dump back to gzip compressed format + with gzip.open(state_file, "wb") as f: + f.write(json.dumps(state, separators=separators).encode("utf-8")) + except: # Revert to ASCII + with open(state_file, "w") as f: + json.dump(state, f, separators=separators) diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/plot_summary.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/plot_summary.py new file mode 100755 index 000000000..2e82f0ab5 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/plot_summary.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +# Standard Library +import argparse +import csv +import re +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Union + +# Third Party +import matplotlib.pyplot as plt +import numpy + +VERSION = "6.2" + +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.") +parser.add_argument('-i', '--ignore', type=str, help="Ignore craft matching the regular expression search term.") +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.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) + +with open(tournamentDir / "summary.csv", 'r', encoding='utf-8') as f: + data = list(csv.reader(f)) +vessel_count = data.index([]) - 1 +names = [data[row][0] for row in range(len(data) - vessel_count, len(data))] +scores = numpy.array([[float(v) for v in data[row][1:]] for row in range(len(data) - vessel_count, len(data))]) +if args.ignore: + keep = [re.search(args.ignore, name) == None for name in names] + names = [name for i,name in enumerate(names) if keep[i]] + scores = scores[keep] +plt.figure(figsize=(16, 10), dpi=200) +plt.plot(scores.transpose(), linewidth=5) +plt.axhline(color='black') +plt.xlabel('round') +plt.ylabel('score') +if len(names) > 16: # Roughly half the plot height, put them outside the graph + plt.legend(names, loc='upper left', bbox_to_anchor=(1, 1)) +else: + plt.legend(names, loc='upper left') +plt.autoscale(enable=True, tight=True) +plt.tight_layout() +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: + if args.save == 'tmp': + fd, filename = tempfile.mkstemp(suffix='.png') + else: + filename = args.save + plt.savefig(filename, dpi='figure', bbox_inches='tight', transparent=args.transparent) + print(f"Image saved to {filename}") + try: + subprocess.run(f'display {filename}'.split()) + except: + pass +else: + plt.show(block=True) diff --git a/BDArmory/Distribution/GameData/BDArmory/Scripts/plot_vessel_traces.py b/BDArmory/Distribution/GameData/BDArmory/Scripts/plot_vessel_traces.py new file mode 100644 index 000000000..f39fe5874 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Scripts/plot_vessel_traces.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# Standard library imports +import json +from pathlib import Path + +# Third party imports +import matplotlib.pyplot as plt + +VERSION = "1.1" + + +def plot(paths, colours): + """ Plot vessel traces using matplotlib. + + Args: + paths (Path): The file paths of the vessels to trace. + colours (str): The colours of each trace. + """ + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + for path, colour in zip(paths, colours): + with open(path, 'r') as f: + m = json.load(f) + p = [m['position'] for m in m[1:]] + x = [p[0] for p in p] + z = [p[1] for p in p] + y = [p[2] for p in p] + ax.scatter(x, y, z, c=colour, marker='.') + plt.show() + + +vesselTracesPath = Path(__file__).parent.parent / 'Logs' / 'VesselTraces' +if vesselTracesPath.exists(): + paths = [p for p in vesselTracesPath.iterdir() if p.suffix == '.json'] + colours = 'rgbcmyk' * (len(paths) // 8 + 1) # Loop colours if there's too many paths. + plot(paths, colours[:len(paths)]) +else: + print("No vessel traces available.") 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/FiringAnglePic.png b/BDArmory/Distribution/GameData/BDArmory/Textures/FiringAnglePic.png new file mode 100644 index 000000000..6e8b51f6f Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/FiringAnglePic.png differ 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/Icons/Icon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon.png new file mode 100644 index 000000000..7e64cc550 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_A.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_A.png new file mode 100644 index 000000000..851fad514 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_A.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Base.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Base.png new file mode 100644 index 000000000..580d655d6 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Base.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_C.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_C.png new file mode 100644 index 000000000..96e15a366 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_C.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Generic.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Generic.png new file mode 100644 index 000000000..ce096ed28 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Generic.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Plane.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Plane.png new file mode 100644 index 000000000..b789e4d93 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Plane.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Probe.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Probe.png new file mode 100644 index 000000000..6fe839ec8 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Probe.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Rover.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Rover.png new file mode 100644 index 000000000..c51da712d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Rover.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Ship.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Ship.png new file mode 100644 index 000000000..3e6882a1e Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Ship.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Sub.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Sub.png new file mode 100644 index 000000000..ed666660d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/Icon_Sub.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/debrisIcon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/debrisIcon.png new file mode 100644 index 000000000..2f3442fe2 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/debrisIcon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/missileIcon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/missileIcon.png new file mode 100644 index 000000000..a19a05773 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/missileIcon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/rocketIcon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/rocketIcon.png new file mode 100644 index 000000000..c34960c36 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/rocketIcon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/targetIcon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/targetIcon.png new file mode 100644 index 000000000..037eecbc7 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Icons/targetIcon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAccuracy.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAccuracy.png new file mode 100644 index 000000000..8cd26ba54 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAccuracy.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAttack.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAttack.png new file mode 100644 index 000000000..e70004622 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAttack.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAttack2.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAttack2.png new file mode 100644 index 000000000..f0d97bfb3 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconAttack2.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconBallistic.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconBallistic.png new file mode 100644 index 000000000..91e70b400 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconBallistic.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconDefense.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconDefense.png new file mode 100644 index 000000000..8fa2ea07c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconDefense.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconLaser.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconLaser.png new file mode 100644 index 000000000..e822d0e6c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconLaser.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconMass.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconMass.png new file mode 100644 index 000000000..24763a8f7 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconMass.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconRegen.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconRegen.png new file mode 100644 index 000000000..249fc4ddf Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconRegen.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconRocket.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconRocket.png new file mode 100644 index 000000000..00f8816b5 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconRocket.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconSkull.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconSkull.png new file mode 100644 index 000000000..27b60548b Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconSkull.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconSpeed.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconSpeed.png new file mode 100644 index 000000000..256e45c42 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconSpeed.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconTarget.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconTarget.png new file mode 100644 index 000000000..88bfbd43b Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconTarget.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconUnknown.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconUnknown.png new file mode 100644 index 000000000..6f501b057 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconUnknown.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconVampire.png b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconVampire.png new file mode 100644 index 000000000..8ff715378 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/Mutators/IconVampire.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/greenCross.png b/BDArmory/Distribution/GameData/BDArmory/Textures/greenCross.png new file mode 100644 index 000000000..0303dc39b Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/greenCross.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/icon_Armor.png b/BDArmory/Distribution/GameData/BDArmory/Textures/icon_Armor.png new file mode 100644 index 000000000..d1df69fb4 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/icon_Armor.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/icon_ai.png b/BDArmory/Distribution/GameData/BDArmory/Textures/icon_ai.png new file mode 100644 index 000000000..e6f9f6951 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/icon_ai.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/Textures/tracerSmoke.png b/BDArmory/Distribution/GameData/BDArmory/Textures/tracerSmoke.png new file mode 100644 index 000000000..764cabf55 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/tracerSmoke.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/BDAc_Test_Drone_MKIII.craft b/BDArmory/Distribution/GameData/BDArmory/craft/BDAc_Test_Drone_MKIII.craft index c9e74b0a1..0c0b67bbf 100644 --- a/BDArmory/Distribution/GameData/BDArmory/craft/BDAc_Test_Drone_MKIII.craft +++ b/BDArmory/Distribution/GameData/BDArmory/craft/BDAc_Test_Drone_MKIII.craft @@ -1276,21 +1276,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -1395,21 +1380,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -3096,21 +3066,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -3227,21 +3182,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -3358,21 +3298,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -3489,21 +3414,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -3971,21 +3881,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -4343,21 +4238,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -4474,21 +4354,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -4605,21 +4470,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -4736,21 +4586,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -5218,21 +5053,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -7153,21 +6973,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -8834,21 +8639,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -9014,21 +8804,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True @@ -9176,21 +8951,6 @@ PART } } MODULE - { - name = BDACategoryModule - isEnabled = True - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True diff --git a/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft b/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft new file mode 100644 index 000000000..cb955a8ef --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft @@ -0,0 +1,205 @@ +ship = SpawnProbe +version = 1.9.0 +description = +type = VAB +size = 0.582821608,0.120635986,0.582823336 +steamPublishedFileId = 0 +persistentId = 920214091 +rot = 0,0,0,0 +missionFlag = Squad/Flags/retro +vesselType = Debris +OverrideDefault = False,False,False,False +OverrideActionControl = 0,0,0,0 +OverrideAxisControl = 0,0,0,0 +OverrideGroupNames = ,,, +PART +{ + part = probeCoreOcto2.v2_4294285576 + partName = Part + persistentId = 537618282 + pos = -0.172197744,11.4987183,-0.248938382 + attPos = 0,0,0 + attPos0 = -0.172197744,11.4987183,-0.248938382 + rot = 0,0,0,1 + attRot = 0,0,0,1 + attRot0 = 0,0,0,1 + mir = 1,1,1 + symMethod = Mirror + autostrutMode = Off + rigidAttachment = False + istg = -1 + resPri = 0 + dstg = 0 + sidx = -1 + sqor = -1 + sepI = -1 + attm = 0 + sameVesselCollision = False + modCost = 0 + modMass = 0 + modSize = 0,0,0 + attN = bottom,Null_0_0|-0.0610621013|0_0|-1|0_0|-0.0610621013|0_0|-1|0 + attN = top,Null_0_0|0.0610621013|0_0|1|0_0|0.0610621013|0_0|1|0 + EVENTS + { + } + ACTIONS + { + ToggleSameVesselInteraction + { + actionGroup = None + wasActiveBeforePartWasAdjusted = False + } + SetSameVesselInteraction + { + actionGroup = None + wasActiveBeforePartWasAdjusted = False + } + RemoveSameVesselInteraction + { + actionGroup = None + wasActiveBeforePartWasAdjusted = False + } + } + PARTDATA + { + } + MODULE + { + name = ModuleCommand + isEnabled = True + hibernation = False + hibernateOnWarp = False + activeControlPointName = _default + stagingEnabled = True + EVENTS + { + } + ACTIONS + { + MakeReferenceToggle + { + actionGroup = None + wasActiveBeforePartWasAdjusted = False + } + HibernateToggle + { + actionGroup = None + wasActiveBeforePartWasAdjusted = False + } + } + UPGRADESAPPLIED + { + } + } + MODULE + { + name = ModuleSAS + isEnabled = True + standaloneToggle = True + stagingEnabled = True + EVENTS + { + } + ACTIONS + { + } + UPGRADESAPPLIED + { + } + } + MODULE + { + name = ModuleKerbNetAccess + isEnabled = True + stagingEnabled = True + EVENTS + { + } + ACTIONS + { + OpenKerbNetAction + { + actionGroup = None + wasActiveBeforePartWasAdjusted = False + } + } + UPGRADESAPPLIED + { + } + } + MODULE + { + name = ModuleDataTransmitter + isEnabled = True + xmitIncomplete = False + stagingEnabled = True + EVENTS + { + } + ACTIONS + { + StartTransmissionAction + { + actionGroup = None + active = False + wasActiveBeforePartWasAdjusted = False + } + } + UPGRADESAPPLIED + { + } + } + MODULE + { + name = HitpointTracker + isEnabled = True + Armor = 10 + maxHitPoints = 0 + ArmorThickness = 0 + ArmorSet = True + ExplodeMode = Never + FireFX = True + FireFXLifeTimeInSeconds = 5 + stagingEnabled = True + EVENTS + { + } + ACTIONS + { + } + UPGRADESAPPLIED + { + } + } + MODULE + { + name = ModuleTripLogger + isEnabled = True + stagingEnabled = True + EVENTS + { + } + ACTIONS + { + } + Log + { + flight = 0 + } + UPGRADESAPPLIED + { + } + } + RESOURCE + { + name = ElectricCharge + amount = 5 + maxAmount = 5 + flowState = True + isTweakable = True + hideFlow = False + isVisible = True + flowMode = Both + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files_v1.1.0.py b/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files_v1.1.0.py deleted file mode 100644 index 6f77e03c5..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files_v1.1.0.py +++ /dev/null @@ -1,92 +0,0 @@ -from pathlib import Path -import argparse - -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.") -args = parser.parse_args() - -log_dir = Path(__file__).parent / "Logs" if len(args.logs) == 0 else Path('.') -output_log_file = log_dir / "results.csv" - -craft_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 = "" - Kills = 0 - Hits = 0 - Shots = 0 - Who_Shot_Me_Lines = [] - Who_Damaged_Me_Lines = [] - Clean_Kill_Lines = [] # Shots, rams and missiles all counted. - for line in file_data: - if not "VesselSpawner" in line: - continue - if " Name:" in line: - craft_data.append(["bug", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) # Name, clean kills,assists, deaths, hits, shots, damage, accuracy, score, hits/spawn, damage/spawn - Craft_Name = line.split(" Name:")[-1].replace("\n", "") - craft_data[-1][0] = Craft_Name - Hits = 0 - Shots = 0 - if " DEATHCOUNT:" in line: # Counts up deaths - craft_data[-1][3] = int(line.split("DEATHCOUNT:")[-1].replace("\n", "")) - if " CLEANKILL:" in line: # Counts up clean kills - Clean_Kill_Lines.extend(line.split(": CLEANKILL:")[-1].replace("\n", "").split(", ")) - if " CLEANRAM:" in line: # Counts up clean ram kills - Clean_Kill_Lines.extend(line.split(": CLEANRAM:")[-1].replace("\n", "").split(", ")) - if " CLEANMISSILEKILL:" in line: # Counts up clean missile kills - Clean_Kill_Lines.extend(line.split(": CLEANMISSILEKILL:")[-1].replace("\n", "").split(", ")) - if " WHOSHOTME:" in line: # Counds up assists - Who_Shot_Me_Lines.extend(line.split(": WHOSHOTME:")[-1].replace("\n", "").split(", ")[:craft_data[-1][3]]) - if " WHODAMAGEDMEWITHBULLETS:" in line: # Counds up damage - Who_Damaged_Me_Lines.extend(line.split(": WHODAMAGEDMEWITHBULLETS:")[-1].replace("\n", "").split(", ")) - if " ACCURACY:" in line: # Counts up hits - for item in line.split(" ACCURACY:")[-1].replace("\n", "").split(","): - Hits += int(item.split(":")[-1].split("/")[0]) - craft_data[-1][4] = Hits - if " ACCURACY:" in line: # Counts hp shots - for item in line.split(" ACCURACY:")[-1].replace("\n", "").split(","): - Shots += int(item.split(":")[-1].split("/")[1]) - craft_data[-1][5] = Shots - - for WHOSHOTME in Who_Shot_Me_Lines: # Counts up assists - # for shooter_round in WHOSHOTME.split(","): - for shooter_hits in WHOSHOTME.split(";"): - shooter = shooter_hits.split(":")[-1] - for person_index in range(len(craft_data)): - if shooter == craft_data[person_index][0]: - craft_data[person_index][2] += 1 - for WHOSHOTME in Who_Damaged_Me_Lines: # Counts up damage - # for shooter_round in WHOSHOTME.split(","): - for shooter_hits in WHOSHOTME.split(";"): - shooter = shooter_hits.split(":")[-1] - damage = float(shooter_hits.split(":")[-2]) - for person_index in range(len(craft_data)): - if shooter == craft_data[person_index][0]: - craft_data[person_index][6] += damage - for kill_line in Clean_Kill_Lines: # Counts up clean kills - killer = kill_line.split(":")[1] - for person_index in range(len(craft_data)): - if killer == craft_data[person_index][0]: - craft_data[person_index][1] += 1 - for i in range(len(craft_data)): - craft_data[i][6] = int(craft_data[i][6]) - craft_data[i][7] = int(10000 * craft_data[i][4] / craft_data[i][5] if craft_data[i][5] > 0 else 0) / 100 - craft_data[i][8] = round(3 * craft_data[i][1] + 1 * craft_data[i][2] - 3 * craft_data[i][3] + .001 * craft_data[i][4], 8) - craft_data[i][9] = int(100 * craft_data[i][4] / (1 + craft_data[i][3])) / 100 - craft_data[i][10] = int(craft_data[i][6] / (1 + craft_data[i][3])) - -if len(craft_data) > 0: - # Write results to console - name_length = max([len(craft[0]) for craft in craft_data]) - print(f"Name{' '*(name_length-4)}\tKills\tAssists\tDeaths\tHits\tShots\tDamage\tAcc\tScore\tHits/Sp\tDmg/Sp") - for item in sorted(craft_data, key=lambda item: item[0]): - print(f"{item[0]}{' '*(name_length-len(item[0]))}\t" + '\t'.join(str(part) for part in item[1:])) - - # Write results to file - with open(output_log_file, "w") as results_data: - results_data.write("Name,Kills,Assists,Deaths,Hits,Shots,Damage,Acc,Score,Hits/Sp,Dmg/Sp\n") - for item in sorted(craft_data, key=lambda item: item[0]): - results_data.write(','.join(str(part) for part in item) + "\n") -else: - print(f"No valid log files found.") diff --git a/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files_v1.3.0.py b/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files_v1.3.0.py deleted file mode 100644 index f0301d960..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files_v1.3.0.py +++ /dev/null @@ -1,120 +0,0 @@ -# Standard library imports -import argparse -import json -from pathlib import Path - -parser = argparse.ArgumentParser(description="Tournament log parser", formatter_class=argparse.ArgumentDefaultsHelpFormatter) -parser.add_argument('tournament', type=str, nargs='?', help="Tournament folder to parse") -args = parser.parse_args() -tournamentDir = Path(args.tournament) if args.tournament is not None else Path('') -tournamentData = {} - - -def CalculateAccuracy(hits, shots): return 100 * hits / shots if shots > 0 else 0 - - -for round in sorted(roundDir for roundDir in tournamentDir.iterdir() if roundDir.is_dir()) if args.tournament is not None else (tournamentDir,): - tournamentData[round.name] = {} - for heat in sorted(round.glob("*.log")): - with open(heat, "r") as logFile: - tournamentData[round.name][heat.name] = {} - for line in logFile: - line = line.strip() - if 'BDArmoryCompetition' not in line: - continue # Ignore irrelevant lines - _, field = line.split(' ', 1) - if field.startswith('ALIVE:'): - state, craft = field.split(':', 1) - tournamentData[round.name][heat.name][craft] = {'state': state} - elif field.startswith('DEAD:'): - state, order, time, craft = field.split(':', 3) - tournamentData[round.name][heat.name][craft] = {'state': state, 'deathOrder': order, 'deathTime': time} - elif field.startswith('MIA:'): - state, craft = field.split(':', 1) - tournamentData[round.name][heat.name][craft] = {'state': state} - elif field.startswith('WHOSHOTWHO:'): - _, craft, shooters = field.split(':', 2) - data = shooters.split(':') - tournamentData[round.name][heat.name][craft].update({'hitsBy': {player: int(hits) for player, hits in zip(data[1::2], data[::2])}}) - elif field.startswith('WHODAMAGEDWHOWITHBULLETS:'): - _, craft, shooters = field.split(':', 2) - data = shooters.split(':') - tournamentData[round.name][heat.name][craft].update({'bulletDamageBy': {player: float(damage) for player, damage in zip(data[1::2], data[::2])}}) - elif field.startswith('WHOSHOTWHOWITHMISSILES:'): - _, craft, shooters = field.split(':', 2) - data = shooters.split(':') - tournamentData[round.name][heat.name][craft].update({'missileHitsBy': {player: int(hits) for player, hits in zip(data[1::2], data[::2])}}) - elif field.startswith('WHODAMAGEDWHOWITHMISSILES:'): - _, craft, shooters = field.split(':', 2) - data = shooters.split(':') - tournamentData[round.name][heat.name][craft].update({'missileDamageBy': {player: float(damage) for player, damage in zip(data[1::2], data[::2])}}) - elif field.startswith('WHORAMMEDWHO:'): - _, craft, rammers = field.split(':', 2) - data = rammers.split(':') - tournamentData[round.name][heat.name][craft].update({'rammedPartsLostBy': {player: int(partsLost) for player, partsLost in zip(data[1::2], data[::2])}}) - # Ignore OTHERKILL for now. - elif field.startswith('CLEANKILL:'): - _, craft, killer = field.split(':', 2) - tournamentData[round.name][heat.name][craft].update({'cleanKillBy': killer}) - elif field.startswith('CLEANMISSILEKILL:'): - _, craft, killer = field.split(':', 2) - tournamentData[round.name][heat.name][craft].update({'cleanMissileKillBy': killer}) - elif field.startswith('CLEANRAM:'): - _, craft, killer = field.split(':', 2) - tournamentData[round.name][heat.name][craft].update({'cleanRamKillBy': killer}) - elif field.startswith('ACCURACY:'): - _, craft, accuracy = field.split(':', 2) - hits, shots = accuracy.split('/') - accuracy = CalculateAccuracy(int(hits), int(shots)) - tournamentData[round.name][heat.name][craft].update({'accuracy': accuracy, 'hits': int(hits), 'shots': int(shots)}) - # Ignore Tag mode for now. - -with open(tournamentDir / 'results.json', 'w') 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.keys()))) -summary = { - craft: { - 'survivedCount': len([1 for round in tournamentData.values() for heat in round.values() if craft in heat and heat[craft]['state'] == 'ALIVE']), - 'deathCount': len([1 for round in tournamentData.values() for heat in round.values() if craft in heat and heat[craft]['state'] == 'DEAD']), - 'cleanKills': len([1 for round in tournamentData.values() for heat in round.values() for data in heat.values() if any((field in data and data[field] == craft) for field in ('cleanKillBy', 'cleanMissileKillBy', 'cleanRamKillBy'))]), - 'assists': len([1 for round in tournamentData.values() for heat in round.values() for data in heat.values() if data['state'] == 'DEAD' and any(field in data and craft in data[field] for field in ('hitsBy', 'missileHitsBy', 'rammedPartsLostBy')) and not any((field in data and data[field] == craft) for field in ('cleanKillBy', 'cleanMissileKillBy', 'cleanRamKillBy'))]), - 'hits': sum([heat[craft]['hits'] for round in tournamentData.values() for heat in round.values() if craft in heat and 'hits' in heat[craft]]), - 'bulletDamage': sum([data[field][craft] for round in tournamentData.values() for heat in round.values() for data in heat.values() for field in ('bulletDamageBy',) if field in data and craft in data[field]]), - 'missileHits': sum([data[field][craft] for round in tournamentData.values() for heat in round.values() for data in heat.values() for field in ('missileHitsBy',) if field in data and craft in data[field]]), - 'missileDamage': sum([data[field][craft] for round in tournamentData.values() for heat in round.values() for data in heat.values() for field in ('missileDamageBy',) if field in data and craft in data[field]]), - 'ramScore': sum([data[field][craft] for round in tournamentData.values() for heat in round.values() for data in heat.values() for field in ('rammedPartsLostBy',) if field in data and craft in data[field]]), - 'accuracy': CalculateAccuracy(sum([heat[craft]['hits'] for round in tournamentData.values() for heat in round.values() if craft in heat and 'hits' in heat[craft]]), sum([heat[craft]['shots'] for round in tournamentData.values() for heat in round.values() if craft in heat and 'shots' in heat[craft]])), - } - for craft in craftNames -} - -for craft in summary.values(): - spawns = craft['survivedCount'] + craft['deathCount'] - craft.update({ - 'damage/hit': craft['bulletDamage'] / craft['hits'] if craft['hits'] > 0 else 0, - 'hits/spawn': craft['hits'] / spawns if spawns > 0 else 0, - 'damage/spawn': craft['bulletDamage'] / spawns if spawns > 0 else 0, - }) - -with open(tournamentDir / 'summary.json', 'w') as outFile: - json.dump(summary, outFile, indent=2) - -if len(summary) > 0: - csv_summary = "craft," + ",".join(k for k in next(iter(summary.values())).keys()) + "\n" - csv_summary += "\n".join(craft + "," + ",".join(str(v) for v in scores.values()) for craft, scores in summary.items()) - with open(tournamentDir / 'summary.csv', 'w') as outFile: - outFile.write(csv_summary) - - # Write results to console - name_length = max([len(craft) for craft in summary]) - print(f"Name{' '*(name_length-4)}\tSurvive\tDeaths\tKills\tAssists\tHits\tDamage\tMisHits\tMisDmg\tRam\tAcc%\tDmg/Hit\tHits/Sp\tDmg/Sp") - for craft in sorted(summary): - spawns = summary[craft]['survivedCount'] + summary[craft]['deathCount'] - print( - f"{craft}{' '*(name_length-len(craft))}\t" - + '\t'.join(f'{score}' if isinstance(score, int) else f'{score:.0f}' if field in ('bulletDamage', 'missileDamage') else f'{score:.1f}' if field in ('damage/hit', 'hits/spawn', 'damage/spawn') else f'{score:.2f}' for field, score in summary[craft].items()) - ) -else: - print("No valid log files found.") diff --git a/BDArmory/Evolution/BDAEvolution.cs b/BDArmory/Evolution/BDAEvolution.cs new file mode 100644 index 000000000..a6562ffe5 --- /dev/null +++ b/BDArmory/Evolution/BDAEvolution.cs @@ -0,0 +1,777 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Settings; +using BDArmory.VesselSpawning; + +namespace BDArmory.Evolution +{ + public enum EvolutionStatus + { + Idle, + Preparing, + GeneratingVariants, + RunningTournament, + ProcessingResults, + } + + public class EvolutionState + { + public string id; + public EvolutionStatus status; + public List groups; + public EvolutionState(string id, EvolutionStatus status, List groups) + { + this.id = id; + this.status = status; + this.groups = groups; + } + } + + public class EvolutionWorkingState + { + public string savegame; + public string evolutionId; + public CircularSpawnConfig spawnConfig; + // public Dictionary> aggregateScores; + } + + public class VariantGroup + { + public int id; + public string seedName; + public string referenceName; + public List variants; + public VariantGroup(int id, string seedName, string referenceName, List variants) + { + this.id = id; + this.seedName = seedName; + this.referenceName = referenceName; + this.variants = variants; + } + } + + public class Variant + { + public string id; + public string name; + public List mutatedParts; + public string key; + public int direction; + public Variant(string id, string name, List mutatedParts, string key, int direction) + { + this.id = id; + this.name = name; + this.mutatedParts = mutatedParts; + this.key = key; + this.direction = direction; + } + } + + public class MutatedPart + { + public string partName; + public string moduleName; + public string paramName; + public float referenceValue; + public float value; + public MutatedPart(string partName, string moduleName, string paramName, float referenceValue, float value) + { + this.partName = partName; + this.moduleName = moduleName; + this.paramName = paramName; + this.referenceValue = referenceValue; + this.value = value; + } + } + + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class BDAModuleEvolution : MonoBehaviour + { + public static BDAModuleEvolution Instance; + + public static string configDirectory; + private static string workingDirectory; + private static string seedDirectory; + private static string adversaryDirectory; + private static string weightMapFile; + private static string stateFile; + + private Coroutine evoCoroutine = null; + + private EvolutionStatus status = EvolutionStatus.Idle; + public EvolutionStatus Status() { return status; } + + private EvolutionState evolutionState = null; + + // Current active variant group (used to render in EvolutionWindow or similar) + public VariantGroup ActiveVariantGroup() {return evolutionState.groups.Last();} + + private VariantEngine engine = null; + + // Spawn settings + private static CircularSpawnConfig spawnConfig; + + // config node for evolution details + private ConfigNode config = null; + + // root node of the active seed craft + private ConfigNode craft = null; + + // evolution id + private string evolutionId = null; + public string EvolutionId { get { return evolutionId; } } + + // group id + private int groupId = 0; + public int GroupId { get { return groupId; } } + + // next variant id + private int nextVariantId = 0; + + private int heat = 0; + public int Heat { get { return heat; } } + + // private VariantOptions options; + + private static Dictionary> aggregateScores = new Dictionary>(); + + public static void ConfigurePaths() + { + configDirectory = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", "evolutions")); + workingDirectory = Path.Combine(configDirectory, "working"); + seedDirectory = Path.Combine(configDirectory, "seeds"); + adversaryDirectory = Path.Combine(configDirectory, "adversaries"); + weightMapFile = Path.Combine(configDirectory, "weights.cfg"); + stateFile = Path.Combine(configDirectory, "evolution.state"); + } + + void Awake() + { + // Debug.Log("[BDArmory.BDAEvolution]: Evolution awake"); + if (Instance) + { + Destroy(Instance); + } + + Instance = this; + if (string.IsNullOrEmpty(configDirectory)) ConfigurePaths(); + } + + private void Start() + { + // Debug.Log("[BDArmory.BDAEvolution]: Evolution start"); + engine = new VariantEngine(); + } + + private void OnDestroy() + { + SaveState(); + } + + public void StartEvolution() + { + if (evoCoroutine != null) + { + // Debug.Log("[BDArmory.BDAEvolution]: Evolution already running"); + return; + } + // Debug.Log("[BDArmory.BDAEvolution]: Evolution starting"); + status = EvolutionStatus.Preparing; + + // initialize evolution + nextVariantId = 1; + groupId = 1; + evolutionId = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + 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_REF_HEADING + ); + evolutionState = new EvolutionState(evolutionId, status, new List()); + + // create new config + CreateEvolutionConfig(); + + evoCoroutine = StartCoroutine(ExecuteEvolution()); + } + + public void ResumeEvolution(EvolutionWorkingState state) + { + if (state == null) return; // No valid state given. + if (evoCoroutine != null) return; // Already running. + + // Copy state to local state. + evolutionId = state.evolutionId; + spawnConfig = state.spawnConfig; + evolutionState = new EvolutionState(evolutionId, EvolutionStatus.Preparing, new List()); + var configFile = Path.Combine(configDirectory, evolutionId + ".cfg"); + ConfigNode existing = null; + if (File.Exists(configFile)) existing = ConfigNode.Load(configFile); + if (existing == null || !existing.HasNode("EVOLUTION")) + { + Debug.Log($"[BDArmory.BDAEvolution]: No pre-existing evolution found, starting a new one."); + StartEvolution(); // No pre-existing evolution, start a new one. + return; + } + ConfigNode evoNode = existing.GetNode("EVOLUTION"); + // groupId = int.Parse(evoNode.GetValue("groupId")); + nextVariantId = int.Parse(evoNode.GetValue("nextVariantId")); + foreach (var groupNode in existing.GetNodes("GROUP")) + { + groupId = int.Parse(groupNode.GetValue("id")); + var seedName = groupNode.GetValue("seedName"); + var referenceName = groupNode.GetValue("referenceName"); + VariantGroup variantGroup = new VariantGroup(groupId, seedName, referenceName, new List()); + + foreach (var variantNode in groupNode.GetNodes("VARIANT")) + { + var varId = variantNode.GetValue("id"); + var varName = variantNode.GetValue("name"); + var variant = new Variant(varId, varName, new List(), "", 0); // key and direction don't seem to be used. + + foreach (var partNode in variantNode.GetNodes("MUTATION")) + { + var partName = partNode.GetValue("partName"); + var moduleName = partNode.GetValue("moduleName"); + var paramName = partNode.GetValue("paramName"); + var referenceValue = float.Parse(partNode.GetValue("referenceValue")); + var value = float.Parse(partNode.GetValue("value")); + variant.mutatedParts.Add(new MutatedPart(partName, moduleName, paramName, referenceValue, value)); + } + variantGroup.variants.Add(variant); + } + evolutionState.groups.Add(variantGroup); + } + this.config = existing; + ++groupId; + Debug.Log($"[BDArmory.BDAEvolution]: Resuming evolutionId: {evolutionId}, groupId: {groupId}"); + + // Resume running. + evoCoroutine = StartCoroutine(ExecuteEvolution()); + } + + public void StopEvolution() + { + if (evoCoroutine == null) + { + // Debug.Log("[BDArmory.BDAEvolution]: Evolution not running"); + return; + } + // Debug.Log("[BDArmory.BDAEvolution]: Evolution stopping"); + status = EvolutionStatus.Idle; + + StopCoroutine(evoCoroutine); + evoCoroutine = null; + } + + private void CreateEvolutionConfig() + { + string configFile = string.Format("{0}/{1}.cfg", configDirectory, evolutionId); + ConfigNode existing = null; + if (File.Exists(configFile)) existing = ConfigNode.Load(configFile); + if (existing == null) + { + existing = new ConfigNode(); + } + if (!existing.HasNode("EVOLUTION")) + { + existing.AddNode("EVOLUTION"); + } + ConfigNode evoNode = existing.GetNode("EVOLUTION"); + evoNode.AddValue("id", evolutionId); + evoNode.AddValue("groupId", groupId); + evoNode.AddValue("nextVariantId", nextVariantId); + existing.Save(configFile); + this.config = existing; + SaveState(); + } + + private void SaveState() + { + if (spawnConfig == null) return; // No spawn config means it hasn't been runnning. + spawnConfig.craftFiles = null; // We don't want to include the specific craft files in the spawn config. + spawnConfig.teamCounts = null; + var workingState = new EvolutionWorkingState + { + savegame = HighLogic.SaveFolder, + evolutionId = evolutionId, + spawnConfig = spawnConfig, + // aggregateScores = aggregateScores + }; + // Write everything to file. + File.WriteAllLines(stateFile, new List{ + JsonUtility.ToJson(workingState), + JsonUtility.ToJson(workingState.spawnConfig), // We need to do this separately as Unity's JSON is really simplistic. + }); + } + + public static EvolutionWorkingState LoadState() + { + var state = new EvolutionWorkingState(); + if (string.IsNullOrEmpty(configDirectory)) ConfigurePaths(); + if (File.Exists(stateFile)) + { + try + { + var strings = File.ReadAllLines(stateFile); + state = JsonUtility.FromJson(strings[0]); + state.spawnConfig = JsonUtility.FromJson(strings[1]); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.BDAEvolution]: Failure to properly read evolution state file: " + e.Message); + } + } + else + { Debug.LogError($"[BDArmory.BDAEvolution]: Failed to find evolution.state file {stateFile}"); } + return state; + } + + private IEnumerator ExecuteEvolution() + { + // 1. generate variants for the latest seed craft + // 2. run tournament + // 3. compute weighted centroid variant + // 4. repeat from 1 + + status = EvolutionStatus.Preparing; + while (status != EvolutionStatus.Idle) // Avoid unnecessary recursion. + { + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution next group {0}", groupId)); + + status = EvolutionStatus.GeneratingVariants; + GenerateVariants(); + + status = EvolutionStatus.RunningTournament; + yield return ExecuteTournament(); + + status = EvolutionStatus.ProcessingResults; + InterpretResults(); + + if (TournamentAutoResume.Instance != null && TournamentAutoResume.Instance.CheckMemoryUsage()) yield break; // Auto-Quit before the next variants are generated. + + ++groupId; + } + } + + private void GenerateVariants() + { + ClearWorkingDirectory(); + + var seedName = LoadSeedCraft(); + engine.Configure(craft, weightMapFile); + + // add the original + // Note: should be called first before variants are created since there appears to be some mutation operations that are not fully isolated (debugging WIP) + // Saving first also allows avoidance of needing to use craft.CreateCopy() for the reference craft + var referenceName = string.Format("R{0}", groupId); + SaveVariant(craft, referenceName); + + // generate dipolar variants for all primary axes + var mutations = engine.GenerateMutations(BDArmorySettings.EVOLUTION_MUTATIONS_PER_HEAT); + List variants = new List(); + foreach (var mutation in mutations) + { + ConfigNode newVariant = mutation.Apply(craft, engine); + var id = nextVariantId; + var name = GetNextVariantName(); + variants.Add(mutation.GetVariant(id.ToString(), name)); + SaveVariant(newVariant, name); + } + + // select random adversary + LoadAdversaryCraft(); + + AddVariantGroupToConfig(new VariantGroup(groupId, seedName, referenceName, variants)); + } + + // deletes all craft files in the working directory + private void ClearWorkingDirectory() + { + if (!Directory.Exists(workingDirectory)) Directory.CreateDirectory(workingDirectory); + var info = new DirectoryInfo(workingDirectory); + var files = info.GetFiles("*.craft").ToList(); + foreach (var file in files) + { + file.Delete(); + } + } + + // attempts to load the latest seed craft and store it in memory + private string LoadSeedCraft() + { + var info = new DirectoryInfo(seedDirectory); + var seeds = info.GetFiles("*.craft").ToList(); + // TODO: + // Pre-existing seed files that are overwritten keep the old creation time + // (e.g. if the evolution program is stopped and restarted or started again on a new save) of seeds + // Need to check to make sure previous seed name doesn't exist when saving to avoid this + // (can't use file modification time since LastWriteTime is only available on windows NT file systems) + var latestSeed = seeds.OrderBy(e => e.CreationTimeUtc).Last().Name; + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution using latest seed: {0}", latestSeed)); + ConfigNode node = ConfigNode.Load(string.Format("{0}/{1}", seedDirectory, latestSeed)); + this.craft = node; + return latestSeed; + } + + // attempts to load an adversary craft into the group + private void LoadAdversaryCraft() + { + var info = new DirectoryInfo(adversaryDirectory); + var adversaries = info.GetFiles("*.craft").ToList(); + if (adversaries.Count == 0) + { + Debug.Log("[BDArmory.BDAEvolution]: Evolution no adversaries found"); + return; + } + else if (adversaries.Count < BDArmorySettings.EVOLUTION_ANTAGONISTS_PER_HEAT) + { + Debug.Log("[BDArmory.BDAEvolution]: Evolution using all available adversaries"); + foreach (var a in adversaries) + { + ConfigNode adversaryNode = ConfigNode.Load(string.Format("{0}/{1}", adversaryDirectory, a)); + adversaryNode.Save(string.Format("{0}/{1}", workingDirectory, a)); + } + return; + } + else + { + for (var k = 0; k < BDArmorySettings.EVOLUTION_ANTAGONISTS_PER_HEAT; k++) + { + var index = UnityEngine.Random.Range(0, adversaries.Count); + var randomAdversary = adversaries[index].Name; + adversaries.RemoveAt(index); + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution using random adversary: {0}", randomAdversary)); + ConfigNode node = ConfigNode.Load(string.Format("{0}/{1}", adversaryDirectory, randomAdversary)); + node.Save(string.Format("{0}/{1}", workingDirectory, randomAdversary)); + } + } + } + + private string GetNextVariantName() => string.Format("V{1}", evolutionId, nextVariantId++); + + private void SaveVariant(ConfigNode variant, string name) + { + // explicitly assign the craft name + variant.SetValue("ship", name); + variant.Save(string.Format("{0}/{1}.craft", workingDirectory, name)); + } + + private void AddVariantGroupToConfig(VariantGroup group) + { + evolutionState.groups.Add(group); + + if (!config.HasNode("EVOLUTION")) + { + config.AddNode("EVOLUTION"); + } + ConfigNode evoNode = config.GetNode("EVOLUTION"); + evoNode.SetValue("nextVariantId", nextVariantId); + + ConfigNode newGroup = config.AddNode("GROUP"); + newGroup.AddValue("id", groupId); + newGroup.AddValue("seedName", group.seedName); + newGroup.AddValue("referenceName", group.referenceName); + + foreach (var e in group.variants) + { + ConfigNode newVariant = newGroup.AddNode("VARIANT"); + newVariant.AddValue("id", e.id); + newVariant.AddValue("name", e.name); + foreach (var p in e.mutatedParts) + { + ConfigNode newMutatedPart = newVariant.AddNode("MUTATION"); + newMutatedPart.AddValue("partName", p.partName); + newMutatedPart.AddValue("moduleName", p.moduleName); + newMutatedPart.AddValue("paramName", p.paramName); + newMutatedPart.AddValue("referenceValue", p.referenceValue); + newMutatedPart.AddValue("value", p.value); + } + } + + var configFile = Path.Combine(configDirectory, evolutionId + ".cfg"); + config.Save(configFile); + } + + private IEnumerator ExecuteTournament() + { + var spawner = CircularSpawning.Instance; + + // clear scores + aggregateScores.Clear(); + + var comp = BDACompetitionMode.Instance; + var specialKills = new HashSet { AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal }; + + // run N tournaments and aggregate their scores + for (heat = 0; heat < BDArmorySettings.EVOLUTION_HEATS_PER_GROUP; heat++) + { + var wait = new WaitForFixedUpdate(); + spawnConfig.craftFiles = null; // We don't want to include the specific craft files in the spawn config. + spawnConfig.teamCounts = null; + SpawnUtils.ResetVesselNamingDeconfliction(); + spawner.SpawnAllVesselsOnce(spawnConfig); + while (spawner.vesselsSpawning) + yield return wait; + if (!spawner.vesselSpawnSuccess) + { + Debug.Log("[BDArmory.BDAEvolution]: Vessel spawning failed."); + yield break; + } + yield return wait; + + 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) + { + // Wait for the competition to finish + yield return new WaitForSeconds(1); + } + + // aggregate scores + var scores = comp.Scores.ScoreData; + var activeGroup = evolutionState.groups.Last(); + List playerNames = new List(); + playerNames.AddRange(activeGroup.variants.Select(e => e.name)); + playerNames.Add(activeGroup.referenceName); + foreach (var name in playerNames) + { + if (!aggregateScores.ContainsKey(name)) + { + aggregateScores[name] = new Dictionary(); + } + if (!scores.ContainsKey(name)) + { + Debug.LogError($"[BDArmory.BDAEvolution]: Variant {name} missing from scores! Valid names were " + string.Join("; ", scores.Keys)); + continue; + } + var scoreData = scores[name]; + var kills = scores.Values.Count(e => specialKills.Contains(e.aliveState) && e.lastPersonWhoDamagedMe == name); + if (aggregateScores[name].ContainsKey("kills")) + { + aggregateScores[name]["kills"] += kills; + } + else + { + aggregateScores[name]["kills"] = kills; + } + if (aggregateScores[name].ContainsKey("hits")) + { + aggregateScores[name]["hits"] += scoreData.hits + scoreData.rocketStrikes; + } + else + { + aggregateScores[name]["hits"] = scoreData.hits + scoreData.rocketStrikes; + } + if (aggregateScores[name].ContainsKey("shots")) + { + aggregateScores[name]["shots"] += scoreData.shotsFired + scoreData.rocketsFired; + } + else + { + aggregateScores[name]["shots"] = scoreData.shotsFired + scoreData.rocketsFired; + } + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution aggregated score data for {0}. kills: {1}, hits: {2}, shots: {3}", name, aggregateScores[name]["kills"], aggregateScores[name]["hits"], aggregateScores[name]["shots"])); + } + } + } + + private void InterpretResults() + { + // compute scores for the dipolar variants + var activeGroup = evolutionState.groups.Last(); + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution compute scores for {0}", activeGroup.id)); + Dictionary scores = ComputeScores(activeGroup); + + // compute weighted centroid from the dipolar variants + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution compute weighted centroid for {0}", activeGroup.id)); + var maxScore = activeGroup.variants.Select(e => scores[e.name]).Max(); + var referenceScore = scores[activeGroup.referenceName]; + if (maxScore > 0 && maxScore > referenceScore) + { + ConfigNode newCraft = craft.CreateCopy(); + + // compute weighted contributions + // map of part/module/param => delta + Dictionary>> agg = new Dictionary>>(); + Dictionary>> rvals = new Dictionary>>(); + + // feedback is based on the scores for each axis + Dictionary> axisScores = new Dictionary>(); + + foreach (var variant in activeGroup.variants) + { + // normalize scores for weighted contribution + // TODO: this is probably a bug. basis should likely be referenceScore. + var score = scores[variant.name] / maxScore; + + // track feedback score + if (!axisScores.ContainsKey(variant.key)) + { + axisScores[variant.key] = new Dictionary(); + } + axisScores[variant.key][variant.direction] = scores[variant.name] - referenceScore; + + foreach (var part in variant.mutatedParts) + { + var partContribution = part.value - part.referenceValue; + var weightedContribution = partContribution * score; + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution variant {0} score: {1}, part: {2}, module: {3}, key: {4}, value: {5}, ref: {6}", variant.name, score, part.partName, part.moduleName, part.paramName, part.value, part.referenceValue)); + if (agg.ContainsKey(part.partName)) + { + if (agg[part.partName].ContainsKey(part.moduleName)) + { + if (agg[part.partName][part.moduleName].ContainsKey(part.paramName)) + { + agg[part.partName][part.moduleName][part.paramName] += weightedContribution; + } + else + { + agg[part.partName][part.moduleName][part.paramName] = weightedContribution; + + rvals[part.partName][part.moduleName][part.paramName] = part.referenceValue; + } + } + else + { + agg[part.partName][part.moduleName] = new Dictionary(); + agg[part.partName][part.moduleName][part.paramName] = weightedContribution; + + rvals[part.partName][part.moduleName] = new Dictionary(); + rvals[part.partName][part.moduleName][part.paramName] = part.referenceValue; + } + } + else + { + agg[part.partName] = new Dictionary>(); + agg[part.partName][part.moduleName] = new Dictionary(); + agg[part.partName][part.moduleName][part.paramName] = weightedContribution; + + rvals[part.partName] = new Dictionary>(); + rvals[part.partName][part.moduleName] = new Dictionary(); + rvals[part.partName][part.moduleName][part.paramName] = part.referenceValue; + } + } + } + + // compute feedback for each axis + foreach (var key in axisScores.Keys) + { + if (axisScores[key].Count == 2) + { + // compute simple xor(negative < 0, positive > 0) + var negativeCondition = axisScores[key][-1] < 0; + var positiveCondition = axisScores[key][1] > 0; + if ((negativeCondition && !positiveCondition) || (!negativeCondition && positiveCondition)) + { + // confirmed linearity + engine.Feedback(key, 0.25f); + } + else + { + // confirmed absence of linearity + engine.Feedback(key, -0.25f); + } + } + else + { + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution wrong score count computing feedback for {0}", key)); + } + } + + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution synthesizing new generation from {0} parts", agg.Keys.Count)); + foreach (var part in agg.Keys) + { + foreach (var module in agg[part].Keys) + { + foreach (var param in agg[part][module].Keys) + { + var newValue = agg[part][module][param] + rvals[part][module][param]; + List partNodes = engine.FindPartNodes(newCraft, part); + if (partNodes.Count > 0) + { + List moduleNodes = engine.FindModuleNodes(partNodes[0], module); + if (moduleNodes.Count > 0) + { + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution mutated part: {0}, module: {1}, key: {2}, value: {3}", part, module, param, newValue)); + engine.MutateNode(moduleNodes[0], param, newValue); + } + else + { + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution failed to find module {0}", module)); + } + } + else + { + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution failed to find part {0}", part)); + } + } + } + } + + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution save result for {0}", activeGroup.id)); + newCraft.Save(string.Format("{0}/G{1}.craft", seedDirectory, activeGroup.id)); + } + else + { + // all variants somehow worse; re-seed + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution bad seed for {0}", activeGroup.id)); + // downvote all variant axes + foreach (var variant in activeGroup.variants) + { + engine.Feedback(variant.key, -0.25f); + } + } + } + + private Dictionary ComputeScores(VariantGroup group) + { + // compute a score for each variant + var results = new Dictionary(); + foreach (var p in group.variants) + { + results[p.name] = ScoreForPlayer(p.name); + } + // also compute a score for the reference craft + results[group.referenceName] = ScoreForPlayer(group.referenceName); + return results; + } + + private float ScoreForPlayer(string name) + { + var kills = aggregateScores[name]["kills"]; + var hits = aggregateScores[name]["hits"]; + var shots = aggregateScores[name]["shots"]; + var accuracy = Mathf.Clamp(shots > 0 ? (float)hits / (float)shots : 0, 0, 1); + float score = 0; + // score is a combination of kills, shots on target, hits, and accuracy + float[] weights = new float[] { 1f, 0.002f, 0.01f, 5f }; + float[] values = new float[] { kills, shots, hits, accuracy }; + for (var k = 0; k < weights.Length; k++) + { + score += weights[k] * values[k]; + } + Debug.Log(string.Format("[BDArmory.BDAEvolution]: Evolution ScoreForPlayer({0} => {1}) raw: [{2}, {3}, {4}, {5}]", name, score, kills, shots, hits, accuracy)); + return score; + } + } +} diff --git a/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs b/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs new file mode 100644 index 000000000..6db8894fc --- /dev/null +++ b/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class ControlSurfaceAxisNudgeMutation : VariantMutation + { + const string moduleName = "ModuleControlSurface"; + + public static int MASK_ROLL = 0x01; + public static int MASK_PITCH = 0x02; + public static int MASK_YAW = 0x04; + // TODO: part name filter + public string paramName; + public float modifier; + public int axisMask; + public string key; + public int direction; + private List mutatedParts = new List(); + public ControlSurfaceAxisNudgeMutation(string paramName, float modifier, int axisMask, string key, int direction) + { + this.paramName = paramName; + this.modifier = modifier; + this.axisMask = axisMask; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + Debug.Log("[BDArmory.ControlSurfaceAxisNudgeMutation]: Evolution ControlSurfaceNudgeMutation applying"); + List matchingNodes = engine.FindModuleNodes(mutatedCraft, moduleName); + foreach (var node in matchingNodes) + { + MutateIfNeeded(node, mutatedCraft, engine); + } + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + + private void MutateIfNeeded(ConfigNode node, ConfigNode craft, VariantEngine engine) + { + // check axis mask for included axes + bool shouldMutate = false; + if( (axisMask & MASK_ROLL) == MASK_ROLL ) + { + if (node.HasValue("ignoreRoll") && node.GetValue("ignoreRoll") == "False" ) + { + shouldMutate = true; + } + } + if ( (axisMask & MASK_PITCH) == MASK_PITCH) + { + if (node.HasValue("ignorePitch") && node.GetValue("ignorePitch") == "False") + { + shouldMutate = true; + } + } + if ((axisMask & MASK_YAW) == MASK_YAW) + { + if (node.HasValue("ignoreYaw") && node.GetValue("ignoreYaw") == "False") + { + shouldMutate = true; + } + } + if (shouldMutate) + { + float existingValue; + float.TryParse(node.GetValue(paramName), out existingValue); + Debug.Log(string.Format("Evolution ControlSurfaceNudgeMutation 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 ControlSurfaceNudgeMutation failed to find parent part for module"); + return; + } + string partName = partNode.GetValue("part"); + var value = existingValue * (1 + modifier); + Debug.Log(string.Format("Evolution ControlSurfaceNudgeMutation 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 ControlSurfaceNudgeMutation unable to mutate {0}", paramName)); + } + } + } + } +} diff --git a/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs b/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs new file mode 100644 index 000000000..711e73d82 --- /dev/null +++ b/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class ControlSurfaceNudgeMutation : VariantMutation + { + const string moduleName = "ModuleControlSurface"; + + public string[] partNames; + public string paramName; + public float modifier; + public string key; + public int direction; + private List mutatedParts = new List(); + + public ControlSurfaceNudgeMutation(string[] partNames, string paramName, float modifier, string key, int direction) + { + this.partNames = partNames; + this.paramName = paramName; + this.modifier = modifier; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + + // Build node map of copy + Dictionary mutationNodeMap = engine.BuildNodeMap(mutatedCraft); + + // Apply mutation to all symmetric parts + Debug.Log("[BDArmory.ControlSurfaceNudgeMutation]: Evolution ControlSurfaceNudgeMutation applying"); + Dictionary matchingNodeMap = new Dictionary(); + foreach (var partName in partNames) + { + matchingNodeMap[partName] = engine.GetNode(partName, mutationNodeMap); + } + MutateMap(matchingNodeMap, mutatedCraft, engine); + + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + + private void MutateMap(Dictionary nodeMap, ConfigNode craft, VariantEngine engine) + { + foreach (var partNames in nodeMap.Keys) + { + foreach (var partName in partNames.Split(',')) + { + MutateNode(nodeMap, engine, partName); + } + } + } + + private void MutateNode(Dictionary nodeMap, VariantEngine engine, string partName) + { + ConfigNode partNode = nodeMap[partName]; + ConfigNode node = engine.FindModuleNodes(partNode, moduleName).First(); + float existingValue; + float.TryParse(node.GetValue(paramName), out existingValue); + Debug.Log(string.Format("Evolution ControlSurfaceNudgeMutation found existing value {0} = {1}", paramName, existingValue)); + if (engine.NudgeNode(node, paramName, modifier)) + { + var value = existingValue * (1 + modifier); + value = Mathf.Clamp(value, -150f, 150f); // Clamp control surfaces to their limits (150%). + Debug.Log(string.Format("Evolution ControlSurfaceNudgeMutation 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 ControlSurfaceNudgeMutation unable to mutate {0}", paramName)); + } + } + } +} diff --git a/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs b/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs new file mode 100644 index 000000000..0960622dd --- /dev/null +++ b/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class EngineGimbalAxisNudgeMutation : VariantMutation + { + const string moduleName = "ModuleGimbal"; + + public static int MASK_ROLL = 0x01; + public static int MASK_PITCH = 0x02; + public static int MASK_YAW = 0x04; + public string paramName; + public float modifier; + public int axisMask; + public string key; + public int direction; + private List mutatedParts = new List(); + public EngineGimbalAxisNudgeMutation(string paramName, float modifier, int axisMask, string key, int direction) + { + this.paramName = paramName; + this.modifier = modifier; + this.axisMask = axisMask; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + Debug.Log("[BDArmory.EngineGimbalAxisNudgeMutation]: Evolution EngineGimbalNudgeMutation applying"); + List matchingModules = engine.FindModuleNodes(mutatedCraft, moduleName); + foreach (var node in matchingModules) + { + MutateIfNeeded(node, mutatedCraft, engine); + } + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + + private void MutateIfNeeded(ConfigNode node, ConfigNode craft, VariantEngine engine) + { + // check axis mask for included axes + bool shouldMutate = false; + if ((axisMask & MASK_ROLL) == MASK_ROLL) + { + if (node.HasValue("enableRoll") && node.GetValue("enableRoll") == "True") + { + shouldMutate = true; + } + } + if ((axisMask & MASK_PITCH) == MASK_PITCH) + { + if (node.HasValue("enablePitch") && node.GetValue("enablePitch") == "True") + { + shouldMutate = true; + } + } + if ((axisMask & MASK_YAW) == MASK_YAW) + { + if (node.HasValue("enableRoll") && node.GetValue("enableRoll") == "True") + { + shouldMutate = true; + } + } + if (shouldMutate) + { + float existingValue; + float.TryParse(node.GetValue(paramName), out existingValue); + Debug.Log(string.Format("Evolution EngineGimbalNudgeMutation found existing value {0} = {1}", paramName, existingValue)); + if (engine.NudgeNode(node, paramName, modifier)) + { + ConfigNode partNode = engine.FindParentPart(craft, node); + if( partNode == null ) + { + Debug.Log("[BDArmory.EngineGimbalAxisNudgeMutation]: Evolution EngineGimbalNudgeMutation failed to find parent part for module"); + return; + } + string partName = partNode.GetValue("part"); + var value = existingValue * (1 + modifier); + Debug.Log(string.Format("Evolution EngineGimbalNudgeMutation 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 EngineGimbalNudgeMutation unable to mutate {0}", paramName)); + } + } + } + } +} diff --git a/BDArmory/Evolution/EngineGimbalNudgeMutation.cs b/BDArmory/Evolution/EngineGimbalNudgeMutation.cs new file mode 100644 index 000000000..bb7944d15 --- /dev/null +++ b/BDArmory/Evolution/EngineGimbalNudgeMutation.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class EngineGimbalNudgeMutation : VariantMutation + { + const string moduleName = "ModuleGimbal"; + + public string[] partNames; + public string paramName; + public float modifier; + public string key; + public int direction; + private List mutatedParts = new List(); + public EngineGimbalNudgeMutation(string[] partNames, string paramName, float modifier, string key, int direction) + { + this.partNames = partNames; + this.paramName = paramName; + this.modifier = modifier; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + + // Build node map of copy + Dictionary mutationNodeMap = engine.BuildNodeMap(mutatedCraft); + + // Apply mutation to all symmetric parts + Debug.Log("[BDArmory.EngineGimbalNudgeMutation]: Evolution EngineGimbalNudgeMutation applying"); + Dictionary matchingNodeMap = new Dictionary(); + foreach (var partName in partNames) + { + matchingNodeMap[partName] = engine.GetNode(partName, mutationNodeMap); + } + MutateMap(matchingNodeMap, mutatedCraft, engine); + + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + + private void MutateMap(Dictionary nodeMap, ConfigNode craft, VariantEngine engine) + { + foreach (var partNames in nodeMap.Keys) + { + foreach (var partName in partNames.Split(',')) + { + MutateNode(nodeMap, engine, partName); + } + } + } + + private void MutateNode(Dictionary nodeMap, VariantEngine engine, string partName) + { + var partNode = nodeMap[partName]; + //var moduleNode = partNode.GetNodes().Where(e => e.name == "MODULE" && e.GetValue("name") == moduleName).First(); + ConfigNode moduleNode = engine.FindModuleNodes(partNode, moduleName).First(); + float existingValue; + float.TryParse(moduleNode.GetValue(paramName), out existingValue); + Debug.Log(string.Format("Evolution EngineGimbalNudgeMutation found existing value {0} = {1}", paramName, existingValue)); + if (engine.NudgeNode(moduleNode, paramName, modifier)) + { + var value = existingValue * (1 + modifier); + Debug.Log(string.Format("Evolution EngineGimbalNudgeMutation 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 EngineGimbalNudgeMutation unable to mutate {0}", paramName)); + } + + } + } +} diff --git a/BDArmory/Evolution/EvolutionWindow.cs b/BDArmory/Evolution/EvolutionWindow.cs new file mode 100644 index 000000000..d4d0a7c68 --- /dev/null +++ b/BDArmory/Evolution/EvolutionWindow.cs @@ -0,0 +1,219 @@ +using System.Collections; +using UnityEngine; +using KSP.Localization; + +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using System.Linq; +using System; + +namespace BDArmory.Evolution +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class EvolutionWindow : MonoBehaviour + { + public static EvolutionWindow Instance; + private BDAModuleEvolution evolution; + + private static int _guiCheckIndex = -1; + private static readonly float _buttonSize = 20; + private static readonly float _margin = 5; + private static readonly float _lineHeight = _buttonSize; + private readonly float _titleHeight = 30; + private float _windowHeight; //auto adjusting + private float _windowWidth; + public bool ready = false; + private EvolutionStatus status; + + GUIStyle leftLabel; + + Rect SLineRect(float line) + { + return new Rect(_margin, line * _lineHeight, _windowWidth - 2 * _margin, _lineHeight); + } + + Rect SLeftSliderRect(float line) + { + return new Rect(_margin, line * _lineHeight, _windowWidth / 2 + _margin / 2, _lineHeight); + } + + Rect SRightSliderRect(float line) + { + return new Rect(_margin + _windowWidth / 2 + _margin / 2, line * _lineHeight, _windowWidth / 2 - 7 / 2 * _margin, _lineHeight); + } + + private void Awake() + { + if (Instance) + Destroy(this); + Instance = this; + } + + private void Start() + { + leftLabel = new GUIStyle(); + leftLabel.alignment = TextAnchor.UpperLeft; + leftLabel.normal.textColor = Color.white; + + ready = false; + StartCoroutine(WaitForSetup()); + } + + private void Update() + { + if (!ready) return; + status = evolution == null ? EvolutionStatus.Idle : evolution.Status(); + } + + private void OnGUI() + { + if (!(ready && BDArmorySettings.EVOLUTION_ENABLED && BDArmorySetup.Instance.showEvolutionGUI)) + { + return; + } + + _windowWidth = BDArmorySettings.EVOLUTION_WINDOW_WIDTH; + + SetNewHeight(_windowHeight); + BDArmorySetup.WindowRectEvolution = new Rect( + BDArmorySetup.WindowRectEvolution.x, + BDArmorySetup.WindowRectEvolution.y, + _windowWidth, + _windowHeight + ); + BDArmorySetup.SetGUIOpacity(); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectEvolution.position); + BDArmorySetup.WindowRectEvolution = GUI.Window( + 8008135, + BDArmorySetup.WindowRectEvolution, + WindowEvolution, + StringUtils.Localize("#LOC_BDArmory_Evolution_Title"),//"BDA Evolution" + BDArmorySetup.BDGuiSkin.window + ); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectEvolution, _guiCheckIndex); + } + + 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) + { + yield return null; + } + evolution = BDAModuleEvolution.Instance; + + BDArmorySetup.Instance.hasEvolution = true; + ready = true; + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + } + + private void WindowEvolution(int id) + { + float line = 0.25f; + 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 ? 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; + } + if (BDArmorySettings.SHOW_EVOLUTION_OPTIONS) + { + int mutationsPerHeat = BDArmorySettings.EVOLUTION_MUTATIONS_PER_HEAT; + var mphDisplayValue = BDArmorySettings.EVOLUTION_MUTATIONS_PER_HEAT.ToString("0"); + 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), $"{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), $"{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; + + offset += 3 * _lineHeight; + } + + float fifth = _windowWidth / 5.0f; + offset += 0.25f * _lineHeight; + 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), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_Status")}: "); + string statusLine; + switch (evolution.Status()) + { + default: + statusLine = status.ToString(); + break; + } + GUI.Label(new Rect(_margin + 2 * fifth, offset, 3 * fifth, _lineHeight), statusLine); + offset += _lineHeight; + + // Display configuration of each variant currently running + if (status == EvolutionStatus.RunningTournament) + { + foreach (var variant in evolution.ActiveVariantGroup().variants) + { + // Print a line for the first mutated part to represent that variant. (we don't want one line per symmetry part here, only one line per variant) + var part = variant.mutatedParts.First(); + GUI.Label(new Rect(_margin, offset, 5 * fifth, _lineHeight), string.Format("{0}:, {2}: {3}, from: {5} to: {4}, part_id: {1}", variant.name, part.partName, part.moduleName, part.paramName, Math.Round(part.value, 3), Math.Round(part.referenceValue, 3))); + offset += _lineHeight; + } + } + + 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), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_Heat")}: "); + GUI.Label(new Rect(_margin + 3 * fifth, offset, fifth, _lineHeight), evolution.Heat.ToString()); + offset += _lineHeight; + string buttonText; + bool nextButton = false; + switch (status) + { + case EvolutionStatus.Idle: + buttonText = "Start"; + nextButton = true; + break; + default: + buttonText = "Cancel"; + break; + } + if (GUI.Button(new Rect(_margin, offset, nextButton ? 2 * _windowWidth / 3 - _margin : _windowWidth - 2 * _margin, _lineHeight), buttonText, BDArmorySetup.BDGuiSkin.button)) + { + switch (status) + { + case EvolutionStatus.Idle: + evolution.StartEvolution(); + break; + default: + evolution.StopEvolution(); + break; + } + } + offset += _lineHeight + _margin; + + _windowHeight = offset; + + 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 new file mode 100644 index 000000000..08b7e274a --- /dev/null +++ b/BDArmory/Evolution/PilotAIMutation.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class PilotAIMutation : VariantMutation + { + const string moduleName = "BDModulePilotAI"; + public string paramName; + public float value; + private List mutatedParts = new List(); + + public PilotAIMutation(string paramName, float value) + { + this.paramName = paramName; + this.value = value; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + List matchingNodes = engine.FindModuleNodes(mutatedCraft, moduleName); + if( matchingNodes.Count == 1 ) + { + var node = matchingNodes[0]; + float existingValue; + float.TryParse(node.GetValue(paramName), out existingValue); + if( engine.MutateNode(node, paramName, value) ) + { + ConfigNode partNode = engine.FindParentPart(mutatedCraft, node); + string partName = partNode.GetValue("part"); + mutatedParts.Add(new MutatedPart(partName, moduleName, paramName, existingValue, value)); + } + } + else + { + Debug.Log("[BDArmory.PilotAIMutation]: Evolution PilotAIMutation wrong number of pilot modules"); + } + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, "", 0); + } + } +} diff --git a/BDArmory/Evolution/PilotAINudgeMutation.cs b/BDArmory/Evolution/PilotAINudgeMutation.cs new file mode 100644 index 000000000..37447aae5 --- /dev/null +++ b/BDArmory/Evolution/PilotAINudgeMutation.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class PilotAINudgeMutation : VariantMutation + { + const string moduleName = "BDModulePilotAI"; + public string paramName; + public float modifier; + private List mutatedParts = new List(); + public string key; + public int direction; + public PilotAINudgeMutation(string paramName, float modifier, string key, int direction) + { + this.paramName = paramName; + this.modifier = modifier; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation applying"); + List matchingNodes = engine.FindModuleNodes(mutatedCraft, "BDModulePilotAI"); + if (matchingNodes.Count == 1) + { + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation found module"); + var node = matchingNodes[0]; + float existingValue; + float.TryParse(node.GetValue(paramName), out existingValue); + Debug.Log(string.Format("Evolution PilotAINudgeMutation found existing value {0}", existingValue)); + if (engine.NudgeNode(node, paramName, modifier) ) + { + ConfigNode partNode = engine.FindParentPart(mutatedCraft, node); + if( partNode == null ) + { + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation failed to find parent part for module"); + return mutatedCraft; + } + string partName = partNode.GetValue("part"); + var value = existingValue * (1 + modifier); + Debug.Log(string.Format("Evolution PilotAINudgeMutation 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 PilotAINudgeMutation unable to mutate {0}", paramName)); + } + } + else + { + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation wrong number of pilot modules"); + } + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + } +} diff --git a/BDArmory/Evolution/VariantEngine.cs b/BDArmory/Evolution/VariantEngine.cs new file mode 100644 index 000000000..a60e98f78 --- /dev/null +++ b/BDArmory/Evolution/VariantEngine.cs @@ -0,0 +1,563 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class VariantEngine + { + const float crystalRadius = 0.1f; + + private string weightMapFile; + + private Dictionary mutationWeightMap = new Dictionary(); + + private Dictionary nodeMap = new Dictionary(); + + List includedModules = new List() { "ModuleGimbal", "ModuleControlSurface", "BDModulePilotAI", "MissileFire" }; + + List includedParams = new List() + { + "steerMult", // ModulePilot + "steerKiAdjust", + "steerDamping", + "DynamicDampingMin", + "DynamicDampingMax", + "dynamicSteerDampingFactor", + "DynamicDampingPitchMin", + "DynamicDampingPitchMax", + "dynamicSteerDampingPitchFactor", + "DynamicDampingYawMin", + "DynamicDampingYawMax", + "dynamicSteerDampingYawFactor", + "DynamicDampingRollMin", + "DynamicDampingRollMax", + "dynamicSteerDampingRollFactor", + "defaultAltitude", + "minAltitude", + "maxAltitude", + "maxSpeed", + "takeOffSpeed", + "minSpeed", + "idleSpeed", + "strafingSpeed", + "ABPriority", + "maxSteer", + "lowSpeedSwitch", + "maxSteerAtMaxSpeed", + "cornerSpeed", + "maxBank", + "maxAllowedGForce", + "maxAllowedAoA", + "minEvasionTime", + "evasionThreshold", + "evasionTimeThreshold", + "collisionAvoidanceThreshold", + "vesselCollisionAvoidanceLookAheadPeriod", + "vesselCollisionAvoidanceStrength", + "vesselStandoffDistance", + "extendDistanceAirToAir", + "extendDistanceAirToGroundGuns", + "extendDistanceAirToGround", + "extendTargetVel", + "extendTargetAngle", + "extendTargetDist", + "turnRadiusTwiddleFactorMin", + "turnRadiusTwiddleFactorMax", + "controlSurfaceLag", + "targetScanInterval", // ModuleWeapon + "fireBurstLength", + "AutoFireCosAngleAdjustment", + "guardAngle", + "guardRange", + "gunRange", + "targetBias", + "targetWeightRange", + "targetWeightATA", + "targetWeightAoD", + "targetWeightAccel", + "targetWeightClosureTime", + "targetWeightWeaponNumber", + "targetWeightMass", + "targetWeightFriendliesEngaging", + "targetWeightThreat", + "targetWeightProtectVIP", + "targetWeightAttackVIP", + "evadeThreshold", + "cmThreshold", + "cmInterval", + "cmWaitTime", + "chaffInterval", + "chaffWaitTime", + "gimbalLimiter", // ModuleGimbal + "authorityLimiter" // ModuleControlSurface + }; + + public void Configure(ConfigNode craft, string weightMapFile) + { + this.weightMapFile = weightMapFile; + + // build map for higher performance access + nodeMap.Clear(); + nodeMap = BuildNodeMap(craft); + + // try to load existing weight map file + try + { + ConfigNode weightMapNode = ConfigNode.Load(weightMapFile); + LoadWeightMap(weightMapNode); + } + catch(Exception) + { + // otherwise init with random weights + InitializeWeightMap(craft); + SaveWeightMap(); + } + } + + public void Feedback(string key, float weight) + { + string[] components = key.Split('/'); + if(components.Length != 3) + { + Debug.Log($"[BDArmory.VariantEngine]: Evolution VariantEngine Feedback {key} => {weight}"); + return; + } + string part = components[0], module = components[1], param = components[2]; + Backpropagate(part, module, param, weight); + + if( !SaveWeightMap() ) + { + Debug.Log("[BDArmory.VariantEngine]: Evolution VariantEngine failed to save weight map"); + } + } + + public Dictionary BuildNodeMap(ConfigNode craft) + { + Debug.Log("[BDArmory.VariantEngine]: Evolution VariantEngine BuildNodeMap"); + Dictionary node_map = new Dictionary(); + // use a fifo queue to recurse through the tree + List nodeQueue = new List(); + nodeQueue.Add(craft); + + while( nodeQueue.Count > 0 ) + { + var nextNode = nodeQueue[0]; + nodeQueue.RemoveAt(0); + if( nextNode == null ) + { + Debug.Log("[BDArmory.VariantEngine]: Evolution VariantEngine weird null nextNode"); + break; + } + + // for part nodes, insert into map + if( nextNode.name == "PART" ) + { + // insert node into map + var partName = nextNode.GetValue("part"); + if (node_map.ContainsKey(partName)) + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine found duplicate part {0}", partName)); + break; + } + node_map[partName] = nextNode; + } + + // add children to the queue + foreach (var node in nextNode.GetNodes().Where(e => e.name == "PART")) + { + nodeQueue.Add(node); + } + } + return node_map; + } + + // Must be private since it refers to and pulls references to the local nodeMap, not any new craft references + // Previously, this was called in other functions when attempting to apply mutations. + // This resulted in mutations being applied to the wrong variant or + // negativePole mutations undoing positivePole mutations for a variant + + // Candidate for full removal since using this can be dangerous and produce unintended effects + private ConfigNode GetNode(string partName) + { + return nodeMap[partName]; + } + + // Version of GetNode that does not rely on global state + public ConfigNode GetNode(string partName, Dictionary targetNodeMap) + { + return targetNodeMap[partName]; + } + + public List GetNodes(List partNames) + { + List results = new List(); + foreach (var partName in partNames) + { + var node = nodeMap[partName]; + if (node != null) + { + results.Add(node); + } + } + return results; + } + + private void LoadWeightMap(ConfigNode weightMapNode) + { + Debug.Log("[BDArmory.VariantEngine]: Evolution VariantEngine LoadWeightMap"); + // start with a fresh map + mutationWeightMap.Clear(); + + // extract weights from the map + foreach (var key in weightMapNode.values.DistinctNames()) + { + var value = weightMapNode.GetValue(key); + try + { + mutationWeightMap[key] = float.Parse(value); + } + catch (Exception e) + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine failed to parse value {0} for key {1}: {2}", value, key, e)); + } + } + } + + private bool SaveWeightMap() + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine SaveWeightMap to {0}", weightMapFile)); + ConfigNode weights = new ConfigNode(); + foreach (var key in mutationWeightMap.Keys) + { + weights.AddValue(key, mutationWeightMap[key]); + } + return weights.Save(weightMapFile); + } + + private void InitializeWeightMap(ConfigNode craft, bool shouldRandomize = true) + { + Debug.Log("[BDArmory.VariantEngine]: Evolution VariantEngine InitializeWeightMap"); + string[] paramModules = new string[] { "BDModulePilotAI", "MissileFire" }; + // start with a fresh map + mutationWeightMap.Clear(); + + var rng = new System.Random(); + // find all parts + List foundParts = new List(); + FindMatchingNode(craft, "PART", foundParts); + //Debug.Log(string.Format("Evolution VariantEngine init found {0} parts", foundParts.Count)); + foreach (var part in foundParts) + { + List foundModules = new List(); + FindMatchingNode(part, "MODULE", foundModules); + var filteredModules = foundModules.Where(e => paramModules.Contains(e.GetValue("name"))).ToList(); + // Debug.Log(string.Format("Evolution VariantEngine init part {0} found {1} modules", part.GetValue("part"), foundModules.Count)); + foreach (var module in filteredModules) + { + var filteredValues = includedParams.Where(e => module.HasValue(e)).ToList(); + // Debug.Log(string.Format("Evolution VariantEngine init part {0} module {1} found {2} params", part.GetValue("part"), module.GetValue("name"), filteredValues.Count)); + foreach (var param in filteredValues) + { + var key = MutationKey(part.GetValue("part"), module.GetValue("name"), param); + mutationWeightMap[key] = 1.0f; + } + } + } + + // check for control surfaces + CheckSymmetry(craft, "ModuleControlSurface", "authorityLimiter"); + + // check for engine gimbals + CheckSymmetry(craft, "ModuleGimbal", "gimbalLimiter"); + + if ( shouldRandomize ) + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine randomizing weight map with {0} keys", mutationWeightMap.Count)); + // randomize weights slightly + var keys = mutationWeightMap.Keys.ToList(); + foreach (var key in keys) + { + mutationWeightMap[key] += (float)rng.Next(0, 100) / 10000.0f - 0.005f; + } + } + } + + private void CheckSymmetry(ConfigNode craft, string moduleName, string paramName) + { + List foundModules = FindModuleNodes(craft, moduleName); + foreach (var node in foundModules) + { + var parentPartNode = FindParentPart(craft, node); + var partName = parentPartNode.GetValue("part"); + // check for symmetry grouping + if (parentPartNode.HasValue("sym")) + { + // is it mirror or radial symmetry? + if (parentPartNode.GetValue("symMethod") == "Radial") + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine RadialSymmetry for {0}", partName)); + // multiple other parts + List symParts = parentPartNode.GetValues("sym").ToList(); + symParts.Add(partName); + var aggPartName = string.Join(",", symParts.OrderBy(e => e)); + var key = MutationKey(aggPartName, moduleName, paramName); + mutationWeightMap[key] = 1.0f; + } + else + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine MirrorSymmetry for {0}", partName)); + // just one other part + var siblingPartName = parentPartNode.GetValue("sym"); + string[] aggParts = new string[] { partName, siblingPartName }; + var aggPartName = string.Join(",", aggParts.OrderBy(e => e)); + var key = MutationKey(aggPartName, moduleName, paramName); + mutationWeightMap[key] = 1.0f; + } + } + else + { + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine NoSymmetry for {0} with {1}", partName, parentPartNode.GetValues("sym"))); + // no symmetry, just one part + var key = MutationKey(partName, moduleName, paramName); + mutationWeightMap[key] = 1.0f; + } + } + } + + public void Backpropagate(string part, string module, string param, float weight) + { + var key = MutationKey(part, module, param); + var clampedWeight = Math.Max(-1, Math.Min(weight, 1)); + var multiplier = 1.0f + (float)(2.0*Math.Atan(clampedWeight)/Math.PI); + Debug.Log(string.Format("[BDArmory.VariantEngine]: Evolution VariantEngine Backpropagate {0} => {1} ({2})", key, clampedWeight, multiplier)); + mutationWeightMap[key] *= multiplier; + } + + public string MutationKey(string part, string module, string param) + { + return string.Format("{0}/{1}/{2}", part, module, param); + } + + // THE NEW WAY + public List GenerateMutations(int mutationsPerGroup) + { + List mutations = new List(); + // order the mutation weight map by weight and select N elements + List bestOptions = mutationWeightMap + .OrderByDescending(e => e.Value) + .Select(e => e.Key) + .Take(mutationsPerGroup) + .ToList(); + foreach (var e in bestOptions) + { + mutations.AddRange(KeyToMutations(e)); + } + return mutations; + } + + private List KeyToMutations(string key) + { + string part; + string module; + string param; + string[] components = key.Split('/'); + if( components.Length != 3 ) + { + throw new Exception(string.Format("VariantEngine::KeyToMutation wrong number of key components: {0}", key)); + } + part = components[0]; + module = components[1]; + param = components[2]; + + switch (module) + { + case "MissileFire": + return GenerateWeaponManagerNudgeMutation(param, key); + case "BDModulePilotAI": + return GeneratePilotAINudgeMutation(param, key); + case "ModuleControlSurface": + return GenerateControlSurfaceMutation(part, key); + case "ModuleGimbal": + return GenerateEngineGimbalMutation(part, key); + } + throw new Exception(string.Format("VariantEngine bad key: {0}", key)); + } + + private List GeneratePilotAINudgeMutation(string paramName, string key) + { + List results = new List(); + var positivePole = new PilotAINudgeMutation(paramName: paramName, modifier: crystalRadius, key, 1); + results.Add(positivePole); + var negativePole = new PilotAINudgeMutation(paramName: paramName, modifier: -crystalRadius, key, -1); + results.Add(negativePole); + return results; + } + + private List GenerateWeaponManagerNudgeMutation(string paramName, string key) + { + List results = new List(); + var positivePole = new WeaponManagerNudgeMutation(paramName: paramName, modifier: crystalRadius, key, 1); + results.Add(positivePole); + var negativePole = new WeaponManagerNudgeMutation(paramName: paramName, modifier: -crystalRadius, key, -1); + results.Add(negativePole); + return results; + } + + private List GenerateControlSurfaceMutation(string parts, string key) + { + string[] partNames = parts.Split(','); + var positivePole = new ControlSurfaceNudgeMutation(partNames, "authorityLimiter", crystalRadius, key, 1); + var negativePole = new ControlSurfaceNudgeMutation(partNames, "authorityLimiter", -crystalRadius, key, -1); + var results = new List() { positivePole, negativePole }; + return results; + } + + private bool CraftHasEngineGimbal(ConfigNode craft) + { + List gimbals = FindModuleNodes(craft, "ModuleGimbal"); + return gimbals.Count != 0; + } + + private List GenerateEngineGimbalMutation(string parts, string key) + { + string[] partNames = parts.Split(','); + var results = new List(); + var positivePole = new EngineGimbalNudgeMutation(partNames, "gimbalLimiter", crystalRadius, key, 1); + var negativePole = new EngineGimbalNudgeMutation(partNames, "gimbalLimiter", -crystalRadius, key, -1); + results.Add(positivePole); + results.Add(negativePole); + return results; + } + + public ConfigNode GenerateNode(ConfigNode source, VariantOptions options) + { + // make a copy of the source and modify the copy + var result = source.CreateCopy(); + + foreach (var mutation in options.mutations) + { + mutation.Apply(result, this); + } + + // return modified copy + return result; + } + + public bool FindValue(ConfigNode node, string nodeType, string nodeName, string paramName, out float result) + { + if (node.name == nodeType && node.HasValue("name") && node.GetValue("name").StartsWith(nodeName) && node.HasValue(paramName)) + { + return float.TryParse(node.GetValue(paramName), out result); + } + foreach (var child in node.nodes) + { + if (FindValue((ConfigNode)child, nodeType, nodeName, paramName, out result)) + { + return true; + } + } + result = 0; + return false; + } + + public List FindPartNodes(ConfigNode source, string partName) + { + List matchingParts = new List(); + FindMatchingNode(source, "PART", "part", partName, matchingParts); + return matchingParts; + } + + public List FindModuleNodes(ConfigNode source, string moduleName) + { + List matchingModules = new List(); + FindMatchingNode(source, "MODULE", "name", moduleName, matchingModules); + return matchingModules; + } + + public ConfigNode FindParentPart(ConfigNode rootNode, ConfigNode node) + { + if( rootNode.name == "PART" ) + { + foreach (var child in rootNode.nodes) + { + if( child == node ) + { + return rootNode; + } + } + } + foreach (var child in rootNode.nodes) + { + var found = FindParentPart((ConfigNode)child, node); + if( found != null ) + { + return found; + } + } + return null; + } + + private void FindMatchingNode(ConfigNode source, string nodeType, string nodeParam, string nodeName, List found) + { + if (source.name == nodeType && source.HasValue(nodeParam) && source.GetValue(nodeParam).StartsWith(nodeName)) + { + found.Add(source); + } + foreach (var child in source.GetNodes()) + { + FindMatchingNode(child, nodeType, nodeParam, nodeName, found); + } + } + + private void FindMatchingNode(ConfigNode source, string nodeType, List found) + { + if( source.name == nodeType) + { + found.Add(source); + } + foreach (var child in source.GetNodes()) + { + FindMatchingNode(child, nodeType, found); + } + } + + public bool MutateNode(ConfigNode node, string key, float value) + { + if (node.HasValue(key)) + { + node.SetValue(key, value); + return true; + } + else + { + return false; + } + } + + public bool NudgeNode(ConfigNode node, string key, float modifier) + { + if (node.HasValue(key) && float.TryParse(node.GetValue(key), out float existingValue)) + { + node.SetValue(key, existingValue * (1 + modifier)); + return true; + } + else + { + return false; + } + } + } + + public class VariantOptions + { + public List mutations; + public VariantOptions(List mutations) + { + this.mutations = mutations; + } + } + +} diff --git a/BDArmory/Evolution/VariantMutation.cs b/BDArmory/Evolution/VariantMutation.cs new file mode 100644 index 000000000..5c8486959 --- /dev/null +++ b/BDArmory/Evolution/VariantMutation.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace BDArmory.Evolution +{ + public interface VariantMutation + { + public ConfigNode Apply(ConfigNode craft, VariantEngine engine); + public Variant GetVariant(string id, string name); + } +} diff --git a/BDArmory/Evolution/WeaponManagerMutation.cs b/BDArmory/Evolution/WeaponManagerMutation.cs new file mode 100644 index 000000000..79ae6db17 --- /dev/null +++ b/BDArmory/Evolution/WeaponManagerMutation.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class WeaponManagerMutation : VariantMutation + { + const string moduleName = "MissileFire"; + + public string paramName; + public float value; + public string key; + public int direction; + private List mutatedParts = new List(); + public WeaponManagerMutation(string paramName, float value, string key, int direction) + { + this.paramName = paramName; + this.value = value; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + List matchingNodes = engine.FindModuleNodes(mutatedCraft, moduleName); + if( matchingNodes.Count == 1 ) + { + var node = matchingNodes[0]; + float existingValue; + float.TryParse(node.GetValue(paramName), out existingValue); + if( engine.MutateNode(node, paramName, value) ) + { + ConfigNode partNode = engine.FindParentPart(mutatedCraft, node); + string partName = partNode.GetValue("part"); + mutatedParts.Add(new MutatedPart(partName, moduleName, paramName, existingValue, value)); + } + } + else + { + Debug.Log("[BDArmory.WeaponManagerMutation]: Evolution WeaponManagerMutation wrong number of weapon managers"); + } + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + } +} diff --git a/BDArmory/Evolution/WeaponManagerNudgeMutation.cs b/BDArmory/Evolution/WeaponManagerNudgeMutation.cs new file mode 100644 index 000000000..72b29df88 --- /dev/null +++ b/BDArmory/Evolution/WeaponManagerNudgeMutation.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.Evolution +{ + public class WeaponManagerNudgeMutation : VariantMutation + { + const string moduleName = "MissileFire"; + + public string paramName; + public float modifier; + public string key; + public int direction; + private List mutatedParts = new List(); + public WeaponManagerNudgeMutation(string paramName, float modifier, string key, int direction) + { + this.paramName = paramName; + this.modifier = modifier; + this.key = key; + this.direction = direction; + } + + public ConfigNode Apply(ConfigNode craft, VariantEngine engine) + { + ConfigNode mutatedCraft = craft.CreateCopy(); + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation applying"); + List matchingNodes = engine.FindModuleNodes(mutatedCraft, "MissileFire"); + if (matchingNodes.Count == 1) + { + 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("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation found existing value {0} = {1}", paramName, existingValue)); + if (engine.NudgeNode(node, paramName, modifier)) + { + ConfigNode partNode = engine.FindParentPart(mutatedCraft, node); + if( partNode == null ) + { + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation failed to find parent part for module"); + return mutatedCraft; + } + string partName = partNode.GetValue("part"); + var value = existingValue * (1 + modifier); + 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("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation unable to mutate {0}", paramName)); + } + } + else + { + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation wrong number of weapon managers"); + } + return mutatedCraft; + } + + public Variant GetVariant(string id, string name) + { + return new Variant(id, name, mutatedParts, key, direction); + } + } +} diff --git a/BDArmory/Evolution/_description b/BDArmory/Evolution/_description new file mode 100644 index 000000000..164bf3b2e --- /dev/null +++ b/BDArmory/Evolution/_description @@ -0,0 +1 @@ +Evolution engine for automated improvements to craft via AI/WM/control surface tuning. \ No newline at end of file diff --git a/BDArmory/Extensions/BodyExtensions.cs b/BDArmory/Extensions/BodyExtensions.cs new file mode 100644 index 000000000..7cc12be23 --- /dev/null +++ b/BDArmory/Extensions/BodyExtensions.cs @@ -0,0 +1,72 @@ +using UnityEngine; +using System; +using BDArmory.Utils; + +namespace BDArmory.Extensions +{ + public static class BodyExtensions + { + public static bool FindFlatSpotNear(this CelestialBody body, double startLatitude, double startLongitude, out double latitude, out double longitude, double maxDeviation) + { + var iLat = startLatitude; + var iLng = startLongitude; + var dLat = 0.0; + var dLng = 0.0; + var threshold = 0.000001; + var maxIterations = 1000; + var k = 0; + + while (k++ < maxIterations) + { + var norm = body.GetRelSurfaceNVector(iLat + dLat, iLng + dLng); + var normA = body.GetRelSurfaceNVector(iLat + dLat - threshold, iLng + dLng); + var normB = body.GetRelSurfaceNVector(iLat + dLat + threshold, iLng + dLng); + var normC = body.GetRelSurfaceNVector(iLat + dLat, iLng + dLng - threshold); + var normD = body.GetRelSurfaceNVector(iLat + dLat, iLng + dLng + threshold); + + var angle = VectorUtils.Angle(norm, Vector3.up); + var angleA = VectorUtils.Angle(normA, Vector3.up); + var angleB = VectorUtils.Angle(normB, Vector3.up); + var angleC = VectorUtils.Angle(normC, Vector3.up); + var angleD = VectorUtils.Angle(normD, Vector3.up); + + if (angleA < angle) + { + dLat -= threshold; + } + else if (angleB < angle) + { + dLat += threshold; + } + else if (angleC < angle) + { + dLng -= threshold; + } + else if (angleD < angle) + { + dLng += threshold; + } + else + { + break; + } + } + + latitude = iLat + dLat; + longitude = iLng + dLng; + + return k < maxIterations; + } + + public static double MinSafeAltitude(this CelestialBody body) + { + double maxTerrainHeight = 200; + if (body.pqsController) + { + PQS pqs = body.pqsController; + maxTerrainHeight = pqs.radiusMax - pqs.radius; + } + return Math.Max(maxTerrainHeight, body.atmosphereDepth); + } + } +} 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/Extensions/CrewExtensions.cs b/BDArmory/Extensions/CrewExtensions.cs new file mode 100644 index 000000000..28ea92baf --- /dev/null +++ b/BDArmory/Extensions/CrewExtensions.cs @@ -0,0 +1,34 @@ +namespace BDArmory.Extensions +{ + public static class CrewExtensions + { + /// + /// Reset the inventory of a crew member to the default of a chute and jetpack. + /// + /// 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); + } + else // Nothing, crew didn't have inventory before. Chute and jetpack were built into KerbalEVA class. + { + } + } + + private static void ResetInventory_1_11(this ProtoCrewMember crew, bool withJetpack = false) // KSP has issues on older versions if this call is in the parent function. + { + crew.KerbalInventoryModule.SetInventoryDefaults(); // Reset the inventory to a chute and a jetpack. + if (!withJetpack) + { + var inventory = crew.KerbalInventoryModule; + if (inventory.ContainsPart("evaJetpack")) + { + inventory.RemoveNPartsFromInventory("evaJetpack", 1, false); + } + } + } + } +} \ No newline at end of file diff --git a/BDArmory/Extensions/DictionaryExtensions.cs b/BDArmory/Extensions/DictionaryExtensions.cs new file mode 100644 index 000000000..faabe2fe7 --- /dev/null +++ b/BDArmory/Extensions/DictionaryExtensions.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace BDArmory.Extensions +{ + /// + /// Extensions to Dictionaries. + /// + public static class DictionaryExtensions + { + /// + /// Equivalent of GetValueOrDefault for dictionaries, but with an option to specify the default value. + /// + /// + /// + /// The dictionary. + /// The key to look for. + /// The default to return if not the default for the type S. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static S GetValueOrDefault(this Dictionary dict, T key, S def = default) => dict.TryGetValue(key, out var value) ? value : def; + } +} \ No newline at end of file diff --git a/BDArmory/Extensions/FloatExtensions.cs b/BDArmory/Extensions/FloatExtensions.cs new file mode 100644 index 000000000..ae3137262 --- /dev/null +++ b/BDArmory/Extensions/FloatExtensions.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; + +namespace BDArmory.Extensions +{ + /// + /// Extensions to floats. + /// + /// These would be better if they were properties instead of full-fledged functions, but that isn't part of C# at the time this was written. + /// + public static class FloatExtensions + { + /// + /// Avoid local temporaries when all you want is simply the square of a float. + /// Note: this is significantly slower than just multiplying the floats despite the inlining hint (which doesn't make any sense), so it should only really be used when the cost of computing the float is itself expensive. + /// + /// The square of f. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Sqr(this float f) => f * f; + + /// + /// Avoid local temporaries when all you want is simply the cube of a float. + /// + /// The cube of f. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Cube(this float f) => f * f * f; + } +} \ No newline at end of file diff --git a/BDArmory/Extensions/OrbitExtensions.cs b/BDArmory/Extensions/OrbitExtensions.cs new file mode 100644 index 000000000..ce2326d17 --- /dev/null +++ b/BDArmory/Extensions/OrbitExtensions.cs @@ -0,0 +1,14 @@ +namespace BDArmory.Extensions +{ + public static class OrbitExtensions + { + public static Vector3d GetPrograde(this Orbit o, double UT) + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 11) || Versioning.version_major > 1) // Introduced in 1.12 + return GetPrograde_1_12(o, UT); + return GetPrograde_pre_1_12(o, UT); + } + static Vector3d GetPrograde_1_12(Orbit o, double UT) => o.Prograde(UT); + static Vector3d GetPrograde_pre_1_12(Orbit o, double UT) => o.getOrbitalVelocityAtUT(UT).normalized; // FIXME Is this correct? + } +} \ No newline at end of file diff --git a/BDArmory/Extensions/PartExtensions.cs b/BDArmory/Extensions/PartExtensions.cs new file mode 100644 index 000000000..f6886a13b --- /dev/null +++ b/BDArmory/Extensions/PartExtensions.cs @@ -0,0 +1,726 @@ +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Damage; +using BDArmory.Initialization; +using BDArmory.Settings; +using Expansions.Serenity; + +namespace BDArmory.Extensions +{ + public enum ExplosionSourceType { Other, Missile, Bullet, Rocket, BattleDamage }; + public static class PartExtensions + { + public static void AddDamage(this Part p, float damage) //Fires, lasers, ammo detonations + { + if (BDArmorySettings.PAINTBALL_MODE) + { + var ti = p.vessel.gameObject.GetComponent(); + if (!(ti != null && ti.isMissile)) return; // Don't add damage when paintball mode is enabled, except against fired missiles + } + damage *= (BDArmorySettings.DMG_MULTIPLIER / 100); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) + { + if (p.vessel.rootPart != null) + { + if (p != p.vessel.rootPart) + { + damage *= BDArmorySettings.ZOMBIE_DMG_MULT; + } + } + } + ////////////////////////////////////////////////////////// + // Basic Add Hitpoints for compatibility (only used by lasers & fires) + ////////////////////////////////////////////////////////// + + if (p.GetComponent() != null) + { + ApplyHitPoints(p.GetComponent(), damage); + } + else + { + Dependencies.Get().AddDamageToPart_svc(p, damage); + 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}"); + } + } + + public static void AddInstagibDamage(this Part p) + { + if (p.GetComponent() != null) + { + p.Destroy(); + } + else + { + if (p.vessel.rootPart != null) + { + p.vessel.rootPart.Destroy(); + } + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) + Debug.Log("[BDArmory.PartExtensions]: Instagib!"); + } + } + + public static float AddExplosiveDamage(this Part p, + float explosiveDamage, + float caliber, + ExplosionSourceType sourceType, + float multiplier = 1) //bullet/rocket/missile explosive damage + { + if (BDArmorySettings.PAINTBALL_MODE) + { + var ti = p.vessel.gameObject.GetComponent(); + if (!(ti != null && ti.isMissile)) return 0f; // Don't add damage when paintball mode is enabled, except against fired missiles + } + /* + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) + { + if (p.vessel.rootPart != null) + { + //if (p != p.vessel.rootPart) return 0f; + } + } + */ + if (Utils.ProjectileUtils.IsArmorPart(p)) return 0; //Armor panels don't have HP, so don't score/deal HP damage + float damage_ = 0f; + ////////////////////////////////////////////////////////// + // Explosive Hitpoints + ////////////////////////////////////////////////////////// + + damage_ = explosiveDamage * ExplosiveDamageModifier(sourceType, multiplier); + + ////////////////////////////////////////////////////////// + // Armor Reduction factors + ////////////////////////////////////////////////////////// + + if (p.HasArmor()) + { + float armorMass_ = p.GetArmorThickness(); + float armorDensity_ = p.GetArmorDensity(); + float armorStrength_ = p.GetArmorSrength(); + float damageReduction = DamageReduction(armorMass_, armorDensity_, armorStrength_, damage_, sourceType, caliber); + + damage_ = damageReduction; + } + ////////////////////////////////////////////////////////// + // Apply Hitpoints + ////////////////////////////////////////////////////////// + + if (p.GetComponent() != null) + { + ApplyHitPoints(p.GetComponent(), (float)damage_); + } + else + { + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) + { + if (p.vessel.rootPart != null) + { + if (p != p.vessel.rootPart) + { + damage_ *= BDArmorySettings.ZOMBIE_DMG_MULT; + } + } + } + ApplyHitPoints(p, damage_); + } + return damage_; + } + + /// + /// Get the appropriate modifier for explosive damage of the given type and multiplier. + /// + /// + /// + /// + public static float ExplosiveDamageModifier(ExplosionSourceType sourceType, float multiplier = 1f) + { + return BDArmorySettings.DMG_MULTIPLIER / 100f * + (sourceType switch + { + ExplosionSourceType.Bullet => BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW * multiplier, + ExplosionSourceType.Rocket => BDArmorySettings.EXP_DMG_MOD_ROCKET * multiplier, + ExplosionSourceType.Missile => BDArmorySettings.EXP_DMG_MOD_MISSILE * multiplier, + ExplosionSourceType.BattleDamage => BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE, + _ => 1f + }); + } + + public static float AddBallisticDamage(this Part p, + float mass, + float caliber, + float multiplier, + float penetrationfactor, + float bulletDmgMult, + float impactVelocity, + ExplosionSourceType sourceType) //bullet/rocket kinetic damage + { + if (BDArmorySettings.PAINTBALL_MODE) + { + var ti = p.vessel.gameObject.GetComponent(); + if (!(ti != null && ti.isMissile)) return 0f; // Don't add damage when paintball mode is enabled, except against fired missiles + } + /* + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) + { + if (p.vessel.rootPart != null) + { + if (p != p.vessel.rootPart) return 0f; + } + } + */ + if (Utils.ProjectileUtils.IsArmorPart(p)) return 0; //Armor panels don't have HP, so don't score/deal HP damage + ////////////////////////////////////////////////////////// + // Basic Kinetic Formula + ////////////////////////////////////////////////////////// + //Hitpoints mult for scaling in settings + //1e-4 constant for adjusting MegaJoules for gameplay + + float damage_; + switch (sourceType) + { + case ExplosionSourceType.Rocket: + damage_ = (0.5f * (mass * impactVelocity * impactVelocity)) + * (BDArmorySettings.DMG_MULTIPLIER / 100) * bulletDmgMult * multiplier + * 1e-4f * BDArmorySettings.BALLISTIC_DMG_FACTOR; + break; + case ExplosionSourceType.BattleDamage: + damage_ = (0.5f * (mass * impactVelocity * impactVelocity)) + * (BDArmorySettings.DMG_MULTIPLIER / 100) * bulletDmgMult * multiplier + * 1e-4f * BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE; + break; + case ExplosionSourceType.Bullet: + damage_ = (0.5f * (mass * impactVelocity * impactVelocity)) + * (BDArmorySettings.DMG_MULTIPLIER / 100) * bulletDmgMult * multiplier + * 1e-4f * BDArmorySettings.BALLISTIC_DMG_FACTOR; + break; + default: // Other? + damage_ = (0.5f * (mass * impactVelocity * impactVelocity)) + * (BDArmorySettings.DMG_MULTIPLIER / 100) * bulletDmgMult * multiplier + * 1e-4f * BDArmorySettings.BALLISTIC_DMG_FACTOR; + break; + } + + ////////////////////////////////////////////////////////// + // Armor Reduction factors + ////////////////////////////////////////////////////////// + + if (p.HasArmor()) + { + float armorMass_ = p.GetArmorThickness(); + float armorDensity_ = p.GetArmorDensity(); + float armorStrength_ = p.GetArmorSrength(); + float damageReduction = DamageReduction(armorMass_, armorDensity_, armorStrength_, damage_, ExplosionSourceType.Bullet, caliber, penetrationfactor); + + damage_ = damageReduction; + } + ////////////////////////////////////////////////////////// + // Apply Hitpoints + ////////////////////////////////////////////////////////// + + if (p.GetComponent() != null) + { + ApplyHitPoints(p.GetComponent(), (float)damage_); + } + else + { + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) + { + if (p.vessel.rootPart != null) + { + if (p != p.vessel.rootPart) + { + damage_ *= BDArmorySettings.ZOMBIE_DMG_MULT; + } + } + } + ApplyHitPoints(p, damage_, caliber, mass, multiplier, impactVelocity, penetrationfactor); + } + return damage_; + } + + /// + /// Ballistic Hitpoint Damage + /// + public static void ApplyHitPoints(Part p, float damage_, float caliber, float mass, float multiplier, float impactVelocity, float penetrationfactor) + { + ////////////////////////////////////////////////////////// + // Apply HitPoints Ballistic + ////////////////////////////////////////////////////////// + Dependencies.Get().AddDamageToPart_svc(p, damage_); + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) + { + Debug.Log("[BDArmory.PartExtensions]: mass: " + mass + " caliber: " + caliber + " multiplier: " + multiplier + " velocity: " + impactVelocity + " penetrationfactor: " + penetrationfactor); + } + } + public static void AddHealth(this Part p, float healing, bool overcharge = false) + { + if (p.GetComponent() != null) + { + ApplyHitPoints(p.GetComponent(), healing); + } + else + { + Dependencies.Get().AddHealthToPart_svc(p, healing, overcharge); + 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}"); + } + } + /// + /// Explosive Hitpoint Damage + /// + public static void ApplyHitPoints(Part p, float damage) + { + ////////////////////////////////////////////////////////// + // Apply Hitpoints / Explosive + ////////////////////////////////////////////////////////// + + Dependencies.Get().AddDamageToPart_svc(p, damage); + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) + Debug.Log("[BDArmory.PartExtensions]: Explosive Hitpoints Applied to " + p.name + ": " + damage); + } + + /// + /// Kerbal Hitpoint Damage + /// + public static void ApplyHitPoints(KerbalEVA kerbal, float damage) + { + ////////////////////////////////////////////////////////// + // Apply Hitpoints / Kerbal + ////////////////////////////////////////////////////////// + + Dependencies.Get().AddDamageToKerbal_svc(kerbal, damage); + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) + Debug.Log("[BDArmory.PartExtensions]: Hitpoints Applied to " + kerbal.name + ": " + damage); + } + + public static void AddForceToPart(Rigidbody rb, Vector3 force, Vector3 position, ForceMode mode) + { + ////////////////////////////////////////////////////////// + // Add The force to part + ////////////////////////////////////////////////////////// + + if (rb == null || rb.mass == 0) return; + rb.AddForceAtPosition(force, position, mode); + Debug.Log("[BDArmory.PartExtensions]: Force Applied : " + force.magnitude); + } + + public static void Destroy(this Part p) + { + Dependencies.Get().SetDamageToPart_svc(p, -1); + } + + public static bool HasArmor(this Part p) + { + return Mathf.FloorToInt(p.GetArmorThickness()) > 0f; + } + + public static bool GetFireFX(this Part p) + { + return Dependencies.Get().HasFireFX_svc(p); + } + + public static float GetFireFXTimeOut(this Part p) + { + return Dependencies.Get().GetFireFXTimeOut(p); + } + + public static float Damage(this Part p) + { + return Dependencies.Get().GetPartDamage_svc(p); + } + + public static float MaxDamage(this Part p) + { + return Dependencies.Get().GetMaxPartDamage_svc(p); + } + + public static void ReduceArmor(this Part p, double massToReduce) + { + if (!p.HasArmor()) return; + //massToReduce = Math.Max(0.10, Math.Round(massToReduce, 2)); + Dependencies.Get().ReduceArmor_svc(p, (float)massToReduce); + + if (BDArmorySettings.DEBUG_ARMOR) + { + //Debug.Log("[BDArmory.PartExtensions]: Armor volume Removed : " + massToReduce); + } + } + + public static float GetArmorThickness(this Part p) + { + if (p == null) return 0f; + float armorthickness = Dependencies.Get().GetPartArmor_svc(p); + if (float.IsNaN(armorthickness)) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.PartExtensions]: GetArmorThickness; thickness is NaN"); + return 0f; + } + else + { + //if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.PartExtensions]: GetArmorThickness; thickness is: " + armorthickness); + return armorthickness; + } + //return Dependencies.Get().GetPartArmor_svc(p); + } + public static float GetArmorMaxThickness(this Part p) + { + if (p == null) return 0f; + return Dependencies.Get().GetPartMaxArmor_svc(p); + } + public static float GetArmorDensity(this Part p) + { + if (p == null) return 0f; + return Dependencies.Get().GetArmorDensity_svc(p); + } + public static float GetArmorSrength(this Part p) + { + if (p == null) return 0f; + return Dependencies.Get().GetArmorStrength_svc(p); + } + + public static float GetArmorPercentage(this Part p) + { + if (p == null) return 0; + float armor_ = Dependencies.Get().GetPartArmor_svc(p); + float maxArmor_ = Dependencies.Get().GetMaxArmor_svc(p); + + return armor_ / maxArmor_; + } + + public static float GetDamagePercentage(this Part p) + { + if (p == null) return 0; + + float damage_ = p.Damage(); + float maxDamage_ = p.MaxDamage(); + + return damage_ / maxDamage_; + } + + public static void RefreshAssociatedWindows(this Part part) + { + //Thanks FlowerChild + //refreshes part action window + + //IEnumerator window = UnityEngine.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(); + + MonoUtilities.RefreshContextWindows(part); + } + + public static bool IsMissile(this Part part) + { + if (part == null || part.Modules == null) return false; + 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) + { + return part.Modules.Contains("ModuleWeapon"); + } + public static bool IsFunctional(this Part part) + { + return (part.isEngine() || part.isAntenna(out ModuleDeployableAntenna antenna) || part.isBaseServo(out BaseServo serv) || part.isDecoupler(out ModuleDecouple decoupler) || part.isGenerator(out ModuleGenerator gen) + || part.isRadiator(out ModuleDeployableRadiator rad) || part.isRobotic() || part.isRoboticHinge() || part.isRoboticPiston() || part.isRoboticRotationServo() || part.isRoboticRotor() + || part.Modules.Contains("ModuleGrappleNode") || part.Modules.Contains("ModuleResourceConverter") || part.Modules.Contains("ModuleCoreHeat")); + } + public static float GetArea(this Part part, bool isprefab = false, Part prefab = null) + { + var size = part.GetSize(); + float sfcAreaCalc = 2f * (size.x * size.y) + 2f * (size.y * size.z) + 2f * (size.x * size.z); + + return sfcAreaCalc; + } + + public static float GetAverageBoundSize(this Part part) + { + var size = part.GetSize(); + + return (size.x + size.y + size.z) / 3f; + } + + public static float GetVolume(this Part part) + { + var size = part.GetSize(); + var volume = size.x * size.y * size.z; + return volume; + } + + public static Vector3 GetSize(this Part part) + { + var meshFilter = part.GetComponentInChildren(); + if (meshFilter == null) + { + Debug.LogWarning($"[BDArmory.PartExtension]: {part.name} has no MeshFilter! Returning zero size."); + return Vector3.zero; + } + var size = meshFilter.mesh.bounds.size; + + // if (part.name.Contains("B9.Aero.Wing.Procedural")) // Covered by SuicidalInsanity's patch. + // { + // size = size * 0.1f; + // } + + float scaleMultiplier = part.GetTweakScaleMultiplier(); + return size * scaleMultiplier; + } + + private static bool tweakScaleChecked = false; + private static bool tweakScaleInstalled = false; + public static float GetTweakScaleMultiplier(this Part part) + { + float scaleMultiplier = 1f; + if (!tweakScaleChecked) + { + foreach (var assy in AssemblyLoader.loadedAssemblies) + if (assy.assembly.FullName.Contains("TweakScale")) + tweakScaleInstalled = true; + tweakScaleChecked = true; + } + if (tweakScaleInstalled && part.Modules.Contains("TweakScale")) + { + var tweakScaleModule = part.Modules["TweakScale"]; + scaleMultiplier = tweakScaleModule.Fields["currentScale"].GetValue(tweakScaleModule) / + tweakScaleModule.Fields["defaultScale"].GetValue(tweakScaleModule); + } + return scaleMultiplier; + } + + public static bool IsAero(this Part part) + { + if (part.Modules.Contains("ModuleLiftingSurface") || part.Modules.Contains("FARWingAerodynamicModel")) + { + if (part.name.Contains("mk2") || part.name.Contains("Mk2") || part.name.Contains("M2X") || part.name.Contains("HeatShield")) // don't grab Mk2 parts or heatshields. Caps-sensitive + { + return false; + } + if (part.partInfo.bulkheadProfiles.Contains("mk2")) return false; + else return true; + } + else if (part.Modules.Contains("ModuleControlSurface") || + part.Modules.Contains("FARControllableSurface")) + { + return true; + } + else return false; + } + public static bool IsMotor(this Part part) + { + if (part.GetComponent() != null) + return true; + else return false; + } + public static string GetExplodeMode(this Part part) + { + return Dependencies.Get().GetExplodeMode_svc(part); + } + + public static bool IgnoreDecal(this Part part) + { + if ( + part.Modules.Contains("FSplanePropellerSpinner") || + part.Modules.Contains("ModuleWheelBase") || + part.Modules.Contains("KSPWheelBase") || + part.gameObject.GetComponentUpwards() || + part.Modules.Contains("ModuleReactiveArmor") || + part.Modules.Contains("ModuleDCKShields") || + part.Modules.Contains("ModuleShieldGenerator") + ) + { + return true; + } + else + { + return false; + } + } + + public static bool HasFuel(this Part part) + { + bool hasFuel = false; + using (IEnumerator resources = part.Resources.GetEnumerator()) + while (resources.MoveNext()) + { + if (resources.Current == null) continue; + switch (resources.Current.resourceName) + { + case "LiquidFuel": + if (resources.Current.amount > 1d) hasFuel = true; + break; + case "Oxidizer": + if (resources.Current.amount > 1d) hasFuel = true; + break; + case "MonoPropellant": + if (resources.Current.amount > 1d) hasFuel = true; + break; + } + } + return hasFuel; + } + + public static float DamageReduction(float armor, float density, float strength, float damage, ExplosionSourceType sourceType, float caliber = 0, float penetrationfactor = 0) + { + float _damageReduction; + + switch (sourceType) + { + case ExplosionSourceType.Missile: + //damage *= Mathf.Clamp(-0.0005f * armor + 1.025f, 0f, 0.5f); // Cap damage reduction at 50% (armor = 1050) + 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, 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.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 + + break; + default: + if (!(penetrationfactor >= 1f)) + { + //if (BDAMath.Between(armor, 100f, 200f)) + //{ + // damage *= 0.300f; + //} + //else if (BDAMath.Between(armor, 200f, 400f)) + //{ + // damage *= 0.250f; + //} + //else if (BDAMath.Between(armor, 400f, 500f)) + //{ + // damage *= 0.200f; + //} + + //y=(98.34817*x)/(97.85935+x) + + _damageReduction = (113 * armor) / (154 + armor); //should look at this later, review? + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.PartExtensions]: Damage Before Reduction : " + damage + + "; Damage Reduction (%) : " + 100 * (1 - Mathf.Clamp01((113f - _damageReduction) / 100f)) + + "; Damage After Armor : " + (damage * Mathf.Clamp01((113f - _damageReduction) / 100f))); + } + + damage *= Mathf.Clamp01((113f - _damageReduction) / 100f); + } + break; + } + + return damage; + } + + public static bool isBattery(this Part part) + { + bool hasEC = false; + using (IEnumerator resources = part.Resources.GetEnumerator()) + while (resources.MoveNext()) + { + if (resources.Current == null) continue; + switch (resources.Current.resourceName) + { + case "ElectricCharge": + if (resources.Current.amount > 1d) hasEC = true; //discount trace EC in alternators + break; + } + } + return hasEC; + } + public static Vector3 GetBoundsSize(Part part) + { + return PartGeometryUtil.MergeBounds(part.GetRendererBounds(), part.transform).size; + } + + /// + /// KSP version dependent query of whether the part is a kerbal on EVA. + /// + /// Part to check. + /// true if the part is a kerbal on EVA. + public static bool IsKerbalEVA(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.IsKerbalEVA_1_11(); + } + else + { + return part.IsKerbalEVA_1_10(); + } + } + + private static bool IsKerbalEVA_1_11(this Part part) // KSP has issues on older versions if this call is in the parent function. + { + return part.isKerbalEVA(); + } + + 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..aaa7576e8 --- /dev/null +++ b/BDArmory/Extensions/VectorExtensions.cs @@ -0,0 +1,192 @@ +using UnityEngine; +using System.Runtime.CompilerServices; + +using BDArmory.Utils; + +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); + } + + /// + /// A (2x) faster Vector3.Dot(v1.normalized, v2.normalized) by only performing a single sqrt and division. + /// The vectors do not need normalising beforehand. + /// + /// A vector + /// Another vector + /// The dot product between the normalised vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DotNormalized(this Vector3 v1, Vector3 v2) + { + var normalisationFactor = BDAMath.Sqrt(v1.sqrMagnitude * v2.sqrMagnitude); + return normalisationFactor > 0 ? Vector3.Dot(v1, v2) / normalisationFactor : 0; + } + + /// + /// Efficient replacement for Vector3.Distance(from, to) < distance. + /// Intel and AMD appear to support sqrt in hardware, but M1 Macs don't. + /// This is the most efficient version from benchmarks and gives the cleanest code. + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CloserToThan(this Vector3 v, Vector3 to, float distance) + { + return (v - to).sqrMagnitude < distance * distance; + } + /// + /// Efficient replacement for Vector3.Distance(from, to) > distance. + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool FurtherFromThan(this Vector3 v, Vector3 from, float distance) + { + return (v - from).sqrMagnitude > distance * distance; + } + + /// + /// Decompose a vector into a magnitude and normalized direction. + /// This performs the same operations as v.normalized but also returns the magnitude, which is often needed. + /// + /// The vector to decompose. + /// The magnitude and normalized unit vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (float, Vector3) MagNorm(this Vector3 v) + { + float mag = v.magnitude; + if (mag > Vector3.kEpsilon) + return (mag, v / mag); + else + return (mag, Vector3.zero); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (double, Vector3d) MagNorm(this Vector3d v) + { + double mag = v.magnitude; + if (mag > Vector3.kEpsilon) + return (mag, v / mag); + else + return (mag, Vector3.zero); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (float, Vector2) MagNorm(this Vector2 v) + { + float mag = v.magnitude; + if (mag > Vector2.kEpsilon) + return (mag, v / mag); + else + return (mag, Vector2.zero); + } + + /// + /// Check if any of the vector elements are NaN. + /// + /// A Vector3. + /// True if any of the elements are NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(this Vector3 v) + { return float.IsNaN(v.x) || float.IsNaN(v.y) || float.IsNaN(v.z); } + /// + /// Check if any of the quaternion elements are NaN. + /// + /// A Quaternion + /// True if any of the elements are NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(this Quaternion q) // Techinically not a Vector3 extension, but it fits here. + { return float.IsNaN(q.w) || float.IsNaN(q.x) || float.IsNaN(q.y) || float.IsNaN(q.z); } + + /// + /// Check if any of the vector elements are Inf. + /// + /// A Vector3. + /// True if any of the elements are Inf. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInf(this Vector3 v) + { return float.IsInfinity(v.x) || float.IsInfinity(v.y) || float.IsInfinity(v.z); } + /// + /// Check if any of the quaternion elements are Inf. + /// + /// A Quaternion + /// True if any of the elements are Inf. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInf(this Quaternion q) // Techinically not a Vector3 extension, but it fits here. + { return float.IsInfinity(q.w) || float.IsInfinity(q.x) || float.IsInfinity(q.y) || float.IsInfinity(q.z); } + + // Combined methods for convenience. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInfOrNaN(this Vector3 v) => v.IsNaN() || v.IsInf(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInfOrNaN(this Quaternion q) => q.IsNaN() || q.IsInf(); + } +} \ No newline at end of file diff --git a/BDArmory/Extensions/VesselExtensions.cs b/BDArmory/Extensions/VesselExtensions.cs new file mode 100644 index 000000000..1bde00fd4 --- /dev/null +++ b/BDArmory/Extensions/VesselExtensions.cs @@ -0,0 +1,262 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using System.Runtime.CompilerServices; + +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + +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 InNearVacuum(this Vessel v) + { + return v.atmDensity <= 0.05f; + } + + 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 == null) return false; + 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". + /// Note: + /// - Use bounds whenever you want a more precise radius from a given perspective. It is more computationally expensive though. + /// - Use average:true for situations such as targeting/proximity checks when not using bounds to better handle non-spherical vessels. + /// - Use average:false (the default) for situations where the maximum radius is important, e.g., collision avoidance. + /// + /// + /// + /// + /// If not using bounds, return the average of the dimensions instead of the max. + /// + public static float GetRadius(this Vessel vessel, Vector3 fireTransform = default, Vector3 bounds = default, bool average = false) + { + if (fireTransform == Vector3.zero || bounds == Vector3.zero) + { + // Get vessel size. + Vector3 size = vessel.vesselSize; + + if (average) + // Get the average dimension (without using bounds) for a more appropriate estimate of the vessel's radius for targeting/proximity checks. + return (size.x + size.y + size.z) / 6f; + else + // 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.7321f); // 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 (BDArmorySettings.DEBUG_OTHER && !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. + } + } + } + +#if DEBUG + static HashSet badBoundsReported = new HashSet(); // Only report vessels with bad bounds once. +#endif + /// + /// 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 partBound = GetRendererPartBounds(vessel.rootPart); + if (partBound.min.sqrMagnitude > 1e6 || partBound.max.sqrMagnitude > 1e6) // Fall back to the first collider bounds if renderer bounds are nonsensical. This is usually temporary. + partBound = vessel.rootPart.GetColliderBounds().FirstOrDefault(); + min = partBound.min; max = partBound.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). + partBound = GetRendererPartBounds(part.Current); + if (partBound.min.sqrMagnitude > 1e6 || partBound.max.sqrMagnitude > 1e6) + partBound = part.Current.GetColliderBounds().FirstOrDefault(); // Fall back to the first collider bounds if renderer bounds are nonsensical. This is usually temporary. + 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 + if (!badBoundsReported.Contains(vessel.vesselName)) + { + 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($"[BDArmory.VesselExtensions]: 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}"))}."); + badBoundsReported.Add(vessel.vesselName); + } +#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(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActiveController ActiveController(this Vessel vessel) => Utils.ActiveController.GetActiveController(vessel); + + /// + /// Strip the vessel type from the end of a vessel's name (as long as it's not an ignored type). + /// KSP automatically adds this whenever a new vessel is made. + /// + /// + public static void StripTypeFromName(this Vessel vessel) + { + if (vessel == null || string.IsNullOrEmpty(vessel.vesselName)) return; + if (!VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType) && vessel.vesselName.EndsWith($" {vessel.vesselType}")) + { + vessel.vesselName = vessel.vesselName.Remove(vessel.vesselName.Length - $" {vessel.vesselType}".Length); + } + } + } +} diff --git a/BDArmory/FX/BDAGaplessParticleEmitter.cs b/BDArmory/FX/BDAGaplessParticleEmitter.cs index c5d7c72f1..1d32ad645 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.GaplessParticleEmitters) return; + if (!part && !rb) { internalVelocity = (transform.position - lastPos) / Time.fixedDeltaTime; @@ -98,6 +102,8 @@ void FixedUpdate() public void EmitParticles() { + if (!BDArmorySettings.GaplessParticleEmitters) 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 9c98da237..84102d77b 100644 --- a/BDArmory/FX/BulletHitFX.cs +++ b/BDArmory/FX/BulletHitFX.cs @@ -1,50 +1,117 @@ -using System; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Misc; 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(); template.SetActive(false); return ObjectPool.CreateObjectPool(template, BDArmorySettings.MAX_NUM_BULLET_DECALS, false, true, 0, true); } - public void AttachAt(Part hitPart, RaycastHit hit, Vector3 offset) + public void AttachAt(Part hitPart, RaycastHit hit, Vector3 offset, Vector3 colliderLocalHitPoint = default) { + if (hitPart is null) return; parentPart = hitPart; - transform.SetParent(hitPart.transform); - transform.position = hit.point + offset; + // parentPartName = parentPart.name; + // parentVesselName = parentPart.vessel.vesselName; + // Set the parent transform to the collider instead of the part in order to account for things like turrets and other moving parts + transform.SetParent(hit.collider.transform); + transform.position = (colliderLocalHitPoint == default ? hit.point : transform.parent.TransformPoint(colliderLocalHitPoint)) + 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) + { + var r = gameObject.GetComponentInChildren(); + if (r != null) + { + r.material.shader = Shader.Find("KSP/Particles/Alpha Blended"); + r.material.SetColor("_TintColor", color); + r.material.color = color; + } + else + { + Debug.Log("[PAINTBALL] no renderer found in decal"); + } + + } - public void OnParentDestroy() + void OnParentDestroy() { - if (parentPart) + if (parentPart is not null) { parentPart.OnJustAboutToDie -= OnParentDestroy; parentPart.OnJustAboutToBeDestroyed -= OnParentDestroy; - parentPart = null; - transform.parent = null; - gameObject.SetActive(false); + Deactivate(); + } + } + + void OnVesselUnloaded(Vessel vessel) + { + if (parentPart is not null && (parentPart.vessel is null || parentPart.vessel == vessel)) + { + OnParentDestroy(); + } + else if (parentPart is null) + { + Deactivate(); } } - public void OnDestroy() + 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() + { + transform.parent = null; + gameObject.SetActive(false); + } + + void OnDisable() { - OnParentDestroy(); // Make sure it's disabled and book-keeping is done. + parentPart = null; + transform.localScale = Vector3.one; // Reset localScale so that Unity doesn't mess with the size. + if (hasOnVesselUnloaded) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); + } + + public 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.BulletHitFX]: BulletHitFX on {parentPartName} ({parentVesselName}) was destroyed!"); + if (hasOnVesselUnloaded) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); } } @@ -52,6 +119,8 @@ public class BulletHitFX : MonoBehaviour { KSPParticleEmitter[] pEmitters; AudioSource audioSource; + enum AudioClipType { Ricochet1, Ricochet2, Ricochet3, BulletHit1, BulletHit2, BulletHit3, Artillery_Shot }; + static Dictionary audioClips; AudioClip hitSound; float startTime; public bool ricochet; @@ -64,6 +133,9 @@ public class BulletHitFX : MonoBehaviour public static ObjectPool decalPool_paint3; public static ObjectPool bulletHitFXPool; public static ObjectPool penetrationFXPool; + public static ObjectPool leakFXPool; + public static ObjectPool FireFXPool; + public static ObjectPool flameFXPool; public static Dictionary> PartsOnFire = new Dictionary>(); public static int MaxFiresPerVessel = 3; @@ -91,6 +163,7 @@ public static void SetupShellPool() if (decalPool_paint3 == null) decalPool_paint3 = Decal.CreateDecalPool("BDArmory/Models/bulletDecal/BulletDecal5"); + } } @@ -128,9 +201,24 @@ public static void SetupBulletHitFXPool() penetrationFXTemplate.SetActive(false); penetrationFXPool = ObjectPool.CreateObjectPool(penetrationFXTemplate, 10, true, true, 10f * Time.deltaTime, false); } + if (flameFXPool == null) + { + var flameTemplate = GameDatabase.Instance.GetModel("BDArmory/FX/FlameEffect2/model"); + flameTemplate.AddComponent(); + DecalEmitterScript.shrinkRateFlame = 0.125f; + DecalEmitterScript.shrinkRateSmoke = 0.125f; + foreach (var pe in flameTemplate.GetComponentsInChildren()) + { + if (!pe.useWorldSpace) continue; + var gpe = pe.gameObject.AddComponent(); + gpe.Emit = false; + } + flameTemplate.SetActive(false); + flameFXPool = ObjectPool.CreateObjectPool(flameTemplate, 10, true, true); + } } - public static void SpawnDecal(RaycastHit hit, Part hitPart, float caliber, float penetrationfactor) + public static void SpawnDecal(RaycastHit hit, Part hitPart, float caliber, float penetrationfactor, string team, Vector3 colliderLocalHitPoint = default) { if (!BDArmorySettings.BULLET_DECALS) return; ObjectPool decalPool_; @@ -168,27 +256,31 @@ public static void SpawnDecal(RaycastHit hit, Part hitPart, float caliber, float if (decalFront != null && hitPart != null) { var decal = decalFront.GetComponentInChildren(); - decal.AttachAt(hitPart, hit, new Vector3(0.25f, 0f, 0f)); + decal.AttachAt(hitPart, hit, new Vector3(0.05f, 0f, 0f), colliderLocalHitPoint); + + if (BDArmorySettings.PAINTBALL_MODE) + { + if (team != null && BDTISetup.Instance.ColorAssignments.ContainsKey(team)) + { + decal.SetColor(BDTISetup.Instance.ColorAssignments[team]); + } + } } //back hole if fully penetrated - if (penetrationfactor >= 1) + if (penetrationfactor >= 1 && !BDArmorySettings.PAINTBALL_MODE) { var decalBack = decalPool_.GetPooledObject(); if (decalBack != null && hitPart != null) { var decal = decalBack.GetComponentInChildren(); - decal.AttachAt(hitPart, hit, new Vector3(-0.25f, 0f, 0f)); - } - - if (CanFlamesBeAttached(hitPart)) - { - AttachFlames(hit, hitPart, caliber); + decal.AttachAt(hitPart, hit, new Vector3(-0.05f, 0f, 0f), colliderLocalHitPoint); } } } private static bool CanFlamesBeAttached(Part hitPart) { + if (hitPart == null || hitPart.vessel == null) return false; if (!BDArmorySettings.FIRE_FX_IN_FLIGHT && !hitPart.vessel.LandedOrSplashed || !hitPart.HasFuel()) return false; @@ -220,6 +312,33 @@ private static bool CanFlamesBeAttached(Part hitPart) return true; } + public static void CleanPartsOnFireInfo() + { + HashSet keysToRemove = new HashSet(); + foreach (var key in PartsOnFire.Keys.ToList()) + { + PartsOnFire[key] = PartsOnFire[key].Where(x => (Time.time - x) < FireLifeTimeInSeconds).ToList(); // Remove expired fires. + if (PartsOnFire[key].Count == 0) { keysToRemove.Add(key); } // Remove parts no longer on fire. + } + PartsOnFire = PartsOnFire.Where(kvp => kvp.Key != null && !keysToRemove.Contains(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null keys (vessels) and those with no parts on fire. + } + + void Awake() + { + if (audioClips == null) + { + audioClips = new Dictionary{ + {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")}, + }; + } + } + void OnEnable() { startTime = Time.time; @@ -240,26 +359,44 @@ void OnEnable() { if (caliber <= 30) { - string path = "BDArmory/Sounds/ricochet" + random; - hitSound = GameDatabase.Instance.GetAudioClip(path); + switch (random) + { + case 1: + hitSound = audioClips[AudioClipType.Ricochet1]; + break; + case 2: + hitSound = audioClips[AudioClipType.Ricochet2]; + break; + case 3: + hitSound = audioClips[AudioClipType.Ricochet3]; + break; + } } else { - string path = "BDArmory/Sounds/Artillery_Shot"; - hitSound = GameDatabase.Instance.GetAudioClip(path); + hitSound = audioClips[AudioClipType.Artillery_Shot]; } } else { if (caliber <= 30) { - string path = "BDArmory/Sounds/bulletHit" + random; - hitSound = GameDatabase.Instance.GetAudioClip(path); + switch (random) + { + case 1: + hitSound = audioClips[AudioClipType.BulletHit1]; + break; + case 2: + hitSound = audioClips[AudioClipType.BulletHit2]; + break; + case 3: + hitSound = audioClips[AudioClipType.BulletHit3]; + break; + } } else { - string path = "BDArmory/Sounds/Artillery_Shot"; - hitSound = GameDatabase.Instance.GetAudioClip(path); + hitSound = audioClips[AudioClipType.Artillery_Shot]; } } @@ -290,18 +427,18 @@ void Update() } } - public static void CreateBulletHit(Part hitPart, Vector3 position, RaycastHit hit, Vector3 normalDirection, bool ricochet, float caliber, float penetrationfactor) + public static void CreateBulletHit(Part hitPart, Vector3 position, RaycastHit hit, Vector3 normalDirection, bool ricochet, float caliber, float penetrationfactor, string team, Vector3 colliderLocalHitPoint = default) { if (decalPool_large == null || decalPool_small == null) SetupShellPool(); if (BDArmorySettings.PAINTBALL_MODE && decalPool_paint1 == null) SetupShellPool(); - if (bulletHitFXPool == null || penetrationFXPool == null) + if (bulletHitFXPool == null || penetrationFXPool == null || flameFXPool == null) SetupBulletHitFXPool(); if ((hitPart != null) && caliber != 0 && !hitPart.IgnoreDecal()) { - SpawnDecal(hit, hitPart, caliber, penetrationfactor); //No bullet decals for laser or ricochet + SpawnDecal(hit, hitPart, caliber, penetrationfactor, team, colliderLocalHitPoint); //No bullet decals for laser or ricochet } GameObject newExplosion = (caliber <= 30 || BDArmorySettings.PAINTBALL_MODE) ? bulletHitFXPool.GetPooledObject() : penetrationFXPool.GetPooledObject(); @@ -318,60 +455,158 @@ public static void CreateBulletHit(Part hitPart, Vector3 position, RaycastHit hi if (pe.gameObject.name == "sparks") { - pe.force = (4.49f * FlightGlobals.getGeeForceAtPosition(position)); + pe.force = 4.49f * FlightGlobals.getGeeForceAtPosition(position); } else if (pe.gameObject.name == "smoke") { - pe.force = (1.49f * FlightGlobals.getGeeForceAtPosition(position)); + pe.force = 1.49f * FlightGlobals.getGeeForceAtPosition(position); } } } - // FIXME Use an object pool for flames? - public static void AttachFlames(RaycastHit hit, Part hitPart, float caliber) + public static void AttachLeak(RaycastHit hit, Part hitPart, float caliber, bool explosive, bool incendiary, string sourcevessel, bool inertTank, Vector3 colliderLocalHitPoint = default) { - var modelUrl = "BDArmory/FX/FlameEffect2/model"; + if (BDArmorySettings.BATTLEDAMAGE && BDArmorySettings.BD_TANKS && hitPart.Modules.GetModule().Hitpoints > 0) + { + if (leakFXPool == null) + leakFXPool = FuelLeakFX.CreateLeakFXPool("BDArmory/FX/FuelLeakFX/model"); + var fuelLeak = leakFXPool.GetPooledObject(); + var leakFX = fuelLeak.GetComponentInChildren(); - var flameObject = (GameObject)Instantiate(GameDatabase.Instance.GetModel(modelUrl), hit.point + new Vector3(0.25f, 0f, 0f), Quaternion.identity); + var leak = hitPart.GetComponentsInChildren(); + if (leak != null) //only apply one leak to engines + { + if (!hitPart.isEngine()) + { + var scale = caliber * caliber / 200f; + leakFX.transform.localScale = scale * Vector3.one; + leakFX.AttachAt(hitPart, hit, new Vector3(0.25f, 0f, 0f), colliderLocalHitPoint); + leakFX.drainRate = scale * BDArmorySettings.BD_TANK_LEAK_RATE; + leakFX.lifeTime = BDArmorySettings.BD_TANK_LEAK_TIME; + if (BDArmorySettings.BD_FIRES_ENABLED && !inertTank) + { + float ammoMod = BDArmorySettings.BD_FIRE_CHANCE_TRACER; //10% chance of AP rounds starting fires from sparks/tracers/etc + if (explosive) + { + ammoMod = BDArmorySettings.BD_FIRE_CHANCE_HE; //20% chance of starting fires from HE rounds + } + if (incendiary) + { + ammoMod = BDArmorySettings.BD_FIRE_CHANCE_INCENDIARY; //90% chance of starting fires from inc rounds + } + double Diceroll = UnityEngine.Random.Range(0, 100); + if (Diceroll <= ammoMod) + { + leakFX.lifeTime = 0; + int leakcount = 0; + foreach (var existingLeakFX in hitPart.GetComponentsInChildren()) + { + existingLeakFX.lifeTime = 0; //kill leakFX, start fire + leakcount++; + } + //if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BullethitFX]: Adding fire. HE? " + explosive + "; Inc? " + incendiary + "; inerttank? " + inertTank); + AttachFire(colliderLocalHitPoint == default ? hit.point : hit.collider.transform.TransformPoint(colliderLocalHitPoint), hitPart, caliber, sourcevessel, -1, leakcount); + } + } + } + } + else + { + var scale = caliber * caliber / 200f; + leakFX.transform.localScale = scale * Vector3.one; + leakFX.AttachAt(hitPart, hit, new Vector3(0.25f, 0f, 0f), colliderLocalHitPoint); + leakFX.drainRate = scale * BDArmorySettings.BD_TANK_LEAK_RATE; + leakFX.lifeTime = BDArmorySettings.BD_TANK_LEAK_TIME; - flameObject.SetActive(true); - flameObject.transform.SetParent(hitPart.transform); - flameObject.AddComponent(); + if (hitPart.isEngine()) + { + leakFX.lifeTime = (10 * BDArmorySettings.BD_TANK_LEAK_TIME); + } + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BulletHitFX]: BulletHit attaching fuel leak, drainrate: " + leakFX.drainRate); - if (hitPart.vessel.LandedOrSplashed && hitPart.GetFireFX() && caliber >= 100f) - { - DecalEmitterScript.shrinkRateFlame = 0.25f; - DecalEmitterScript.shrinkRateSmoke = 0.125f; + fuelLeak.SetActive(true); } - - foreach (var pe in flameObject.GetComponentsInChildren()) + } + public static void AttachFire(Vector3 hit, Part hitPart, float caliber, string sourcevessel, float burntime = -1, int ignitedLeaks = 1, bool surfaceFire = false) + { + if (BDArmorySettings.BATTLEDAMAGE && BDArmorySettings.BD_FIRES_ENABLED && hitPart.Modules.GetModule().Hitpoints > 0) { - if (!pe.useWorldSpace) continue; - var gpe = pe.gameObject.AddComponent(); - gpe.Emit = true; + if (FireFXPool == null) + FireFXPool = FireFX.CreateFireFXPool("BDArmory/FX/FireFX/model"); + var fire = FireFXPool.GetPooledObject(); + var fireFX = fire.GetComponentInChildren(); + fireFX.burnTime = burntime; //this apparently never got implemented... !? + fireFX.AttachAt(hitPart, hit, new Vector3(0.25f, 0f, 0f), sourcevessel); + fireFX.burnRate = (((caliber / 50) * BDArmorySettings.BD_TANK_LEAK_RATE) * ignitedLeaks); + fireFX.surfaceFire = surfaceFire; + //fireFX.transform.localScale = Vector3.one * (caliber/10); + + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BulletHitFX]: BulletHit fire, burn rate: " + fireFX.burnRate + "; Surface fire: " + surfaceFire); + fire.SetActive(true); } } - public static void AttachFlames(Vector3 contactPoint, Part hitPart) { if (!CanFlamesBeAttached(hitPart)) return; - var modelUrl = "BDArmory/FX/FlameEffect2/model"; - - var flameObject = (GameObject)Instantiate(GameDatabase.Instance.GetModel(modelUrl), contactPoint, Quaternion.identity); - - flameObject.SetActive(true); + if (flameFXPool == null) SetupBulletHitFXPool(); + var flameObject = flameFXPool.GetPooledObject(); + if (flameObject == null) + { + Debug.LogError("[BDArmory.BulletHitFX]: flameFXPool gave a null flameObject!"); + return; + } flameObject.transform.SetParent(hitPart.transform); - flameObject.AddComponent(); - - DecalEmitterScript.shrinkRateFlame = 0.125f; - DecalEmitterScript.shrinkRateSmoke = 0.125f; + flameObject.SetActive(true); + } - foreach (var pe in flameObject.GetComponentsInChildren()) + public static void DisableAllFX() + { + if (leakFXPool != null && leakFXPool.pool != null) { - if (!pe.useWorldSpace) continue; - var gpe = pe.gameObject.AddComponent(); - gpe.Emit = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BulletHitFX]: Setting {leakFXPool.pool.Count(leak => leak != null && leak.activeInHierarchy)} leak FX inactive."); + foreach (var leak in leakFXPool.pool) + { + if (leak == null) continue; + leak.SetActive(false); + } + } + if (FireFXPool != null && FireFXPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BulletHitFX]: Setting {FireFXPool.pool.Count(fire => fire != null && fire.activeInHierarchy)} fire FX inactive."); + foreach (var fire in FireFXPool.pool) + { + if (fire == null) continue; + fire.SetActive(false); + } + } + if (flameFXPool != null && flameFXPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BulletHitFX]: Setting {flameFXPool.pool.Count(flame => flame != null && flame.activeInHierarchy)} flame FX inactive."); + foreach (var flame in flameFXPool.pool) + { + if (flame == null) continue; + flame.SetActive(false); + } + } + if (bulletHitFXPool != null && bulletHitFXPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BulletHitFX]: Setting {bulletHitFXPool.pool.Count(hit => hit != null && hit.activeInHierarchy)} bullet hit FX inactive."); + foreach (var hit in bulletHitFXPool.pool) + { + if (hit == null) continue; + hit.SetActive(false); + } + } + if (penetrationFXPool != null && penetrationFXPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BulletHitFX]: Setting {penetrationFXPool.pool.Count(pen => pen != null && pen.activeInHierarchy)} penetration FX inactive."); + foreach (var pen in penetrationFXPool.pool) + { + if (pen == null) continue; + pen.SetActive(false); + } } } } 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/DecalEmitterScript.cs b/BDArmory/FX/DecalEmitterScript.cs index fd3be49b4..41f6f4306 100644 --- a/BDArmory/FX/DecalEmitterScript.cs +++ b/BDArmory/FX/DecalEmitterScript.cs @@ -27,7 +27,6 @@ public void Start() if (!(pe.maxEnergy > _highestEnergy)) continue; _destroyer = pe.gameObject; _highestEnergy = pe.maxEnergy; - EffectBehaviour.AddParticleEmitter(pe); } } @@ -64,5 +63,31 @@ private void OnDestroy() if (pe) EffectBehaviour.RemoveParticleEmitter(pe); } + + void OnEnable() + { + foreach (var pe in gameObject.GetComponentsInChildren()) + { + if (pe == null) continue; + EffectBehaviour.AddParticleEmitter(pe); + if (!pe.useWorldSpace) continue; + var gpe = pe.gameObject.GetComponent(); + if (gpe != null) + gpe.Emit = true; + } + } + + void OnDisable() + { + foreach (var pe in gameObject.GetComponentsInChildren()) + { + if (pe == null) continue; + EffectBehaviour.RemoveParticleEmitter(pe); + if (!pe.useWorldSpace) continue; + var gpe = pe.gameObject.GetComponent(); + if (gpe != null) + gpe.Emit = false; + } + } } } diff --git a/BDArmory/FX/DecalGaplessParticleEmitter.cs b/BDArmory/FX/DecalGaplessParticleEmitter.cs index 5833c5ec2..56a2b581f 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.GaplessParticleEmitters) return; + if (!part && !rb) { internalVelocity = (transform.position - lastPos) / Time.fixedDeltaTime; @@ -63,7 +66,6 @@ private void FixedUpdate() if (!Emit) return; - //var velocity = part?.GetComponent().velocity ?? rb.velocity; var originalLocalPosition = gameObject.transform.localPosition; var originalPosition = gameObject.transform.position; var startPosition = gameObject.transform.position + velocity * Time.fixedDeltaTime; @@ -88,7 +90,10 @@ private void FixedUpdate() public void EmitParticles() { - var velocity = part?.GetComponent().velocity ?? rb.velocity; + if (!BDArmorySettings.GaplessParticleEmitters) return; + + var partRB = part != null ? part.GetComponent() : null; + var velocity = partRB != null ? partRB.velocity : rb.velocity; var originalLocalPosition = gameObject.transform.localPosition; var originalPosition = gameObject.transform.position; var startPosition = gameObject.transform.position + velocity * Time.fixedDeltaTime; diff --git a/BDArmory/FX/ExplosionFX.cs b/BDArmory/FX/ExplosionFX.cs index c5ce15a7f..7cc13ad3d 100644 --- a/BDArmory/FX/ExplosionFX.cs +++ b/BDArmory/FX/ExplosionFX.cs @@ -1,57 +1,130 @@ using System; using System.Collections.Generic; using System.Linq; -using BDArmory.Bullets; -using BDArmory.Competition; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.UI; using UnityEngine; +using BDArmory.Armor; +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.GameModes; +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; } public string SourceVesselName { get; set; } + public string SourceVesselTeam { get; set; } + public string SourceWeaponName { get; set; } public float Power { get; set; } - public Vector3 Position { get; set; } + public Vector3 Position { get { return _position; } set { _position = value; transform.position = _position; } } + Vector3 _position; public Vector3 Direction { get; set; } + public Vector3 Velocity { 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; } + bool isReportingWeapon = false; + bool bulletHitRegistered = true; // Whether the bullet hit has been registered or not before triggering the explosion (for proxi-detonations). + public Part projectileHitPart { get; set; } + public float ImpactSpeed { get; set; } // For kinetic impactors. public float TimeIndex => Time.time - StartTime; + Dictionary totalPartsHit = []; + Dictionary totalDamageApplied = []; + private bool disabled = true; - public Queue ExplosionEvents = new Queue(); + float blastRange; + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); // Why 19 and 23? - public static List IgnoreParts = new List(); + Queue explosionEvents = new(); + List explosionEventsPreProcessing = []; + List explosionEventsPartsAdded = []; + List explosionEventsBuildingAdded = []; + Dictionary explosionEventsVesselsHit = []; - public static List IgnoreBuildings = new List(); - internal static readonly float ExplosionVelocity = 343f; + 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; + internal static readonly float ExplosionVelocity = 422.75f; + internal static float KerbinSeaLevelAtmDensity + { + get + { + if (_KerbinSeaLevelAtmDensity == 0) _KerbinSeaLevelAtmDensity = (float)FlightGlobals.GetBodyByName("Kerbin").atmDensityASL; + return _KerbinSeaLevelAtmDensity; + } + } + internal static float _KerbinSeaLevelAtmDensity = 0; private float particlesMaxEnergy; + internal static HashSet ignoreCasingFor = new HashSet { ExplosionSourceType.Missile, ExplosionSourceType.Rocket }; + public enum WarheadTypes + { + Standard, + ShapedCharge, + ContinuousRod, + Kinetic // Expanding cone from kinetic impact. + } + + public WarheadTypes warheadType; + + static List> LoSIntermediateParts = []; // Worker list for LoS checks to avoid reallocations. + static HashSet _LoSIntermediateParts = []; // Hashset of unique parts in LoS. + + 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(); } + } 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. - CalculateBlastEvents(); + 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 + totalPartsHit.Clear(); + totalDamageApplied.Clear(); + if (!isFX) + { + CalculateBlastEvents(); + } pEmitters = gameObject.GetComponentsInChildren(); foreach (var pe in pEmitters) if (pe != null) @@ -59,244 +132,580 @@ private void OnEnable() if (pe.maxEnergy > particlesMaxEnergy) particlesMaxEnergy = pe.maxEnergy; pe.emit = true; + pe.useWorldSpace = false; // Don't use worldspace, so that we can move the FX properly. var emission = pe.ps.emission; emission.enabled = true; EffectBehaviour.AddParticleEmitter(pe); } + if (BDArmorySettings.LightFX) + { + LightFx = gameObject.GetComponent(); + LightFx.range = Range * 3f; + LightFx.intensity = 8f; // Reset light intensity. + } + //comment out above and uncomment below if !LIGHTFX = light range/intensity remains 0; + //LightFx = gameObject.GetComponent(); + //LightFx.range = BDArmorySettings.LIGHTFX ? 0 : Range * 3f; + //LightFx.intensity = BDArmorySettings.LIGHTFX ? 0 : 8f; // Reset light intensity. + if (BDArmorySettings.waterHitEffect && FlightGlobals.currentMainBody.ocean) + { + Vector3 up = VectorUtils.GetUpDirection(Position, out double alt); + if (alt < 0 && (Power > 100f || alt < 2f * Range)) + FXMonger.Splash(Position - up * (float)alt, 20f * Power); + } - LightFx = gameObject.GetComponent(); - LightFx.range = Range * 3f; + audioSource = gameObject.GetComponent(); + // if (ExSound == null) + // { + // ExSound = SoundUtils.GetAudioClip(SoundPath); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + // 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 && BodyUtils.GetRadarAltitudeAtPos(Position) > Caliber / 60) { - Debug.Log("[BDArmory]:Explosion started tntMass: {" + Power + "} BlastRadius: {" + Range + "} StartTime: {" + StartTime + "}, Duration: {" + MaxTime + "}"); + if (FlightGlobals.getAltitudeAtPos(Position) > Caliber / 60) + { + FXEmitter.CreateFX(Position, (Caliber / 30), "BDArmory/Models/explosion/flakSmoke", "", 0.3f, Caliber / 6); + } } + */ } void OnDisable() { foreach (var pe in pEmitters) + { if (pe != null) { pe.emit = false; EffectBehaviour.RemoveParticleEmitter(pe); } + } ExplosivePart = null; // Clear the Part reference. + explosionEvents.Clear(); // Make sure we don't have any left over events leaking memory. + explosionEventsPreProcessing.Clear(); + explosionEventsPartsAdded.Clear(); + explosionEventsBuildingAdded.Clear(); + explosionEventsVesselsHit.Clear(); + totalDamageApplied.Clear(); + totalPartsHit.Clear(); } private void CalculateBlastEvents() { - var temporalEventList = new List(); - - temporalEventList.AddRange(ProcessingBlastSphere()); - //Let's convert this temporal list on a ordered queue - using (var enuEvents = temporalEventList.OrderBy(e => e.TimeToImpact).GetEnumerator()) + // using (var enuEvents = temporalEventList.OrderBy(e => e.TimeToImpact).GetEnumerator()) + using (var enuEvents = ProcessingBlastSphere().OrderBy(e => e.TimeToImpact).GetEnumerator()) { while (enuEvents.MoveNext()) { if (enuEvents.Current == null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_DAMAGE) { - Debug.Log("[BDArmory]: Enqueueing Blast Event"); + Debug.Log("[BDArmory.ExplosionFX]: Enqueueing Blast Event"); } - ExplosionEvents.Enqueue(enuEvents.Current); + explosionEvents.Enqueue(enuEvents.Current); } } } private List ProcessingBlastSphere() { - List result = new List(); - List partsAdded = new List(); - List buildingAdded = new List(); - Dictionary vesselsHitByMissiles = new Dictionary(); + explosionEventsPreProcessing.Clear(); + explosionEventsPartsAdded.Clear(); + explosionEventsBuildingAdded.Clear(); + explosionEventsVesselsHit.Clear(); - using (var hitCollidersEnu = Physics.OverlapSphere(Position, Range, 9076737).AsEnumerable().GetEnumerator()) + SCRange = 0; + if (warheadType == WarheadTypes.ShapedCharge || warheadType == WarheadTypes.Kinetic) // FIXME is this a valid place to handle the Kinetic case? { - while (hitCollidersEnu.MoveNext()) + // 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; + SCRange = 0.12521980673998822299891372544895f * Mathf.Sqrt(penetration - 5f) * Caliber; // Set it to 5 mm of pen instead so that large warheads aren't penalized + + //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. { - if (hitCollidersEnu.Current == null) continue; + 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); - Part partHit = hitCollidersEnu.Current.GetComponentInParent(); + using (var hitsEnu = orderedHits.GetEnumerator()) + { + while (hitsEnu.MoveNext()) + { + RaycastHit SChit = hitsEnu.Current; + Part hitPart = null; - if (partHit != null && partHit.mass > 0 && !partsAdded.Contains(partHit)) + hitPart = SChit.collider.gameObject.GetComponentInParent(); + + if (hitPart != null) + { + if (ProjectileUtils.IsIgnoredPart(hitPart)) continue; // Ignore ignored parts. + if (hitPart.vessel.vesselName == SourceVesselName) continue; //avoid autohit; + if (hitPart.mass > 0 && !explosionEventsPartsAdded.Contains(hitPart)) + { + var damaged = ProcessPartEvent(hitPart, SChit.distance, SourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded, true, Direction, true); + // If the explosion derives from a missile explosion, count the parts damaged for missile hit scores. + if (damaged && hitPart.vessel != null && BDACompetitionMode.Instance) + { + bool registered = false; + var damagedVesselName = hitPart.vessel.GetName(); + 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 (isReportingWeapon || !bulletHitRegistered) + registered = true; + break; + } + if (damagedVesselName != null) + { + if (registered) + explosionEventsVesselsHit[damagedVesselName] = explosionEventsVesselsHit.GetValueOrDefault(damagedVesselName) + 1; + totalPartsHit[damagedVesselName] = totalPartsHit.GetValueOrDefault(damagedVesselName) + 1; // Include non-competition craft (like debris). + } + } + } + } + else + { + if (!BDArmorySettings.PAINTBALL_MODE) + { + DestructibleBuilding building = SChit.collider.gameObject.GetComponentUpwards(); + if (building != null) + { + ProjectileUtils.CheckBuildingHit(SChit, Power * 0.0555f, Direction.normalized * 4000f, 1); + } + } + } + } + } + } + } + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(Position, blastRange, overlapSphereColliders, explosionLayerMask); + if (overlapSphereColliderCount == overlapSphereColliders.Length) + { + overlapSphereColliders = Physics.OverlapSphere(Position, blastRange, explosionLayerMask); + overlapSphereColliderCount = overlapSphereColliders.Length; + } + using (var hitCollidersEnu = overlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) + { + while (hitCollidersEnu.MoveNext()) + { + if (hitCollidersEnu.Current == null) continue; + try { - string sourceVesselName = null; - if (BDACompetitionMode.Instance) + Part partHit = hitCollidersEnu.Current.gameObject.GetComponentInParent(); + if (partHit != null) { - switch (ExplosionSource) + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (ExplosivePart != null && partHit.name == ExplosivePart.name) { - case ExplosionSourceType.Missile: - sourceVesselName = ExplosivePart.FindModuleImplementing()?.sourcevessel.GetName(); - break; - case ExplosionSourceType.Bullet: - sourceVesselName = SourceVesselName; - break; - default: - break; + var partHitExplosivePart = partHit.GetComponent(); + if (partHitExplosivePart != null && SourceVesselTeam == partHitExplosivePart.Team.Name && !string.IsNullOrEmpty(SourceVesselTeam)) 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 && partHit.vessel != null && BDACompetitionMode.Instance) + { + bool registered = false; + var damagedVesselName = partHit.vessel.GetName(); + 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 (isReportingWeapon || !bulletHitRegistered) + registered = true; + break; + case ExplosionSourceType.BattleDamage: + if (BDACompetitionMode.Instance.competitionIsActive) + registered = true; + break; + } + if (registered) + explosionEventsVesselsHit[damagedVesselName] = explosionEventsVesselsHit.GetValueOrDefault(damagedVesselName) + 1; + totalPartsHit[damagedVesselName] = totalPartsHit.GetValueOrDefault(damagedVesselName) + 1; // Include non-competition craft (like debris). + } } } - var damaged = ProcessPartEvent(partHit, sourceVesselName, result, partsAdded); - // If the explosion derives from a missile explosion, count the parts damaged for missile hit scores. - if (damaged && ExplosionSource == ExplosionSourceType.Missile && BDACompetitionMode.Instance) + else { - if (sourceVesselName != null && BDACompetitionMode.Instance.Scores.ContainsKey(sourceVesselName)) // Check that the source vessel is in the competition. + DestructibleBuilding building = hitCollidersEnu.Current.GetComponentInParent(); + + if (building != null) { - var damagedVesselName = partHit.vessel?.GetName(); - if (damagedVesselName != null && damagedVesselName != sourceVesselName && BDACompetitionMode.Instance.Scores.ContainsKey(damagedVesselName)) // Check that the damaged vessel is in the competition and isn't the source vessel. + if (!explosionEventsBuildingAdded.Contains(building)) { - if (BDACompetitionMode.Instance.Scores[damagedVesselName].missilePartDamageCounts.ContainsKey(sourceVesselName)) - ++BDACompetitionMode.Instance.Scores[damagedVesselName].missilePartDamageCounts[sourceVesselName]; - else - BDACompetitionMode.Instance.Scores[damagedVesselName].missilePartDamageCounts[sourceVesselName] = 1; - if (!BDACompetitionMode.Instance.Scores[damagedVesselName].everyoneWhoHitMeWithMissiles.Contains(sourceVesselName)) - BDACompetitionMode.Instance.Scores[damagedVesselName].everyoneWhoHitMeWithMissiles.Add(sourceVesselName); - ++BDACompetitionMode.Instance.Scores[sourceVesselName].totalDamagedPartsDueToMissiles; - BDACompetitionMode.Instance.Scores[damagedVesselName].lastMissileHitTime = Planetarium.GetUniversalTime(); - BDACompetitionMode.Instance.Scores[damagedVesselName].lastPersonWhoHitMeWithAMissile = sourceVesselName; - if (vesselsHitByMissiles.ContainsKey(damagedVesselName)) - ++vesselsHitByMissiles[damagedVesselName]; - else - vesselsHitByMissiles[damagedVesselName] = 1; - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - BDAScoreService.Instance.TrackMissileParts(sourceVesselName, damagedVesselName, 1); + //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); + } + } } } } } - else + catch (Exception e) { - DestructibleBuilding building = hitCollidersEnu.Current.GetComponentInParent(); - - if (building != null && !buildingAdded.Contains(building)) - { - ProcessBuildingEvent(building, result, buildingAdded); - } + Debug.LogError($"[BDArmory.ExplosionFX]: Exception in overlapSphereColliders processing: {e.Message}\n{e.StackTrace}"); } } } - if (vesselsHitByMissiles.Count > 0) + if (explosionEventsVesselsHit.Count > 0) { - string message = ""; - foreach (var vesselName in vesselsHitByMissiles.Keys) - message += (message == "" ? "" : " and ") + vesselName + " had " + vesselsHitByMissiles[vesselName]; - message += " parts damaged due to missile strike."; - 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. + if (!BDArmorySettings.REPORT_DAMAGE_NOT_PARTS_HIT) + { + string message = ""; + foreach (var vesselName in explosionEventsVesselsHit.Keys) + //message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName]; + switch (ExplosionSource) + { + 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: + if (isReportingWeapon) + { + 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 (isReportingWeapon) + { + 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; + case ExplosionSourceType.BattleDamage: + message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName] + " parts damaged from"; + message += SourceWeaponName != null ? SourceWeaponName.Contains("Fuel") ? $" Fuel detonation ({ExplosivePart.partInfo.title})" : $" Ammo explosion({SourceWeaponName})" + (SourceVesselName != null ? $" from {SourceVesselName}" : "") + "." : "part failure."; + break; + } + 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. + { + switch (ExplosionSource) + { + case ExplosionSourceType.Bullet: + if (!bulletHitRegistered) + BDACompetitionMode.Instance.Scores.RegisterBulletHit(SourceVesselName, vesselName); + break; + case ExplosionSourceType.Rocket: + BDACompetitionMode.Instance.Scores.RegisterRocketStrike(SourceVesselName, vesselName); + break; + case ExplosionSourceType.Missile: + BDACompetitionMode.Instance.Scores.RegisterMissileStrike(SourceVesselName, vesselName); + break; + } + } } - return result; + return explosionEventsPreProcessing; } - private void ProcessBuildingEvent(DestructibleBuilding building, List eventList, List bulidingAdded) + private void ProcessBuildingEvent(DestructibleBuilding building, List eventList, List buildingAdded) { Ray ray = new Ray(Position, building.transform.position - Position); RaycastHit rayHit; - if (Physics.Raycast(ray, out rayHit, Range, 557057)) + if (Physics.Raycast(ray, out rayHit, Range, explosionLayerMask)) { //TODO: Maybe we are not hitting building because we are hitting explosive parts. - DestructibleBuilding destructibleBuilding = rayHit.collider.GetComponentInParent(); + DestructibleBuilding destructibleBuilding = rayHit.collider.gameObject.GetComponentUpwards(); // Is not a direct hit, because we are hitting a different part - if (destructibleBuilding != null && destructibleBuilding.Equals(building)) + if (destructibleBuilding != null && destructibleBuilding.Equals(building) && building.IsIntact) { var distance = Vector3.Distance(Position, rayHit.point); eventList.Add(new BuildingBlastHitEvent() { Distance = Vector3.Distance(Position, rayHit.point), Building = building, TimeToImpact = distance / ExplosionVelocity }); - bulidingAdded.Add(building); + buildingAdded.Add(building); + explosionEventsBuildingAdded.Add(building); } } } - private bool ProcessPartEvent(Part part, string sourceVesselName, List eventList, List partsAdded) + private bool ProcessPartEvent(Part part, float hitDist, string sourceVesselName, List eventList, List partsAdded, bool angleOverride = false, Vector3 direction = default, bool directionOverride = false) { RaycastHit hit; - float distance = 0; - if (IsInLineOfSight(part, ExplosivePart, out hit, out distance)) + float distance; + if (IsInLineOfSight(part, ExplosivePart, hitDist, out hit, out distance, direction, directionOverride)) { - if (IsAngleAllowed(Direction, hit)) + //if (IsAngleAllowed(Direction, hit)) + //{ + //Adding damage hit + if (distance <= (blastRange > SCRange ? blastRange : SCRange))//part within total range of shrapnel + blast? { - //Adding damage hit eventList.Add(new PartBlastHitEvent() { Distance = distance, Part = part, TimeToImpact = distance / ExplosionVelocity, HitPoint = hit.point, - SourceVesselName = sourceVesselName + Hit = hit, + SourceVesselName = sourceVesselName, + withinAngleofEffect = angleOverride ? true : IsAngleAllowed(Direction, hit, part), + IntermediateParts = LoSIntermediateParts, // A copy is made internally. + ColliderLocalHitPoint = hit.collider is not null ? hit.collider.transform.InverseTransformPoint(hit.point) : default }); - partsAdded.Add(part); - return true; } + partsAdded.Add(part); + + return true; + //} } return false; } - private bool IsAngleAllowed(Vector3 direction, RaycastHit hit) + private bool IsAngleAllowed(Vector3 direction, RaycastHit hit, Part p) { - if (ExplosionSource == ExplosionSourceType.Missile || direction == default(Vector3)) + if (direction == default(Vector3)) { + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.ExplosionFX]: Default Direction param! " + p.name + " angle from explosion dir irrelevant!"); return true; } - - return Vector3.Angle(direction, (hit.point - Position).normalized) < 100f; + if (warheadType == WarheadTypes.ContinuousRod) + { + float dotProduct = Vector3.Dot(direction, (hit.point - Position).normalized); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ExplosionFX]: {p.name} at {Mathf.Acos(dotProduct)} angle from CR explosion direction"); + //if (VectorUtils.Angle(direction, (hit.point - Position).normalized) >= 60 && VectorUtils.Angle(direction, (hit.point - Position).normalized) <= 90) + // 30-60° AoE instead of 60-90° + if (dotProduct <= 0.866025388240814208984375f && dotProduct >= 0.5) + { + return true; + } + else return false; + } + else + { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ExplosionFX]: {p.name} at {VectorUtils.Angle(direction, (hit.point - Position).normalized)} angle from {warheadType} explosion direction"); + return (Vector3.Dot(direction, (hit.point - Position).normalized) >= cosAngleOfEffect); + } } /// /// This method will calculate if there is valid line of sight between the explosion origin and the specific Part - /// In order to avoid collisions with the same missile part, It will not take into account those parts beloging to same vessel that contains the explosive part + /// In order to avoid collisions with the same missile part, It will not take into account those parts belonging to same vessel that contains the explosive part /// /// /// - /// 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) + private bool IsInLineOfSight(Part part, Part explosivePart, float startDist, out RaycastHit hit, out float distance, Vector3 direction = default, bool directionOverride = false, bool intermediateParts = true) { - Ray partRay = new Ray(Position, part.transform.position - Position); + Ray partRay; + float range = blastRange > SCRange ? blastRange : SCRange; + if (directionOverride) + { + partRay = new Ray(Position, direction); + } + else + { + var partPosition = part.transform.position; //transition over to part.Collider.ClosestPoint(Position);? Test later + partRay = new Ray(Position, partPosition - Position); + } - var hits = Physics.RaycastAll(partRay, Range, 9076737).AsEnumerable(); - using (var hitsEnu = hits.OrderBy(x => x.distance).GetEnumerator()) + + 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, range, explosionLayerMask); + hitCount = lineOfSightHits.Length; + } + //check if explosion is originating inside a part + 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(reverseRay, range, explosionLayerMask); + reverseHitCount = reverseHits.Length; + } + for (int i = 0; i < reverseHitCount; ++i) { - while (hitsEnu.MoveNext()) + reverseHits[i].distance = range - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } + + LoSIntermediateParts.Clear(); + _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.Clamp(hit.distance, 0.05f, startDist); //in case of (large) multi-collider parts where ProcessPartHit is grabbing a further collider (and thus startDist) than LoS dist due to Physics.overlapSphere not being sorted + //} + //if (startDist < 0) distance = hit.distance; + + if (partHit == part) { - Part partHit = hitsEnu.Current.collider.GetComponentInParent(); - if (partHit == null) continue; - hit = hitsEnu.Current; - distance = Vector3.Distance(Position, hit.point); - if (partHit == part) + return true; + } + 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 - if (explosivePart != null && partHit.vessel == explosivePart.vessel) + var partHP = partHit.Damage(); + if (ProjectileUtils.IsArmorPart(partHit)) partHP = 100f; + //var partArmour = partHit.GetArmorThickness(); + float partArmour = 0f; + var Armor = partHit.FindModuleImplementing(); + if (Armor != null && partHit.Rigidbody != null) { - continue; + Vector3 correctedDirection = hit.point + partHit.Rigidbody.velocity * TimeIndex - Position; + float armorCos = Mathf.Abs(Vector3.Dot(correctedDirection.sqrMagnitude < 1E-10f ? partRay.direction : correctedDirection.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; + + var RA = partHit.FindModuleImplementing(); + if (RA != null) + { + if (RA.NXRA) + { + partArmour *= RA.armorModifier; + } + else + { + 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 * Mathf.Clamp(0.405f * Mathf.Tan(Mathf.Acos(armorCos)), 0f, 2f); + // Complex models for ERA interactions with HEAT would be far too excessive given the amount of times + // this code is being used so a simple tan based model inspired by "Stopping Power of Explosive Reactive Armours + // Against Different Shaped Charge Diameters or at Different Angles" and "Momentum Theory of Explosive Reactive Armours" + // by Manfred Held was used. + } + } + } } - // if there are parts in between but we still inside the critical sphere of damage. - if (distance <= 0.1f * Range) + if (partHP > 0 && !_LoSIntermediateParts.Contains(partHit)) // Ignore parts that are already dead but not yet removed from the game or have already been added. { - continue; + LoSIntermediateParts.Add((hit.distance, partHP, partArmour)); + _LoSIntermediateParts.Add(partHit); } - - return false; } } } - 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) { - if (!gameObject.activeInHierarchy) return; + 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 (!HighLogic.LoadedSceneIsFlight || !gameObject.activeInHierarchy) return; - if (LightFx != null) LightFx.intensity -= 12 * Time.deltaTime; + if (LightFx != null && BDArmorySettings.LightFX) LightFx.intensity -= 12 * Time.deltaTime; if (!disabled && TimeIndex > 0.3f && pEmitters != null) // 0.3s seems to be enough to always show the explosion, but 0.2s isn't for some reason. { @@ -311,206 +720,549 @@ public void Update() public void FixedUpdate() { - if (!gameObject.activeInHierarchy) return; + if (!HighLogic.LoadedSceneIsFlight || !gameObject.activeInHierarchy) return; - //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (UI.BDArmorySetup.GameIsPaused) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; } - while (ExplosionEvents.Count > 0 && ExplosionEvents.Peek().TimeToImpact <= TimeIndex) + //floating origin and velocity offloading corrections + if (BDKrakensbane.IsActive) { - BlastHitEvent eventToExecute = ExplosionEvents.Dequeue(); + Position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + { // Explosion centre velocity depends on atmospheric density relative to Kerbin sea level. + var atmDensity = (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(Position), FlightGlobals.getExternalTemperature(Position)); + Velocity /= 1 + atmDensity / KerbinSeaLevelAtmDensity; + Position += Velocity * TimeWarp.fixedDeltaTime; // Krakensbane is already accounted for above. + } - var partBlastHitEvent = eventToExecute as PartBlastHitEvent; - if (partBlastHitEvent != null) + if (!isFX) + { + while (explosionEvents.Count > 0 && explosionEvents.Peek().TimeToImpact <= TimeIndex) { - ExecutePartBlastEvent(partBlastHitEvent); + BlastHitEvent eventToExecute = explosionEvents.Dequeue(); + + var partBlastHitEvent = eventToExecute as PartBlastHitEvent; + if (partBlastHitEvent != null) + { + ExecutePartBlastEvent(partBlastHitEvent); + } + else + { + ExecuteBuildingBlastEvent((BuildingBlastHitEvent)eventToExecute); + } } - else + } + + // Do a separate check here for events being empty so we can report asap. + if (BDArmorySettings.REPORT_DAMAGE_NOT_PARTS_HIT && isReportingWeapon && explosionEvents.Count == 0 && totalDamageApplied.Count > 0) + { + // Debug.Log($"DEBUG dmg: {string.Join(", ", totalDamageApplied.Select(kvp => $"{kvp.Key}:{kvp.Value:0}"))}, parts: {string.Join(", ", totalPartsHit.Select(kvp => $"{kvp.Key}:{kvp.Value}"))}"); + List debrisNames = ["Debris", "Probe"]; + debrisNames.AddRange(VesselModuleRegistry.ValidVesselTypes.Select(t => t.ToString())); + foreach (var vesselName in totalDamageApplied.Keys.ToList()) // Merge debris and vessel hits. Note: if only debris is hit, they won't get merged — it's dead, Jim! { - ExecuteBuildingBlastEvent((BuildingBlastHitEvent)eventToExecute); + foreach (var debrisName in debrisNames.Select(name => $"{vesselName} {name}")) + { + if (totalDamageApplied.ContainsKey(debrisName) || totalPartsHit.ContainsKey(debrisName)) + { + totalDamageApplied[vesselName] = totalDamageApplied.GetValueOrDefault(vesselName) + totalDamageApplied.GetValueOrDefault(debrisName); + totalDamageApplied.Remove(debrisName); + totalPartsHit[vesselName] = totalPartsHit.GetValueOrDefault(vesselName) + totalPartsHit.GetValueOrDefault(debrisName); + totalPartsHit.Remove(debrisName); + } + } } + string message = $"{SourceVesselName} damaged {string.Join(" and ", totalDamageApplied.Select(kvp => $"{kvp.Key} for {kvp.Value:0} ({totalPartsHit.GetValueOrDefault(kvp.Key)} parts)"))} with a {( + ExplosionSource switch + { + ExplosionSourceType.Bullet => "shell", + ExplosionSourceType.Rocket => "rocket", + _ => SourceWeaponName + } + )}{(travelDistance > 0 ? $" at {travelDistance:F3}m" : "")}."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.ExplosionFX]: {message}"); + totalDamageApplied.Clear(); + totalPartsHit.Clear(); } - if (disabled && ExplosionEvents.Count == 0 && TimeIndex > MaxTime) + if (disabled && explosionEvents.Count == 0 && TimeIndex > MaxTime) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_DAMAGE) { - Debug.Log("[BDArmory]:Explosion Finished"); + Debug.Log("[BDArmory.ExplosionFX]: Explosion Finished"); } gameObject.SetActive(false); 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.DEBUG_LINES) + { + if (warheadType == WarheadTypes.ContinuousRod) + { + if (explosionEventsPartsAdded.Count > 0) + { + RaycastHit hit; + float distance; + for (int i = 0; i < explosionEventsPartsAdded.Count; i++) + { + try + { + Part part = explosionEventsPartsAdded[i]; + if (IsInLineOfSight(part, null, -1, out hit, out distance, false)) + { + if (IsAngleAllowed(Direction, hit, explosionEventsPartsAdded[i])) + { + GUIUtils.DrawLineBetweenWorldPositions(Position, hit.point, 2, Color.blue); + } + else if (distance < Range / 2) + { + GUIUtils.DrawLineBetweenWorldPositions(Position, hit.point, 2, Color.red); + } + } + } + catch + { + Debug.Log("[BDArmory.ExplosioNFX] nullref in ContinuousRod Debug lines in onGUI"); + } + } + } + } + if (warheadType == WarheadTypes.ShapedCharge) + { + 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]: 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; } - try + 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.Kinetic || warheadType == WarheadTypes.ContinuousRod) && eventToExecute.withinAngleofEffect; + string vesselHit = part.vessel.GetName(); + + if (BDArmorySettings.DEBUG_WEAPONS && shapedEffect) { - Part part = eventToExecute.Part; - Rigidbody rb = part.Rigidbody; - var realDistance = eventToExecute.Distance; + Debug.Log($"[BDArmory.ExplosionFX] Part: {part.name}; Real Distance: {realDistance}m; SCRange: {SCRange}m;"); + } + if ((realDistance <= Range) || (realDistance <= SCRange)) //within radius of Blast + { if (!eventToExecute.IsNegativePressure) { - BlastInfo blastInfo = - BlastPhysicsUtils.CalculatePartBlastEffects(part, realDistance, - part.vessel.totalMass * 1000f, Power, Range); + BlastInfo blastInfo; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (eventToExecute.withinAngleofEffect) //within AoE of shaped warheads, or otherwise standard blast { - Debug.Log( - "[BDArmory]: Executing blast event Part: {" + part.name + "}, " + - " VelocityChange: {" + blastInfo.VelocityChange + "}," + - " Distance: {" + realDistance + "}," + - " TotalPressure: {" + blastInfo.TotalPressure + "}," + - " Damage: {" + blastInfo.Damage + "}," + - " EffectiveArea: {" + blastInfo.EffectivePartArea + "}," + - " Positive Phase duration: {" + blastInfo.PositivePhaseDuration + "}," + - " Vessel mass: {" + Math.Round(part.vessel.totalMass * 1000f) + "}," + - " TimeIndex: {" + TimeIndex + "}," + - " TimePlanned: {" + eventToExecute.TimeToImpact + "}," + - " NegativePressure: {" + eventToExecute.IsNegativePressure + "}"); + blastInfo = BlastPhysicsUtils.CalculatePartBlastEffects(part, realDistance, vesselMass * 1000f, Power, Range); + } + else //majority of force concentrated in blast AoE for shaped warheads, not going to apply much force to stuff outside + { + 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. + + float cumulativeHPOfIntermediateParts = 0f; + float cumulativeArmorOfIntermediateParts = 0f; - // Add Reverse Negative Event - ExplosionEvents.Enqueue(new PartBlastHitEvent() + if (eventToExecute.IntermediateParts.Count > 0) { - Distance = Range - realDistance, - Part = part, - TimeToImpact = 2 * (Range / ExplosionVelocity) + (Range - realDistance) / ExplosionVelocity, - IsNegativePressure = true, - NegativeForce = blastInfo.VelocityChange * 0.25f - }); + cumulativeHPOfIntermediateParts = eventToExecute.IntermediateParts.Select(p => p.Item2).Sum(); + cumulativeArmorOfIntermediateParts = eventToExecute.IntermediateParts.Select(p => p.Item3).Sum(); + } - AddForceAtPosition(rb, - (eventToExecute.HitPoint + part.rb.velocity * TimeIndex - Position).normalized * - blastInfo.VelocityChange * - BDArmorySettings.EXP_IMP_MOD, - eventToExecute.HitPoint + part.rb.velocity * TimeIndex); + float damageWithoutIntermediateParts = blastInfo.Damage; + float dmgModifier = PartExtensions.ExplosiveDamageModifier(ExplosionSource, dmgMult); // Scale the HP and Armour by the appropriate modifier for how the damage will be applied. + blastInfo.Damage = dmgModifier > 0f ? Mathf.Max(0f, blastInfo.Damage - (0.2f * cumulativeHPOfIntermediateParts) / dmgModifier - 10f * BDArmorySettings.EXP_PEN_RESIST_MULT * cumulativeArmorOfIntermediateParts) : 0f; - var damage = part.AddExplosiveDamage(blastInfo.Damage, Caliber, ExplosionSource); - // Debug.Log("DEBUG Explosive damage to " + part + ": " + damage + ", calibre: " + Caliber + ", source: " + ExplosionSource); + if (CASEClamp > 0) + { + 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); + } + } - // Update scoring structures - switch (ExplosionSource) + if (blastInfo.Damage > 0 || shapedEffect) { - case ExplosionSourceType.Bullet: - case ExplosionSourceType.Missile: - var aName = eventToExecute.SourceVesselName; // Attacker - var tName = part.vessel.GetName(); // Target - if (aName != tName && BDACompetitionMode.Instance.Scores.ContainsKey(tName) && BDACompetitionMode.Instance.Scores.ContainsKey(aName)) + if (BDArmorySettings.DEBUG_DAMAGE) + { + Debug.Log($"[BDArmory.ExplosionFX]: Executing blast event Part: [{part.name}], VelocityChange: [{blastInfo.VelocityChange}], Distance: [{realDistance}], TotalPressure: [{blastInfo.TotalPressure}], Damage: [{blastInfo.Damage * dmgModifier}] (reduced from {damageWithoutIntermediateParts * dmgModifier} by {eventToExecute.IntermediateParts.Count} parts) (modifier: {dmgModifier}), EffectiveArea: [{blastInfo.EffectivePartArea}], Positive Phase duration: [{blastInfo.PositivePhaseDuration}], Vessel mass: [{Math.Round(vesselMass * 1000f)}], TimeIndex: [{TimeIndex}], TimePlanned: [{eventToExecute.TimeToImpact}], NegativePressure: [{eventToExecute.IsNegativePressure}]"); + } + + // 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!"); + } + totalDamageApplied[vesselHit] = totalDamageApplied.GetValueOrDefault(vesselHit); // Initialise damage to 0 so it still gets reported even if no damage gets through. + + 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 (shapedEffect && ((warheadType == WarheadTypes.ShapedCharge || warheadType == WarheadTypes.Kinetic) ? (realDistance <= SCRange) : warheadType == WarheadTypes.ContinuousRod)) { - var tData = BDACompetitionMode.Instance.Scores[tName]; - // Track damage - switch (ExplosionSource) + //float HitAngle = VectorUtils.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: {cumulativeArmorOfIntermediateParts}"); + //float penetration = 0; + + float remainingPen = penetration; + float standoffTemp = 0f; + float standoffFactor = 1f; + + if (warheadType == WarheadTypes.ShapedCharge) { - case ExplosionSourceType.Bullet: - if (tData.damageFromBullets.ContainsKey(aName)) - tData.damageFromBullets[aName] += damage; - else - tData.damageFromBullets.Add(aName, damage); - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - BDAScoreService.Instance.TrackDamage(aName, tName, damage); - break; - case ExplosionSourceType.Missile: - if (tData.damageFromMissiles.ContainsKey(aName)) - tData.damageFromMissiles[aName] += damage; + standoffTemp = realDistance / (14f * Caliber * 20f * 0.001f); // Unfocused jet formula after armor penetration begins, focused jet prior + if (cumulativeArmorOfIntermediateParts > 0f) + { + float kOffset = 0f; + float EPrev = 14f; + float ECurr = 14f; + + for (int ii = 0; ii < eventToExecute.IntermediateParts.Count; ii++) + { + ECurr = EPrev - 6f * 1.25f * eventToExecute.IntermediateParts[ii].Item3 / penetration; + if (ECurr < 8f) + ECurr = 8f; + kOffset = (EPrev - ECurr) * (eventToExecute.IntermediateParts[ii].Item1 - kOffset) / EPrev + kOffset; + if (ECurr == 8f) + break; + EPrev = ECurr; + } + standoffTemp = (realDistance - kOffset) / (ECurr * Caliber * 20f * 0.001f); + } + // standoffTemp = (realDistance - 3f / 7f * eventToExecute.IntermediateParts[0].Item1) / (8f * Caliber * 20f * 0.001f); + + standoffFactor = 1f / (1f + standoffTemp * standoffTemp); + + remainingPen *= standoffFactor; + } + + remainingPen -= cumulativeArmorOfIntermediateParts; + + 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; + 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) + { + if (eventToExecute.IntermediateParts.Count > 0) + Debug.Log($"[BDArmory.ExplosionFX] Part: {part.name}; Distance: {realDistance}; StandoffTemp: {standoffTemp}; Distance From First Pen: {eventToExecute.IntermediateParts[0].Item1}m; SCRange: {SCRange}m;"); else - tData.damageFromMissiles.Add(aName, damage); - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - BDAScoreService.Instance.TrackMissileDamage(aName, tName, damage); - break; - default: - break; + Debug.Log($"[BDArmory.ExplosionFX] Part: {part.name}; Distance: {realDistance}; StandoffTemp: {standoffTemp}; Distance From First Pen: 0m; SCRange: {SCRange}m;"); + Debug.Log($"[BDArmory.ExplosionFX] Penetration: {penetration} mm; Thickness: {thickness * armorEquiv} mm; armorEquiv: {armorEquiv}; Intermediate Armor: {cumulativeArmorOfIntermediateParts} 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 switch + { + WarheadTypes.ShapedCharge => 5000f, + WarheadTypes.Kinetic => ImpactSpeed, + _ => ExplosionVelocity + }, + SourceVesselName, ExplosionSource, type); + } + } + else + { + // 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, SourceVesselTeam, eventToExecute.ColliderLocalHitPoint); + damage = part.AddBallisticDamage(warheadType == WarheadTypes.ShapedCharge ? Power * 0.0555f : ProjMass, Caliber, 1f, penetrationFactor, dmgMult, + warheadType switch + { + WarheadTypes.ShapedCharge => 5000f, + WarheadTypes.Kinetic => ImpactSpeed, + _ => ExplosionVelocity //technically this should be the sum vector of the explosion vel (perpendicular to missile vel), and missile vel since the rods are physical projectiles that would be inheriting their parent's vel + }, + ExplosionSource); + totalDamageApplied[vesselHit] += damage; + } + + if (penetrationFactor > 1 && warheadType != WarheadTypes.Kinetic) + { + if (blastInfo.Damage > 0) + { + damage += part.AddExplosiveDamage(shapedEffect ? (0.2f * blastInfo.Damage + 0.8f * damageWithoutIntermediateParts) : blastInfo.Damage, Caliber, ExplosionSource, dmgMult); + totalDamageApplied[vesselHit] += damage; + } + + if (float.IsNaN(damage)) Debug.LogError("DEBUG NaN damage!"); } } - break; - default: - break; + else + { + 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); + totalDamageApplied[vesselHit] += damage; + 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 / 10f; //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!"); + } + } + } + if (damage > 0) //else damage from spalling done in CalcExplArmorDamage + { + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(part, Caliber, penetrationFactor, true, warheadType == WarheadTypes.ShapedCharge ? true : false, SourceVesselName, eventToExecute.Hit, colliderLocalHitPoint: eventToExecute.ColliderLocalHitPoint); + } + // Update scoring structures + //damage = Mathf.Clamp(damage, 0, part.Damage()); //if we want to clamp overkill score inflation + ProjectileUtils.ApplyScore(part, eventToExecute.SourceVesselName, 0, damage, null, ExplosionSource); + } + } + } + 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) + if (BDArmorySettings.DEBUG_DAMAGE) { Debug.Log( - "[BDArmory]: Executing blast event Part: {" + part.name + "}, " + - " VelocityChange: {" + eventToExecute.NegativeForce + "}," + - " Distance: {" + realDistance + "}," + - " Vessel mass: {" + Math.Round(part.vessel.totalMass * 1000f) + "}," + - " TimeIndex: {" + TimeIndex + "}," + - " TimePlanned: {" + eventToExecute.TimeToImpact + "}," + - " NegativePressure: {" + eventToExecute.IsNegativePressure + "}"); + $"[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}]"); } - AddForceAtPosition(rb, (Position - part.transform.position).normalized * eventToExecute.NegativeForce * BDArmorySettings.EXP_IMP_MOD * 0.25f, part.transform.position); + 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); } } - catch + if (warheadType == WarheadTypes.Standard && ProjMass > 0 && realDistance <= blastRange) //check shrapnel damage of stuff in shrapnel range { - // ignored due to depending on previous event an object could be disposed + 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) + { + thickness *= Armor.HEEquiv; + } + 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 } + eventToExecute.Finished(); } // 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); - var soundClip = GameDatabase.Instance.GetAudioClip(soundPath); + if (explosionFXTemplate == null) + { + Debug.LogError("[BDArmory.ExplosionFX]: " + explModelPath + " was not found, using the default explosion instead. Please fix your model."); + explosionFXTemplate = GameDatabase.Instance.GetModel(ModuleWeapon.defaultExplModelPath); + } 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 = Misc.Misc.ParseColor255("255,238,184,255"); - eFx.LightFx.intensity = 8; - eFx.LightFx.shadows = LightShadows.None; + if (BDArmorySettings.LightFX) //comment out if check if !LIGHTFX = light range/intensity remains 0 + { + eFx.LightFx = explosionFXTemplate.AddComponent(); + eFx.LightFx.color = GUIUtils.ParseColor255("255,238,184,255"); + eFx.LightFx.intensity = 8; + eFx.LightFx.shadows = LightShadows.None; + } + else + { + Light[] bakedLights = explosionFXTemplate.GetComponentsInChildren(); //remove any Light components intrinsic to the Model + foreach (var bL in bakedLights) + if (bL != null) + { + Destroy(bL); + } + } 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 = 0, Part explosivePart = null, string sourceVesselName = null, Vector3 direction = default(Vector3)) + public static void CreateExplosion(Vector3 position, float tntMassEquivalent, string explModelPath, string soundPath, ExplosionSourceType explosionSourceType, + float caliber = 120, Part explosivePart = null, string sourceVesselName = null, string sourceVesselTeam = null, string sourceWeaponName = null, Vector3 direction = default, + float angle = 100f, bool isfx = false, float projectilemass = 0, float caseLimiter = -1, float dmgMutator = 1, WarheadTypes warheadType = WarheadTypes.Standard, Part Hitpart = null, + float apMod = 1f, float distancetravelled = -1, Vector3 sourceVelocity = default, bool bulletHitRegistered = true) { + 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; @@ -521,21 +1273,104 @@ static void CreateObjectPool(string explModelPath, string soundPath) else { rotation = Quaternion.LookRotation(direction); + if (warheadType == WarheadTypes.ShapedCharge) + { + direction = direction.normalized; + position = position - direction * 0.05f; + } } - 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); eFx.Position = position; eFx.Power = tntMassEquivalent; eFx.ExplosionSource = explosionSourceType; - eFx.SourceVesselName = sourceVesselName != null ? sourceVesselName : explosionSourceType == ExplosionSourceType.Missile ? explosivePart?.vessel.GetName() : null; // Use the sourceVesselName if specified, otherwise get the sourceVesselName from the missile if it is one. + eFx.SourceVesselName = !string.IsNullOrEmpty(sourceVesselName) ? sourceVesselName : explosionSourceType == ExplosionSourceType.Missile ? (explosivePart != null && explosivePart.vessel != null ? explosivePart.vessel.GetName() : null) : null; // Use the sourceVesselName if specified, otherwise get the sourceVesselName from the missile if it is one. + eFx.SourceVesselTeam = sourceVesselTeam; + eFx.SourceWeaponName = sourceWeaponName; eFx.Caliber = caliber; eFx.ExplosivePart = explosivePart; eFx.Direction = direction; + sourceVelocity = sourceVelocity != default ? sourceVelocity : (explosivePart != null && explosivePart.rb != null) ? explosivePart.rb.velocity + BDKrakensbane.FrameVelocityV3f : default; // Use the explosive part's velocity if the sourceVelocity isn't specified. + eFx.Velocity = Hitpart != null ? Hitpart.vessel.Velocity() : sourceVelocity; // sourceVelocity is the real velocity w/o offloading. + eFx.isFX = isfx; + eFx.ProjMass = projectilemass; + eFx.CASEClamp = caseLimiter; + eFx.dmgMult = dmgMutator; + eFx.projectileHitPart = Hitpart; eFx.pEmitters = newExplosion.GetComponentsInChildren(); eFx.audioSource = newExplosion.GetComponent(); + eFx.SoundPath = soundPath; + eFx.warheadType = warheadType; + switch (eFx.warheadType) + { + case WarheadTypes.ContinuousRod: + //eFx.AngleOfEffect = 165; + eFx.Caliber = caliber > 0 ? caliber / 4 : 30; + eFx.ProjMass = 0.3f + (tntMassEquivalent / 75); + break; + case WarheadTypes.ShapedCharge: + //eFx.AngleOfEffect = 10f; + //eFx.AngleOfEffect = 5f; + eFx.cosAngleOfEffect = BDArmorySettings.HEAT_CONE_HALF_ANGLE > 0f ? Mathf.Cos(Mathf.Deg2Rad * BDArmorySettings.HEAT_CONE_HALF_ANGLE) : 2f; // 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; + break; + case WarheadTypes.Kinetic: + eFx.cosAngleOfEffect = Mathf.Cos(Mathf.Deg2Rad * 45f); // cos(45 degrees) + eFx.Caliber = caliber; + eFx.apMod = apMod; + eFx.ImpactSpeed = (sourceVelocity - (Hitpart != null ? Hitpart.vessel.Velocity() : Vector3.zero)).magnitude; + break; + case WarheadTypes.Standard: + eFx.cosAngleOfEffect = angle >= 0f ? Mathf.Clamp(angle, 0f, 180f) : 100f; + eFx.cosAngleOfEffect = Mathf.Cos(Mathf.Deg2Rad * eFx.cosAngleOfEffect); + break; + default: + Debug.LogError($"[BDArmory.ExplosionFX]: Unhandled warheadType {eFx.warheadType}, defaulting to {WarheadTypes.Standard}."); + goto case WarheadTypes.Standard; + } + eFx.isReportingWeapon = explosionSourceType == ExplosionSourceType.Missile || distancetravelled > 0; + eFx.bulletHitRegistered = bulletHitRegistered; + eFx.travelDistance = distancetravelled; // Used for reporting weapons. + + switch (eFx.warheadType) + { + case WarheadTypes.ShapedCharge: + case WarheadTypes.ContinuousRod: + eFx.penetration = ProjectileUtils.CalculatePenetration(eFx.Caliber, eFx.warheadType == WarheadTypes.ShapedCharge ? 5000f : ExplosionVelocity, eFx.warheadType == WarheadTypes.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. + break; + case WarheadTypes.Kinetic: + eFx.penetration = ProjectileUtils.CalculatePenetration(eFx.Caliber, sourceVelocity.magnitude, eFx.ProjMass, apMod); + break; + default: + eFx.penetration = 0; + break; + } + + + if (direction == default(Vector3) && explosionSourceType == ExplosionSourceType.Missile) + { + eFx.warheadType = WarheadTypes.Standard; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ExplosionFX]: No direction param specified, defaulting warhead type!"); + } if (tntMassEquivalent <= 5) { eFx.audioSource.minDistance = 4f; @@ -550,11 +1385,26 @@ public static void AddForceAtPosition(Rigidbody rb, Vector3 force, Vector3 posit ////////////////////////////////////////////////////////// // Add The force to part ////////////////////////////////////////////////////////// - if (rb == null) return; + 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]: Force Applied | Explosive : " + Math.Round(force.magnitude, 2)); + Debug.Log($"[BDArmory.ExplosionFX]: Force Applied | Explosive : {Math.Round(force.magnitude, 2)}"); + } + } + + public static void DisableAllExplosionFX() + { + if (explosionFXPools == null) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ExplosionFx]: Setting {explosionFXPools.Values.Where(pool => pool != null && pool.pool != null).Sum(pool => pool.pool.Count(fx => fx != null && fx.activeInHierarchy))} explosion FX inactive."); + foreach (var pool in explosionFXPools.Values) + { + if (pool == null || pool.pool == null) continue; + foreach (var fx in pool.pool) + { + if (fx == null) continue; + fx.SetActive(false); + } } } } @@ -570,10 +1420,46 @@ internal class PartBlastHitEvent : BlastHitEvent { public Part Part { get; set; } public Vector3 HitPoint { get; set; } + public Vector3 ColliderLocalHitPoint { get; set; } = default; + public RaycastHit Hit { get; set; } public float NegativeForce { get; set; } public string SourceVesselName { get; set; } + 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 = []; + _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(); // Pool the IntermediateParts lists to avoid GC alloc. } + internal class BuildingBlastHitEvent : BlastHitEvent { public DestructibleBuilding Building { get; set; } diff --git a/BDArmory/FX/FXEmitter.cs b/BDArmory/FX/FXEmitter.cs new file mode 100644 index 000000000..6fc68d647 --- /dev/null +++ b/BDArmory/FX/FXEmitter.cs @@ -0,0 +1,242 @@ +using System.Linq; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Utils; +using BDArmory.Weapons; +using System.Collections; +using BDArmory.Settings; + +namespace BDArmory.FX +{ + class FXEmitter : MonoBehaviour + { + public static Dictionary FXPools = new Dictionary(); + public KSPParticleEmitter[] pEmitters { get; set; } + 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; } + private bool overrideLifeTime { get; set; } + public Vector3 Position { get { return _position; } set { _position = value; transform.position = _position; } } + Vector3 _position; + public Vector3 Direction { get; set; } + public float TimeIndex => Time.time - StartTime; + + private bool disabled = true; + public static string defaultModelPath = "BDArmory/Models/explosion/explosion"; + public static string defaultSoundPath = "BDArmory/Sounds/explode1"; + private float particlesMaxEnergy; + private float maxEnergy; + private void OnEnable() + { + StartTime = Time.time; + disabled = false; + + pEmitters = gameObject.GetComponentsInChildren(); + foreach (var pe in pEmitters) + if (pe != null) + { + pe.maxSize *= Power; + pe.maxParticleSize *= Power; + pe.minSize *= Power; + if (maxTime > 0) + { + maxEnergy = pe.maxEnergy; + pe.maxEnergy = maxTime; + pe.minEnergy = maxTime * .66f; + } + if (pe.maxEnergy > particlesMaxEnergy) + particlesMaxEnergy = pe.maxEnergy; + pe.emit = true; + var emission = pe.ps.emission; + 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(Position, FlightGlobals.ActiveVessel.CoM) / 343f)); + } + } + + void OnDisable() + { + foreach (var pe in pEmitters) + { + if (pe != null) + { + pe.maxSize /= Power; + pe.maxParticleSize /= Power; + pe.minSize /= Power; + if (maxTime > 0) + { + pe.maxEnergy = maxEnergy; + pe.minEnergy = maxEnergy * .66f; + } + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + } + } + + public void Update() + { + if (!gameObject.activeInHierarchy) return; + + if (!disabled && TimeIndex > emitTime && pEmitters != null) + { + if (!overrideLifeTime) + { + foreach (var pe in pEmitters) + { + if (pe == null) continue; + pe.emit = false; + } + } + disabled = true; + } + } + + public void FixedUpdate() + { + if (!gameObject.activeInHierarchy) return; + + if (UI.BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + + if (BDKrakensbane.IsActive) + { + Position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + + if ((disabled || overrideLifeTime) && TimeIndex > particlesMaxEnergy) + { + 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; + if (!FXPools.ContainsKey(key) || FXPools[key] == null) + { + var FXTemplate = GameDatabase.Instance.GetModel(ModelPath); + if (FXTemplate == null) + { + Debug.LogError("[BDArmory.FXBase]: " + ModelPath + " was not found, using the default model instead. Please fix your model."); + FXTemplate = GameDatabase.Instance.GetModel(defaultModelPath); + } + var eFx = FXTemplate.AddComponent(); + if (!string.IsNullOrEmpty(soundPath)) + { + 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); + } + } + + public static FXEmitter CreateFX(Vector3 position, float scale, string ModelPath, string soundPath, float time = 0.3f, float lifeTime = -1, Vector3 direction = default(Vector3), bool scaleEmitter = false, bool fixedLifetime = false) + { + CreateObjectPool(ModelPath, soundPath); + + Quaternion rotation; + if (direction == default(Vector3)) + { + rotation = Quaternion.LookRotation(VectorUtils.GetUpDirection(position)); + } + else + { + rotation = Quaternion.LookRotation(direction); + } + + GameObject newFX = FXPools[ModelPath + soundPath].GetPooledObject(); + newFX.transform.SetPositionAndRotation(position, rotation); + if (scaleEmitter) + { + newFX.transform.localScale = Vector3.one; + newFX.transform.localScale *= scale; + } + //Debug.Log("[FXEmitter] start scale: " + newFX.transform.localScale); + FXEmitter eFx = newFX.GetComponent(); + + eFx.Position = position; + eFx.Power = scale; + eFx.emitTime = time; + eFx.maxTime = lifeTime; + eFx.overrideLifeTime = fixedLifetime; + eFx.pEmitters = newFX.GetComponentsInChildren(); + if (!string.IsNullOrEmpty(soundPath)) + { + eFx.audioSource = newFX.GetComponent(); + if (scale > 3) + { + eFx.audioSource.minDistance = 4f; + eFx.audioSource.maxDistance = 3000; + eFx.audioSource.priority = 9999; + } + eFx.SoundPath = soundPath; + } + newFX.SetActive(true); + return eFx; + } + + public static void DisableAllFX() + { + if (FXPools != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FXEmitter]: Setting {FXPools.Values.Where(pool => pool != null && pool.pool != null).Sum(pool => pool.pool.Count(fx => fx != null && fx.activeInHierarchy))} FXEmitter FX inactive."); + foreach (var pool in FXPools.Values) + { + if (pool == null || pool.pool == null) continue; + foreach (var fx in pool.pool) + { + if (fx == null) continue; + fx.SetActive(false); + } + } + } + } + } +} diff --git a/BDArmory/FX/FireFX.cs b/BDArmory/FX/FireFX.cs new file mode 100644 index 000000000..72fa9cd1c --- /dev/null +++ b/BDArmory/FX/FireFX.cs @@ -0,0 +1,597 @@ +using System; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.Modules; +using BDArmory.Settings; +using BDArmory.UI; +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); + var decal = template.AddComponent(); + template.SetActive(false); + return ObjectPool.CreateObjectPool(template, 10, true, true); + } + + private float disableTime = -1; + private float _highestEnergy = 1; + public float burnTime = -1; + private float burnScale = -1; + private float startTime; + public bool hasFuel = true; + public float burnRate = 1; + private float fireIntensity = 0; + private float tntMassEquivalent = 0; + public bool surfaceFire = false; + private bool isSRB = false; + public string SourceVessel; + private string explModelPath = "BDArmory/Models/explosion/explosion"; + private string explSoundPath = "BDArmory/Sounds/explode1"; + 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; + PartResource solid; + PartResource ox; + PartResource ec; + PartResource mp; + + private KerbalSeat Seat; + ModuleEngines engine; + // bool lookedForEngine = false; + + KSPParticleEmitter[] pEmitters; + + Collider[] blastHitColliders = new Collider[100]; + bool vacuum = false; + void OnEnable() + { + if (parentPart == null || !HighLogic.LoadedSceneIsFlight) + { + gameObject.SetActive(false); + return; + } + 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; + engine = parentPart.FindModuleImplementing(); + foreach (var existingLeakFX in parentPart.GetComponentsInChildren()) + { + existingLeakFX.lifeTime = 0; //kill leak FX + } + solid = parentPart.Resources.Where(pr => pr.resourceName == "SolidFuel").FirstOrDefault(); + if (engine != null) + { + if (solid != null) + { + isSRB = true; + } + } + fireIntensity = burnRate; + BDArmorySetup.numberOfParticleEmitters++; + pEmitters = gameObject.GetComponentsInChildren(); + vacuum = FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(transform.position), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody) < 0.05f; + + using (var pe = pEmitters.AsEnumerable().GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + pe.Current.emit = true; + _highestEnergy = pe.Current.maxEnergy; + if (vacuum) + { + pe.Current.localVelocity = new Vector3(0, (float)parentPart.vessel.obt_speed, 0); + } + EffectBehaviour.AddParticleEmitter(pe.Current); + } + + Seat = null; + if (parentPart.parent != null) + { + var kerbalSeats = parentPart.parent.Modules.OfType(); + if (kerbalSeats.Count() > 0) + Seat = kerbalSeats.First(); + } + if (parentPart.protoModuleCrew.Count > 0) //crew can extingusih fire + { + burnTime = 10; + } + if (parentPart.parent != null && parentPart.parent.protoModuleCrew.Count > 0 || (Seat != null && Seat.Occupant != null)) + { + burnTime = 20; //though adjacent parts will take longer to get to and extingusih + } + if (!surfaceFire) + { + if (parentPart.GetComponent() != null) + { + ModuleSelfSealingTank FBX; + FBX = parentPart.GetComponent(); + FBX.Extinguishtank(); + if (FBX.InertTank) burnTime = 0.01f; //check is looking for > 0, value of 0 not getting caught. + /* + if (FBX.FireBottles > 0) + { + //FBX.FireBottles -= 1; + if (engine != null && engine.EngineIgnited && engine.allowRestart) + { + engine.Shutdown(); + enginerestartTime = Time.time; + } + burnTime = 4; + GUIUtils.RefreshAssociatedWindows(parentPart); + Debug.Log("[FireFX] firebottles remaining in " + parentPart.name + ": " + FBX.FireBottles); + } + else + { + if (engine != null && engine.EngineIgnited && engine.allowRestart) + { + if (parentPart.vessel.verticalSpeed < 30) //not diving/trying to climb. With the vessel registry, could also grab AI state to add a !evading check + { + engine.Shutdown(); + enginerestartTime = Time.time + 5; + burnTime = 10; + } + //though if it is diving, then there isn't a second call to cycle engines. Add an Ienumerator to check once every couple sec? + } + } + */ + } + } + parentBeingDestroyed = false; + } + + void OnDisable() + { + // Clean up emitters. + 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; + engine = null; + fuel = null; + solid = null; + ox = null; + ec = null; + mp = null; + tntMassEquivalent = 0; + fireIntensity = 1; + } + + void FixedUpdate() + { + if (!gameObject.activeInHierarchy || !HighLogic.LoadedSceneIsFlight || BDArmorySetup.GameIsPaused) + { + return; + } + if (vacuum) transform.rotation = Quaternion.FromToRotation(Vector3.up, parentPart.vessel.obt_velocity.normalized); + else transform.rotation = Quaternion.FromToRotation(Vector3.up, parentPart.vessel.up); + fuel = parentPart.Resources.Where(pr => pr.resourceName == "LiquidFuel").FirstOrDefault(); + if (disableTime < 0) //only have fire do it's stuff while burning and not during FX timeout + { + if (!surfaceFire) //is fire inside tank, or an incendiary substance on the part's surface? + { + // if (!lookedForEngine) // This is done in OnEnable. + // { + // engine = parentPart.FindModuleImplementing(); + // lookedForEngine = true; //have this only called once, not once per update tick + // } + if (engine != null) + { + if (isSRB) + { + if (parentPart.RequestResource("SolidFuel", (double)(burnRate * TimeWarp.fixedDeltaTime)) <= 0) + { + hasFuel = false; + } + solid = parentPart.Resources.Where(pr => pr.resourceName == "SolidFuel").FirstOrDefault(); + if (solid != null && solid.amount > 0) + { + if (solid.amount < solid.maxAmount * 0.66f) + { + engine.Activate(); //SRB lights from unintended ignition source + } + if (solid.amount < solid.maxAmount * 0.15f) + { + tntMassEquivalent += Mathf.Clamp((float)solid.amount, ((float)solid.maxAmount * 0.05f), ((float)solid.maxAmount * 0.2f)); + Detonate(); //casing's full of holes and SRB fuel's burnt to the point it can easily start venting through those holes + return; + } + } + } + else + { + if (engine.EngineIgnited) + { + if (parentPart.RequestResource("LiquidFuel", (double)(burnRate * TimeWarp.fixedDeltaTime)) <= 0) + { + hasFuel = false; + } + } + else + { + hasFuel = false; + } + } + } + else + { + if (fuel != null) + { + if (parentPart.vessel.InNearVacuum() && ox == null) + { + hasFuel = false; + } + else + { + if (fuel.amount > 0) + { + 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.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); + } + else if (fuel.amount < (fuel.maxAmount * 0.15f) && fuel.amount > (fuel.maxAmount * 0.10f)) + { + Detonate(); + return; + } + } + else + { + hasFuel = false; + } + } + } + ox = parentPart.Resources.Where(pr => pr.resourceName == "Oxidizer").FirstOrDefault(); + if (ox != null && fuel != null) + { + 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.fixedDeltaTime); + } + else + { + hasFuel = false; + } + } + mp = parentPart.Resources.Where(pr => pr.resourceName == "MonoPropellant").FirstOrDefault(); + if (mp != null) + { + 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.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); + } + } + else if (mp.amount < (mp.maxAmount * 0.15f) && mp.amount > (mp.maxAmount * 0.10f)) + { + Detonate(); + return; + } + else + { + hasFuel = false; + } + } + ec = parentPart.Resources.Where(pr => pr.resourceName == "ElectricCharge").FirstOrDefault(); + if (ec != null) + { + if (parentPart.vessel.InNearVacuum()) + { + hasFuel = false; + } + else + { + if (ec.amount > 0) + { + ec.amount -= (burnRate * TimeWarp.deltaTime); + Mathf.Clamp((float)ec.amount, 0, Mathf.Infinity); + if (burnScale < 0) + { + burnScale = 1; + } + } + if ((Time.time - startTime > 30) && engine == null) + { + Detonate(); + return; + } + } + } + } + } + if (BDArmorySettings.BD_FIRE_HEATDMG) + { + if (parentPart.temperature < 1300) + { + if (fuel != null) + { + parentPart.temperature += burnRate * Mathf.Clamp((float)((1 - (fuel.amount / fuel.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * Time.deltaTime; + } + else if (mp != null) + { + 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) + { + parentPart.temperature += burnRate * BDArmorySettings.BD_FIRE_DAMAGE * Time.fixedDeltaTime; + } + } + } + if (BDArmorySettings.BATTLEDAMAGE && BDArmorySettings.BD_FIRE_DOT) + { + if (BDArmorySettings.BD_INTENSE_FIRES) + { + 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.fixedDeltaTime); + } + else + parentPart.AddDamage(BDArmorySettings.BD_FIRE_DAMAGE * Time.fixedDeltaTime); + } + + 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))) + { + disableTime = Time.time; //grab time when emission stops + foreach (var pe in pEmitters) + if (pe != null) + pe.emit = false; + } + else + { + foreach (var pe in pEmitters) + { + pe.minSize = burnScale; + pe.maxSize = burnScale * 1.2f; + } + } + if (surfaceFire && parentPart.vessel.horizontalSrfSpeed > 120 && SourceVessel != "GM") //blow out surface fires if moving fast enough + { + 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 + { + Deactivate(); + } + if (vacuum || !FlightGlobals.currentMainBody.atmosphereContainsOxygen && (ox == null && mp == null)) + { + 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() + { + if (!HighLogic.LoadedSceneIsFlight) { Deactivate(); return; } + if (surfaceFire) return; + if (!BDArmorySettings.BD_FIRE_FUELEX) return; + if (!parentPart.partName.Contains("exploding")) + { + bool excessFuel = false; + parentPart.partName += "exploding"; + PartResource fuel = parentPart.Resources.Where(pr => pr.resourceName == "LiquidFuel").FirstOrDefault(); + PartResource ox = parentPart.Resources.Where(pr => pr.resourceName == "Oxidizer").FirstOrDefault(); + float tntFuel = 0, tntOx = 0, tntMP = 0, tntEC = 0; + if (fuel != null && fuel.amount > 0) + { + tntFuel = (Mathf.Clamp((float)fuel.amount, ((float)fuel.maxAmount * 0.05f), ((float)fuel.maxAmount * 0.2f)) / 2); + tntMassEquivalent += tntFuel; + if (fuel != null && (ox != null && ox.amount > 0)) + { + tntOx = (Mathf.Clamp((float)ox.amount, ((float)ox.maxAmount * 0.1f), ((float)ox.maxAmount * 0.3f)) / 2); + tntMassEquivalent += tntOx; + tntMassEquivalent *= 1.3f; + } + if (fuel.amount > fuel.maxAmount * 0.3f) + { + excessFuel = true; + } + } + PartResource mp = parentPart.Resources.Where(pr => pr.resourceName == "MonoPropellant").FirstOrDefault(); + if (mp != null && mp.amount > 0) + { + tntMP = (Mathf.Clamp((float)mp.amount, ((float)mp.maxAmount * 0.1f), ((float)mp.maxAmount * 0.3f)) / 3); + tntMassEquivalent += tntMP; + if (mp.amount > mp.maxAmount * 0.3f) + { + excessFuel = true; + } + } + tntMassEquivalent /= 6f; //make this not have a 1 to 1 ratio of fuelmass -> tntmass + PartResource ec = parentPart.Resources.Where(pr => pr.resourceName == "ElectricCharge").FirstOrDefault(); + if (ec != null && ec.amount > 0) + { + tntEC = ((float)ec.maxAmount / 5000); //fix for cockpit batteries weighing a tonne+ + tntMassEquivalent += tntEC; + ec.maxAmount = 0; + ec.isVisible = false; + if (!parentBeingDestroyed) parentPart.RemoveResource(ec);//destroy battery. not calling part.destroy, since some batteries in cockpits. + GUIUtils.RefreshAssociatedWindows(parentPart); + } + //tntMassEquivilent *= BDArmorySettings.BD_AMMO_DMG_MULT; //handled by EXP_DMG_MOD_BATTLE_DAMAGE + 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); + 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; + try + { + Part partHit = blastHits.Current.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.Modules.GetModule().Hitpoints <= 0) continue; // Ignore parts that are already dead. + if (partHit.Rigidbody != null && partHit.mass > 0) + { + Vector3 distToG0 = parentPart.transform.position - partHit.transform.position; + + Ray LoSRay = new Ray(parentPart.transform.position, partHit.transform.position - parentPart.transform.position); + RaycastHit hit; + if (Physics.Raycast(LoSRay, out hit, distToG0.magnitude, explosionLayerMask)) + { + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (p == partHit) + { + BulletHitFX.AttachFire(hit.point, p, 1, SourceVessel, BDArmorySettings.WEAPON_FX_DURATION * (1 - (distToG0.magnitude / blastRadius)), 1, true); + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log("[BDArmory.FireFX]: " + this.parentPart.name + " hit by burning fuel"); + } + } + } + } + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.FireFX]: Exception thrown in Detonate: " + e.Message + "\n" + e.StackTrace); + } + } + } + if (tntMassEquivalent > 0) //don't explode if nothing to detonate if called from OnParentDestroy() + { + ExplosionFx.CreateExplosion(parentPart.transform.position, tntMassEquivalent, explModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 120, parentPart, parentPart.vessel != null ? parentPart.vessel.vesselName : null, null, "Fuel", sourceVelocity: parentPart.vessel.Velocity()); + if (BDArmorySettings.RUNWAY_PROJECT_ROUND != 42) + { + if (tntFuel > 0 || tntMP > 0) + { + var tmpParentPart = parentPart; // Temporarily store the parent part so we can destroy it without destroying ourselves. + Deactivate(); + tmpParentPart.Destroy(); + } + } + } + } + if (parentPart != null && parentPart.Modules.GetModule().ignitionTemp < 0) Deactivate(); //wooden batteries keep burning + } + + public void AttachAt(Part hitPart, Vector3 hit, Vector3 offset, string sourcevessel) + { + 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, hitPart.vessel.up); + 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 is not null) + { + parentBeingDestroyed = true; + parentPart.OnJustAboutToDie -= OnParentDestroy; + parentPart.OnJustAboutToBeDestroyed -= OnParentDestroy; + if (!surfaceFire) Detonate(); + 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 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 new file mode 100644 index 000000000..ef6269d9b --- /dev/null +++ b/BDArmory/FX/FuelLeakFX.cs @@ -0,0 +1,295 @@ +using System.Linq; +using UnityEngine; + +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Extensions; + +namespace BDArmory.FX +{ + class FuelLeakFX : MonoBehaviour + { + Part parentPart; + // string parentPartName = ""; + // string parentVesselName = ""; + public static ObjectPool CreateLeakFXPool(string modelPath) + { + var template = GameDatabase.Instance.GetModel(modelPath); + var decal = template.AddComponent(); + template.SetActive(false); + return ObjectPool.CreateObjectPool(template, 10, true, true); + } + + public float drainRate = 1; + private int fuelLeft = 0; + public float lifeTime = 20; + private float startTime; + private float disableTime = -1; + private float _highestEnergy = 1; + + PartResource fuel; + PartResource ox; + PartResource mp; + ModuleEngines engine; + private bool isSRB = false; + KSPParticleEmitter[] pEmitters; + Vector3 force; + + void OnEnable() + { + if (parentPart == null) + { + gameObject.SetActive(false); + return; + } + 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(); + if (engine != null) + { + if (solid != null) + { + isSRB = true; + } + } + BDArmorySetup.numberOfParticleEmitters++; + startTime = Time.time; + pEmitters = gameObject.GetComponentsInChildren(); + bool useWorldSpace = !parentPart.vessel.InVacuum(); + Vector3 localVelocity = GetLeakVelocity() * Vector3.up; // ~6.4 m/s in vacuum, 0 m/s at Kerbin sea level and denser + force = GetGForce(); + var localForce = Quaternion.Inverse(transform.rotation) * force; + using var pe = pEmitters.AsEnumerable().GetEnumerator(); + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + pe.Current.emit = true; + _highestEnergy = pe.Current.maxEnergy; + pe.Current.useWorldSpace = useWorldSpace; // FIXME These should use the same useWorldSpace to avoid a discontinuity when reaching 0 atmo, otherwise localVelocity should be adjusted to compensate in one or the other cases. But adding the velocity to localVelocity doesn't seem to work properly. In any case, it's good enough for now. + pe.Current.localVelocity = localVelocity; + pe.Current.force = localForce; // Align force to local reference frame of emitter. + pe.Current.SetDirty(); + EffectBehaviour.AddParticleEmitter(pe.Current); + } + } + + /// + /// Combination of gravity and centripetal force. + /// + /// The overall force in the local reference frame. + Vector3 GetGForce() + { + // Calculate whether gravity is placing a force based on ratio of centripetal acceleration to body gravity (code below avoids Gravity.magnitude) + var vessel = parentPart.vessel; + float r = (float)(vessel.altitude + vessel.orbit.referenceBody.Radius); + float bodyGravity = (float)vessel.orbit.referenceBody.gravParameter / (r * r); + float centripetalAccel = (float)(vessel.obt_speed * vessel.obt_speed / r); + Vector3 force = FlightGlobals.getGeeForceAtPosition(vessel.CoM); + return Vector3.Lerp(force, Vector3.zero, Mathf.Clamp01(centripetalAccel / bodyGravity)); // Full force of gravity outside of orbital conditions, no gravity in orbit, somewhere in-between for sub-orbital + } + + /// + /// Computes leak velocity based on atmospheric density and temperature and the resource density + /// + /// The leak velocity as a float. + float GetLeakVelocity(float resourceDensity = 5f) // fuel and oxidizer are 5 kg/l, monopropellant is 4 kg/l (close enough to 5 for FX) + { + float tankPressure = 1f; // in atmospheres + float outsidePressure = Mathf.Clamp((float)parentPart.vessel.atmDensity * (float)parentPart.vessel.atmosphericTemperature * 287.053f / 101325f, 0f, tankPressure); // p = rho * R * T, in atmospheres + float velocity = BDAMath.Sqrt(2f * (tankPressure - outsidePressure) * 101.325f / resourceDensity); // Incompressible bernoulli flow, returns value in m/s (0 at 1 atm, 6.37 m/s in vacuum for 5 fuel/oxidizer) + return velocity; + } + + void OnDisable() + { + if (pEmitters != null) // Getting enabled when the parent part is null immediately disables it again before setting any of this up. + { + BDArmorySetup.numberOfParticleEmitters--; + foreach (var pe in pEmitters) + if (pe != null) + { + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + } + parentPart = null; + fuel = null; + ox = null; + mp = null; + drainRate = 1; + } + + void FixedUpdate() + { + if (!gameObject.activeInHierarchy || !HighLogic.LoadedSceneIsFlight || BDArmorySetup.GameIsPaused) + { + return; + } + if (force != default) + { + var localForce = Quaternion.Inverse(transform.rotation) * force; + foreach (var pe in pEmitters.Where(pe => pe != null)) + { + pe.force = localForce; // Update the force direction for moving parts. + pe.SetDirty(); + } + } + fuel = parentPart.Resources.Where(pr => pr.resourceName == "LiquidFuel").FirstOrDefault(); + if (disableTime < 0) //only have fire do its stuff while burning and not during FX timeout + { + float impulse = 0f; + if (engine != null) + { + if (engine.EngineIgnited && !isSRB) + { + if (fuel != null) + { + if (fuel.amount > 0) + { + parentPart.RequestResource("LiquidFuel", (double)(drainRate * Time.fixedDeltaTime)); + fuelLeft++; + } + } + } + } + else + { + if (fuel != null) + { + if (fuel.amount > 0) + { + //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 + double amount = ((double)drainRate * Mathf.Clamp((float)fuel.amount, 40, 400) / Mathf.Clamp((float)fuel.maxAmount, 400, (float)fuel.maxAmount)) * Time.fixedDeltaTime; + fuel.amount -= amount; + fuel.amount = Mathf.Clamp((float)fuel.amount, 0, (float)fuel.maxAmount); + fuelLeft++; + impulse += (float)amount * fuel.info.density / 1000f * GetLeakVelocity(fuel.info.density); // m * v + } + } + ox = parentPart.Resources.Where(pr => pr.resourceName == "Oxidizer").FirstOrDefault(); + if (ox != null) + { + if (ox.amount > 0) + { + //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 + double amount = ((double)drainRate * Mathf.Clamp((float)ox.amount, 40, 400) / Mathf.Clamp((float)ox.maxAmount, 400, (float)ox.maxAmount)) * Time.fixedDeltaTime; + ox.amount -= amount; + ox.amount = Mathf.Clamp((float)ox.amount, 0, (float)ox.maxAmount); + fuelLeft++; + impulse += (float)amount * ox.info.density / 1000f * GetLeakVelocity(ox.info.density); // m * v + } + } + mp = parentPart.Resources.Where(pr => pr.resourceName == "MonoPropellant").FirstOrDefault(); + if (mp != null) + { + 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); + double amount = ((double)drainRate * Mathf.Clamp((float)mp.amount, 40, 400) / Mathf.Clamp((float)mp.maxAmount, 400, (float)mp.maxAmount)) * Time.fixedDeltaTime; + mp.amount -= amount; + mp.amount = Mathf.Clamp((float)mp.amount, 0, (float)mp.maxAmount); + fuelLeft++; + impulse += (float)amount * mp.info.density / 1000f * GetLeakVelocity(mp.info.density); // m * v + } + } + } + // Add thrust to leaking tanks based on drain rate (mass flow rate) and leak velocity (F = m_dot * v) + if (disableTime < 0 && (fuelLeft > 0 && (lifeTime >= 0 && Time.time - startTime < lifeTime))) + parentPart.Rigidbody.AddForce(impulse * transform.up, ForceMode.Impulse); + } + + if (disableTime < 0 && (fuelLeft <= 0 || (lifeTime >= 0 && Time.time - startTime > lifeTime))) + { + disableTime = Time.time; //grab time when emission stops + foreach (var pe in pEmitters) + if (pe != null) + pe.emit = false; + } + fuelLeft = 0; + if (disableTime > 0 && Time.time - disableTime > _highestEnergy) //wait until last emitted particle has finished + { + Deactivate(); + } + } + public void AttachAt(Part hitPart, RaycastHit hit, Vector3 offset, Vector3 colliderLocalHitPoint = default) + { + if (hitPart is null) return; + parentPart = hitPart; + // parentPartName = parentPart.name; + // parentVesselName = parentPart.vessel.vesselName; + transform.SetParent(hit.collider.transform); + transform.position = (colliderLocalHitPoint == default ? hit.point : transform.parent.TransformPoint(colliderLocalHitPoint)) + offset; + transform.rotation = Quaternion.FromToRotation(Vector3.up, hit.normal); + 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); + } + + 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(); // 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 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 new file mode 100644 index 000000000..366a69ea2 --- /dev/null +++ b/BDArmory/FX/NukeFX.cs @@ -0,0 +1,812 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.GameModes; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; + +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; + float yieldCubeRoot; + private float lastValidAtmDensity = 0f; + + HashSet partsHit = new HashSet(); + public Light LightFx { get; set; } + public KSPParticleEmitter[] pEmitters { get; set; } + public float StartTime { 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 + public float detonationTimer { get; set; } //seconds to delay before detonation + public bool isEMP { get; set; } //do EMP effects? + private float MaxTime { get; set; } + public ExplosionSourceType ExplosionSource { get; set; } + public string SourceVesselName { get; set; } + public string ReportingName { get; set; } + public float yield { get; set; } //kilotons + public Vector3 Position { get { return _position; } set { _position = value; transform.position = _position; } } + Vector3 _position; + public Vector3 Velocity { get; set; } + //public Part ExplosivePart { get; set; } + public float nukeMass { get; set; } + public float TimeIndex => Time.time - StartTime; + public string flashModelPath { get; set; } + public string shockModelPath { get; set; } + public string blastModelPath { get; set; } + public string plumeModelPath { get; set; } + public string debrisModelPath { get; set; } + public string blastSoundPath { get; set; } + + public string explModelPath = "BDArmory/Models/explosion/explosion"; + + public string explSoundPath = "BDArmory/Sounds/explode1"; + + bool bulletHitRegistered = true; + + Queue explosionEvents = new Queue(); + List explosionEventsPreProcessing = new List(); + List explosionEventsPartsAdded = new List(); + List explosionEventsBuildingAdded = new List(); + Dictionary explosionEventsVesselsHit = new Dictionary(); + + private float EMPRadius = 100; + private float scale = 1; + private float lightScale = 1; + 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 RaycastHit[] electroHits; + Collider[] blastHitColliders = new Collider[100]; + public static List IgnoreParts; + public static List IgnoreBuildings; + internal static readonly float ExplosionVelocity = 422.75f; + internal static float KerbinSeaLevelAtmDensity + { + get + { + if (_KerbinSeaLevelAtmDensity == 0) _KerbinSeaLevelAtmDensity = (float)FlightGlobals.GetBodyByName("Kerbin").atmDensityASL; + return _KerbinSeaLevelAtmDensity; + } + } + internal static float _KerbinSeaLevelAtmDensity = 0; + List fxEmitters = new(); + + internal static HashSet ignoreCasingFor = new HashSet { ExplosionSourceType.Missile, ExplosionSourceType.Rocket }; + + void Awake() + { + if (lineOfSightHits == null) { lineOfSightHits = new RaycastHit[100]; } + if (reverseHits == null) { reverseHits = new RaycastHit[100]; } + if (electroHits == null) { electroHits = new RaycastHit[100]; } + if (IgnoreParts == null) { IgnoreParts = new List(); } + if (IgnoreBuildings == null) { IgnoreBuildings = new List(); } + } + + private void OnEnable() + { + StartTime = Time.time; + 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! yield: {yield} BlastRadius: {thermalRadius} StartTime: {StartTime}, Duration: {MaxTime}"); + } + if (HighLogic.LoadedSceneIsFlight) + { + yieldCubeRoot = Mathf.Pow(yield, 1f / 3f); + startTime = Time.time; + if (FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(Position), + FlightGlobals.getExternalTemperature(Position)) > 0) + lastValidAtmDensity = (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(Position), + FlightGlobals.getExternalTemperature(Position)); + hasDetonated = false; + + //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(Position) < 10000) + { + EMPRadius = BDAMath.Sqrt(yield) * 500; + } + else + { + EMPRadius = BDAMath.Sqrt(yield) * 1000; + } + + fxEmitters.Clear(); + pEmitters = gameObject.GetComponentsInChildren(); + foreach (var pe in pEmitters) + if (pe != null) + { + pe.emit = true; + pe.useWorldSpace = false; // Don't use worldspace, so that we can move the FX properly. + var emission = pe.ps.emission; + emission.enabled = true; + EffectBehaviour.AddParticleEmitter(pe); + } + + if (BDArmorySettings.LightFX) + { + LightFx = gameObject.GetComponent(); + LightFx.range = BDAMath.Sqrt(yield) * 500; + lightScale = 8f * Mathf.Log(yield); + LightFx.intensity = lightScale; // Reset light intensity. + } + //comment out the above and uncomment the below if !LIGHTFX = light range/intensity remains 0; + //LightFx = gameObject.GetComponent(); + //LightFx.range = BDArmorySettings.LIGHTFX ? 0 : BDAMath.Sqrt(yield) * 500; + //lightScale = 8f * Mathf.Log(yield); + //LightFx.intensity = BDArmorySettings.LIGHTFX ? 0 : lightScale; // Reset light intensity. + + audioSource = gameObject.GetComponent(); + if (!string.IsNullOrEmpty(SoundPath)) + { + audioSource.PlayOneShot(audioClips[SoundPath]); + } + } + else + { + hasDetonated = false; + if (LightFx != null) + { + LightFx.intensity = 0; + LightFx.range = 0; + } + gameObject.SetActive(false); + return; + } + } + + void OnDisable() + { + foreach (var pe in pEmitters) + { + if (pe != null) + { + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + } + fxEmitters.Clear(); + //ExplosivePart = null; // Clear the Part reference. + explosionEvents.Clear(); // Make sure we don't have any left over events leaking memory. + explosionEventsPreProcessing.Clear(); + explosionEventsPartsAdded.Clear(); + explosionEventsBuildingAdded.Clear(); + explosionEventsVesselsHit.Clear(); + } + + private void CalculateBlastEvents() + { + using (var enuEvents = ProcessingBlastSphere().OrderBy(e => e.TimeToImpact).GetEnumerator()) + { + while (enuEvents.MoveNext()) + { + if (enuEvents.Current == null) continue; + + explosionEvents.Enqueue(enuEvents.Current); + } + } + } + + private List ProcessingBlastSphere() + { + explosionEventsPreProcessing.Clear(); + explosionEventsPartsAdded.Clear(); + explosionEventsBuildingAdded.Clear(); + explosionEventsVesselsHit.Clear(); + + 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()) + { + if (hitCollidersEnu.Current == null) continue; + try + { + Part partHit = hitCollidersEnu.Current.GetComponentInParent(); + if (partHit != null) + { + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.mass > 0 && !explosionEventsPartsAdded.Contains(partHit)) + { + var damaged = ProcessPartEvent(partHit, SourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded); + } + } + else + { + DestructibleBuilding building = hitCollidersEnu.Current.GetComponentInParent(); + if (building != null) + { + 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, thermalRadius, explosionLayerMask)) + { + //DestructibleBuilding destructibleBuilding = rayHit.collider.gameObject.GetComponentUpwards(); + + distance = Vector3.Distance(Position, rayHit.point); + if (building.IsIntact) + { + explosionEventsPreProcessing.Add(new BuildingNukeHitEvent() { Distance = distance, Building = building, TimeToImpact = distance / ExplosionVelocity }); + explosionEventsBuildingAdded.Add(building); + } + } + } + } + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.NukeFX]: Exception in overlapSphere collider processing: {e.Message}\n{e.StackTrace}"); + } + } + } + return explosionEventsPreProcessing; + } + + private bool ProcessPartEvent(Part part, string sourceVesselName, List eventList, List partsAdded) + { + Ray LoSRay = new Ray(Position, part.transform.position - Position); + RaycastHit hit; + var distToG0 = Math.Max((Position - part.transform.position).magnitude, 1f); + if (Physics.Raycast(LoSRay, out hit, distToG0, explosionLayerMask)) // only add impulse to parts with line of sight to detonation + { + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (lastValidAtmDensity < 0.1) + { + 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, + ColliderLocalHitPoint = hit.collider is not null ? hit.collider.transform.InverseTransformPoint(hit.point) : default + }); + + partsAdded.Add(part); + return true; + } + return false; + } + else + { + eventList.Add(new PartNukeHitEvent() //else everything heated/hit by shockwave + { + Distance = distToG0, + Part = part, + TimeToImpact = distToG0 / ExplosionVelocity, + HitPoint = hit.point, + Hit = hit, + SourceVesselName = sourceVesselName, + ColliderLocalHitPoint = hit.collider is not null ? hit.collider.transform.InverseTransformPoint(hit.point) : default + }); + + partsAdded.Add(part); + return true; + } + } + + return false; + } + + public void Update() + { + if (!gameObject.activeInHierarchy) return; + + if (hasDetonated) + { + if (LightFx != null && BDArmorySettings.LightFX) + { + LightFx.intensity -= 3 * lightScale * 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. + { + 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. + { + foreach (var pe in pEmitters) + { + if (pe == null) continue; + pe.emit = false; + } + } + } + foreach (var fx in fxEmitters) if (fx.gameObject.activeSelf) fx.Position = Position; // Update FX emitter positions. + } + } + + public void FixedUpdate() + { + if (!gameObject.activeInHierarchy) return; + + //floating origin and velocity offloading corrections + if (BDKrakensbane.IsActive) + { + Position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + { // Explosion centre velocity depends on atmospheric density relative to Kerbin sea level. + var atmDensity = (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(Position), FlightGlobals.getExternalTemperature(Position)); + Velocity /= 1 + atmDensity / KerbinSeaLevelAtmDensity; + Position += Velocity * TimeWarp.fixedDeltaTime; // Krakensbane is already accounted for above. + } + if (Time.time - startTime > detonationTimer) + { + if (!hasDetonated) + { + hasDetonated = true; + CalculateBlastEvents(); + if (isEMP) CalculateEMPEvent(); + if (lastValidAtmDensity < 0.05) + { + if (!string.IsNullOrWhiteSpace(flashModelPath)) + fxEmitters.Add(FXEmitter.CreateFX(Position, scale * 50f, flashModelPath, "", 0.4f, 0.4f)); + if (!string.IsNullOrWhiteSpace(shockModelPath)) + fxEmitters.Add(FXEmitter.CreateFX(Position, scale * 14f, shockModelPath, "", 0.2f, 0.6f)); + } + 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)) + fxEmitters.Add(FXEmitter.CreateFX(Position, scale, flashModelPath, "", 0.3f, -1, default, true)); + if (!string.IsNullOrWhiteSpace(shockModelPath)) + fxEmitters.Add(FXEmitter.CreateFX(Position, scale * lastValidAtmDensity, shockModelPath, "", 0.3f, -1, default, true)); + if (!string.IsNullOrWhiteSpace(blastModelPath)) + fxEmitters.Add(FXEmitter.CreateFX(Position, scale, blastModelPath, blastSoundPath, 1.5f, Mathf.Clamp(30 * scale, 30f, 90f), default, true)); + + if (BodyUtils.GetRadarAltitudeAtPos(Position) < 200 * scale) + { + double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(Position); + double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(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) + { + NukeHitEvent eventToExecute = explosionEvents.Dequeue(); + + var partBlastHitEvent = eventToExecute as PartNukeHitEvent; + if (partBlastHitEvent != null) + { + ExecutePartBlastEvent(partBlastHitEvent); + } + else + { + ExecuteBuildingBlastEvent((BuildingNukeHitEvent)eventToExecute); + } + } + } + + if (hasDetonated && explosionEvents.Count == 0 && TimeIndex > MaxTime) + { + hasDetonated = false; + if (LightFx != null) + { + LightFx.intensity = 0; + LightFx.range = 0; + } + foreach (var vesselName in explosionEventsVesselsHit.Keys) //once blast completed, register vessel strikes as appropriate + { + switch (ExplosionSource) + { + case ExplosionSourceType.Bullet: + if (!bulletHitRegistered) + BDACompetitionMode.Instance.Scores.RegisterBulletHit(SourceVesselName, vesselName); + break; + case ExplosionSourceType.Rocket: + BDACompetitionMode.Instance.Scores.RegisterRocketStrike(SourceVesselName, vesselName); + break; + case ExplosionSourceType.Missile: + BDACompetitionMode.Instance.Scores.RegisterMissileStrike(SourceVesselName, vesselName); + break; + } + } + gameObject.SetActive(false); + return; + } + } + + private void ExecuteBuildingBlastEvent(BuildingNukeHitEvent eventToExecute) + { + DestructibleBuilding building = eventToExecute.Building; + //Debug.Log("[BDArmory.NukeFX] Beginning building hit"); + if (building && building.IsIntact) + { + var distToEpicenter = Mathf.Max((Position - building.transform.position).magnitude, 1f); + 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); + if (blastImpulse > 140) + { + building.Demolish(); + } + } + } + } + + private void ExecutePartBlastEvent(PartNukeHitEvent eventToExecute) + { + if (eventToExecute.Part == null || eventToExecute.Part.Rigidbody == null || eventToExecute.Part.vessel == null || eventToExecute.Part.partInfo == null) return; + + Part part = eventToExecute.Part; + Rigidbody rb = part.Rigidbody; + //var realDistance = eventToExecute.Distance; //this provides a snapshot of distance at time of detonation; with multi-second lag between detonation and blastwave reaching target, target could fly outzide blastzone + var realDistance = Math.Max((Position - part.transform.position).magnitude, 1f); + if (realDistance > thermalRadius) return; //craft has flown out of blast zone by time blastfront has arrived at original 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. + float radiativeArea = !double.IsNaN(part.radiativeArea) ? (float)part.radiativeArea : part.GetArea(); + if (!BDArmorySettings.PAINTBALL_MODE) + { + if (!eventToExecute.IsNegativePressure) + { + if (BDArmorySettings.DEBUG_DAMAGE && double.IsNaN(part.radiativeArea)) + { + Debug.Log($"[BDArmory.NukeFX]: radiative area of part {part} was NaN, using approximate area {radiativeArea} instead."); + } + 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 = (nukeMass * 15295.74) / (4 * Math.PI * Math.Pow(realDistance, 2.0));// * (part.radiativeArea / 3.0); + if (blastImpulse > 0) + { + 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}"); + } + else + { + if (!ProjectileUtils.CalculateExplosiveArmorDamage(part, blastImpulse, realDistance, SourceVesselName, eventToExecute.Hit, ExplosionSource, thermalRadius - realDistance)) //false = armor blowthrough + { + damage = ProjectileUtils.IsArmorPart(part) ? blastDamage : part.AddExplosiveDamage(blastDamage, 1, ExplosionSource, 1); //armor panels return damage = 0, so adding exception so they still score properly + // no damage reduction from very thick armor, but no multiplier from damage type, either, should balance out. And any comp that allows nukes probably isn't going to be weighting DamageIn... + } + if (damage > 0) + { + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(part, 50, 0.5f, true, false, SourceVesselName, eventToExecute.Hit, colliderLocalHitPoint: eventToExecute.ColliderLocalHitPoint); + } + // Update scoring structures + if (BDACompetitionMode.Instance) //moving this here - only give scores to stuff still inside blast radius when blastfront arrives + { + var tName = part.vessel != null ? part.vessel.GetName() : null; //target + var aName = eventToExecute.SourceVesselName; // Attacker + switch (ExplosionSource) + { + case ExplosionSourceType.Missile: + BDACompetitionMode.Instance.Scores.RegisterMissileDamage(aName, tName, damage); //FIXME/TODO - damage should probably correlate in some way to armor mass lost/damage to armor, instead of '0' + break; + case ExplosionSourceType.Bullet: + BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, damage); + break; + case ExplosionSourceType.Rocket: + BDACompetitionMode.Instance.Scores.RegisterRocketDamage(aName, tName, damage); //FIXME/TODO - damage should probably correlate in some way to armor mass lost/damage to armor, instead of '0' + break; + case ExplosionSourceType.BattleDamage: + BDACompetitionMode.Instance.Scores.RegisterBattleDamage(aName, part.vessel, damage); + break; + + } + } + } + } + if (BDACompetitionMode.Instance) //register blastfront impact, regardless if it makes it through armor or not + { + bool registered = false; + var damagedVesselName = part.vessel != null ? part.vessel.GetName() : null; + switch (ExplosionSource) + { + case ExplosionSourceType.Missile: + if (BDACompetitionMode.Instance.Scores.RegisterMissileHit(SourceVesselName, damagedVesselName, 1)) + registered = true; + break; + case ExplosionSourceType.Rocket: + if (BDACompetitionMode.Instance.Scores.RegisterRocketHit(SourceVesselName, damagedVesselName, 1)) + registered = true; + break; + case ExplosionSourceType.Bullet: + if (!bulletHitRegistered) + registered = true; + break; + } + if (registered) + { + if (explosionEventsVesselsHit.ContainsKey(damagedVesselName)) + ++explosionEventsVesselsHit[damagedVesselName]; + else + explosionEventsVesselsHit[damagedVesselName] = 1; + } + } + 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 - 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.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 + } + else + { + if (rb != null && rb.mass > 0) + { + if (double.IsNaN(eventToExecute.NegativeForce)) + { + 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 " + 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); + } + } + } + } + } + + private void CalculateEMPEvent() + { + foreach (Vessel v in FlightGlobals.Vessels) + { + if (v == null || !v.loaded || v.packed) continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(v.vesselType)) continue; + if (!v.HoldPhysics) + { + double targetDistance = Vector3d.Distance(Position, v.GetWorldPos3D()); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.NukeFX]: Detonating EMP from {ReportingName} with blast range {targetDistance}m."); + + if (targetDistance <= EMPRadius) + { + var EMPDamage = ((EMPRadius / (float)targetDistance) * 100) * BDArmorySettings.DMG_MULTIPLIER; //this way craft at edge of blast might only get disabled instead of bricked + + Vector3 commandDir = Vector3.zero; + float shieldvalue = float.PositiveInfinity; + foreach (var moduleCommand in VesselModuleRegistry.GetModuleCommands(v)) + { + //see how many parts are between emitter and the nearest command part to see which one is least shielded + var distToCommand = commandDir.magnitude; + var ElecRay = new Ray(Position, commandDir); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Wheels); + var partCount = Physics.RaycastNonAlloc(ElecRay, electroHits, distToCommand, layerMask); + if (partCount == electroHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. + { + electroHits = Physics.RaycastAll(ElecRay, distToCommand, layerMask); + partCount = electroHits.Length; + } + for (int mwh = 0; mwh < partCount; ++mwh) + { + Part partHit = electroHits[mwh].collider.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; + float testShieldValue = 0; + //AoE EMP field EMP damage mitigation - -1 EMP damage per mm of conductive armor/5t of conductive hull mass per part occluding command part from emission source + var Armor = partHit.FindModuleImplementing(); + if (Armor != null && partHit.Rigidbody != null) + { + if (Armor.Diffusivity > 15) testShieldValue += Armor.Armour; + if (Armor.HullMassAdjust > 0) testShieldValue += (partHit.mass * 4); + } + if (testShieldValue < shieldvalue) shieldvalue = testShieldValue; + } + } + EMPDamage -= shieldvalue; + if (EMPDamage > 0) + { + var emp = v.rootPart.FindModuleImplementing(); + if (emp == null) + { + emp = (ModuleDrainEC)v.rootPart.AddModule("ModuleDrainEC"); + } + emp.softEMP = false; //can bypass DMP damage cap + emp.incomingDamage = EMPDamage; + } + //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 + //IRL EMP intensity/magnitude enerated by nuke explosion is more or less constant within AoE rather than tapering off, but that's no fun + } + } + } + } + + // We use an ObjectPool for the ExplosionFx instances as they leak KSPParticleEmitters otherwise. + static void SetupPool(string ModelPath, string soundPath, float radius) + { + if (!string.IsNullOrEmpty(soundPath) && (!audioClips.ContainsKey(soundPath) || audioClips[soundPath] is null)) + { + var audioClip = SoundUtils.GetAudioClip(soundPath); + if (audioClip is null) + { + Debug.LogError("[BDArmory.NukeFX]: " + soundPath + " was not found, using the default sound instead. Please fix your model."); + audioClip = SoundUtils.GetAudioClip(ModuleWeapon.defaultExplSoundPath); + } + audioClips.Add(soundPath, audioClip); + } + + if (!nukePool.ContainsKey(ModelPath) || nukePool[ModelPath] == null) + { + GameObject templateFX; + if (!string.IsNullOrEmpty(ModelPath)) + { + 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.audioSource = templateFX.AddComponent(); + eFx.audioSource.minDistance = 200; + eFx.audioSource.maxDistance = radius * 3; + eFx.audioSource.spatialBlend = 1; + eFx.audioSource.volume = 5; + if (BDArmorySettings.LightFX) //comment this if check out if swapping out for !LightFX = light range/intensity remains 0 + { + eFx.LightFx = templateFX.AddComponent(); + eFx.LightFx.color = GUIUtils.ParseColor255("255,238,184,255"); + eFx.LightFx.range = BDArmorySettings.LightFX ? 0 : 2000; + eFx.LightFx.intensity = BDArmorySettings.LightFX ? 0 : 8f; // Reset light intensity. + eFx.LightFx.shadows = LightShadows.None; + } + else + { + Light[] bakedLights = templateFX.GetComponentsInChildren(); //remove any Light components intrinsic to the Model + foreach (var bL in bakedLights) + if (bL != null) + { + Destroy(bL); + } + } + templateFX.SetActive(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 = "", + string flashModel = "", string shockModel = "", string blastModel = "", string plumeModel = "", string debrisModel = "", string ModelPath = "", string soundPath = "", + Part nukePart = null, Part hitPart = null, Vector3 sourceVelocity = default, bool bulletHitRegistered = true) + { + if (blastRadius < 100) blastRadius = 100; + SetupPool(ModelPath, soundPath, blastRadius); + + Quaternion rotation; + rotation = Quaternion.LookRotation(VectorUtils.GetUpDirection(position)); + GameObject newExplosion = nukePool[ModelPath + soundPath].GetPooledObject(); + NukeFX eFx = newExplosion.GetComponent(); + newExplosion.transform.SetPositionAndRotation(position, rotation); + + eFx.Position = position; + sourceVelocity = sourceVelocity != default ? sourceVelocity : (nukePart != null && nukePart.rb != null) ? nukePart.rb.velocity + BDKrakensbane.FrameVelocityV3f : default; // Use the explosive part's velocity if the sourceVelocity isn't specified. + eFx.Velocity = (hitPart != null && hitPart.rb != null) ? hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f : sourceVelocity; // sourceVelocity is the real velocity w/o offloading. + eFx.ExplosionSource = explosionSourceType; + eFx.SourceVesselName = sourceVesselName; + eFx.ReportingName = sourceWeaponName; + eFx.explModelPath = ModelPath; + eFx.explSoundPath = soundPath; + eFx.thermalRadius = blastRadius; + + eFx.flashModelPath = flashModel; + eFx.shockModelPath = shockModel; + eFx.blastModelPath = blastModel; + eFx.plumeModelPath = plumeModel; + eFx.debrisModelPath = debrisModel; + eFx.blastSoundPath = blastSound; + + eFx.yield = Yield; + eFx.fluence = thermalShock; + eFx.nukeMass = nukePart != null ? nukePart.mass : Mathf.Pow(Yield, 1 / 3) / 10; + eFx.isEMP = emp; + eFx.detonationTimer = delay; + newExplosion.SetActive(true); + eFx.audioSource = newExplosion.GetComponent(); + eFx.SoundPath = soundPath; + eFx.bulletHitRegistered = bulletHitRegistered; + newExplosion.SetActive(true); + } + public static void DisableAllExplosionFX() + { + if (nukePool == null) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.NukeFx]: Setting {nukePool.Values.Where(pool => pool != null && pool.pool != null).Sum(pool => pool.pool.Count(fx => fx != null && fx.activeInHierarchy))} explosion FX inactive."); + foreach (var pool in nukePool.Values) + { + if (pool == null || pool.pool == null) continue; + foreach (var fx in pool.pool) + { + if (fx == null) continue; + fx.SetActive(false); + } + } + } + } + + public abstract class NukeHitEvent + { + public float Distance { get; set; } + public float TimeToImpact { get; set; } + public bool IsNegativePressure { get; set; } + } + + internal class PartNukeHitEvent : NukeHitEvent + { + public Part Part { get; set; } + public Vector3 HitPoint { get; set; } + public RaycastHit Hit { get; set; } + public float NegativeForce { get; set; } + public string SourceVesselName { get; set; } + public Vector3 ColliderLocalHitPoint { get; set; } = default; + } + + internal class BuildingNukeHitEvent : NukeHitEvent + { + public DestructibleBuilding Building { get; set; } + } +} + 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 86% rename from BDArmory/Parts/SeismicChargeFX.cs rename to BDArmory/FX/SeismicChargeFX.cs index e8f3608f1..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; @@ -77,8 +78,9 @@ void OnTriggerEnter(Collider other) explodePart = other.gameObject.GetComponentUpwards(); explodePart.Unpack(); } - catch (NullReferenceException) + catch (NullReferenceException e) { + Debug.LogWarning("[BDArmory.SeismicChargeFX]: Exception thrown in OnTriggerEnter: " + e.Message + "\n" + e.StackTrace); } if (explodePart != null) @@ -93,8 +95,9 @@ void OnTriggerEnter(Collider other) { hitBuilding = other.gameObject.GetComponentUpwards(); } - catch (NullReferenceException) + catch (NullReferenceException e) { + Debug.LogWarning("[BDArmory.SeismicChargeFX]: Exception thrown in OnTriggerEnter: " + e.Message + "\n" + e.StackTrace); } if (hitBuilding != null && hitBuilding.IsIntact) { diff --git a/BDArmory/FX/ShellCasing.cs b/BDArmory/FX/ShellCasing.cs index e4068146b..229b86f95 100644 --- a/BDArmory/FX/ShellCasing.cs +++ b/BDArmory/FX/ShellCasing.cs @@ -1,40 +1,45 @@ -using BDArmory.Core; using UnityEngine; +using BDArmory.Settings; +using BDArmory.Utils; + namespace BDArmory.FX { public class ShellCasing : MonoBehaviour { public float startTime; public Vector3 initialV; + public Vector3 configV; + public float configD; + public float lifeTime = 2; Vector3 velocity; Vector3 angularVelocity; float atmDensity; + const int collisionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Wheels); void OnEnable() { startTime = Time.time; - velocity = initialV; - velocity += transform.rotation * - new Vector3(Random.Range(-.1f, .1f), Random.Range(-.1f, .1f), - Random.Range(6f, 8f)); - angularVelocity = - new Vector3(Random.Range(-10f, 10f), Random.Range(-10f, 10f), - Random.Range(-10f, 10f)) * 10; - - atmDensity = - (float) - FlightGlobals.getAtmDensity( + Vector3 randV = Random.insideUnitSphere; + velocity = initialV + transform.rotation * new Vector3( + configV.x + (configD + 0.1f * Mathf.Abs(configV.x)) * randV.x, + configV.y + (configD + 0.1f * Mathf.Abs(configV.y)) * randV.y, + configV.z + (configD + 0.1f * Mathf.Abs(configV.z)) * randV.z + ); + angularVelocity = 100f * Random.insideUnitSphere; + atmDensity = (float)FlightGlobals.getAtmDensity( FlightGlobals.getStaticPressure(transform.position, FlightGlobals.currentMainBody), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody); } void FixedUpdate() { - if (!gameObject.activeInHierarchy) + if (!gameObject.activeInHierarchy) return; + if (Time.time - startTime > lifeTime) { + gameObject.SetActive(false); return; } @@ -43,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; @@ -51,8 +56,7 @@ void FixedUpdate() if (BDArmorySettings.SHELL_COLLISIONS) { RaycastHit hit; - if (Physics.Linecast(transform.position, transform.position + velocity * Time.fixedDeltaTime, out hit, - 557057)) + if (Physics.Linecast(transform.position, transform.position + velocity * Time.fixedDeltaTime, out hit, collisionLayerMask)) { velocity = Vector3.Reflect(velocity, hit.normal); velocity *= 0.55f; @@ -61,18 +65,5 @@ void FixedUpdate() } } } - - void Update() - { - if (!gameObject.activeInHierarchy) - { - return; - } - - if (Time.time - startTime > 2) - { - gameObject.SetActive(false); - } - } } } diff --git a/BDArmory/FX/_description b/BDArmory/FX/_description new file mode 100644 index 000000000..7c11e5182 --- /dev/null +++ b/BDArmory/FX/_description @@ -0,0 +1 @@ +Effects, decals and the like. \ No newline at end of file diff --git a/BDArmory/GameModes/Asteroids.cs b/BDArmory/GameModes/Asteroids.cs new file mode 100644 index 000000000..058d4ac98 --- /dev/null +++ b/BDArmory/GameModes/Asteroids.cs @@ -0,0 +1,1162 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.ModIntegration; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.GameModes +{ + public class AsteroidUtils + { + public static UntrackedObjectClass[] UntrackedObjectClasses = (UntrackedObjectClass[])Enum.GetValues(typeof(UntrackedObjectClass)); // Get the UntrackedObjectClasses as an array of enum values. + static System.Random RNG = new System.Random(); + + + /// + /// Spawn an asteroid of the given class at the given position. + /// + /// The position to spawn the asteroid. + /// The class of the asteroid. -1 picks one at random. + /// The asteroid vessel. + public static Vessel SpawnAsteroid(Vector3d position, int untrackedObjectClassIndex = -1) + { + if (untrackedObjectClassIndex < 0) + { + untrackedObjectClassIndex = RNG.Next(UntrackedObjectClasses.Length); + } + var asteroid = DiscoverableObjectsUtil.SpawnAsteroid(DiscoverableObjectsUtil.GenerateAsteroidName(), GetOrbitForApoapsis2(position), (uint)RNG.Next(), UntrackedObjectClasses[untrackedObjectClassIndex], double.MaxValue, double.MaxValue); + if (asteroid != null && asteroid.vesselRef != null) + { return asteroid.vesselRef; } + else + { return null; } + } + + /// + /// Calculate an orbit that has the specified position as the apoapsis and orbital velocity that matches that of the ground below. + /// This doesn't quite give the correct orbits for some reason, use GetOrbitForApoapsis2 instead. + /// + /// The position of the apoapsis. + /// The orbit. + public static Orbit GetOrbitForApoapsis(Vector3d position) + { + // FIXME this is still giving orbits that are slightly off, e.g., an asteroid field at the KSC is coming out oval instead of round. + // Figure out the orbit of an asteroid with apoapsis at the spawn point and the same velocity as that of the surface under the spawn point. + double latitude, longitude, altitude; + FlightGlobals.currentMainBody.GetLatLonAlt(position, out latitude, out longitude, out altitude); + longitude = (longitude + FlightGlobals.currentMainBody.rotationAngle + 180d) % 360d; // Compensate coordinates for planet rotation then normalise to 0°—360°. + var inclination = Math.Abs(latitude); + var apoapsisAltitude = FlightGlobals.currentMainBody.Radius + altitude; + var velocity = 2d * Math.PI * (FlightGlobals.currentMainBody.Radius + altitude) * Math.Cos(Mathf.Deg2Rad * latitude) / FlightGlobals.currentMainBody.rotationPeriod; + var semiMajorAxis = -FlightGlobals.currentMainBody.gravParameter / (velocity * velocity / 2d - FlightGlobals.currentMainBody.gravParameter / apoapsisAltitude) / 2d; + var eccentricity = apoapsisAltitude / semiMajorAxis - 1d; + var upDirection = (FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitude, longitude, altitude) - FlightGlobals.currentMainBody.transform.position).normalized; + var longitudeOfAscendingNode = (Mathf.Rad2Deg * Mathf.Acos(Vector3.Dot(Vector3.Cross(upDirection, Vector3d.Cross(Vector3d.up, upDirection)).normalized, Vector3.forward)) + longitude + (latitude > 0 ? 0d : 180d)) % 360d; + var argumentOfPeriapsis = latitude < 0d ? 90d : 270d; + var meanAnomalyAtEpoch = Math.PI; + return new Orbit(inclination, eccentricity, semiMajorAxis, longitudeOfAscendingNode, argumentOfPeriapsis, meanAnomalyAtEpoch, Planetarium.GetUniversalTime(), FlightGlobals.currentMainBody); + } + + /// + /// Calculate an orbit that has the specified position as the apoapsis and orbital velocity that matches that of the ground below. + /// This one gives the correct orbit to within around float precision. + /// + /// The position of the apoapsis. + /// The orbit. + public static Orbit GetOrbitForApoapsis2(Vector3d position) + { + double latitude, longitude, altitude; + FlightGlobals.currentMainBody.GetLatLonAlt(position, out latitude, out longitude, out altitude); + longitude = (longitude + FlightGlobals.currentMainBody.rotationAngle + 180d) % 360d; // Compensate coordinates for planet rotation then normalise to 0°—360°. + var orbitVelocity = FlightGlobals.currentMainBody.getRFrmVel(position); + var orbitPosition = position - FlightGlobals.currentMainBody.transform.position; + var orbit = new Orbit(); + orbit.UpdateFromStateVectors(orbitPosition.xzy, orbitVelocity.xzy, FlightGlobals.currentMainBody, Planetarium.GetUniversalTime()); + return orbit; + } + + /// + /// Debugging: Compare orbit of current vessel with that of the generated ones. + /// + public static void CheckOrbit() + { + if (FlightGlobals.ActiveVessel == null) { return; } + var v = FlightGlobals.ActiveVessel; + var orbit = FlightGlobals.ActiveVessel.orbit; + Debug.Log($"DEBUG orbit.pos: {orbit.pos}, orbit.vel: {orbit.vel}"); + Debug.Log($"DEBUG pos: {(v.CoM - (Vector3d)v.mainBody.transform.position).xzy}, vel: {v.mainBody.getRFrmVel(v.CoM).xzy}"); + Debug.Log($"DEBUG Δpos: {orbit.pos - ((Vector3d)v.CoM - (Vector3d)v.mainBody.transform.position).xzy}, Δvel: {orbit.vel - v.mainBody.getRFrmVel(v.CoM).xzy}"); + Debug.Log($"DEBUG Current vessel's orbit: inc: {orbit.inclination}, e: {orbit.eccentricity}, sma: {orbit.semiMajorAxis}, lan: {orbit.LAN}, argPe: {orbit.argumentOfPeriapsis}, mEp: {orbit.meanAnomalyAtEpoch}"); + orbit = GetOrbitForApoapsis(v.CoM); + Debug.Log($"DEBUG Predicted orbit: inc: {orbit.inclination}, e: {orbit.eccentricity}, sma: {orbit.semiMajorAxis}, lan: {orbit.LAN}, argPe: {orbit.argumentOfPeriapsis}, mEp: {orbit.meanAnomalyAtEpoch}"); + orbit = GetOrbitForApoapsis2(v.CoM); + Debug.Log($"DEBUG Predicted orbit 2: inc: {orbit.inclination}, e: {orbit.eccentricity}, sma: {orbit.semiMajorAxis}, lan: {orbit.LAN}, argPe: {orbit.argumentOfPeriapsis}, mEp: {orbit.meanAnomalyAtEpoch}"); + } + + /// + /// Strip out various modules from the asteroid as they make excessive amounts of GC allocations. + /// This seems only to be possible once the asteroid is active, loaded and unpacked. + /// + public static void CleanOutAsteroid(Vessel asteroid) + { + if (asteroid == null) return; + var mod = asteroid.GetComponent(); + if (mod != null) + { + UnityEngine.Object.Destroy(mod); + } + var modInfo = asteroid.GetComponent(); + if (modInfo != null) + { + UnityEngine.Object.Destroy(modInfo); + } + var modResource = asteroid.GetComponent(); + if (modResource != null) + { + UnityEngine.Object.Destroy(modResource); + } + } + + public static bool IsManagedAsteroid(Vessel asteroid) => + (BDArmorySettings.ASTEROID_RAIN && AsteroidRain.IsManagedAsteroid(asteroid)) || (BDArmorySettings.ASTEROID_FIELD && AsteroidField.IsManagedAsteroid(asteroid)); + } + + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class AsteroidRain : MonoBehaviour + { + #region Fields + public static AsteroidRain Instance; + + bool raining = false; + int numberOfAsteroids; + float altitude; + float radius; + float initialSpeed = -150f; + float initialSpeedVariation = 50f; // random inside sphere added to initial vertical velocity + double spawnRate; + Vector2d geoCoords; + Vector3d spawnPoint; + Vector3d upDirection; + Vector3d refDirection; + int cleaningInProgress; + System.Random RNG; + + Coroutine rainCoroutine; + Coroutine cleanUpCoroutine; + HashSet beingRemoved = new HashSet(); + + // Pooling of asteroids + List asteroidPool; + int lastPoolIndex = 0; + HashSet asteroidNames = new HashSet(); + #endregion + + #region Monobehaviour functions + /// + /// Initialisation. + /// + void Awake() + { + if (Instance) + Destroy(Instance); + Instance = this; + + if (RNG == null) + { + RNG = new System.Random(); + } + GameEvents.onGameSceneSwitchRequested.Add(HandleSceneChange); + } + + /// + /// Destructor. + /// + void OnDestroy() + { + Reset(true); + GameEvents.onGameSceneSwitchRequested.Remove(HandleSceneChange); + } + #endregion + + #region Rain functions + public static bool IsRaining() + { + if (Instance == null) return false; + return Instance.raining; + } + + /// + /// Reset the asteroid rain, deactivating all the asteroids. + /// + public void Reset(bool destroyAsteroids = false) + { + raining = false; + StopAllCoroutines(); + beingRemoved.Clear(); + if (asteroidPool != null) + { + foreach (var asteroid in asteroidPool) + { + if (asteroid == null || asteroid.gameObject == null) continue; + if (asteroid.gameObject.activeInHierarchy) { asteroid.gameObject.SetActive(false); } + if (destroyAsteroids) { Destroy(asteroid); } + } + if (destroyAsteroids) { asteroidPool.Clear(); } + } + UpdatePooledAsteroidNames(); + cleaningInProgress = 0; + } + + /// + /// Handle scene changes. + /// + /// The scenes changed from and to. + public void HandleSceneChange(GameEvents.FromToAction fromTo) + { + if (fromTo.from == GameScenes.FLIGHT) + { + Reset(); + if (fromTo.to != GameScenes.FLIGHT) + { + if (asteroidPool != null) + { + foreach (var asteroid in asteroidPool) + { if (asteroid != null) Destroy(asteroid); } + asteroidPool.Clear(); + } + } + } + } + + /// + /// Update the asteroid rain settings. + /// + public void UpdateSettings(bool warning = false) + { + altitude = BDArmorySettings.ASTEROID_RAIN_ALTITUDE * 100f; // Convert to m. + if (!(BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID && BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD)) radius = BDArmorySettings.ASTEROID_RAIN_RADIUS * 1000f; // Convert to m. + numberOfAsteroids = BDArmorySettings.ASTEROID_RAIN_NUMBER; + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + if (spawnPoint.magnitude > PhysicsRangeExtender.GetPRERange()) + { + if (warning) { BDACompetitionMode.Instance.competitionStatus.Add($"Asteroid Rain spawning point is {spawnPoint.magnitude / 1000:F1}km away, which is more than the PRE range away. Spawning here instead."); } + geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(Vector3d.zero); + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + } + upDirection = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + 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; + var b = initialSpeed; + var c = altitude; + var timeToFall = (-b - Math.Sqrt(b * b - 4f * a * c)) / 2f / a; + spawnRate = numberOfAsteroids / timeToFall * Time.fixedDeltaTime; + 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. + } + + /// + /// Spawn asteroid rain. + /// + public void SpawnRain(Vector3d geoCoords) + { + Reset(); + this.geoCoords = new Vector2d(geoCoords.x, geoCoords.y); + if (BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID && BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD) this.radius = 1000f; // Initial radius for spawn in case we don't have any planes yet. + UpdateSettings(true); + StartCoroutine(StartRain()); + } + + /// + /// Start raining asteroids. + /// + IEnumerator StartRain() + { + Debug.Log($"[BDArmory.Asteroids]: Spawning asteroid rain with {numberOfAsteroids} asteroids, altitude {altitude / 1000f}km and radius {radius / 1000f}km at coordinates ({geoCoords.x:F4}, {geoCoords.y:F4})."); + + BDACompetitionMode.Instance.competitionStatus.Add($"Spawning Asteroid Rain with ~{numberOfAsteroids} asteroids from an altitude of {altitude}m, please be patient."); + yield return new WaitForEndOfFrame(); // Wait for the message to display. + yield return new WaitForFixedUpdate(); + SetupAsteroidPool(Mathf.RoundToInt(numberOfAsteroids * 1.1f)); // Give ourselves a 10% buffer. + + rainCoroutine = StartCoroutine(Rain()); + cleanUpCoroutine = StartCoroutine(CleanUp(0.5f)); + } + + /// + /// Rain asteroids. + /// + IEnumerator Rain() + { + raining = true; + var spawnRateTracker = 0d; + var waitForFixedUpdate = new WaitForFixedUpdate(); + var relocationTimer = Time.time; + var relocationTimeout = 2d; + while (raining) + { + if ( + cleaningInProgress > 0 || // Don't spawn anything if asteroids are getting added to the pool. + (TimeWarp.WarpMode == TimeWarp.Modes.HIGH && TimeWarp.CurrentRate > 1) // Or we're in high warp. + ) + { + yield return waitForFixedUpdate; + continue; + } + while (spawnRateTracker > 1d) + { + var asteroid = GetAsteroid(); + if (asteroid != null) + { + asteroid.Landed = false; + asteroid.Splashed = false; + var direction = (Quaternion.AngleAxis((float)RNG.NextDouble() * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; + var x = (float)RNG.NextDouble(); + var distance = BDAMath.Sqrt(1f - x) * radius; + StartCoroutine(RepositionWhenReady(asteroid, direction * distance)); + } + --spawnRateTracker; + } + yield return waitForFixedUpdate; + if (Time.time - relocationTimer > relocationTimeout) + { + UpdateRainLocation(); + relocationTimer = Time.time; + } + spawnRateTracker += spawnRate; + } + } + + void UpdateRainLocation() + { + if (BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID) + { + Vector3 averagePosition = spawnPoint; + float maxSqrDistance = 0; + int count = 1; + foreach (var vessel in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).Select(wm => wm.vessel)) + { + averagePosition += vessel.transform.position; + ++count; + } + averagePosition /= (float)count; + geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(averagePosition); + if (BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD) + { + 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 ? 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.DEBUG_OTHER) Debug.Log("[BDArmory.Asteroids]: Planet has rotated significantly, updating settings."); + } + } + UpdateSettings(); + } + + /// + /// Reposition the asteroid to the desired position once it's properly spawned. + /// + /// The asteroid. + /// The offset from the central spawn point. + /// + IEnumerator RepositionWhenReady(Vessel asteroid, Vector3 offset) + { + var wait = new WaitForFixedUpdate(); + asteroid.gameObject.SetActive(true); + while (asteroid != null && (asteroid.packed || !asteroid.loaded || asteroid.rootPart.Rigidbody == null)) yield return wait; + if (asteroid != null) + { + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + var position = spawnPoint + offset; + position += (altitude - BodyUtils.GetRadarAltitudeAtPos(position, false)) * upDirection; + asteroid.transform.position = position; + asteroid.SetWorldVelocity(initialSpeed * upDirection + initialSpeedVariation * UnityEngine.Random.insideUnitSphere); + // Apply a gaussian random torque to the asteroid. + asteroid.rootPart.Rigidbody.angularVelocity = Vector3.zero; + asteroid.rootPart.Rigidbody.AddTorque(VectorUtils.GaussianVector3d(Vector3d.zero, 300 * Vector3d.one), ForceMode.Acceleration); + } + } + + /// + /// Clean-up routine. + /// Checks every interval for asteroids that are going to impact soon and schedules them for removal. + /// + /// The interval to check. Keep low for accuracy, but not too low for performance. + IEnumerator CleanUp(float interval) + { + var wait = new WaitForSeconds(interval); // Don't bother checking too often. + while (raining) + { + foreach (var asteroid in asteroidPool) + { + if (asteroid == null || !asteroid.gameObject.activeInHierarchy || asteroid.packed || !asteroid.loaded || asteroid.rootPart.Rigidbody == null) continue; + var timeToImpact = (float)((asteroid.radarAltitude - asteroid.GetRadius()) / asteroid.srfSpeed); // Simple estimate (verticalSpeed doesn't seem to work very well). + if (!beingRemoved.Contains(asteroid) && (timeToImpact < 1.5f * interval || asteroid.LandedOrSplashed)) + { + StartCoroutine(RemoveAfterDelay(asteroid, timeToImpact - TimeWarp.fixedDeltaTime)); + } + } + yield return wait; + } + } + + /// + /// Remove the asteroid after the delay, generating an explosion at the point it disappears from. + /// + /// The asteroid to remove. + /// The delay to wait before removing the asteroid. + IEnumerator RemoveAfterDelay(Vessel asteroid, float delay) + { + beingRemoved.Add(asteroid); + yield return new WaitForSeconds(delay); + if (asteroid != null) + { + // Make an explosion where the impact is going to be and remove the asteroid before it actually impacts, so that KSP doesn't destroy it (regenerating the asteroid is expensive). + var impactPosition = asteroid.transform.position + asteroid.srf_velocity * TimeWarp.fixedDeltaTime; + FXMonger.ExplodeWithDebris(impactPosition, Math.Pow(asteroid.GetTotalMass(), 0.3d) / 12d, null); + asteroid.transform.position += 10000f * upDirection; // Put the asteroid where it won't immediately die on re-activating, since we apparently can't reposition it immediately upon activation. + asteroid.SetWorldVelocity(Vector3.zero); // Also, reset its velocity. + asteroid.Landed = false; + asteroid.Splashed = false; + asteroid.gameObject.SetActive(false); + beingRemoved.Remove(asteroid); + } + else + { if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.Asteroids]: Asteroid {asteroid.vesselName} is null, unable to remove."); } + } + #endregion + + #region Pooling + /// + /// Wait until the collider bounds have been generated, then remove various modules from the asteroid for performance reasons. + /// + /// The asteroid to clean. + IEnumerator CleanAsteroid(Vessel asteroid) + { + ++cleaningInProgress; + var wait = new WaitForFixedUpdate(); + asteroid.gameObject.SetActive(true); + var startTime = Time.time; + while (asteroid != null && Time.time - startTime < 10 && (asteroid.packed || !asteroid.loaded || asteroid.rootPart.GetColliderBounds().Length < 2)) yield return wait; + if (asteroid != null) + { + if (Time.time - startTime >= 10) Debug.LogWarning($"[BDArmory.Asteroids]: Timed out waiting for colliders on {asteroid.vesselName} to be generated."); + AsteroidUtils.CleanOutAsteroid(asteroid); + asteroid.rootPart.crashTolerance = float.MaxValue; // Make the asteroids nigh indestructible. + asteroid.rootPart.maxTemp = float.MaxValue; + asteroid.gameObject.SetActive(false); + } + --cleaningInProgress; + } + + /// + /// Set up the asteroid pool to contain at least count asteroids. + /// + /// The minimum number of asteroids in the pool. + void SetupAsteroidPool(int count) + { + var preRange = PhysicsRangeExtender.GetPRERange(); + if (asteroidPool == null) { asteroidPool = []; } + else { asteroidPool = asteroidPool.Where(a => a != null && a.transform.position.magnitude < preRange).ToList(); } + foreach (var asteroid in asteroidPool) + { + if (asteroid.FindPartModuleImplementing() != null || asteroid.FindPartModuleImplementing() != null || asteroid.FindPartModuleImplementing() != null) // We don't use the VesselModuleRegistry here as we'd need to force update it for each asteroid anyway. + { StartCoroutine(CleanAsteroid(asteroid)); } + } + if (count > asteroidPool.Count) { AddAsteroidsToPool(count - asteroidPool.Count); } + } + + /// + /// Replace an asteroid at position i in the pool. + /// + /// + void ReplacePooledAsteroid(int 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) + { + StartCoroutine(CleanAsteroid(asteroid)); + asteroidPool[i] = asteroid; + } + } + + /// + /// Add a number of asteroids to the pool. + /// + /// + void AddAsteroidsToPool(int count) + { + Debug.Log($"[BDArmory.Asteroids]: Increasing asteroid pool size to {asteroidPool.Count + count} from {asteroidPool.Count}."); + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + upDirection = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + 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 = (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) + { + StartCoroutine(CleanAsteroid(asteroid)); + asteroidPool.Add(asteroid); + } + } + UpdatePooledAsteroidNames(); + } + + /// + /// Get an asteroid from the pool. + /// + /// An asteroid vessel. + Vessel GetAsteroid() + { + // Start at the last index returned and cycle round for efficiency. This makes this a typically O(1) seek operation. + for (int i = lastPoolIndex + 1; i < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) + { + ReplaceNullPooledAsteroids(); + } + if (!asteroidPool[i].gameObject.activeInHierarchy) + { + lastPoolIndex = i; + return asteroidPool[i]; + } + } + for (int i = 0; i < lastPoolIndex + 1; ++i) + { + if (asteroidPool[i] == null) + { + ReplaceNullPooledAsteroids(); + } + if (!asteroidPool[i].gameObject.activeInHierarchy) + { + lastPoolIndex = i; + return asteroidPool[i]; + } + } + + var size = (int)(asteroidPool.Count * 1.1) + 1; // Grow by 10% + 1 + AddAsteroidsToPool(size - asteroidPool.Count); + + return asteroidPool[asteroidPool.Count - 1]; // Return the last entry in the pool + } + + /// + /// Scan for and replace null asteroids in one go to reduce delays due to the CollisionManager. + /// + void ReplaceNullPooledAsteroids() + { + BDACompetitionMode.Instance.competitionStatus.Add("Replacing lost asteroids."); + for (int i = 0; i < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) + { ReplacePooledAsteroid(i); } + } + UpdatePooledAsteroidNames(); + } + + /// + /// Update the asteroid names hashset so we can know whether it's managed or not. + /// + void UpdatePooledAsteroidNames() + { + if (asteroidPool == null) asteroidNames.Clear(); + else asteroidNames = asteroidPool.Select(a => a.vesselName).ToHashSet(); + } + + /// + /// Is the vessel a managed asteroid. + /// + /// + public static bool IsManagedAsteroid(Vessel vessel) + { + if (Instance == null || Instance.asteroidNames == null) return false; + return Instance.asteroidNames.Contains(vessel.vesselName); + } + + /// + /// Run some debugging checks on the pooled asteroids. + /// + public void CheckPooledAsteroids() + { + if (asteroidPool == null) { Debug.Log("DEBUG Asteroid pool is not set up yet."); return; } + int activeCount = 0; + int withModulesCount = 0; + int withCollidersCount = 0; + double minMass = double.MaxValue; + double maxMass = 0d; + double minRadius = double.MaxValue; + double maxRadius = 0d; + for (int i = 0; i < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) { Debug.Log($"DEBUG asteroid at position {i} is null"); continue; } + Debug.Log($"{asteroidPool[i].vesselName} has mass {asteroidPool[i].GetTotalMass()}"); + if (asteroidPool[i].gameObject != null) + { + if (asteroidPool[i].gameObject.activeInHierarchy) + ++activeCount; + maxMass = Math.Max(maxMass, asteroidPool[i].GetTotalMass()); + minMass = Math.Min(minMass, asteroidPool[i].GetTotalMass()); + maxRadius = Math.Max(maxRadius, asteroidPool[i].GetRadius()); + minRadius = Math.Min(minRadius, asteroidPool[i].GetRadius()); + if (asteroidPool[i].FindPartModuleImplementing() != null || asteroidPool[i].FindPartModuleImplementing() != null || asteroidPool[i].FindPartModuleImplementing() != null) ++withModulesCount; + if (asteroidPool[i].rootPart != null && asteroidPool[i].rootPart.GetColliderBounds().Length > 1) ++withCollidersCount; + } + } + Debug.Log($"DEBUG {activeCount} asteroids active of {asteroidPool.Count}, mass range: {minMass}t — {maxMass}t, radius range: {minRadius}—{maxRadius}, #withModules: {withModulesCount}, #withCollidersCount: {withCollidersCount}, cleaning in progress: {cleaningInProgress}"); + } + #endregion + } + + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class AsteroidField : MonoBehaviour + { + #region Fields + public static AsteroidField Instance; + Vessel[] asteroids; // We use both an array of asteroids that are currently in use and a pool of asteroids for quick re-use between rounds. + + float altitude; + float radius; + Vector2d geoCoords; + Vector3d spawnPoint; + Vector3d upDirection; + Vector3d refDirection; + int cleaningInProgress; + bool floating; + bool inOrbit; + Coroutine floatingCoroutine; + public Vector3d anomalousAttraction = Vector3d.zero; + int vesselCount = 0; + Vector3d averageVelocity = default; + System.Random RNG; + + // Pooling of asteroids + List asteroidPool = []; + int lastPoolIndex = 0; + int maxPoolSize = int.MaxValue; + HashSet asteroidNames = []; + readonly Dictionary attractionFactors = []; + #endregion + + void Awake() + { + if (Instance) + Destroy(Instance); + Instance = this; + + if (RNG == null) + { + RNG = new System.Random(); + } + } + + void OnDestroy() + { + Reset(true); + } + + /// + /// Reset the asteroid field, deactivating all the asteroids. + /// + public void Reset(bool destroyAsteroids = false) + { + floating = false; + StopAllCoroutines(); + + asteroids = null; // Clear the current array of asteroids. + if (asteroidPool != null) + { + foreach (var asteroid in asteroidPool) + { + if (asteroid == null || asteroid.gameObject == null) continue; + if (asteroid.gameObject.activeInHierarchy) { asteroid.gameObject.SetActive(false); } + if (asteroid.mainBody != FlightGlobals.currentMainBody) { Destroy(asteroid); } // Destroy asteroids that have changes SoI as they don't reset properly. + if (destroyAsteroids) { Destroy(asteroid); } + } + if (destroyAsteroids) { asteroidPool.Clear(); } + } + UpdatePooledAsteroidNames(); + cleaningInProgress = 0; + } + + /// + /// Spawn an asteroid field. + /// + /// The number of asteroids in the field. + /// The maximum altitude AGL of the field, minimum altitude AGL is 50m. + /// The radius of the field from the spawn point. + /// The spawn point (centre) of the field. + public void SpawnField(int numberOfAsteroids, float altitude, float radius, Vector2d geoCoords) + { + Reset(); + + radius *= 1000f; // Convert to m. + this.altitude = altitude; + this.radius = radius; + this.geoCoords = geoCoords; + UpdateSpawnPoint(); // Adjust spawn point if we're in orbit. + if (spawnPoint.magnitude > PhysicsRangeExtender.GetPRERange()) + { + var message = $"Asteroid field location is beyond the PRE range, unable to spawn asteroids."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogError($"[BDArmory.Asteroids]: {message}"); + return; + } + if (vesselCount == 0) averageVelocity = Math.Sqrt(FlightGlobals.getGeeForceAtPosition(spawnPoint, FlightGlobals.currentMainBody).magnitude * (FlightGlobals.currentMainBody.Radius + altitude)) * FlightGlobals.currentMainBody.getRFrmVel(spawnPoint).normalized; + upDirection = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + refDirection = Math.Abs(Vector3.Dot(Vector3.up, upDirection)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. + + { // Logging + var message = $"Spawning asteroid field with {numberOfAsteroids} asteroids with height {(altitude < 1000 ? $"{altitude}m" : $"{altitude / 1000}km")} and radius {radius / 1000f}km at coordinate ({geoCoords.x:F4}, {geoCoords.y:F4})"; + Debug.Log($"[BDArmory.Asteroids]: {message}."); + BDACompetitionMode.Instance.competitionStatus.Add($"{message}, please be patient."); + } + + StartCoroutine(SpawnField(numberOfAsteroids)); + } + + IEnumerator SpawnField(int numberOfAsteroids) + { + var wait = new WaitForFixedUpdate(); + yield return new WaitForEndOfFrame(); // Give the message a chance to show. + yield return wait; // And wait for the next update before doing anything. + FloatingOrigin.SetOffset(spawnPoint); // Re-centre the origin on the spawn point. + yield return wait; // Wait once more to let KSP update stuff for the origin shift. + SetupAsteroidPool(numberOfAsteroids); + yield return new WaitWhileFixed(() => cleaningInProgress > 0); // Wait until the asteroid pool is finished being set up. + UpdateSpawnPoint(); // Refresh the spawn point as it could have drifted significantly in orbit while we were waiting. + asteroids = new Vessel[numberOfAsteroids]; + for (int i = 0; i < asteroids.Length; ++i) + { + var direction = (Quaternion.AngleAxis((float)RNG.NextDouble() * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; + var x = (float)RNG.NextDouble(); + var distance = BDAMath.Sqrt(1f - x) * radius; + var height = inOrbit ? + altitude + (RNG.NextDouble() - 0.5) * radius : // radius/2 vertical spread around the altitude + RNG.NextDouble() * (altitude - 50f) + 50f; // From altitude down to 50m AGL + var position = spawnPoint + direction * distance; + if (inOrbit) position += (height - altitude) * upDirection; + else position += (height - BodyUtils.GetRadarAltitudeAtPos(position)) * upDirection; + var asteroid = GetAsteroid(); + if (asteroid != null) + { + asteroid.SetPosition(position); + Vector3d worldVelocity = inOrbit ? averageVelocity : Vector3d.zero; + if (BDKrakensbane.IsActive) worldVelocity -= BDKrakensbane.FrameVelocityV3f; // SetWorldVelocity does not take Krakensbane into account. + asteroid.SetWorldVelocity(worldVelocity); + asteroid.gameObject.SetActive(true); + StartCoroutine(SetInitialRotation(asteroid)); + asteroids[i] = asteroid; + } + else Debug.LogWarning($"[BDArmory.Asteroids]: Failed to spawn asteroid {i + 1} of {asteroids.Length}."); + } + floatingCoroutine = StartCoroutine(Float()); + } + + /// + /// Apply forces to counteract gravity, decay overall motion and add Brownian noise. + /// + IEnumerator Float() + { + var wait = new WaitForFixedUpdate(); + floating = true; + Vector3d offset; + Vector3 averagePosition; + float factor = 0; + float repulseTimer = Time.time; + float radiusSqr = radius * radius; + float Rscale = 0.04f * radiusSqr; // 20% of the field radius. + Vector3d rVelLimit = 1500 * Vector3d.one; + while (floating) + { + for (int i = 0; i < asteroids.Length; ++i) + { + if (asteroids[i] == null || asteroids[i].packed || !asteroids[i].loaded || asteroids[i].rootPart.Rigidbody == null) continue; + var nudge = (inOrbit ? 200f : 100f) * UnityEngine.Random.insideUnitSphere; + Vector3d anomalousAttractionHOS = Vector3d.zero; + if (BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION) + { + anomalousAttraction = Vector3d.zero; + foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value)) + { + if (weaponManager == null) continue; + offset = weaponManager.vessel.transform.position - asteroids[i].transform.position; + // factor = 1f - (float)offset.sqrMagnitude / 1e6f; // 1-(r/1000)^2 attraction, i.e., asteroids within 1km. + var R = (float)offset.sqrMagnitude / Rscale; + factor = 0.25f + 3f * R * (1f - R); // 0.25 at 0m, 1 at 707m, 0 at 1.038km (for 1km Rscale) (reduced attraction at close range to avoid inescapable asteroids). + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 70) // Punish immobile turrets + { + float twr = VesselModuleRegistry.GetModuleEngines(weaponManager.vessel).Where(e => e != null && e.allowRestart && !e.flameout && !e.independentThrottle).Sum(e => e.MaxThrustOutputVac(true)) / (weaponManager.vessel.GetTotalMass() * (float)PhysicsGlobals.GravitationalAcceleration); + factor *= 1f / Mathf.Clamp(twr, 0.01f, 1f); + } + if (factor > 0) anomalousAttraction += factor * attractionFactors[asteroids[i].vesselName] * offset.normalized; + } + anomalousAttraction *= BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH; + } + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Count > 0 && BDArmorySettings.HOS_ASTEROID) + { + foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value)) + { + if (weaponManager == null) continue; + if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(weaponManager.vessel.GetName())) + { + offset = weaponManager.vessel.transform.position - asteroids[i].transform.position; + factor = Vector3.Dot(asteroids[i].Velocity(), weaponManager.vessel.Velocity()) < 0 ? (float)(asteroids[i].Velocity() - weaponManager.vessel.Velocity()).magnitude : 1; + if (offset.sqrMagnitude < 6250000) anomalousAttractionHOS += factor * attractionFactors[asteroids[i].vesselName] * offset.normalized; + } + } + } + var force = nudge + anomalousAttraction + anomalousAttractionHOS; + if (inOrbit) + { // Orbiting asteroids don't need anti-grav forces. + if (vesselCount > 0) + { + var relVel = asteroids[i].Velocity() - averageVelocity; + var relVelSqr = relVel.sqrMagnitude; + force -= Math.Min(0.1 + 4e-7 * relVelSqr, 1) * relVel; // Reduce motion to average velocity of vessels (0.1 + 4e-7 v^2 should be stable below 1500m/s). + } + } + else + force -= FlightGlobals.getGeeForceAtPosition(asteroids[i].transform.position) + 0.1f * asteroids[i].Velocity(); // Float and reduce motion. + asteroids[i].rootPart.Rigidbody.AddForce(force * TimeWarp.CurrentRate, ForceMode.Acceleration); + } + if (Time.time - repulseTimer > 1) // Once per second repulse nearby asteroids from each other to avoid them sticking, and attract them to vessel centroid if outside of radius. Not too often since it's O(N^2). This might be more performant using an OverlapSphere. + { + averagePosition = Vector3.zero; + averageVelocity = Vector3.zero; + vesselCount = 0; + foreach (var vessel in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).Select(wm => wm.vessel)) + { + averagePosition += vessel.CoM; + averageVelocity += vessel.Velocity(); + ++vesselCount; + } + if (vesselCount > 0) + { + averagePosition /= vesselCount; + averageVelocity /= vesselCount; + geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(averagePosition); + altitude = (float)FlightGlobals.currentMainBody.GetAltitude(averagePosition); + } + + for (int i = 0; i < asteroids.Length - 1; ++i) + { + if (asteroids[i] == null || asteroids[i].packed || !asteroids[i].loaded || asteroids[i].rootPart.Rigidbody == null) continue; + + if (vesselCount > 0) + { + // Attract to vessel centroid if in the outer region (90%) of the asteroid field. + if ((asteroids[i].CoM - averagePosition).sqrMagnitude > 0.81f * radiusSqr) + { + float centroidFactor = TimeWarp.CurrentRate * Mathf.Min((asteroids[i].CoM - averagePosition).sqrMagnitude - 0.81f * radiusSqr, radiusSqr) * 5e-7f; + Vector3 radialDir = asteroids[i].CoM - averagePosition + 0.5f * radius * UnityEngine.Random.onUnitSphere; // Add 1/2 field radius noise to avoid clustering. + Vector3 radialVel = asteroids[i].Velocity() - averageVelocity; + if (Vector3.Dot(radialDir, radialVel) < 0) centroidFactor *= 0.25f; // Less of a push when heading towards the centroid to avoid pinballing. + Vector3 attraction = -centroidFactor * radialDir; + asteroids[i].rootPart.Rigidbody.AddForce(attraction, ForceMode.Acceleration); + } + } + + // Repulse from nearby asteroids + for (int j = i + 1; j < asteroids.Length; ++j) + { + if (asteroids[j] == null || asteroids[j].packed || !asteroids[j].loaded || asteroids[j].rootPart.Rigidbody == null) continue; + var separation = asteroids[i].CoM - asteroids[j].CoM; + var sepSqr = separation.sqrMagnitude; + var proximityFactor = asteroids[i].GetRadius() + asteroids[j].GetRadius(); + proximityFactor *= (inOrbit ? 10 : BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION ? 100 : 4) * proximityFactor; // Without anomalous attraction, they don't get stirred up much, so they don't need as much repulsion. In space they don't need much repulsion either. + if (sepSqr < proximityFactor) + { + var repulseAmount = TimeWarp.CurrentRate * BDAMath.Sqrt(proximityFactor - sepSqr) * separation.normalized; + asteroids[i].rootPart.Rigidbody.AddForce(repulseAmount, ForceMode.Acceleration); + asteroids[j].rootPart.Rigidbody.AddForce(-repulseAmount, ForceMode.Acceleration); + } + } + + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 70 && BDACompetitionMode.Instance.competitionIsActive) + { // Kill off any vessels that don't have propulsion and are significantly beyond the distance to the vessel centroid. + foreach (var vessel in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).Select(wm => wm.vessel)) + { + var oai = vessel.ActiveController().OrbitalAI; + if ((oai == null || !oai.pilotEnabled || !oai.HasPropulsion) && (vessel.CoM - averagePosition).sqrMagnitude > 2 * radiusSqr) // Beyond sqrt(2) field radius. + StartCoroutine(BDACompetitionMode.Instance.DelayedGMKill(vessel, BDArmorySettings.COMPETITION_GM_KILL_TIME, " crippled and significantly beyond asteroid field range. Terminated by GM.")); + } + } + } + repulseTimer = Time.time; + } + yield return wait; + } + } + + /// + /// Set the initial rotation of the asteroid. + /// + /// + IEnumerator SetInitialRotation(Vessel asteroid) + { + var wait = new WaitForFixedUpdate(); + while (asteroid != null && asteroid.gameObject.activeInHierarchy && (asteroid.packed || !asteroid.loaded || asteroid.rootPart.Rigidbody == null)) yield return wait; + if (asteroid != null && asteroid.gameObject.activeInHierarchy) + { + asteroid.rootPart.Rigidbody.angularVelocity = Vector3.zero; + asteroid.rootPart.Rigidbody.AddTorque(VectorUtils.GaussianVector3d(Vector3d.zero, 50 * Vector3d.one), ForceMode.Acceleration); // Apply a gaussian random torque to each asteroid. + } + } + + void UpdateSpawnPoint() + { + var minSafeAltitude = FlightGlobals.currentMainBody.MinSafeAltitude(); + inOrbit = altitude >= minSafeAltitude; + if (inOrbit) // If we're in orbit, use the centroid of existing craft for the spawn point instead of the asked for one, as that very quickly goes out of range. + { + var averagePosition = Vector3.zero; + averageVelocity = Vector3.zero; + vesselCount = 0; + LoadedVesselSwitcher.Instance.UpdateList(); + foreach (var vessel in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).Select(wm => wm.vessel)) + { + averagePosition += vessel.CoM; + averageVelocity += vessel.Velocity(); + ++vesselCount; + } + if (vesselCount > 0) + { + averagePosition /= vesselCount; + averageVelocity /= vesselCount; + geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(averagePosition); + altitude = (float)FlightGlobals.currentMainBody.GetAltitude(averagePosition); + } + } + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + } + + #region Pooling + /// + /// Wait until the collider bounds have been generated, then remove various modules from the asteroid for performance reasons. + /// + /// The asteroid to clean. + IEnumerator CleanAsteroid(Vessel asteroid) + { + ++cleaningInProgress; + var wait = new WaitForFixedUpdate(); + asteroid.gameObject.SetActive(true); + var startTime = Time.time; + while (asteroid != null && Time.time - startTime < 10 && (asteroid.packed || !asteroid.loaded || asteroid.rootPart.GetColliderBounds().Length < 2)) yield return wait; + if (asteroid != null) + { + if (Time.time - startTime >= 10) Debug.LogWarning($"[BDArmory.Asteroids]: Timed out waiting for colliders on {asteroid.vesselName} to be generated."); + AsteroidUtils.CleanOutAsteroid(asteroid); + asteroid.rootPart.crashTolerance = float.MaxValue; // Make the asteroids nigh indestructible. + asteroid.rootPart.maxTemp = float.MaxValue; + asteroid.gameObject.SetActive(false); + } + --cleaningInProgress; + } + + /// + /// Set up the asteroid pool to contain at least count asteroids. + /// + /// The minimum number of asteroids in the pool. + void SetupAsteroidPool(int count) + { + // First lay out the existing asteroids in "safe" positions. + asteroidPool = asteroidPool.Where(a => a != null).ToList(); + foreach (var asteroid in asteroidPool) asteroid.gameObject.SetActive(false); + LayoutAsteroids(); + // Then make sure they're cleaned out. + foreach (var asteroid in asteroidPool) + { + if (asteroid.FindPartModuleImplementing() != null || asteroid.FindPartModuleImplementing() != null || asteroid.FindPartModuleImplementing() != null) // We don't use the VesselModuleRegistry here as we'd need to force update it for each asteroid anyway. + { StartCoroutine(CleanAsteroid(asteroid)); } + } + // Finally, add more if needed. + maxPoolSize = 2 * count; // If we need more than this, then something has broken. + if (count > asteroidPool.Count) { AddAsteroidsToPool(count - asteroidPool.Count); } + } + + /// + /// Replace an asteroid at position i in the pool. + /// + /// + void ReplacePooledAsteroid(int 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) + { + StartCoroutine(CleanAsteroid(asteroid)); + asteroidPool[i] = asteroid; + } + } + + /// + /// Add a number of asteroids to the pool. + /// + /// + void AddAsteroidsToPool(int count) + { + Debug.Log($"[BDArmory.Asteroids]: Increasing asteroid pool size to {asteroidPool.Count + count} from {asteroidPool.Count}."); + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + upDirection = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + 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) + { + int j = asteroidPool.Count + 1; + var direction = (Quaternion.AngleAxis(j / 60f * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; // 60 asteroids per layer of the spiral (approx. 100m apart). + var position = spawnPoint + (1e4f + 1e2f * j / 60) * upDirection + 1e3f * direction; // 100m altitude difference per layer of the spiral. + var asteroid = AsteroidUtils.SpawnAsteroid(position); + if (asteroid != null) + { + StartCoroutine(CleanAsteroid(asteroid)); + asteroidPool.Add(asteroid); + } + } + UpdatePooledAsteroidNames(); + } + + void LayoutAsteroids() + { + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + upDirection = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + 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 < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) continue; + 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. + asteroidPool[i].SetPosition(position); + } + } + + /// + /// Get an asteroid from the pool. + /// + /// An asteroid vessel. + Vessel GetAsteroid() + { + // Start at the last index returned and cycle round for efficiency. This makes this a typically O(1) seek operation. + for (int i = lastPoolIndex + 1; i < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) + { + ReplaceNullPooledAsteroids(); + } + if (!asteroidPool[i].gameObject.activeInHierarchy) + { + lastPoolIndex = i; + return asteroidPool[i]; + } + } + for (int i = 0; i < lastPoolIndex + 1; ++i) + { + if (asteroidPool[i] == null) + { + ReplaceNullPooledAsteroids(); + } + if (!asteroidPool[i].gameObject.activeInHierarchy) + { + lastPoolIndex = i; + return asteroidPool[i]; + } + } + + if (asteroidPool.Count >= maxPoolSize) return null; // Something is going wrong with adding asteroids to the pool, don't keep trying to add more. + var size = Math.Min((int)(asteroidPool.Count * 1.1) + 1, maxPoolSize); // Grow by 10% + 1 + AddAsteroidsToPool(size - asteroidPool.Count); + + return asteroidPool[asteroidPool.Count - 1]; // Return the last entry in the pool + } + + /// + /// Scan for and replace null asteroids in one go to reduce delays due to the CollisionManager. + /// + void ReplaceNullPooledAsteroids() + { + BDACompetitionMode.Instance.competitionStatus.Add("Replacing lost asteroids."); + for (int i = 0; i < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) + { ReplacePooledAsteroid(i); } + } + UpdatePooledAsteroidNames(); + } + + /// + /// Update the asteroid names hashset so we can know whether it's managed or not. + /// + void UpdatePooledAsteroidNames() + { + asteroidNames = asteroidPool.Select(a => a.vesselName).ToHashSet(); + UpdateAttractionFactors(); + } + + /// + /// Is the vessel a managed asteroid. + /// + /// + public static bool IsManagedAsteroid(Vessel vessel) + { + if (Instance == null || Instance.asteroidNames == null) return false; + return Instance.asteroidNames.Contains(vessel.vesselName); + } + + /// + /// Update the attraction factors for the asteroids. + /// + void UpdateAttractionFactors() + { + attractionFactors.Clear(); + foreach (var asteroid in asteroidPool) attractionFactors[asteroid.vesselName] = 50f * Mathf.Clamp(4f / Mathf.Log(asteroid.GetRadius() + 3f) - 1f, 0.1f, 2f); + } + + /// + /// Run some debugging checks on the pooled asteroids. + /// Middle click the "Spawn Field Now" button to trigger this. + /// + public void CheckPooledAsteroids() + { + int activeCount = 0; + int withModulesCount = 0; + int withCollidersCount = 0; + double minMass = double.MaxValue; + double maxMass = 0d; + double minRadius = double.MaxValue; + double maxRadius = 0d; + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); + for (int i = 0; i < asteroidPool.Count; ++i) + { + if (asteroidPool[i] == null) { Debug.Log($"DEBUG asteroid at position {i} is null"); continue; } + Debug.Log($"DEBUG {asteroidPool[i].vesselName} has mass {asteroidPool[i].GetTotalMass()} and is {(asteroidPool[i].gameObject.activeInHierarchy ? "active" : "inactive")} at distance {(asteroidPool[i].CoM - spawnPoint).magnitude}m from the spawn point."); + if (asteroidPool[i].gameObject != null) + { + if (asteroidPool[i].gameObject.activeInHierarchy) + ++activeCount; + maxMass = Math.Max(maxMass, asteroidPool[i].GetTotalMass()); + minMass = Math.Min(minMass, asteroidPool[i].GetTotalMass()); + maxRadius = Math.Max(maxRadius, asteroidPool[i].GetRadius()); + minRadius = Math.Min(minRadius, asteroidPool[i].GetRadius()); + if (asteroidPool[i].FindPartModuleImplementing() != null || asteroidPool[i].FindPartModuleImplementing() != null || asteroidPool[i].FindPartModuleImplementing() != null) ++withModulesCount; + if (asteroidPool[i].rootPart != null && asteroidPool[i].rootPart.GetColliderBounds().Length > 1) ++withCollidersCount; + } + } + Debug.Log($"DEBUG {activeCount} asteroids active of {asteroidPool.Count}, mass range: {minMass}t — {maxMass}t, radius range: {minRadius}—{maxRadius}, #withModules: {withModulesCount}, #withCollidersCount: {withCollidersCount}, cleaning in progress: {cleaningInProgress}"); + } + #endregion + } +} \ No newline at end of file diff --git a/BDArmory/GameModes/BattleDamage/BattleDamageHandler.cs b/BDArmory/GameModes/BattleDamage/BattleDamageHandler.cs new file mode 100644 index 000000000..47e7796c2 --- /dev/null +++ b/BDArmory/GameModes/BattleDamage/BattleDamageHandler.cs @@ -0,0 +1,549 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Ammo; +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Modules; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using Expansions.Serenity; + +namespace BDArmory.GameModes +{ + class BattleDamageHandler + { + public static void CheckDamageFX(Part part, float caliber, float penetrationFactor, bool explosivedamage, bool incendiary, string attacker, RaycastHit hitLoc, bool firsthit = true, bool cockpitPen = false, Vector3 colliderLocalHitPoint = default) + { + if (!BDArmorySettings.BATTLEDAMAGE || BDArmorySettings.PAINTBALL_MODE) return; + penetrationFactor = Mathf.Clamp(penetrationFactor, 0.01f, 4f); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) + //if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == -1) + { + if (!BDArmorySettings.ALLOW_ZOMBIE_BD) + { + if (part.vessel.rootPart != null) + { + if (part != part.vessel.rootPart) return; + } + } + } + if (ProjectileUtils.IsIgnoredPart(part)) return; // Ignore ignored parts. + + double damageChance = Mathf.Clamp((BDArmorySettings.BD_DAMAGE_CHANCE * ((1f - part.GetDamagePercentage()) * 10f) * ((penetrationFactor - BDArmorySettings.BD_DAMAGE_PENETRATION) * 0.5f)), 0, 100); //more heavily damaged parts more likely to take battledamage + Vector3 hitPoint = colliderLocalHitPoint == default ? hitLoc.point : hitLoc.collider.transform.TransformPoint(colliderLocalHitPoint); + + if (BDArmorySettings.BD_TANKS) + { + if (part.HasFuel()) + { + var alreadyburning = part.GetComponentInChildren(); + var rubbertank = part.FindModuleImplementing(); + if (rubbertank != null) + { + if (rubbertank.SSTank && part.GetDamagePercentage() > 0.5f) + return; + } + //Debug.Log("[BDHandler] Hit on fueltank. SST = " + rubbertank.SSTank + "; inerting = " + rubbertank.InertTank); + if (penetrationFactor > 1.2) + { + if (alreadyburning != null) + { + if (rubbertank == null || !rubbertank.InertTank) BulletHitFX.AttachFire(hitPoint, part, caliber, attacker); + } + else + { + BulletHitFX.AttachLeak(hitLoc, part, caliber, explosivedamage, incendiary, attacker, rubbertank != null ? rubbertank.InertTank : false, colliderLocalHitPoint); + } + } + } + } + if (BDArmorySettings.BD_FIRES_ENABLED) + { + if (part.isBattery() && part.GetDamagePercentage() < 0.95f) + { + var alreadyburning = part.GetComponentInChildren(); + if (alreadyburning == null) + { + double Diceroll = UnityEngine.Random.Range(0, 100); + if (explosivedamage) + { + Diceroll *= 0.33; + } + if (incendiary) + { + Diceroll *= 0.66; + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Battery Dice Roll: " + Diceroll); + if (Diceroll <= BDArmorySettings.BD_DAMAGE_CHANCE) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker); + } + } + } + } + var Armor = part.FindModuleImplementing(); + if (Armor != null) + { + if (Armor.ignitionTemp > 0) //wooden parts can potentially catch fire + { + if (incendiary) + { + double Diceroll = UnityEngine.Random.Range(0, 100); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Wood part Dice Roll: " + Diceroll); + if (Diceroll <= BDArmorySettings.BD_DAMAGE_CHANCE) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker, 90, surfaceFire: true); + } + } + } + } + //AmmoBins + if (BDArmorySettings.BD_AMMOBINS && part.GetDamagePercentage() < 0.95f) //explosions have penetration of 0.5, should stop explosions phasing though parts from detonating ammo + { + var ammo = part.FindModuleImplementing(); + if (ammo != null) + { + ammo.SourceVessel = attacker; //moving this here so shots that destroy ammoboxes outright still report attacker if 'Ammo Explodes When Destroyed' is enabled + if (penetrationFactor > 1.2) + { + double Diceroll = UnityEngine.Random.Range(0, 100); + if (incendiary) + { + Diceroll *= 0.66; + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Ammo TAC DiceRoll: " + Diceroll + "; needs: " + damageChance); + if (Diceroll <= (damageChance) && part.GetDamagePercentage() < 0.95f) + { + ammo.DetonateIfPossible(); + } + } + if (!ammo.hasDetonated) //hit didn't destroy box + { + ammo.SourceVessel = ammo.vessel.GetName(); + } + } + } + //Propulsion Damage + if (BDArmorySettings.BD_PROPULSION) + { + BattleDamageTracker tracker = part.gameObject.AddOrGetComponent(); + tracker.Part = part; + if (part.isEngine() && part.GetDamagePercentage() < 0.95f) //first hit's free + { + foreach (var engine in part.GetComponentsInChildren()) + { + if (engine.thrustPercentage > BDArmorySettings.BD_PROP_FLOOR) //engines take thrust damage per hit + { + //AP does bonus damage + 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.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: engine thrust: " + engine.thrustPercentage); + engine.PlayFlameoutFX(true); + tracker.oldDamagePercent = part.GetDamagePercentage(); + /* + if (BDArmorySettings.BD_BALANCED_THRUST && !isSRB) //need to poke this more later, not working properly + { + using (List.Enumerator pSym = part.vessel.Parts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + if (pSym.Current != part) + { + if (pSym.Current.isSymmetryCounterPart(part)) + { + foreach (var SymEngine in pSym.Current.GetComponentsInChildren()) + { + SymEngine.thrustPercentage = engine.thrustPercentage; + } + } + } + } + } + */ + } + if (part.GetDamagePercentage() < 0.75f || (part.GetDamagePercentage() < 0.82f && penetrationFactor > 2)) + { + var leak = part.GetComponentInChildren(); + if (leak == null && !tracker.isSRB) //engine isn't a srb + { + BulletHitFX.AttachLeak(hitLoc, part, caliber, explosivedamage, incendiary, attacker, false, colliderLocalHitPoint); + } + } + if (part.GetDamagePercentage() < 0.50f || (part.GetDamagePercentage() < 0.625f && penetrationFactor > 2)) + { + var alreadyburning = part.GetComponentInChildren(); + if (tracker.isSRB) //srbs are steel tubes full of explosives; treat differently + { + if ((explosivedamage || incendiary) && tracker.SRBFuelled) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker); + } + } + else + { + if (alreadyburning == null) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker, -1, 1); + } + } + } + if (part.GetDamagePercentage() < (BDArmorySettings.BD_PROP_FLAMEOUT / 100)) + { + if (engine.EngineIgnited) + { + if (tracker.isSRB && tracker.SRBFuelled) //SRB is lit, and casing integrity fails due to damage; boom + { + if (tracker.SRBFuelled) + { + var Rupture = part.GetComponent(); + if (Rupture == null) Rupture = (ModuleCASE)part.AddModule("ModuleCASE"); + Rupture.CASELevel = 0; + Rupture.DetonateIfPossible(); + } + } + else + { + engine.PlayFlameoutFX(true); + engine.Shutdown(); //kill a badly damaged engine and don't allow restart + engine.allowRestart = false; + } + } + } + } + } + if (BDArmorySettings.BD_INTAKES) //intake damage + { + var intake = part.FindModuleImplementing(); + //if (part.isAirIntake(intake)) instead? or use vesselregistry + if (intake != null) + { + if (tracker.origIntakeArea < 0) + { + tracker.origIntakeArea = intake.area; + } + float HEBonus = 0.7f; + if (explosivedamage) + { + HEBonus = 1.4f; + } + + if (incendiary) + { + HEBonus = 1.1f; + } + intake.intakeSpeed *= (1 - ((tracker.oldDamagePercent - part.GetDamagePercentage()) * HEBonus) * BDArmorySettings.BD_PROP_DAM_RATE); //HE does bonus damage + intake.intakeSpeed = Mathf.Clamp((float)intake.intakeSpeed, 0, 99999); + + 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.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 + { + var gimbal = part.FindModuleImplementing(); + if (gimbal != null) + { + double HEBonus = 1; + if (explosivedamage) + { + HEBonus = 1.4; + } + if (incendiary) + { + HEBonus = 1.25; + } + //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.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Gimbal DiceRoll: " + Diceroll); + if (Diceroll <= (BDArmorySettings.BD_DAMAGE_CHANCE * HEBonus)) + { + gimbal.enabled = false; + gimbal.gimbalRange = 0; + if (incendiary) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker, 20); + } + } + } + } + } + //Aero Damage + if (BDArmorySettings.BD_AEROPARTS && firsthit) + { + float HEBonus = 1; + if (explosivedamage) + { + HEBonus = 2; //explosive rounds blow bigger holes in wings + } + HEBonus *= Mathf.Clamp(penetrationFactor, 0.5f, 1.5f); + float liftDam = ((caliber / 20000) * HEBonus) * BDArmorySettings.BD_LIFT_LOSS_RATE; + if (part.GetComponent() != null) + { + ModuleLiftingSurface wing; + wing = part.GetComponent(); + //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.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) + { + if (part.GetComponent() != null && part.GetDamagePercentage() > 0.125f) + //if ( part.isControlSurface(aileron))? + { + ModuleControlSurface aileron; + aileron = part.GetComponent(); + if (aileron.deflectionLiftCoeff > ((part.mass * 2.5f) + liftDam)) //stock ctrl surface mass/lift ratio is 5 + { + aileron.deflectionLiftCoeff -= liftDam; + } + int Diceroll = (int)UnityEngine.Random.Range(0f, 100f); + if (explosivedamage) + { + HEBonus = 1.2f; + } + if (incendiary) + { + HEBonus = 1.1f; + } + if (Diceroll <= (BDArmorySettings.BD_DAMAGE_CHANCE * HEBonus)) + { + if (aileron.actuatorSpeed > 3) + { + aileron.actuatorSpeed /= 2; + aileron.authorityLimiter /= 2; + aileron.ctrlSurfaceRange /= 2; + if (Diceroll <= ((BDArmorySettings.BD_DAMAGE_CHANCE * HEBonus) / 2)) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker, 10); + } + } + else + { + aileron.actuatorSpeed = 0; + aileron.authorityLimiter = 0; + aileron.ctrlSurfaceRange = 0; + } + } + } + } + } + //Subsystems + if (BDArmorySettings.BD_SUBSYSTEMS && firsthit) + { + double Diceroll = UnityEngine.Random.Range(0, 100); + bool subsysCrit = false; + 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 + { + ModuleReactionWheel SAS; //critical hit to SAS reduces torque. Don't ask how a damaged Gyro functions correctly. + SAS = part.GetComponent(); + SAS.authorityLimiter = Mathf.Min(SAS.authorityLimiter, part.GetDamagePercentage());//SAS can ge clamped to less than full if more SAS than legal in RWP, so don't increase clamped SAS if they take a glancing nick + //part.RemoveModule(SAS); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleRadar radar; //would need to mod detection curve to degrade performance on hit + radar = part.GetComponent(); //otoh, radars kinda fragile, probably wouldn't work with a chunk of it missing... + part.RemoveModule(radar); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleAlternator alt; //damaging alternator is probably just petty. Could reduce output per hit + alt = part.GetComponent(); + part.RemoveModule(alt); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleAnimateGeneric anim; + anim = part.GetComponent(); // reduce anim speed + anim.animSpeed *= 0.9f; + //part.RemoveModule(anim); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleDecouple stage; + stage = part.GetComponent(); //decouplers decouple + stage.Decouple(); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleECMJammer ecm; + ecm = part.GetComponent(); //could reduce ecm strngth/rcs modifier, but ECM equipment also probably fragile + part.RemoveModule(ecm); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleGenerator gen; + gen = part.GetComponent(); + gen.efficiency = part.GetDamagePercentage(); //generators produce reduced output as they take daamge + //part.RemoveModule(gen); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleResourceConverter isru; + isru = part.GetComponent(); //converters produce reduced output as they take daamge + isru.EfficiencyBonus = part.GetDamagePercentage(); + if (part.GetDamagePercentage() < 0.5f) part.RemoveModule(isru); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleTurret turret; + turret = part.GetComponent(); //could reduce traverse speed, range per hit + turret.yawSpeedDPS *= part.GetDamagePercentage(); + turret.pitchSpeedDPS *= 0.9f; + //part.RemoveModule(turret); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleRoboticRotationServo servo; + servo = part.GetComponent(); + servo.maxMotorOutput *= part.GetDamagePercentage(); + //part.RemoveModule(turret); + subsysCrit = true; + } + if (part.GetComponent() != null) + { + ModuleRoboticServoHinge hinge; + hinge = part.GetComponent(); + hinge.maxMotorOutput *= part.GetDamagePercentage(); + //part.RemoveModule(turret); + subsysCrit = true; + } + //piston/rotor? + if (part.GetComponent() != null) + { + ModuleTargetingCamera cam; + cam = part.GetComponent(); // gimbal range?? + cam.gimbalLimit *= part.GetDamagePercentage(); + if (cam.gimbalLimit < 30 || part.GetDamagePercentage() < 0.5) part.RemoveModule(cam); + subsysCrit = true; + } + //if a wheel, disable the wheel and swap it to the broken state/model? + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.BattleDamageHandler]: {part.name} on {part.vessel.vesselName} took subsystem damage"); + if (subsysCrit && Diceroll <= (damageChance / 2)) //only start fire on part that actually contains destroyed subsystem + { + if (incendiary) + { + BulletHitFX.AttachFire(hitPoint, part, caliber, attacker, 20); + } + } + } + } + //Command parts + if (BDArmorySettings.BD_COCKPITS && penetrationFactor > 1.2f && part.GetDamagePercentage() < 0.9f && firsthit) //lets have this be triggered by penetrative damage, not blast splash + { + if (part.GetComponent() != null) + { + double ControlDiceRoll = UnityEngine.Random.Range(0, 100); + 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()) + { + using (var control = VesselModuleRegistry.GetModules(part.vessel).GetEnumerator()) // FIXME should this be IBDAIControl? + while (control.MoveNext()) + { + if (control.Current == null) continue; + control.Current.evasionThreshold += 5; //pilot jitteriness increases + control.Current.maxSteer *= 0.9f; + if (control.Current.steerDamping > 0.625f) //damage to controls + { + control.Current.steerDamping -= 0.125f; + } + if (control.Current.dynamicSteerDampingPitchFactor > 0.625f) + { + control.Current.dynamicSteerDampingPitchFactor -= 0.125f; + } + if (control.Current.dynamicSteerDampingRollFactor > 0.625f) + { + control.Current.dynamicSteerDampingRollFactor -= 0.125f; + } + if (control.Current.dynamicSteerDampingYawFactor > 0.625f) + { + control.Current.dynamicSteerDampingYawFactor -= 0.125f; + } + } + //GuardRange reduction to sim canopy/sensor damage? + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "took command damage"); + } + } + } + } + if (BDArmorySettings.BD_PILOT_KILLS) + { + bool canKill = true; + var armorglass = part.FindModuleImplementing(); + if (armorglass != null) + { + if (armorglass.armoredCockpit && !cockpitPen) //round stopped by internal cockpit armor + { + canKill = false; + } + } + if (canKill && part.protoModuleCrew.Count > 0 && penetrationFactor > 1.5f && part.GetDamagePercentage() < 0.95f && firsthit) + { + 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.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); + if (crewMember != null) + { + crewMember.UnregisterExperienceTraits(part); + //crewMember.outDueToG = true; //implement temp KO to simulate wounding? + crewMember.Die(); + if (part.IsKerbalEVA()) + { + part.Die(); + } + else + { + part.RemoveCrewmember(crewMember); // sadly, I wasn't able to get the K.I.A. portrait working + } + //Vessel.CrewWasModified(part.vessel); + //Debug.Log("[BDArmory.BattleDamageHandler]: " + crewMember.name + " was killed by damage to cabin!"); + if (HighLogic.CurrentGame.Parameters.Difficulty.MissingCrewsRespawn) + { + 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_CENTER); + if (BDACompetitionMode.Instance) + { + BDACompetitionMode.Instance.competitionStatus.Add($"Cockpit snipe on {part.vessel.GetName()}! {crewMember.name} killed!"); + BDACompetitionMode.Instance.OnVesselModified(part.vessel); + } + } + } + } + } + + } + } +} diff --git a/BDArmory/GameModes/BattleDamage/BattleDamageTracker.cs b/BDArmory/GameModes/BattleDamage/BattleDamageTracker.cs new file mode 100644 index 000000000..bddf9b930 --- /dev/null +++ b/BDArmory/GameModes/BattleDamage/BattleDamageTracker.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.GameModes +{ + public class BattleDamageTracker : MonoBehaviour + { + public float oldDamagePercent = 1; + public double origIntakeArea = -1; + + public bool isSRB = false; + public bool SRBFuelled = false; + public Part Part { get; set; } + // { + // get + // { + // return Part; // FIXME If you want custom getters and setters then you need your own backing variable, e.g., private Part _Part;, auto-getters and setters will make the backing variable automatically. + // } + // set + // { + // Part = value; + // } + // } + + void Awake() + { + if (!Part) + { + Part = GetComponent(); + } + if (!Part) + { + //Debug.Log ("[BDArmory]: BDTracker attached to non-part, removing"); + Destroy(this); + return; + } + //destroy this there's already one attached + foreach (var prevTracker in Part.gameObject.GetComponents()) + { + if (prevTracker != this) + { + Destroy(this); + return; + } + } + foreach (var engine in Part.GetComponentsInChildren()) + { + if (!engine.allowShutdown && engine.throttleLocked) + { + isSRB = true; + using (IEnumerator resources = Part.Resources.GetEnumerator()) + while (resources.MoveNext()) + { + if (resources.Current == null) continue; + if (resources.Current.resourceName.Contains("SolidFuel")) + { + if (resources.Current.amount > 1d) + { + SRBFuelled = true; + } + } + } + } + } + Part.OnJustAboutToBeDestroyed += AboutToBeDestroyed; + } + void OnDestroy() + { + Part.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; + } + void AboutToBeDestroyed() + { + Destroy(this); + } + + } +} diff --git a/BDArmory/GameModes/ModuleSpaceFriction.cs b/BDArmory/GameModes/ModuleSpaceFriction.cs new file mode 100644 index 000000000..234b44566 --- /dev/null +++ b/BDArmory/GameModes/ModuleSpaceFriction.cs @@ -0,0 +1,289 @@ +using System.Linq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; +using System.Collections.Generic; +using BDArmory.Extensions; + +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 decelerate 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 + + public bool repulsorActivated = false; + + [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_Settings_Repulsor", active = true)] + public void ToggleRepulsor() + { + repulsorActivated = !repulsorActivated; + isLanded = part.vessel.LandedOrSplashed; + targetAlt = 0.1f; + } + bool isLanded = true; + + [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 + + [KSPField(isPersistant = true)] + public float maxRepulsorMass = 10; //levitate up to 10t per repulsor + + [KSPField(isPersistant = true)] + public float resourcePerSec = -1; + + [KSPField(isPersistant = true)] + public string resourceName = "ElectricCharge"; + private int resourceID; + + public float frictMult; //engine thrust of craft + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_VesselMover_Help_AdjustAltitude"), + UI_FloatRange(minValue = 1f, maxValue = 100, stepIncrement = 1f, scene = UI_Scene.All)] + public float repulsorAlt = 10; + + float targetAlt = 0.1f; + //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 + + [KSPAction("#autoLOC_6001380")] + public void AGToggleRepulsor(KSPActionParam param) + { + ToggleRepulsor(); + } + + List repulsors; + List spaceFrictionModules; + public static bool GameIsPaused + { + get { return PauseMenu.isOpen || Time.timeScale == 0; } + } + + ModuleEngines foundEngine + { + get + { + if (_engine == null || _engine.vessel != vessel) + _engine = VesselModuleRegistry.GetModuleEngines(vessel).FirstOrDefault(); + return _engine; + } + } + ModuleEngines _engine; + MissileFire WeaponManager + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; + } + } + MissileFire _weaponManager; + IBDAIControl AI + { + get + { + if (_AI == null || !_AI.pilotEnabled || _AI.vessel != vessel) _AI = vessel.ActiveController().AI; + return _AI; + } + } + IBDAIControl _AI; + + void Start() + { + if (vessel.rootPart == 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); + } + } + } + resourceID = PartResourceLibrary.Instance.GetDefinition(resourceName).id; + if (HighLogic.LoadedSceneIsFlight) + { + if (!RepulsorOverride) //MSF added via Spawn utilities for Space Hacks + { + using (var engine = VesselModuleRegistry.GetModuleEngines(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; + } + } + else + { + spaceFrictionModules = VesselModuleRegistry.GetModules(vessel); + } + } + } + + public void FixedUpdate() + { + if ((!BDArmorySettings.SPACE_HACKS && (!AntiGravOverride && !RepulsorOverride)) || !HighLogic.LoadedSceneIsFlight || !FlightGlobals.ready || vessel.packed || GameIsPaused) return; + + IBDAIControl ai = AI; + if (part.vessel.situation == Vessel.Situations.FLYING || part.vessel.situation == Vessel.Situations.SUB_ORBITAL) + { + if (BDArmorySettings.SF_FRICTION) + { + if (part.vessel.speed > 10) + { + if (ai != null) + { + maxVelocity = ai.aiType switch + { + AIType.PilotAI => (ai as BDModulePilotAI).maxSpeed, + AIType.SurfaceAI => (ai as BDModuleSurfaceAI).MaxSpeed, + AIType.VTOLAI => (ai as BDModuleVTOLAI).MaxSpeed, + AIType.OrbitalAI => (ai as BDModuleOrbitalAI).ManeuverSpeed, + _ => 0 + }; + } + + 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 + VectorUtils.Angle(part.vessel.srf_vel_direction, 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); + using (var p = part.vessel.Parts.GetEnumerator()) + while (p.MoveNext()) + { + if (p.Current == null || p.Current.PhysicsSignificance == 1) continue; + p.Current.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 + { + using (var p = part.vessel.Parts.GetEnumerator()) + while (p.MoveNext()) + { + if (p.Current == null || p.Current.PhysicsSignificance == 1) continue; //attempting to apply rigidbody force to non-significant parts will NRE + p.Current.Rigidbody.AddForce(-FlightGlobals.getGeeForceAtPosition(p.Current.transform.position), ForceMode.Acceleration); + } + } + else //out of control/engineless craft get hurtled into the ground + { + using (var p = part.vessel.Parts.GetEnumerator()) + while (p.MoveNext()) + { + if (p.Current == null || p.Current.PhysicsSignificance == 1) continue; + p.Current.Rigidbody.AddForce(FlightGlobals.getGeeForceAtPosition(p.Current.transform.position), ForceMode.Acceleration); + } + } + } + } + if (!(part.vessel.situation == Vessel.Situations.ORBITING || part.vessel.situation == Vessel.Situations.DOCKED || part.vessel.situation == Vessel.Situations.ESCAPING || part.vessel.situation == Vessel.Situations.PRELAUNCH)) + { + if ((BDArmorySettings.SF_REPULSOR || RepulsorOverride) && repulsorActivated) + { + if ((ai != null || RepulsorOverride) && foundEngine != null) + { + repulsorAlt = ai.aiType switch + { + AIType.PilotAI => (ai as BDModulePilotAI).defaultAltitude, // Use default alt instead of min alt to keep the vessel away from 'gain alt' behaviour. + AIType.SurfaceAI => (ai as BDModuleSurfaceAI).MaxSlopeAngle * 2, + AIType.VTOLAI => (ai as BDModuleVTOLAI).defaultAltitude, + _ => 0 + }; + + Vector3d grav = FlightGlobals.getGeeForceAtPosition(vessel.CoM); + var vesselMass = part.vessel.GetTotalMass(); + if (RepulsorOverride) //Asking this first, so SPACEHACKS repulsor mode will ignore it + { + if (isLanded) + { + targetAlt = Mathf.Lerp(targetAlt, repulsorAlt, 0.02f / 4); + if (targetAlt >= repulsorAlt) isLanded = false; + } + else + targetAlt = repulsorAlt; + float pointAltitude = BodyUtils.GetRadarAltitudeAtPos(part.transform.position); + if (pointAltitude <= 0 || pointAltitude > 2f * targetAlt) return; + if (!DrainResource()) 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 = Mathf.Min(vesselMass * factor / spaceFrictionModules.Count, maxRepulsorMass); // 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); + } + } + } + } + } + } + bool DrainResource() + { + if (resourcePerSec <= 0) + { + return true; + } + + double drainAmount = resourcePerSec * TimeWarp.fixedDeltaTime; + double chargeAvailable = part.RequestResource(resourceID, drainAmount, ResourceFlowMode.ALL_VESSEL); + if (chargeAvailable < drainAmount * 0.95f) + { + return false; + } + return true; + } + public static void AddSpaceFrictionToAllValidVessels() + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel.ActiveController().WM != null && vessel.rootPart.FindModuleImplementing() == null) + { + vessel.rootPart.AddModule("ModuleSpaceFriction"); + } + } + } + } +} \ No newline at end of file diff --git a/BDArmory/GameModes/Mutators/BDAMutator.cs b/BDArmory/GameModes/Mutators/BDAMutator.cs new file mode 100644 index 000000000..b6bc7d087 --- /dev/null +++ b/BDArmory/GameModes/Mutators/BDAMutator.cs @@ -0,0 +1,471 @@ +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; +using static BDArmory.Weapons.ModuleWeapon; + +namespace BDArmory.GameModes +{ + class BDAMutator : PartModule + { + float startTime; + bool mutatorEnabled = false; + public List mutators; + private MutatorInfo mutatorInfo; + + public string mutatorName; + private float Vampirism = 0; + private float Regen = 0; + private float engineMult = 1; + + private bool Vengeance = false; + private List ResourceTax; + private double TaxRate = 0; + private bool hasTaxes; + private int oldScore = 0; + bool applyVampirism = false; + private float Accumulator; + + private string iconPath; + private string iconcolor; + private Color iconColor; + private Texture2D icon; + public Material IconMat; + private int ActiveMutators = 1; + public int progressionIndex = 0; + + public override void OnStart(StartState state) + { + if (HighLogic.LoadedSceneIsFlight) + { + part.force_activate(); + } + base.OnStart(state); + } + + + public void EnableMutator(string name = "def", bool HOS = false) + { + if (string.IsNullOrEmpty(name)) name = "def"; + if (mutatorEnabled) //replace current mutator with new one + { + DisableMutator(); + } + if (name == "def") //mutator not specified, randomly choose from selected mutators + { + if (BDArmorySettings.MUTATOR_LIST.Count == 0) return; + 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.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: random mutator list built: " + name + " on " + part.vessel.GetName()); + } + mutatorName = name; + mutators = BDAcTools.ParseNames(name); + var mf = vessel.ActiveController().WM; + for (int r = 0; r < (HOS ? mutators.Count : BDArmorySettings.MUTATOR_APPLY_NUM); r++) + { + name = MutatorInfo.mutators[mutators[r]].name; + mutatorInfo = MutatorInfo.mutators[name]; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: beginning mutator initialization of " + name + " on " + part.vessel.GetName()); + + if (mutatorInfo.weaponMod) + { + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + weapon.Current.shortName = mutatorInfo.name; + weapon.Current.WeaponDisplayName = weapon.Current.shortName; + if (vessel.isActiveVessel && mf && (IBDWeapon)weapon.Current == mf.selectedWeapon) mf.selectedWeaponString = mutatorInfo.name; + if (mutatorInfo.weaponType != "def") + { + weapon.Current.ParseWeaponType(mutatorInfo.weaponType); + } + if (mutatorInfo.bulletType != "def") + { + weapon.Current.currentType = mutatorInfo.bulletType; + weapon.Current.useCustomBelt = false; + weapon.Current.SetupBulletPool(); + weapon.Current.ParseAmmoStats(); + } + + if (mutatorInfo.RoF > 0) + { + weapon.Current.roundsPerMinute = mutatorInfo.RoF; + weapon.Current.baseRPM = mutatorInfo.RoF; + } + if (mutatorInfo.MaxDeviation > 0) + { + weapon.Current.maxDeviation = mutatorInfo.MaxDeviation; + } + if (mutatorInfo.laserDamage > 0) + { + weapon.Current.laserDamage = mutatorInfo.laserDamage; + } + if (mutatorInfo.instaGib) + { + weapon.Current.instagib = mutatorInfo.instaGib; + } + else + { + if (weapon.Current.strengthMutator > 0) + { + weapon.Current.strengthMutator = weapon.Current.strengthMutator; + } + } + if (weapon.Current.eWeaponType == ModuleWeapon.WeaponTypes.Laser) + { + if (!string.IsNullOrEmpty(mutatorInfo.bulletType) && mutatorInfo.bulletType != "def") + { + weapon.Current.projectileColor = BulletInfo.bullets[mutatorInfo.bulletType].projectileColor; + } + else + { + var WM = part.vessel.ActiveController().WM; + 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; + + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BDAMutator]: 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; + } + if (weapon.Current.eWeaponType == ModuleWeapon.WeaponTypes.Rocket && weapon.Current.weaponType != "rocket") + { + weapon.Current.rocketPod = false; + weapon.Current.externalAmmo = true; + } + weapon.Current.resourceSteal = mutatorInfo.resourceSteal; + //if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BDAMutator]: current weapon status: " + weapon.Current.WeaponStatusdebug()); + } + } + if (mutatorInfo.EngineMult > 0) + { + engineMult *= mutatorInfo.EngineMult; + } + if (mutatorInfo.Vampirism > 0) + { + Vampirism += mutatorInfo.Vampirism; + } + if (mutatorInfo.Regen != 0) + { + Regen += mutatorInfo.Regen; + } + using (List.Enumerator part = vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (mutatorInfo.Defense > 0) + { + var HPT = part.Current.FindModuleImplementing(); + HPT.defenseMutator = mutatorInfo.Defense; + } + if (mutatorInfo.MassMod != 0) + { + var MM = part.Current.FindModuleImplementing(); + if (MM == null) + { + MM = (ModuleMassAdjust)part.Current.AddModule("ModuleMassAdjust"); + } + if (BDArmorySettings.MUTATOR_DURATION > 0 && BDArmorySettings.MUTATOR_APPLY_TIMER) + { + MM.duration = BDArmorySettings.MUTATOR_DURATION; //MMA will time out and remove itself when mutator expires + } + else + { + MM.duration = BDArmorySettings.COMPETITION_DURATION; + } + MM.massMod += mutatorInfo.MassMod / vessel.Parts.Count; //evenly distribute mass change across entire vessel + } + } + if (!Vengeance && mutatorInfo.Vengeance) + { + Vengeance = mutatorInfo.Vengeance; + } + if (Vengeance) + { + part.OnJustAboutToBeDestroyed += Detonate; + } + if (!string.IsNullOrEmpty(mutatorInfo.resourceTax)) + { + hasTaxes = true; + } + } + if (engineMult != 1) + { + using (var engine = VesselModuleRegistry.GetModuleEngines(vessel).GetEnumerator()) + while (engine.MoveNext()) + { + engine.Current.thrustPercentage *= engineMult; + } + } + startTime = Time.time; + if (IconMat == null) + { + IconMat = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + } + ActiveMutators = BDArmorySettings.MUTATOR_APPLY_NUM; + if (ActiveMutators < 1) + { + ActiveMutators = 1; + } + mutatorEnabled = true; + } + + public void DisableMutator() + { + if (!mutatorEnabled) return; + mutatorEnabled = false; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: Disabling " + mutatorInfo.name + "Mutator on " + part.vessel.vesselName); + var mf = vessel.ActiveController().WM; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + weapon.Current.shortName = weapon.Current.OriginalShortName; + weapon.Current.WeaponDisplayName = weapon.Current.shortName; + if (vessel.isActiveVessel && mf && (IBDWeapon)weapon.Current == mf.selectedWeapon) mf.selectedWeaponString = weapon.Current.GetShortName(); + weapon.Current.ParseWeaponType(weapon.Current.weaponType); + if (!string.IsNullOrEmpty(weapon.Current.ammoBelt) && weapon.Current.ammoBelt != "def") + { + weapon.Current.useCustomBelt = true; + } + weapon.Current.maxDeviation = weapon.Current.baseDeviation; + weapon.Current.laserDamage = weapon.Current.baseLaserdamage; + weapon.Current.pulseLaser = weapon.Current.pulseInConfig; + if (weapon.Current.eWeaponType != WeaponTypes.Laser || weapon.Current.pulseLaser) + { + try + { + weapon.Current.baseRPM = float.Parse(ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleWeapon", "roundsPerMinute", "fireTransformName", weapon.Current.fireTransformName)); //if multiple moduleWeapons, make sure this grabs the right one unsing fireTransformname as an ID + } + catch + { + weapon.Current.baseRPM = 3000; + } + } + else weapon.Current.baseRPM = 3000; + weapon.Current.roundsPerMinute = weapon.Current.baseRPM; + weapon.Current.instagib = false; + weapon.Current.strengthMutator = 1; + weapon.Current.SetupAmmo(null, null); + weapon.Current.resourceSteal = false; + } + if (engineMult != 1) + { + using (var engine = VesselModuleRegistry.GetModuleEngines(vessel).GetEnumerator()) + while (engine.MoveNext()) + { + engine.Current.thrustPercentage /= engineMult; + } + } + engineMult = 1; //changing this to 1 from 0, this makes sure that there isn't a multiply by 0 issue with if later calling EnableMutator on a mutator with an engine mult + Vampirism = 0; + Regen = 0; + using (List.Enumerator part = vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + var HPT = part.Current.FindModuleImplementing(); + if (HPT != null) HPT.defenseMutator = 1; + var MM = part.Current.FindModuleImplementing(); + if (MM != null) part.Current.RemoveModule(MM); + } + if (Vengeance) + { + Vengeance = false; + part.OnJustAboutToBeDestroyed -= Detonate; + } + List ResourceTax = new List(); + TaxRate = 0; + hasTaxes = false; + } + + void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight && !BDArmorySetup.GameIsPaused && !vessel.packed) + { + if (!mutatorEnabled) return; + + if ((BDArmorySettings.MUTATOR_DURATION > 0 && Time.time - startTime > BDArmorySettings.MUTATOR_DURATION * 60) && (BDArmorySettings.MUTATOR_APPLY_TIMER || BDArmorySettings.MUTATOR_APPLY_KILL)) + { + DisableMutator(); + } + if (BDACompetitionMode.Instance.Scores.ScoreData.ContainsKey(vessel.vesselName)) + { + if (BDACompetitionMode.Instance.Scores.ScoreData[vessel.vesselName].hits > oldScore) //apply HP gain every time a hit is scored + { + oldScore = BDACompetitionMode.Instance.Scores.ScoreData[vessel.vesselName].hits; + applyVampirism = true; + } + } + if (Regen != 0 || Vampirism > 0) + { + using (List.Enumerator part = vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (Regen != 0 && Accumulator > 5) //Add regen HP every 5 seconds + { + part.Current.AddHealth(Regen, Vampirism > 0); //don't clamp HP to default if vampirism also enabled to prevent regen resetting gained HP from vampirism + } + if (Vampirism > 0 && applyVampirism) + { + part.Current.AddHealth(Vampirism, true); + } + } + } + applyVampirism = false; + //To allow multiple tax mutators, but have it so resources are taxed per mutator - i.e. Mut1 has ammo regen, mut2 has fueltax, and both proc + if (hasTaxes && Accumulator > 5) //Apply resource tax once every 5 seconds) + { + for (int r = 0; r < ActiveMutators; r++) + { + TaxRate = MutatorInfo.mutators[mutators[r]].resourceTaxRate; + if (TaxRate != 0) + { + try + { + ResourceTax = BDAcTools.ParseNames(MutatorInfo.mutators[mutators[r]].resourceTax); + int Tax = ResourceTax.Count; + if (Tax >= 1) + { + for (int i = 0; i < Tax; i++) + { + part.RequestResource(ResourceTax[i], TaxRate, ResourceFlowMode.ALL_VESSEL); + } + } + } + catch + { + Debug.Log("[BDArmory.BDAMutator]: mutator not configured correctly. Set ResourceTaxRate to 0 or add resource to ResourceTax"); + } + } + } + } + if (hasTaxes || Regen != 0) + { + if (Accumulator > 5) + { + Accumulator = 0; + } + else + { + Accumulator += TimeWarp.fixedDeltaTime; + } + } + + } + } + void OnGUI() + { + if (Event.current.type.Equals(EventType.Repaint)) + { + if (HighLogic.LoadedSceneIsFlight && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDArmorySettings.MUTATOR_ICONS) + { + if (mutatorEnabled) + { + 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; + float yPos = ((1 - screenPos.y) * Screen.height) - (0.5f * (30 * BDTISettings.ICONSCALE)) - (30 * BDTISettings.ICONSCALE); + + for (int i = 0; i < ActiveMutators; i++) + { + float xPos = (screenPos.x * Screen.width) - (0.5f * 30 * BDTISettings.ICONSCALE) - ((ActiveMutators - 1) * 0.5f * 30 * BDTISettings.ICONSCALE); + Rect iconRect = new Rect(xPos + (i * 30 * BDTISettings.ICONSCALE), yPos, (30 * BDTISettings.ICONSCALE), (30 * BDTISettings.ICONSCALE)); + + iconPath = MutatorInfo.mutators[mutators[i]].icon; + iconcolor = MutatorInfo.mutators[mutators[i]].iconColor; + iconColor = GUIUtils.ParseColor255(iconcolor); + iconColor.a = BDTISettings.OPACITY * BDTISetup.iconOpacity; + switch (iconPath) + { + case "IconAccuracy": + icon = BDTISetup.Instance.MutatorIconAcc; + break; + case "IconAttack": + icon = BDTISetup.Instance.MutatorIconAtk; + break; + case "IconAttack2": + icon = BDTISetup.Instance.MutatorIconAtk2; + break; + case "IconBallistic": + icon = BDTISetup.Instance.MutatorIconBullet; + break; + case "IconDefense": + icon = BDTISetup.Instance.MutatorIconDefense; + break; + case "IconLaser": + icon = BDTISetup.Instance.MutatorIconLaser; + break; + case "IconMass": + icon = BDTISetup.Instance.MutatorIconMass; + break; + case "IconRegen": + icon = BDTISetup.Instance.MutatorIconRegen; + break; + case "IconRocket": + icon = BDTISetup.Instance.MutatorIconRocket; + break; + case "IconSkull": + icon = BDTISetup.Instance.MutatorIconDoom; + break; + case "IconSpeed": + icon = BDTISetup.Instance.MutatorIconSpeed; + break; + case "IconTarget": + icon = BDTISetup.Instance.MutatorIconTarget; + break; + case "IconVampire": + icon = BDTISetup.Instance.MutatorIconVampire; + break; + case "IconUnknown": + icon = BDTISetup.Instance.MutatorIconNull; + break; + default: // Other? + icon = BDTISetup.Instance.MutatorIconNull; + break; + } + if (icon != null) + { + //GUI.DrawTexture(iconRect, icon); + + IconMat.SetColor("_TintColor", iconColor); + IconMat.mainTexture = icon; + Graphics.DrawTexture(iconRect, icon, IconMat); + } + } + } + } + } + } + bool hasDetonated = false; + void Detonate() + { + if (hasDetonated) return; + if (!Vengeance) return; + if (!BDACompetitionMode.Instance.competitionIsActive) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: triggering vengeance nuke"); + NukeFX.CreateExplosion(part.transform.position, ExplosionSourceType.Other, this.vessel.GetName(), "Vengeance Explosion", BDArmorySettings.VENGEANCE_DELAY, 200 * BDArmorySettings.VENGEANCE_YIELD, BDArmorySettings.VENGEANCE_YIELD, BDArmorySettings.VENGEANCE_YIELD, false, + "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", + nukePart: part); + hasDetonated = true; + } + } +} + diff --git a/BDArmory/GameModes/Mutators/MutatorInfo.cs b/BDArmory/GameModes/Mutators/MutatorInfo.cs new file mode 100644 index 000000000..ff021f162 --- /dev/null +++ b/BDArmory/GameModes/Mutators/MutatorInfo.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace BDArmory.GameModes +{ + public class MutatorInfo + { + public string name { get; private set; } + public bool weaponMod { get; private set; } + public string weaponType { get; private set; } + public string bulletType { get; private set; } + public int RoF { get; private set; } + public float MaxDeviation { get; private set; } + public float laserDamage { get; private set; } + public float Vampirism { get; private set; } + public float Regen { get; private set; } + public float Strength { get; private set; } + public float Defense { get; private set; } + public bool Vengeance { get; private set; } + public float EngineMult { get; private set; } + public float MassMod { get; private set; } + public bool resourceSteal { get; private set; } + public string resourceTax { get; private set; } + public float resourceTaxRate { get; private set; } + public bool instaGib { get; private set; } + public string icon { get; private set; } + public string iconColor { get; private set; } + + public static MutatorInfos mutators; + public static HashSet mutatorNames; + public static MutatorInfo defaultMutator; + + public MutatorInfo(string name, bool weaponMod, string weaponType, string bulletType, int RoF, float MaxDeviation, float laserDamage, + float Vampirism, float Regen, float Strength, float Defense, bool Vengeance, float EngineMult, float MassMod, bool resourceSteal, string resourceTax, float resourceTaxRate, bool instaGib, string icon, string iconColor) + { + this.name = name; + this.weaponMod = weaponMod; + this.weaponType = weaponType; + this.bulletType = bulletType; + this.RoF = RoF; + this.MaxDeviation = MaxDeviation; + this.laserDamage = laserDamage; + this.Vampirism = Vampirism; + this.Regen = Regen; + this.Strength = Strength; + this.Defense = Defense; + this.Vengeance = Vengeance; + this.EngineMult = EngineMult; + this.MassMod = MassMod; + this.resourceSteal = resourceSteal; + this.resourceTax = resourceTax; + this.resourceTaxRate = resourceTaxRate; + this.instaGib = instaGib; + this.icon = icon; + this.iconColor = iconColor; + } + + public static void Load() + { + if (mutators != null) return; // Only load them once on startup. + mutators = new MutatorInfos(); + if (mutatorNames == null) mutatorNames = new HashSet(); + UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("MUTATOR"); + ConfigNode node; + + // First locate BDA's default rocket definition so we can fill in missing fields. + if (defaultMutator == null) + for (int i = 0; i < nodes.Length; ++i) + { + if (nodes[i].parent.name != "BD_Mutators") 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.MutatorInfo]: Parsing default mutator definition from " + nodes[i].parent.name); + defaultMutator = new MutatorInfo( + "def", + (bool)ParseField(node, "weaponMod", typeof(bool)), + (string)ParseField(node, "weaponType", typeof(string)), + (string)ParseField(node, "bulletType", typeof(string)), + (int)ParseField(node, "RoF", typeof(int)), + (float)ParseField(node, "MaxDeviation", typeof(float)), + (float)ParseField(node, "laserDamage", typeof(float)), + (float)ParseField(node, "Vampirism", typeof(float)), + (float)ParseField(node, "Regen", typeof(float)), + (float)ParseField(node, "Strength", typeof(float)), + (float)ParseField(node, "Defense", typeof(float)), + (bool)ParseField(node, "Vengeance", typeof(bool)), + (float)ParseField(node, "EngineMult", typeof(float)), + (float)ParseField(node, "MassMod", typeof(float)), + (bool)ParseField(node, "resourceSteal", typeof(bool)), + (string)ParseField(node, "resourceTax", typeof(string)), + (float)ParseField(node, "resourceTaxRate", typeof(float)), + (bool)ParseField(node, "instaGib", typeof(bool)), + (string)ParseField(node, "icon", typeof(string)), + (string)ParseField(node, "iconColor", typeof(string)) + ); + mutators.Add(defaultMutator); + mutatorNames.Add("def"); + break; + } + if (defaultMutator == null) throw new ArgumentException("Failed to find BDArmory's default mutator definition.", "defaultMutator"); + + // Now add in the rest of the rockets. + for (int i = 0; i < nodes.Length; i++) + { + string name_ = ""; + try + { + node = nodes[i].config; + name_ = (string)ParseField(node, "name", typeof(string)); + if (mutatorNames.Contains(name_)) // Avoid duplicates. + { + if (nodes[i].parent.name != "BD_Mutators" || name_ != "def") // Don't report the default bullet definition as a duplicate. + Debug.LogError("[BDArmory.MutatorInfo]: Mutator definition " + name_ + " from " + nodes[i].parent.name + " already exists, skipping."); + continue; + } + Debug.Log("[BDArmory.MutatorInfo]: Parsing definition of mutator " + name_ + " from " + nodes[i].parent.name); + mutators.Add( + new MutatorInfo( + name_, + (bool)ParseField(node, "weaponMod", typeof(bool)), + (string)ParseField(node, "weaponType", typeof(string)), + (string)ParseField(node, "bulletType", typeof(string)), + (int)ParseField(node, "RoF", typeof(int)), + (float)ParseField(node, "MaxDeviation", typeof(float)), + (float)ParseField(node, "laserDamage", typeof(float)), + (float)ParseField(node, "Vampirism", typeof(float)), + (float)ParseField(node, "Regen", typeof(float)), + (float)ParseField(node, "Strength", typeof(float)), + (float)ParseField(node, "Defense", typeof(float)), + (bool)ParseField(node, "Vengeance", typeof(bool)), + (float)ParseField(node, "EngineMult", typeof(float)), + (float)ParseField(node, "MassMod", typeof(float)), + (bool)ParseField(node, "resourceSteal", typeof(bool)), + (string)ParseField(node, "resourceTax", typeof(string)), + (float)ParseField(node, "resourceTaxRate", typeof(float)), + (bool)ParseField(node, "instaGib", typeof(bool)), + (string)ParseField(node, "icon", typeof(string)), + (string)ParseField(node, "iconColor", typeof(string)) + ) + ); + mutatorNames.Add(name_); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.MutatorInfo]: Error Loading Mutator Config '" + name_ + "' | " + e.ToString()); + } + } + //once mutators are loaded, remove the def mutator so it isn't found in later list parsings + mutators.Remove(defaultMutator); + mutatorNames.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}' | {e.Message}", field); + } + } + catch (Exception e) + { + if (field == "name") throw; // Sanity check for field "name" to avoid potential stack overflow. + if (defaultMutator != null) + { + // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. + string name = "unknown"; + try { name = (string)ParseField(node, "name", typeof(string)); } catch { } + var defaultValue = typeof(MutatorInfo).GetProperty(field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultMutator); + Debug.LogError($"[BDArmory.MutatorInfo]: Using default value of {defaultValue} for {field} of {name} | {e.Message}"); + + return defaultValue; + } + else + throw; + } + } + /// + /// Quick hack rushjob to have S6R1 mutators set up without havingto worry about juggling MM patches/extra mutator configs. + /// Ideally this would draw from a list of weapons specified somewhere, and weapon stats grabbed from the prefabs at runtime when mutators applied + /// to not have to worry about, say, someone not using BDAe and not having some of the bulletTypes hadcoded here + /// revise later + /// + public static bool gunGameConfigured = false; + public static void SetupGunGame() + { + // if (mutatorNames.Contains("Brownings")) return; // This wasn't always working properly for some reason. + if (gunGameConfigured) return; + if (mutators == null) Load(); // Load the mutators if they haven't been loaded yet. + mutators.Add(new MutatorInfo("Brownings", true, "ballistic", "12.7mmBullet", 1150, 0.32f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconTarget", "0,200,0,255")); + mutatorNames.Add("Brownings"); + mutators.Add(new MutatorInfo("Vulcans", true, "ballistic", "20x102mmHEBullet", 5500, 0.8f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconAccuracy", "222,0,0,255")); + mutatorNames.Add("Vulcans"); + mutators.Add(new MutatorInfo("Chainguns", true, "ballistic", "30x173HEBullet", 625, 0.4f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconAttack", "222,146,0,255")); + mutatorNames.Add("Chainguns"); + mutators.Add(new MutatorInfo("Mausers", true, "ballistic", "27x145HEAmmo", 1050, 0.384f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconBallistic", "222,146,0,255")); + mutatorNames.Add("Mausers"); + mutators.Add(new MutatorInfo("GAU-22s", true, "ballistic", "25x137mmAPEXBullet", 3300, 0.6405f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconBallistic", "255,255,0,255")); + mutatorNames.Add("GAU-22s"); + mutators.Add(new MutatorInfo("N-37s", true, "ballistic", "37x155HEShell", 400, 0.37f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconTarget", "255,0,0,255")); + mutatorNames.Add("N-37s"); + mutators.Add(new MutatorInfo("AT Guns", true, "ballistic", "57mmShell", 110, 0.17f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconTarget", "255,255,255,255")); + mutatorNames.Add("AT Guns"); + mutators.Add(new MutatorInfo("GAU-8s", true, "ballistic", "30x173HEBullet", 3900, 0.6405f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconBallistic", "222,55,0,255")); + mutatorNames.Add("GAU-8s"); + mutators.Add(new MutatorInfo("Railguns", true, "ballistic", "37mmHVProjectile", 50, 0.01f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconLaser", "105,250,234,255")); + mutatorNames.Add("Railguns"); + mutators.Add(new MutatorInfo("Abrams", true, "ballistic", "130mmShell", 10, 0.1f, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconSkull", "175,175,24,255")); + mutatorNames.Add("Abrams"); + mutators.Add(new MutatorInfo("Rockets", true, "rocket", "8KOMS", 450, -1, 0, 0, 0, 1, 1, false, 1, 0, false, "", 0, false, "IconRocket", "84,0,222,255")); + mutatorNames.Add("Rockets"); + + gunGameConfigured = true; + } + } + + public class MutatorInfos : List + { + public MutatorInfo this[string name] + { + get { return Find((value) => { return value.name == name; }); } + } + } +} diff --git a/BDArmory/GameModes/Waypoints/CourseBuilderGUI.cs b/BDArmory/GameModes/Waypoints/CourseBuilderGUI.cs new file mode 100644 index 000000000..0e495688b --- /dev/null +++ b/BDArmory/GameModes/Waypoints/CourseBuilderGUI.cs @@ -0,0 +1,737 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using BDArmory.Competition.OrchestrationStrategies; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Competition; +using System.IO; + +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class CourseBuilderGUI : MonoBehaviour + { + #region Fields + public static CourseBuilderGUI Instance; + private static int _guiCheckIndex = -1; + private static readonly float _buttonSize = 20; + private static readonly float _margin = 5; + private static readonly float _lineHeight = _buttonSize; + private float _windowHeight; //auto adjusting + private float _windowWidth; + public bool _ready = false; + Dictionary spawnFields; + + int selected_index = -1; + int selected_gate_index = -1; + public float SelectedGate = 0; + public static string Gatepath; + public string SelectedModel; + double movementIncrement = 1; + float recordingIncrement = 10; + + private bool ShowLoadMenu = false; + private bool ShowNewCourse = false; + private string newCourseName = ""; + private bool recording = false; + private bool showCourseWPsComboBox = false; + private bool showPositioningControls = false; + private bool showCoursePath = false; + bool moddingSpawnPoint = false; + public List loadedGates; + #endregion + + #region Styles + /// + /// Need Left 1/3 width button + /// Need Right 2/3 textfield + /// Needleft/mid/right numfied boxes + /// need Left <> Mid <> right <> buttons to flank the L/M/R numfields + /// Need 1/2 width buttons + /// + /// + /// + + Rect SLineRect(float line) + { + return new Rect(_margin, line * _lineHeight, _windowWidth - 2 * _margin, _lineHeight); + } + + Rect SLeftButtonRect(float line) + { + return new Rect(_margin, line * _lineHeight, (_windowWidth - 2 * _margin) / 2 - _margin / 4, _lineHeight); + } + + 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, 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 SFieldButtonRect(float line, float offset) + { + var rectGap = _windowWidth / 24; + return new Rect(rectGap * offset, line * _lineHeight, rectGap, _lineHeight); + } + + List SRight3Rects(float line) + { + var rectGap = _windowWidth / 24; + var rectWidth = _windowWidth / 6; + var rects = new List(); + rects.Add(new Rect(rectGap * 2.5f, line * _lineHeight, rectWidth, _lineHeight)); + rects.Add(new Rect(rectGap * 10, line * _lineHeight, rectWidth, _lineHeight)); + rects.Add(new Rect(rectGap * 17.5f, line * _lineHeight, rectWidth, _lineHeight)); + return rects; + } + + GUIStyle leftLabel; + GUIStyle listStyle; + GUIStyle centreLabel; + #endregion + private string txtName = string.Empty; + private void Awake() + { + if (Instance) + Destroy(this); + Instance = this; + } + + private void Start() + { + _ready = false; + StartCoroutine(WaitForBdaSettings()); + + 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 + centreLabel = new GUIStyle(); + centreLabel.alignment = TextAnchor.UpperCenter; + centreLabel.normal.textColor = Color.white; + + // Spawn fields + spawnFields = new Dictionary { + { "lat", gameObject.AddComponent().Initialise(0, FlightGlobals.currentMainBody.GetLatitudeAndLongitude(FlightGlobals.ActiveVessel.CoM).x, -90, 90) }, + { "lon", gameObject.AddComponent().Initialise(0, FlightGlobals.currentMainBody.GetLatitudeAndLongitude(FlightGlobals.ActiveVessel.CoM).y, -180, 180) }, + { "alt", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_ALTITUDE) }, + { "increment", gameObject.AddComponent().Initialise(0.001f, movementIncrement) }, + { "diameter", gameObject.AddComponent().Initialise(0, 500) }, + { "interval", gameObject.AddComponent().Initialise(0, recordingIncrement) }, + { "speed", gameObject.AddComponent().Initialise(0, -1) }, + }; + loadedGates = new List(); + } + + private IEnumerator WaitForBdaSettings() + { + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); + + BDArmorySetup.Instance.hasWPCourseSpawner = true; + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + _ready = true; + SetVisible(BDArmorySetup.showWPBuilderGUI); + } + float startTime = 0; + + IEnumerator RecordCourse() + { + startTime = Time.time; + while (recording) + { + while (Time.time - recordingIncrement < startTime) + { + yield return null; + } + if (!recording) yield break; + string newName = "Waypoint " + WaypointCourses.CourseLocations[selected_index].waypoints.Count.ToString(); + AddGate(newName); + startTime = Time.time; + } + } + + void SnapCameraToGate(bool revert = false) + { + if (!revert) + { + if (selected_gate_index < 0 || selected_index < 0 || selected_index >= WaypointCourses.CourseLocations.Count || selected_gate_index >= WaypointCourses.CourseLocations[selected_index].waypoints.Count) return; + } + var flightCamera = FlightCamera.fetch; + var cameraHeading = FlightCamera.CamHdg; + var cameraPitch = FlightCamera.CamPitch; + var distance = 1000; + + double terrainAltitude; + Vector3d spawnPoint; + if (!revert) + { + Waypoint gate = WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index]; + terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(gate.location.x, gate.location.y); + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(gate.location.x, gate.location.y, terrainAltitude + gate.location.z); + } + else + { + terrainAltitude = FlightGlobals.ActiveVessel.radarAltitude; + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(FlightGlobals.ActiveVessel.CoM.x, FlightGlobals.ActiveVessel.CoM.y, terrainAltitude + FlightGlobals.ActiveVessel.CoM.z); + } + Vector3 origin; + if (moddingSpawnPoint) + { + float terrainAlt = (float)FlightGlobals.currentMainBody.TerrainAltitude(WaypointCourses.CourseLocations[selected_index].spawnPoint.x, WaypointCourses.CourseLocations[selected_index].spawnPoint.y); + Vector3d SpawnCoords = new Vector3((float)WaypointCourses.CourseLocations[selected_index].spawnPoint.x, (float)WaypointCourses.CourseLocations[selected_index].spawnPoint.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE + terrainAlt); + origin = SpawnCoords; + } + else + { + if (!revert) + origin = loadedGates[selected_gate_index].transform.position; + else origin = FlightGlobals.ActiveVessel.CoM; + } + FloatingOrigin.SetOffset(origin);// This adjusts local coordinates, such that gate is (0,0,0). + flightCamera.transform.parent.position = Vector3.zero; + if (!moddingSpawnPoint) flightCamera.SetTarget(revert ? FlightGlobals.ActiveVessel.ReferenceTransform : loadedGates[selected_gate_index].transform); + 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); + flightCamera.transform.localPosition = cameraPosition; + flightCamera.transform.localRotation = Quaternion.identity; + flightCamera.ActivateUpdate(); + + flightCamera.SetDistanceImmediate(distance); + FlightCamera.CamHdg = cameraHeading; + FlightCamera.CamPitch = cameraPitch; + } + + private void OnGUI() + { + if (!(_ready && BDArmorySetup.GAME_UI_ENABLED && BDArmorySetup.showWPBuilderGUI && HighLogic.LoadedSceneIsFlight)) + return; + + _windowWidth = BDArmorySettings.VESSEL_WAYPOINT_WINDOW_WIDTH; + SetNewHeight(_windowHeight); + BDArmorySetup.WindowRectWayPointSpawner = new Rect( + BDArmorySetup.WindowRectWayPointSpawner.x, + BDArmorySetup.WindowRectWayPointSpawner.y, + _windowWidth, + _windowHeight + ); + BDArmorySetup.SetGUIOpacity(); + var guiMatrix = GUI.matrix; + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectWayPointSpawner.position); + BDArmorySetup.WindowRectWayPointSpawner = GUI.Window( + GUIUtility.GetControlID(FocusType.Passive), + BDArmorySetup.WindowRectWayPointSpawner, + WindowWaypointSpawner, + StringUtils.Localize("#LOC_BDArmory_BDAWaypointBuilder_Title"),//"BDA Vessel Spawner" + BDArmorySetup.BDGuiSkin.window + ); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectWayPointSpawner, _guiCheckIndex); + if (showCourseWPsComboBox) + { + //draw spawnpoint + float terrainAlt = (float)FlightGlobals.currentMainBody.TerrainAltitude(WaypointCourses.CourseLocations[selected_index].spawnPoint.x, WaypointCourses.CourseLocations[selected_index].spawnPoint.y); + Vector3d SpawnCoords = new Vector3((float)WaypointCourses.CourseLocations[selected_index].spawnPoint.x, (float)WaypointCourses.CourseLocations[selected_index].spawnPoint.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE + terrainAlt); + GUIUtils.DrawTextureOnWorldPos(VectorUtils.GetWorldSurfacePostion(SpawnCoords, FlightGlobals.currentMainBody), BDArmorySetup.Instance.greenPointCircleTexture, new Vector2(96, 96), 0); + if (selected_index >= 0 && selected_gate_index >= 0) + { + terrainAlt = (float)FlightGlobals.currentMainBody.TerrainAltitude(WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.x, WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.y); + terrainAlt += (WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].scale * 1.1f); + SpawnCoords = new Vector3((float)WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.x, (float)WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE < 0 ? WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.z : BDArmorySettings.WAYPOINTS_ALTITUDE) + terrainAlt); + GUIUtils.DrawTextureOnWorldPos(VectorUtils.GetWorldSurfacePostion(SpawnCoords, FlightGlobals.currentMainBody), BDArmorySetup.Instance.greenDiamondTexture, new Vector2(36, 36), 0); + } + } + if (selected_index >= 0 && showCoursePath && HighLogic.LoadedSceneIsFlight && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled) + { + for (int gate = 0; gate < loadedGates.Count; gate++) + { + GUIUtils.DrawLineBetweenWorldPositions(loadedGates[gate].Position, loadedGates[Math.Max(gate - 1, 0)].Position, 4, Color.red); + } + } + } + + private void SetNewHeight(float windowHeight) + { + var previousWindowHeight = BDArmorySetup.WindowRectWayPointSpawner.height; + BDArmorySetup.WindowRectWayPointSpawner.height = windowHeight; + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectWayPointSpawner, previousWindowHeight); + } + + private void WindowWaypointSpawner(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.CloseButtonStyle)) + { + SetVisible(false); + BDArmorySetup.SaveConfig(); + } + + float line = 0.25f; + var rects = new List(); + + if (GUI.Button(SLeftButtonRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_LoadCourse")}", ShowLoadMenu ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Load Course + { + ShowLoadMenu = !ShowLoadMenu; + } + + if (GUI.Button(SRightButtonRect(line), $"{StringUtils.Localize("#LOC_BDArmory_WP_NewCourse")}", ShowNewCourse ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Load Course + { + ShowNewCourse = !ShowNewCourse; + } + if (ShowLoadMenu) + { + line++; + int i = 0; + foreach (var wpCourse in WaypointCourses.CourseLocations) + { + if (GUI.Button(SQuarterRect(line, i++), wpCourse.name, i - 1 == selected_index ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + switch (Event.current.button) + { + case 1: // right click + if (selected_index == i - 1) //deleting currently loaded course + { + foreach (var gate in loadedGates) + { + gate.disabled = true; + gate.gameObject.SetActive(false); + } + selected_gate_index = -1; + showCourseWPsComboBox = false; + moddingSpawnPoint = false; + } + WaypointCourses.CourseLocations.Remove(wpCourse); + //WaypointField.Save(); + if (selected_index >= WaypointCourses.CourseLocations.Count) selected_index = WaypointCourses.CourseLocations.Count - 1; + break; + default: + //if loading an off-world course, warp to world + //if (wpCourse.worldIndex != FlightGlobals.GetBodyIndex(FlightGlobals.currentMainBody)) + SpawnUtils.ShowSpawnPoint(wpCourse.worldIndex, wpCourse.spawnPoint.x, wpCourse.spawnPoint.y, 500); + Vector3 previousLocation = FlightGlobals.ActiveVessel.transform.position; + //clear any previously loaded gates + foreach (var gate in loadedGates) + { + gate.disabled = true; + gate.gameObject.SetActive(false); + } + loadedGates.Clear(); + ShowLoadMenu = false; + selected_index = i - 1; + selected_gate_index = -1; + showCourseWPsComboBox = true; + moddingSpawnPoint = false; + //spawn in course gates + Debug.Log($"Loading Course; selected index: {selected_index}, ({WaypointCourses.CourseLocations[selected_index].name}) starting gate spawn"); + for (int wp = 0; wp < wpCourse.waypoints.Count; wp++) + { + if (!string.IsNullOrEmpty(wpCourse.waypoints[wp].model)) + SelectedModel = wpCourse.waypoints[wp].model; + else SelectedModel = "Ring"; + float terrainAltitude = (float)FlightGlobals.currentMainBody.TerrainAltitude(wpCourse.waypoints[wp].location.x, wpCourse.waypoints[wp].location.y); + Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(wpCourse.waypoints[wp].location.x, wpCourse.waypoints[wp].location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE < 0 ? wpCourse.waypoints[wp].location.z : BDArmorySettings.WAYPOINTS_ALTITUDE) + terrainAltitude), FlightGlobals.currentMainBody); + var direction = (WorldCoords - previousLocation).normalized; + WayPointMarker.CreateWaypoint(WorldCoords, direction, SelectedModel, wpCourse.waypoints[wp].scale); + + previousLocation = WorldCoords; + var location = string.Format("({0:##.###}, {1:##.###}, {2:####}", wpCourse.waypoints[wp].location.x, wpCourse.waypoints[wp].location.y, wpCourse.waypoints[wp].location.z); + Debug.Log("[BDArmory.Waypoints]: Creating waypoint marker at " + " " + location + " World: " + FlightGlobals.currentMainBody.flightGlobalsIndex + " scale: " + wpCourse.waypoints[wp].scale + " model:" + wpCourse.waypoints[wp].model); + } + break; + } + } + } + line += (int)((i - 1) / 4); + line += 0.3f; + } + + if (ShowNewCourse) + { + newCourseName = GUI.TextField(SLeftButtonRect(++line), newCourseName); + if (!string.IsNullOrEmpty(newCourseName)) + { + if (GUI.Button(SQuarterRect(line, 2), StringUtils.Localize("#LOC_BDArmory_WP_Create"), BDArmorySetup.BDGuiSkin.button)) + { + Vector3d spawnCoords = Vector3d.zero; + FlightGlobals.currentMainBody.GetLatLonAlt(FlightGlobals.ActiveVessel.CoM, out spawnCoords.x, out spawnCoords.y, out spawnCoords.z); + + if (!WaypointCourses.CourseLocations.Select(l => l.name).ToList().Contains(newCourseName)) + WaypointCourses.CourseLocations.Add(new WaypointCourse(newCourseName, FlightGlobals.GetBodyIndex(FlightGlobals.currentMainBody), new Vector2d(spawnCoords.x, spawnCoords.y), new List())); + //WaypointField.Save(); + selected_index = WaypointCourses.CourseLocations.FindIndex(l => l.name == newCourseName); + newCourseName = ""; + ShowNewCourse = false; + showCourseWPsComboBox = true; + //clear any previously loaded gates + foreach (var gate in loadedGates) + { + gate.disabled = true; + gate.gameObject.SetActive(false); + } + loadedGates.Clear(); + } + if (GUI.Button(SQuarterRect(line, 3), StringUtils.Localize("#LOC_BDArmory_WP_Record"), BDArmorySetup.BDGuiSkin.button)) + { + Vector3d spawnCoords = Vector3d.zero; + FlightGlobals.currentMainBody.GetLatLonAlt(FlightGlobals.ActiveVessel.CoM, out spawnCoords.x, out spawnCoords.y, out spawnCoords.z); + + if (!WaypointCourses.CourseLocations.Select(l => l.name).ToList().Contains(newCourseName)) + WaypointCourses.CourseLocations.Add(new WaypointCourse(newCourseName, FlightGlobals.GetBodyIndex(FlightGlobals.currentMainBody), new Vector2d(spawnCoords.x, spawnCoords.y), new List())); + //WaypointField.Save(); + selected_index = WaypointCourses.CourseLocations.FindIndex(l => l.name == newCourseName); + newCourseName = ""; + ShowNewCourse = false; + showCourseWPsComboBox = true; + recording = true; + selected_gate_index = -1; + //clear any previously loaded gates + foreach (var gate in loadedGates) + { + gate.disabled = true; + gate.gameObject.SetActive(false); + } + loadedGates.Clear(); + StartCoroutine(RecordCourse()); + } + GUI.Label(SQuarterRect(++line, 2), StringUtils.Localize("#LOC_BDArmory_WP_TimeStep"), leftLabel); + spawnFields["interval"].tryParseValue(GUI.TextField(SQuarterRect(line, 3), spawnFields["interval"].possibleValue, 8, spawnFields["interval"].style)); + if (spawnFields["interval"].currentValue != recordingIncrement) recordingIncrement = (float)spawnFields["interval"].currentValue; + } + } + if (showCourseWPsComboBox) + { + if (WaypointCourses.CourseLocations[selected_index].worldIndex != FlightGlobals.GetBodyIndex(FlightGlobals.currentMainBody)) return; + line += 1.25f; + GUI.Label(SLineRect(line++), WaypointCourses.CourseLocations[selected_index].name, centreLabel); + line += 0.25f; + if (recording) + { + GUI.Label(SLineRect(line++), StringUtils.Localize("#LOC_BDArmory_WP_Recording"), centreLabel); + line += 0.25f; + GUI.Label(SQuarterRect(++line, 1), StringUtils.Localize("#LOC_BDArmory_WP_TimeStep"), leftLabel); + spawnFields["interval"].tryParseValue(GUI.TextField(SQuarterRect(line, 2), spawnFields["interval"].possibleValue, 8, spawnFields["interval"].style)); + if (spawnFields["interval"].currentValue != recordingIncrement) recordingIncrement = (float)spawnFields["interval"].currentValue; + + line += 0.25f; + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_WP_FinishRecording"), BDArmorySetup.BDGuiSkin.button)) + { + recording = false; + } + line++; + } + if (GUI.Button(SQuarterRect(line, 0), StringUtils.Localize("#LOC_BDArmory_WP_Spawnpoint"), moddingSpawnPoint ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + if (Event.current.button == 0) + { + //SnapCameraToGate(); // Don't snap every frame when the spawn point button is selected... unless that's intended behaviour? + } + //show gate position buttons/location textboxes + spawnFields["lat"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].spawnPoint.x); + spawnFields["lon"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].spawnPoint.y); + spawnFields["alt"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_ALTITUDE); + showPositioningControls = true; + moddingSpawnPoint = true; + selected_gate_index = -1; + } + int i = 1; + foreach (var gate in WaypointCourses.CourseLocations[selected_index].waypoints) + { + if (GUI.Button(SQuarterRect(line, i++), gate.name, Math.Max(i - 2, 0) == selected_gate_index ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + moddingSpawnPoint = false; + selected_gate_index = Math.Max(i - 2, 0); + Debug.Log($"selected gate index: {selected_gate_index}, ({WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].name})"); + moddingSpawnPoint = false; + switch (Event.current.button) + { + case 1: // right click, remove gate from course + SnapCameraToGate(true); + loadedGates[selected_gate_index].disabled = true; + loadedGates[selected_gate_index].gameObject.SetActive(false); + WaypointCourses.CourseLocations[selected_index].waypoints.Remove(gate); + + if (selected_gate_index >= WaypointCourses.CourseLocations[selected_index].waypoints.Count) + { + if (WaypointCourses.CourseLocations[selected_index].waypoints.Count > 0) + { + selected_gate_index = WaypointCourses.CourseLocations[selected_index].waypoints.Count - 1; + spawnFields["lat"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.x); + spawnFields["lon"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.y); + spawnFields["alt"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].location.z); + spawnFields["diameter"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].scale); + spawnFields["speed"].SetCurrentValue(WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].maxSpeed); + txtName = WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].name; + } + else selected_gate_index = -1; + } + //WaypointField.Save(); + break; + default: + //snap camera to selected gate + //SnapCameraToGate(); + //show gate position buttons/location textboxes + spawnFields["lat"].SetCurrentValue(gate.location.x); + spawnFields["lon"].SetCurrentValue(gate.location.y); + spawnFields["alt"].SetCurrentValue(gate.location.z); + spawnFields["diameter"].SetCurrentValue(gate.scale); + spawnFields["speed"].SetCurrentValue(gate.maxSpeed); + txtName = WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].name; + showPositioningControls = true; + break; + } + } + } + line += (int)((i - 1) / 4); + line += 0.5f; + if (!recording) + { + txtName = GUI.TextField(SRightButtonRect(++line), txtName); + if (GUI.Button(SLeftButtonRect(line), selected_gate_index < WaypointCourses.CourseLocations[selected_index].waypoints.Count - 1 ? StringUtils.Localize("InsertGate") : StringUtils.Localize("#LOC_BDArmory_WP_AddGate"), BDArmorySetup.BDGuiSkin.button)) + { + string newName = string.IsNullOrEmpty(txtName.Trim()) ? $"{StringUtils.Localize("#LOC_BDArmory_WP_AddGate")} {(WaypointCourses.CourseLocations[selected_index].waypoints.Count.ToString())}" : txtName.Trim(); + AddGate(newName); + } + GUI.Label(SLeftButtonRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_SelectModel")}: {Path.GetFileNameWithoutExtension(VesselSpawnerWindow.Instance.gateFiles[(int)SelectedGate])}", leftLabel); //Waypoint Type + if (VesselSpawnerWindow.Instance.gateModelsCount > 0) + { + if (SelectedGate != (SelectedGate = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightButtonRect(line), SelectedGate, -1, VesselSpawnerWindow.Instance.gateModelsCount), 1))) + { + SelectedModel = SelectedGate >= 0 ? Path.GetFileNameWithoutExtension(VesselSpawnerWindow.Instance.gateFiles[(int)SelectedGate]) : ""; + } + } + } + } + else + { + showPositioningControls = false; + moddingSpawnPoint = false; + } + + if (showPositioningControls) + { + //need to get these setup and configured - TODO + if (selected_gate_index < 0 && !moddingSpawnPoint) return; + Waypoint currGate = null; + if (selected_gate_index >= 0) + { + currGate = WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index]; + if (loadedGates[selected_gate_index] == null) return; + } + line += 0.5f; + rects = SRight3Rects(++line); + GUI.Label(rects[0], StringUtils.Localize("#autoLOC_463474"), centreLabel); //latitude + GUI.Label(rects[1], StringUtils.Localize("#autoLOC_463478"), centreLabel); //longitude + GUI.Label(rects[2], StringUtils.Localize("#autoLOC_463493"), centreLabel); //Altitude + rects = SRight3Rects(++line); + if (GUI.RepeatButton(SFieldButtonRect(line, 1), "<", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["lat"].SetCurrentValue(spawnFields["lat"].currentValue - (movementIncrement / 100)); //having lat/long increase by 1 per frame while the button is held is going to cause gates to go *flying* across the continent + if (spawnFields["lat"].currentValue < -90) spawnFields["lat"].SetCurrentValue(spawnFields["lat"].currentValue + 180); + } + spawnFields["lat"].tryParseValue(GUI.TextField(rects[0], spawnFields["lat"].possibleValue, 8, spawnFields["lat"].style)); + if (GUI.RepeatButton(SFieldButtonRect(line, 7), ">", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["lat"].SetCurrentValue(spawnFields["lat"].currentValue + (movementIncrement / 100)); + if (spawnFields["lat"].currentValue > 90) spawnFields["lat"].SetCurrentValue(spawnFields["lat"].currentValue - 180); + } + + if (GUI.RepeatButton(SFieldButtonRect(line, 8.5f), "<", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["lon"].SetCurrentValue(spawnFields["lon"].currentValue - (movementIncrement / 100)); + if (spawnFields["lon"].currentValue < -180) spawnFields["lon"].SetCurrentValue(spawnFields["lon"].currentValue + 360); + } + spawnFields["lon"].tryParseValue(GUI.TextField(rects[1], spawnFields["lon"].possibleValue, 8, spawnFields["lon"].style)); + if (GUI.RepeatButton(SFieldButtonRect(line, 14.5f), ">", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["lon"].SetCurrentValue(spawnFields["lon"].currentValue + (movementIncrement / 100)); + if (spawnFields["lon"].currentValue > 180) spawnFields["lon"].SetCurrentValue(spawnFields["lon"].currentValue - 360); + } + + if (GUI.RepeatButton(SFieldButtonRect(line, 16), "<", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["alt"].SetCurrentValue(spawnFields["alt"].currentValue - movementIncrement); + if (spawnFields["alt"].currentValue < 0) spawnFields["alt"].SetCurrentValue(0); + } + spawnFields["alt"].tryParseValue(GUI.TextField(rects[2], spawnFields["alt"].possibleValue, 8, spawnFields["alt"].style)); + if (GUI.RepeatButton(SFieldButtonRect(line, 22), ">", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["alt"].SetCurrentValue(spawnFields["alt"].currentValue + movementIncrement); + if (spawnFields["alt"].currentValue > (FlightGlobals.currentMainBody.atmosphere ? FlightGlobals.currentMainBody.atmosphereDepth : 40000)) spawnFields["alt"].SetCurrentValue((FlightGlobals.currentMainBody.atmosphere ? FlightGlobals.currentMainBody.atmosphereDepth : 40000)); + } + + rects = SRight3Rects(++line); + if (!moddingSpawnPoint) + { + GUI.Label(rects[0], StringUtils.Localize("#autoLOC_8200035"), centreLabel); //Radius + GUI.Label(rects[1], StringUtils.Localize("#LOC_BDArmory_WP_SpeedLimit"), centreLabel); + } + GUI.Label(rects[moddingSpawnPoint ? 0 : 2], StringUtils.Localize("#LOC_BDArmory_WP_Increment"), centreLabel); + rects = SRight3Rects(++line); + if (!moddingSpawnPoint) + { + if (GUI.RepeatButton(SFieldButtonRect(line, 1), "<", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["diameter"].SetCurrentValue(spawnFields["diameter"].currentValue - movementIncrement); + } + spawnFields["diameter"].tryParseValue(GUI.TextField(rects[0], spawnFields["diameter"].possibleValue, 8, spawnFields["diameter"].style)); + if (GUI.RepeatButton(SFieldButtonRect(line, 7), ">", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["diameter"].SetCurrentValue(spawnFields["diameter"].currentValue + movementIncrement); + if (spawnFields["diameter"].currentValue > 1000) spawnFields["diameter"].SetCurrentValue(1000); + } + if (spawnFields["diameter"].currentValue < 5) spawnFields["diameter"].SetCurrentValue(5); + if (GUI.RepeatButton(SFieldButtonRect(line, 8.5f), "<", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["speed"].SetCurrentValue(spawnFields["speed"].currentValue - (movementIncrement)); + if (spawnFields["speed"].currentValue < 0) spawnFields["speed"].SetCurrentValue(-1); + } + spawnFields["speed"].tryParseValue(GUI.TextField(rects[1], spawnFields["speed"].possibleValue, 8, spawnFields["speed"].style)); + if (GUI.RepeatButton(SFieldButtonRect(line, 14.5f), ">", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["speed"].SetCurrentValue(spawnFields["speed"].currentValue + movementIncrement); + if (spawnFields["speed"].currentValue > 3000) spawnFields["speed"].SetCurrentValue(3000); + } + } + if (GUI.Button(SFieldButtonRect(line, moddingSpawnPoint ? 1 : 16), "<", BDArmorySetup.BDGuiSkin.button)) + { + if (movementIncrement >= 2) + spawnFields["increment"].SetCurrentValue(spawnFields["increment"].currentValue - 1); + else + { + if (movementIncrement >= 0.2) + spawnFields["increment"].SetCurrentValue(spawnFields["increment"].currentValue - 0.1); //there is almost certainly a more elegant way to do scaling, FIXME later + else + spawnFields["increment"].SetCurrentValue(spawnFields["increment"].currentValue - 0.01); + } + if (spawnFields["increment"].currentValue < 0.001f) spawnFields["increment"].SetCurrentValue(0.001f); + } + spawnFields["increment"].tryParseValue(GUI.TextField(rects[moddingSpawnPoint ? 0 : 2], spawnFields["increment"].possibleValue, 8, spawnFields["increment"].style)); + if (GUI.Button(SFieldButtonRect(line, moddingSpawnPoint ? 7 : 22), ">", BDArmorySetup.BDGuiSkin.button)) + { + spawnFields["increment"].SetCurrentValue(spawnFields["increment"].currentValue + 1); + if (spawnFields["increment"].currentValue > 1000) spawnFields["increment"].SetCurrentValue(1000); + } + movementIncrement = spawnFields["increment"].currentValue; + if (!moddingSpawnPoint) + { + if (spawnFields["lat"].currentValue != currGate.location.x || + spawnFields["lon"].currentValue != currGate.location.y || + spawnFields["alt"].currentValue != currGate.location.z || + spawnFields["speed"].currentValue != currGate.maxSpeed || + spawnFields["diameter"].currentValue != currGate.scale) + { + currGate.location = new Vector3d(spawnFields["lat"].currentValue, spawnFields["lon"].currentValue, spawnFields["alt"].currentValue); + currGate.scale = (float)spawnFields["diameter"].currentValue; + currGate.maxSpeed = (float)spawnFields["speed"].currentValue; + //WaypointField.Save(); //instead have a separate button and do saving manually? + currGate.model = SelectedModel; + loadedGates[selected_gate_index].UpdateWaypoint(currGate, selected_gate_index, WaypointCourses.CourseLocations[selected_index].waypoints); + //SnapCameraToGate(false); + + } + } + else + { + if (spawnFields["lat"].currentValue != WaypointCourses.CourseLocations[selected_index].spawnPoint.x || +spawnFields["lon"].currentValue != WaypointCourses.CourseLocations[selected_index].spawnPoint.y || +spawnFields["alt"].currentValue != BDArmorySettings.VESSEL_SPAWN_ALTITUDE) + { + WaypointCourses.CourseLocations[selected_index].spawnPoint = new Vector2d(spawnFields["lat"].currentValue, spawnFields["lon"].currentValue); + BDArmorySettings.VESSEL_SPAWN_ALTITUDE = (float)spawnFields["alt"].currentValue; + + //WaypointField.Save(); //instead have a separate button and do saving manually? + //SnapCameraToGate(false); + } + } + if (currGate != null && !string.IsNullOrEmpty(txtName.Trim()) && txtName != currGate.name) + currGate.name = txtName; + line += 0.3f; + } + + if (selected_index >= 0 && GUI.Button(SLeftButtonRect(++line), StringUtils.Localize("#autoLOC_900627") + StringUtils.Localize("#autoLOC_6003085"), showCoursePath ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) //view path + { + showCoursePath = !showCoursePath; + } + + if (selected_index >= 0 && GUI.Button(SRightButtonRect(line), StringUtils.Localize("Snap Camera"), BDArmorySetup.BDGuiSkin.button)) //view path + { + SnapCameraToGate(); + } + + line += 1.25f; // Bottom internal margin + _windowHeight = (line * _lineHeight); + } + + public void SetVisible(bool visible) + { + BDArmorySetup.showWPBuilderGUI = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); + if (!visible && !TournamentCoordinator.Instance.IsRunning) //don't delete gates if running course + { + foreach (var gate in loadedGates) + { + gate.disabled = true; + gate.gameObject.SetActive(false); + } + loadedGates.Clear(); + } + } + void AddGate(string newName) + { + Vector3d gateCoords; + FlightGlobals.currentMainBody.GetLatLonAlt(FlightGlobals.ActiveVessel.CoM, out gateCoords.x, out gateCoords.y, out gateCoords.z); + int wp; + if (!recording && selected_gate_index < WaypointCourses.CourseLocations[selected_index].waypoints.Count - 1) + { + WaypointCourses.CourseLocations[selected_index].waypoints.Insert(selected_gate_index + 1, (new Waypoint(newName, gateCoords, 500, -1, SelectedModel))); + wp = selected_gate_index + 1; + } + else + { + WaypointCourses.CourseLocations[selected_index].waypoints.Add(new Waypoint(newName, gateCoords, 500, -1, SelectedModel)); + wp = WaypointCourses.CourseLocations[selected_index].waypoints.Count - 1; + } + //WaypointField.Save(); + if (string.IsNullOrEmpty(SelectedModel)) + SelectedModel = "Ring"; + + Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(WaypointCourses.CourseLocations[selected_index].waypoints[wp].location.x, WaypointCourses.CourseLocations[selected_index].waypoints[wp].location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE < 0 ? WaypointCourses.CourseLocations[selected_index].waypoints[wp].location.z : BDArmorySettings.WAYPOINTS_ALTITUDE)), FlightGlobals.currentMainBody); + Vector3d previousLocation = FlightGlobals.ActiveVessel.transform.position; + if (wp > 0) previousLocation = VectorUtils.GetWorldSurfacePostion(new Vector3(WaypointCourses.CourseLocations[selected_index].waypoints[wp - 1].location.x, WaypointCourses.CourseLocations[selected_index].waypoints[wp - 1].location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE < 0 ? WaypointCourses.CourseLocations[selected_index].waypoints[wp - 1].location.z : BDArmorySettings.WAYPOINTS_ALTITUDE)), FlightGlobals.currentMainBody); + + var direction = (WorldCoords - previousLocation).normalized; + WayPointMarker.CreateWaypoint(WorldCoords, direction, SelectedModel, WaypointCourses.CourseLocations[selected_index].waypoints[wp].scale); + + var location = string.Format("({0:##.###}, {1:##.###}, {2:####}", WaypointCourses.CourseLocations[selected_index].waypoints[wp].location.x, WaypointCourses.CourseLocations[selected_index].waypoints[wp].location.y, WaypointCourses.CourseLocations[selected_index].waypoints[wp].location.z); + Debug.Log("[BDArmory.Waypoints]: Creating waypoint marker at " + " " + location + " World: " + FlightGlobals.currentMainBody.flightGlobalsIndex + " scale: " + WaypointCourses.CourseLocations[selected_index].waypoints[wp].scale + " model:" + WaypointCourses.CourseLocations[selected_index].waypoints[wp].model); + if (!recording) + { + if (selected_gate_index < WaypointCourses.CourseLocations[selected_index].waypoints.Count - 1) + selected_gate_index++; + else + selected_gate_index = wp; + //SnapCameraToGate(false); //this will need to change if adding ability to insert gates into middle of course, not at end + + var newGate = WaypointCourses.CourseLocations[selected_index].waypoints[wp]; + spawnFields["lat"].SetCurrentValue(newGate.location.x); + spawnFields["lon"].SetCurrentValue(newGate.location.y); + spawnFields["alt"].SetCurrentValue(newGate.location.z); + spawnFields["diameter"].SetCurrentValue(500); + spawnFields["speed"].SetCurrentValue(-1); + + moddingSpawnPoint = false; + txtName = WaypointCourses.CourseLocations[selected_index].waypoints[selected_gate_index].name; + showPositioningControls = true; + } + } + } +} \ No newline at end of file diff --git a/BDArmory/GameModes/Waypoints/WaypointCourses.cs b/BDArmory/GameModes/Waypoints/WaypointCourses.cs new file mode 100644 index 000000000..31433106c --- /dev/null +++ b/BDArmory/GameModes/Waypoints/WaypointCourses.cs @@ -0,0 +1,264 @@ +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 float maxSpeed; + public string model; + //public string mutator; + //have it so gates can trigger a specific mutator on passing? Speed boost/add ballast/fuel regen/etc? + public Waypoint(string _name, Vector3 _location, float _scale, float _speed, string _model) { name = _name; location = _location; scale = _scale; maxSpeed = _speed; model = _model; } + public override string ToString() { return name + "| " + location.ToString("G6") + "| " + scale.ToString() + "| " + maxSpeed.ToString() + "| " + model + ": "; } + } + + public class WaypointCourse + { + public static string waypointLocationsCfg = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/Waypoint_locations.cfg")); + public string name; + public int worldIndex; + public Vector2d 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, Vector2d _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 Vector2d(27.97f, -39.35f), new List { + new Waypoint("Start", new Vector3(28.33f, -39.11f, 50), 200, -1, ""), + new Waypoint("Run-off Corner", new Vector3(28.83f, -38.06f, 50), 200, -1, ""), + new Waypoint("Careful River", new Vector3(29.54f, -38.68f, 50), 200, -1, ""), + new Waypoint("Lake of Mercy", new Vector3(30.15f, -38.6f, 50), 200, -1, ""), + new Waypoint("Danger Zone Narrows", new Vector3(30.83f, -38.87f, 50), 200, -1, ""), + new Waypoint("Chicane of Pain", new Vector3(30.73f, -39.6f, 50), 200, -1, ""), + new Waypoint("Bumpy Boi Lane", new Vector3(30.9f, -40.23f, 50), 200, -1, ""), + new Waypoint("Blaring Straights", new Vector3(30.83f, -41.26f, 50), 200, -1, "") + }), + new WaypointCourse("Slalom", 1, new Vector2d(-21.0158f, 72.2085f), new List { + new Waypoint("Waypoint 0", new Vector3(-21.0763f, 72.7194f, 100), 200, -1, ""), + new Waypoint("Waypoint 1", new Vector3(-21.3509f, 73.7466f, 100), 200, -1, ""), + new Waypoint("Waypoint 2", new Vector3(-20.8125f, 73.8125f, 100), 200, -1, ""), + new Waypoint("Waypoint 3", new Vector3(-20.6478f, 74.8177f, 100), 200, -1, ""), + new Waypoint("Waypoint 4", new Vector3(-20.2468f, 74.5046f, 100), 200, -1, ""), + new Waypoint("Waypoint 5", new Vector3(-19.7469f, 75.1252f, 100), 200, -1, ""), + new Waypoint("Waypoint 6", new Vector3(-19.2360f, 75.1363f, 100), 200, -1, ""), + new Waypoint("Waypoint 7", new Vector3(-18.8954f, 74.6530f, 100), 200, -1, "") + }), + new WaypointCourse("Coast Circuit", 1, new Vector2d(-7.7134f, -42.7633f), new List { + new Waypoint("Waypoint 0", new Vector3(-8.1628f, -42.7478f, 50), 200, -1, ""), + new Waypoint("Waypoint 1", new Vector3(-8.6737f, -42.7423f, 50), 200, -1, ""), + new Waypoint("Waypoint 2", new Vector3(-9.2230f, -42.5208f, 50), 200, -1, ""), + new Waypoint("Waypoint 3", new Vector3(-9.6624f, -43.3355f, 50), 200, -1, ""), + new Waypoint("Waypoint 4", new Vector3(-10.6732f, -43.3410f, 50), 200, -1, ""), + new Waypoint("Waypoint 5", new Vector3(-11.3379f, -42.9236f, 50), 200, -1, ""), + new Waypoint("Waypoint 6", new Vector3(-10.9415f, -42.3449f, 50), 200, -1, ""), + new Waypoint("Waypoint 7", new Vector3(-10.8591f, -41.8670f, 50), 200, -1, ""), + new Waypoint("Waypoint 8", new Vector3(-10.5515f, -41.6198f, 50), 200, -1, ""), + new Waypoint("Waypoint 9", new Vector3(-10.4746f, -41.2133f, 50), 200, -1, ""), + new Waypoint("Waypoint 10", new Vector3(-9.6945f, -41.2847f, 50), 200, -1, ""), + new Waypoint("Waypoint 11", new Vector3(-9.5407f, -42.1911f, 50), 200, -1, ""), + new Waypoint("Waypoint 12", new Vector3(-9.1342f, -42.0757f, 50), 200, -1, "") + }) + }; + + 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), 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, "WaypointCourse"); + 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, string what) + { + try + { + 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], "WaypointCourse Name"); + var worldIndex = (int)ParseValue(typeof(int), parts[1], "WaypointCourse World Index"); + var spawnPoint = (Vector2d)ParseValue(typeof(Vector2d), parts[2], "WaypointCourse Spawn Point"); + 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], "Waypoint Name"); + WPname = WPname.Trim(' '); + if (string.IsNullOrEmpty(WPname)) WPname = $"Waypoint {i}"; + var location = (Vector3)ParseValue(typeof(Vector3), datavars[1], "Waypoint Location"); + var scale = (float)ParseValue(typeof(float), datavars[2], "Waypoint Scale"); + float speed; + try + { + speed = (float)ParseValue(typeof(float), datavars[3], "Waypoint Speed"); + } + catch { speed = -1f; } + string model; + try + { + model = (string)ParseValue(typeof(string), datavars[4], "Waypoint Model"); + model = model.Trim(' '); + } + catch { model = ""; } + if (name != null && location != null) + waypointList.Add(new Waypoint(WPname, location, scale, speed, model)); + } + + if (name != null && spawnPoint != null && waypointList.Count > 0) + return new WaypointCourse(name, worldIndex, spawnPoint, waypointList); + } + } + else + { + return BDAPersistentSettingsField.ParseValue(type, value, what); + } + } + catch (Exception e) + { + Debug.LogException(e); + } + Debug.LogError("[BDArmory.WaypointCourses]: Failed to parse settings field of type " + type + " and value " + value); + return null; + } + } +} \ No newline at end of file diff --git a/BDArmory/GameModes/_description b/BDArmory/GameModes/_description new file mode 100644 index 000000000..bb758cd3e --- /dev/null +++ b/BDArmory/GameModes/_description @@ -0,0 +1 @@ +Code for game modes. \ No newline at end of file diff --git a/BDArmory/Guidances/BallisticGuidance.cs b/BDArmory/Guidances/BallisticGuidance.cs index 1da643604..8ff5d9b1f 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 @@ -12,7 +14,7 @@ class BallisticGuidance : IGuidance private double _originalDistance = float.MinValue; - public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) + public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 targetVelocity) { //set up if (_originalDistance == float.MinValue) @@ -26,21 +28,17 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) var pendingDistance = _originalDistance - surfaceDistanceVector.magnitude; - if (missile.TimeIndex < 1) - { - return missile.vessel.CoM + missile.vessel.Velocity() * 10; - } + // Not really necessary, especially with the new controller, if desired it can be implemented via guidance delay + //if (missile.TimeIndex < 1) + //{ + // return missile.vessel.CoM + missile.vessel.Velocity() * 10; + //} Vector3 agmTarget; 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,40 +47,35 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) 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; - Vector3 direction = Quaternion.AngleAxis(Mathf.Clamp(missile.maxOffBoresight * 0.9f, 0, missile.BallisticAngle), Vector3.Cross(dToTarget, VectorUtils.GetUpDirection(missile.vessel.CoM))) * dToTarget; + Vector3 direction = Quaternion.AngleAxis(Mathf.Clamp(missile.maxOffBoresight * 0.9f, 0, missile.BallisticAngle), Vector3.Cross(dToTarget, missile.vessel.up)) * dToTarget; 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); - agmTarget = MissileGuidance.GetAirToGroundTarget(targetPosition, missile.vessel, 1.85f); + 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); } @@ -104,17 +97,17 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) private double CalculateFreeFallTime(MissileBase missile, int predictionTime = 10) { - double vi = CalculateFutureVerticalSpeed(missile, predictionTime) * -1; + double vi = -CalculateFutureVerticalSpeed(missile, predictionTime); 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 ce190bd3a..945952b4e 100644 --- a/BDArmory/Guidances/CruiseGuidance.cs +++ b/BDArmory/Guidances/CruiseGuidance.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 { public enum GuidanceState @@ -11,7 +13,8 @@ public enum GuidanceState Ascending, Cruising, Descending, - Terminal + Terminal, + Popup } public enum PitchDecision @@ -32,7 +35,7 @@ public enum ThrottleDecision public class CruiseGuidance : IGuidance { private readonly MissileBase _missile; - + private float _pitchAngle; private double _futureAltitude; @@ -49,9 +52,17 @@ public class CruiseGuidance : IGuidance private Vector3 planarDirectionToTarget; private Vector3 upDirection; - public CruiseGuidance(MissileBase missile) + private float _popupCos = -1f; + private float _popupSin = -1f; + + // Popup 1/g + float invG = 1f / 10f; // 1/maneuver G + const float invg = 1f / 9.80665f; // 1/gravity on Earth/Kerbin + + public CruiseGuidance(MissileBase missile, float invManeuvergLimit = 0.1f) // 1/maneuver G { _missile = missile; + invG = invManeuvergLimit; } public ThrottleDecision ThrottleDecision { get; set; } @@ -59,42 +70,30 @@ public CruiseGuidance(MissileBase missile) public GuidanceState GuidanceState { get; set; } - public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) + public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 targetVelocity) { //set up - if (_missile.TimeIndex < 1) - return _missile.vessel.CoM + _missile.vessel.Velocity() * 10; + // Not really necessary, especially with the new controller, if desired it can be implemented via guidance delay + //if (_missile.TimeIndex < 1) + // return _missile.vessel.CoM + _missile.vessel.Velocity() * 10; - upDirection = VectorUtils.GetUpDirection(_missile.vessel.CoM); + upDirection = _missile.vessel.up; - planarDirectionToTarget = - Vector3.ProjectOnPlane(targetPosition - _missile.vessel.CoM, upDirection).normalized; + planarDirectionToTarget = (targetPosition - _missile.vessel.CoM).ProjectOnPlanePreNormalized(upDirection).normalized; // Ascending - _missile.debugString.Append("State=" + GuidanceState); - _missile.debugString.Append(Environment.NewLine); - var missileAltitude = GetCurrentAltitude(_missile.vessel); - _missile.debugString.Append("Altitude=" + missileAltitude); - _missile.debugString.Append(Environment.NewLine); - - _missile.debugString.Append("Apoapsis=" + _missile.vessel.orbit.ApA); - _missile.debugString.Append(Environment.NewLine); - - _missile.debugString.Append("Future Altitude=" + _futureAltitude); - _missile.debugString.Append(Environment.NewLine); - - _missile.debugString.Append("Pitch angle=" + _pitchAngle); - _missile.debugString.Append(Environment.NewLine); - - _missile.debugString.Append("Pitch decision=" + PitchDecision); - _missile.debugString.Append(Environment.NewLine); - - _missile.debugString.Append("lastVerticalSpeed=" + _lastVerticalSpeed); - _missile.debugString.Append(Environment.NewLine); - - _missile.debugString.Append("verticalAcceleration=" + _verticalAcceleration); - _missile.debugString.Append(Environment.NewLine); + 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(); @@ -113,21 +112,20 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) CheckIfTerminal(missileAltitude, targetPosition, upDirection); - return _missile.vessel.CoM + (planarDirectionToTarget.normalized + upDirection.normalized) * 10f; + return _missile.vessel.CoM + (planarDirectionToTarget + upDirection) * 10f; case GuidanceState.Cruising: - CheckIfTerminal(missileAltitude, targetPosition, upDirection); //Altitude control UpdatePitch(missileAltitude); UpdateThrottle(); + CheckIfTerminal(missileAltitude, targetPosition, upDirection); - return _missile.vessel.CoM + 10 * planarDirectionToTarget.normalized + _pitchAngle * upDirection; + return _missile.vessel.CoM + 10 * planarDirectionToTarget + _pitchAngle * upDirection; 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); @@ -135,7 +133,15 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition) if (_missile.vessel.InVacuum()) return _missile.vessel.CoM + _missile.vessel.Velocity() * 10; - return MissileGuidance.GetAirToGroundTarget(targetPosition, _missile.vessel, 1.85f); + return MissileGuidance.GetAirToGroundTarget(targetPosition, targetVelocity, _missile.vessel, 1.85f); + + case GuidanceState.Popup: + _missile.Throttle = Mathf.Clamp((float)(_missile.vessel.atmDensity * 10f), 0.01f, 1f); + + if (missileAltitude > _missile.CruisePopupAltitude) + GuidanceState = GuidanceState.Terminal; + + return _missile.vessel.CoM + 50f * (planarDirectionToTarget * _popupCos + upDirection * _popupSin); } return _missile.vessel.CoM + _missile.vessel.Velocity() * 10; @@ -147,10 +153,12 @@ 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 temp = Math.Sqrt(vi * vi - 4 * (0.5f * a) * (-d)); - return Math.Max(time1, time2); + double time1 = (-vi + temp); + double time2 = (-vi - temp); + + return Math.Max(time1, time2) / a; } private float GetProperDescentRatio(double missileAltitude) @@ -173,22 +181,62 @@ private void GetTelemetryData() private bool CheckIfTerminal(double altitude, Vector3 targetPosition, Vector3 upDirection) { - Vector3 surfacePos = this._missile.vessel.transform.position + - Vector3.Project(targetPosition - this._missile.vessel.transform.position, -upDirection); + Vector3 surfacePos = _missile.vessel.CoM + + Vector3.Project(targetPosition - _missile.vessel.CoM, -upDirection); float distanceToTarget = Vector3.Distance(surfacePos, targetPosition); - _missile.debugString.Append($"Distance to target" + distanceToTarget); - _missile.debugString.Append(Environment.NewLine); - double freefallTime = CalculateFreeFallTime(altitude); + if (_missile.CruisePopup) + { + if (_popupCos < 0) + { + _popupCos = Mathf.Cos(_missile.CruisePopupAngle * Mathf.Deg2Rad); + _popupSin = Mathf.Sin(_missile.CruisePopupAngle * Mathf.Deg2Rad); + } + + if (distanceToTarget < _missile.CruisePopupRange + _futureSpeed * _missile.CruisePredictionTime) + { + float a = Vector3.Dot(_missile.GetForwardTransform(), upDirection); - _missile.debugString.Append($"freefallTime" + freefallTime); - _missile.debugString.Append(Environment.NewLine); + // Time taken to sweep through the turning arc is arc length / cruise speed + // arc length = (angle in rad) * r + // mv^2/r = ma -> v^2/a = r, a = (9.81) * gLoad + // arc length = (angle in rad) * v^2 * invG * invg + // thus: t = (angle in rad) * v * invG * invg + _futureSpeed = CalculateFutureSpeed((_missile.CruisePopupAngle * Mathf.Deg2Rad - Mathf.Acos(a)) * (float)_lastHorizontalSpeed * invG * invg); - if (distanceToTarget < (freefallTime * _missile.vessel.horizontalSrfSpeed)) + // turn dist is just r * (sin(popup angle) - cos(curr angle)) due to simple trigonometry + float turnDist = (float)(_futureSpeed * _futureSpeed) * invG * invg * (_popupSin - a); + + _missile.Throttle = 1f; + + //if (BDArmorySettings.DEBUG_MISSILES) + // Debug.Log($"[BDArmory.CruiseGuidance] a = {a}, futureSpeed = {_futureSpeed} m/s, turnDist = {turnDist} m."); + + if (distanceToTarget < _missile.CruisePopupRange + turnDist) + { + GuidanceState = GuidanceState.Popup; + return true; + } + } + + return false; + } + else { - GuidanceState = GuidanceState.Terminal; - return true; + double freefallTime = CalculateFreeFallTime(altitude); + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + _missile.debugString.AppendLine($"Distance to target={distanceToTarget}m"); + _missile.debugString.AppendLine($"freefallTime={freefallTime}s"); + } + + if (distanceToTarget < (freefallTime * _missile.vessel.horizontalSrfSpeed)) + { + GuidanceState = GuidanceState.Terminal; + return true; + } } return false; } @@ -221,7 +269,7 @@ private double GetCurrentAltitudeAtPosition(Vector3 position) // var terrainRay = new Ray(position, tRayDirection); // RaycastHit rayHit; - // if (Physics.Raycast(terrainRay, out rayHit, 30000, (1 << 15) | (1 << 17))) + // if (Physics.Raycast(terrainRay, out rayHit, 30000, (int)(LayerMasks.Scenery | LayerMasks.EVA))) // Why EVA? // { // var detectedAlt = // Vector3.Project(rayHit.point - position, upDirection).magnitude; @@ -235,13 +283,13 @@ private bool CalculateFutureCollision(float predictionTime) { var terrainRay = new Ray(this._missile.vessel.CoM, this._missile.vessel.Velocity()); RaycastHit hit; - return Physics.Raycast(terrainRay, out hit, (float)(this._missile.vessel.srfSpeed * predictionTime), (1 << 15) | (1 << 17)); + return Physics.Raycast(terrainRay, out hit, (float)(this._missile.vessel.srfSpeed * predictionTime), (int)(LayerMasks.Scenery | LayerMasks.EVA)); // Why EVA? } private void MakeDecisionAboutThrottle(MissileBase missile) { const double maxError = 10; - _futureSpeed = CalculateFutureSpeed(); + _futureSpeed = CalculateFutureSpeed(_missile.CruisePredictionTime); var currentSpeedDelta = missile.vessel.horizontalSrfSpeed - _missile.CruiseSpeed; @@ -328,14 +376,14 @@ 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); } - private double CalculateFutureSpeed() + private double CalculateFutureSpeed(float time) { - return _missile.vessel.horizontalSrfSpeed + (_horizontalAcceleration / Time.fixedDeltaTime) * _missile.CruisePredictionTime; + return _missile.vessel.horizontalSrfSpeed + (_horizontalAcceleration / Time.fixedDeltaTime) * time; } private bool MissileWillReachAltitude(double currentAltitude) diff --git a/BDArmory/Guidances/IGuidance.cs b/BDArmory/Guidances/IGuidance.cs index be7a16c90..b65a19554 100644 --- a/BDArmory/Guidances/IGuidance.cs +++ b/BDArmory/Guidances/IGuidance.cs @@ -1,10 +1,11 @@ -using BDArmory.Modules; -using UnityEngine; +using UnityEngine; + +using BDArmory.Weapons.Missiles; namespace BDArmory.Guidances { public interface IGuidance { - Vector3 GetDirection(MissileBase missile, Vector3 targetPosition); + Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 targetVelocity); } } \ No newline at end of file diff --git a/BDArmory/Guidances/MissileGuidance.cs b/BDArmory/Guidances/MissileGuidance.cs index 1da12d353..f7d8b2a76 100644 --- a/BDArmory/Guidances/MissileGuidance.cs +++ b/BDArmory/Guidances/MissileGuidance.cs @@ -1,21 +1,29 @@ -using System; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.Modules; +using System; using UnityEngine; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; +using BDArmory.Weapons; + namespace BDArmory.Guidances { public class MissileGuidance { - public static Vector3 GetAirToGroundTarget(Vector3 targetPosition, Vessel missileVessel, float descentRatio) + const float invg = 1f / 9.80665f; // 1/gravity on Earth/Kerbin + + public static Vector3 GetAirToGroundTarget(Vector3 targetPosition, Vector3 targetVelocity, Vessel missileVessel, float descentRatio, float minSpeed = 200) { - Vector3 upDirection = VectorUtils.GetUpDirection(missileVessel.CoM); - //-FlightGlobals.getGeeForceAtPosition(targetPosition).normalized; - Vector3 surfacePos = missileVessel.transform.position + - Vector3.Project(targetPosition - missileVessel.transform.position, upDirection); + // Incorporate lead for target velocity + Vector3 currVel = Mathf.Max((float)missileVessel.srfSpeed, minSpeed) * missileVessel.Velocity().normalized; + float targetDistance = Vector3.Distance(targetPosition, missileVessel.CoM); + float leadTime = Mathf.Clamp(targetDistance / (targetVelocity - currVel).magnitude, 0f, 8f); + targetPosition += targetVelocity * leadTime; + + Vector3 upDirection = missileVessel.up; + Vector3 surfacePos = missileVessel.CoM + + Vector3.Project(targetPosition - missileVessel.CoM, upDirection); //((float)missileVessel.altitude*upDirection); Vector3 targetSurfacePos; @@ -26,18 +34,17 @@ public static Vector3 GetAirToGroundTarget(Vector3 targetPosition, Vessel missil if (missileVessel.srfSpeed < 75 && missileVessel.verticalSpeed < 10) //gain altitude if launching from stationary { - return missileVessel.transform.position + (5 * missileVessel.transform.forward) + (1 * upDirection); + return missileVessel.CoM + (5 * missileVessel.transform.forward) + (1 * upDirection); } float altitudeClamp = Mathf.Clamp( (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; } @@ -45,27 +52,27 @@ public static Vector3 GetAirToGroundTarget(Vector3 targetPosition, Vessel missil public static bool GetBallisticGuidanceTarget(Vector3 targetPosition, Vessel missileVessel, bool direct, out Vector3 finalTarget) { - Vector3 up = VectorUtils.GetUpDirection(missileVessel.transform.position); - Vector3 forward = Vector3.ProjectOnPlane(targetPosition - missileVessel.transform.position, up); + Vector3 up = missileVessel.up; + Vector3 forward = (targetPosition - missileVessel.CoM).ProjectOnPlanePreNormalized(up); float speed = (float)missileVessel.srfSpeed; float sqrSpeed = speed * speed; float sqrSpeedSqr = sqrSpeed * sqrSpeed; - float g = (float)FlightGlobals.getGeeForceAtPosition(missileVessel.transform.position).magnitude; + float g = (float)FlightGlobals.getGeeForceAtPosition(missileVessel.CoM).magnitude; float height = FlightGlobals.getAltitudeAtPos(targetPosition) - - FlightGlobals.getAltitudeAtPos(missileVessel.transform.position); + FlightGlobals.getAltitudeAtPos(missileVessel.CoM); 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); if (!float.IsNaN(theta)) { Vector3 finalVector = Quaternion.AngleAxis(theta * Mathf.Rad2Deg, Vector3.Cross(forward, up)) * forward; - finalTarget = missileVessel.transform.position + (100 * finalVector); + finalTarget = missileVessel.CoM + (100 * finalVector); return true; } else @@ -79,7 +86,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; @@ -87,11 +94,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); @@ -108,10 +115,134 @@ public static bool GetBallisticGuidanceTarget(Vector3 targetPosition, Vector3 mi } } + public static Vector3 GetCLOSTarget(Vector3 sensorPos, Vector3 currentPos, Vector3 currentVelocity, Vector3 targetPos, Vector3 targetVel, + float correctionFactor, float N, out float gLimit) + { + targetPos += targetVel * Time.fixedDeltaTime; + Vector3 accel = GetCLOSAccel(sensorPos, Vector3.zero, currentPos, currentVelocity, targetPos, targetVel, Vector3.zero, correctionFactor, N); + gLimit = accel.magnitude; + + return currentPos + 4f * currentVelocity + 16f * accel; + } + + public static Vector3 GetThreePointTarget(Vector3 sensorPos, Vector3 sensorVel, Vector3 currentPos, Vector3 currentVelocity, Vector3 targetPos, Vector3 targetVel, + float correctionFactor, float N, out float gLimit) + { + Vector3 relVelocity = targetVel - sensorVel; + Vector3 relRange = targetPos - sensorPos; + Vector3 angVel = Vector3.Cross(relRange, relVelocity) / relRange.sqrMagnitude; + + Vector3 accel = GetCLOSAccel(sensorPos, sensorVel, currentPos, currentVelocity, targetPos, targetVel, angVel, correctionFactor, N); + + accel -= 2f * Vector3.Cross(currentVelocity, angVel); + gLimit = accel.magnitude / 9.80665f; + + return currentPos + 4f * currentVelocity + 16f * accel; + } + + public static Vector3 GetCLOSLeadTarget(Vector3 sensorPos, Vector3 sensorVel, Vector3 currentPos, Vector3 currentVelocity, Vector3 targetPos, Vector3 targetVel, + float correctionFactor, float N, float beamLeadFactor, out float gLimit, MissileLauncher ml) + { + Vector3 relVelocity = targetVel - sensorVel; + Vector3 relRange = targetPos - sensorPos; + float RSqr = relRange.sqrMagnitude; + + float currVel = currentVelocity.magnitude; + if (currVel < 200f) + currentVelocity *= 200f / currVel; + + (float rangeM, Vector3 dirM) = (currentPos - sensorPos).MagNorm(); + (float rangeT, Vector3 dirT) = (targetPos - sensorPos).MagNorm(); + float leadTime = Mathf.Clamp((rangeT - rangeM) / (Mathf.Max(currVel, 200f) - Vector3.Dot(targetVel, dirT)), 0f, 8f); + + Vector3 deltaLOS = (Mathf.Clamp01(beamLeadFactor) * leadTime / RSqr) * Vector3.Cross(relRange, relVelocity); + Quaternion rotation = Quaternion.AngleAxis(deltaLOS.magnitude * Mathf.Rad2Deg, deltaLOS); + Vector3 corrRelRange = rotation * relRange; + + Vector3 angVel = Vector3.Cross(relRange, relVelocity) / RSqr; + + // Once below the max leadTime, the LoS vector moves towards the target at half of angVel due to the nature of half-rectification + // guidance, hence when we get the CLOS accel we use half of angVel + // Now that half-rectification has been generalized, this is beamLeadFactor rather than 0.5. + if (leadTime < 8) + angVel *= (1f - beamLeadFactor); + + Vector3 accel = GetCLOSAccel(sensorPos, sensorVel, currentPos, currentVelocity, sensorPos + corrRelRange, targetVel, angVel, correctionFactor, N); + + ml.DrawDebugLine(sensorPos, sensorPos + corrRelRange); + + //accel -= 2f * Vector3.Cross(currentVelocity, angVel); + gLimit = accel.magnitude / 9.80665f; + + return currentPos + 4f * currentVelocity + 16f * accel; + } + + /*public static Vector3 GetCLOSAccel(Vector3 sensorPos, Vector3 currentPos, Vector3 currentVelocity, Vector3 targetPos, Vector3 targetVel, + float correctionFactor, float correctionDamping) + { + Vector3 beamDir = (targetPos - sensorPos).normalized; + float onBeamDistance = Vector3.Dot(currentPos - sensorPos, beamDir); + Vector3 onBeamPos = sensorPos + beamDir * onBeamDistance; + + (float beamError, Vector3 beamErrorV) = (onBeamPos - currentPos).MagNorm(); + + Vector3 rotVec = Vector3.Cross(beamDir, (currentPos - sensorPos).normalized); + + (float velAngularErr, Vector3 velAngularErrV) = Vector3.Cross(beamDir, currentVelocity.normalized).MagNorm(); + velAngularErr = Mathf.Acos(velAngularErr); + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] beamError: {beamError}, beamErrorV: {beamErrorV}, velAngularErr: {velAngularErr}, velAngularErrV: {velAngularErrV}."); + + Vector3 accel = Vector3.Cross(currentVelocity, (correctionFactor * beamError * rotVec + correctionFactor * correctionDamping * velAngularErr * velAngularErrV)); + + return accel; + }*/ + + public static Vector3 GetCLOSAccel(Vector3 sensorPos, Vector3 sensorVel, Vector3 currentPos, Vector3 currentVelocity, Vector3 targetPos, Vector3 targetVel, Vector3 beamAngVel, + float correctionFactor, float N) + { + Vector3 beamDir = (targetPos - sensorPos).normalized; + float onBeamDistance = Vector3.Dot(currentPos - sensorPos, beamDir); + Vector3 onBeamPos = sensorPos + beamDir * onBeamDistance; + + Vector3 beamVelocity = Vector3.Cross(beamAngVel, beamDir * onBeamDistance); + + (float beamError, Vector3 beamErrorV) = (currentPos - onBeamPos).MagNorm(); + + (float currentSpeed, Vector3 velDir) = currentVelocity.MagNorm(); + + currentSpeed = Mathf.Max(currentSpeed, 200f); + + // This gives the velocity command normal to the beam + Vector3 velCommand = -correctionFactor * beamError * beamErrorV + beamVelocity + sensorVel; + + // We calculate how much remains of currentVelocity once we subtract away the velocity command + float temp = 1f - velCommand.sqrMagnitude / (currentSpeed * currentSpeed); + + if (temp < 0f) + // If the velocity command is greater than the currentSpeed then pointing we maximize + // our velocity normal to the beam + velCommand = velCommand.normalized; + else + // Otherwise, we put what remains of currentVelocity into velocity along the beamDir + velCommand = velCommand / currentSpeed + BDAMath.Sqrt(temp) * beamDir; + + // We use velCommand crossed with velDir as our angular velocity command since it's more + // efficient than using a linear angular error based command like was used in the previous + // attempt at CLOS guidance. Velocity crossed with the angular velocity command gives us our + // normal acceleration. This being a triple product we simplify to a vector difference and a + // dot product. We use a proportional constant N just like in pronav to modify the acceleration + Vector3 accel = N * currentSpeed * (velCommand - Vector3.Dot(velCommand, velDir) * velDir); + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] beamError: {beamError}, beamErrorV: {beamErrorV}, velCommand: {velCommand}, accel: {accel}."); + + return accel; + } + public static Vector3 GetBeamRideTarget(Ray beam, Vector3 currentPosition, Vector3 currentVelocity, float correctionFactor, float correctionDamping, Ray previousBeam) { - float onBeamDistance = Vector3.Project(currentPosition - beam.origin, beam.direction).magnitude; + float onBeamDistance = Vector3.Dot(currentPosition - beam.origin, beam.direction); //Vector3.Project(currentPosition - beam.origin, beam.direction).magnitude; //Vector3 onBeamPos = beam.origin+Vector3.Project(currentPosition-beam.origin, beam.direction);//beam.GetPoint(Vector3.Distance(Vector3.Project(currentPosition-beam.origin, beam.direction), Vector3.zero)); Vector3 onBeamPos = beam.GetPoint(onBeamDistance); Vector3 previousBeamPos = previousBeam.GetPoint(onBeamDistance); @@ -121,7 +252,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; @@ -131,139 +262,1037 @@ public static Vector3 GetAirToAirTarget(Vector3 targetPosition, Vector3 targetVe Vector3 targetAcceleration, Vessel missileVessel, out float timeToImpact, float minSpeed = 200) { float leadTime = 0; - float targetDistance = Vector3.Distance(targetPosition, missileVessel.transform.position); Vector3 currVel = Mathf.Max((float)missileVessel.srfSpeed, minSpeed) * missileVessel.Velocity().normalized; - leadTime = (float)(1 / ((targetVelocity - currVel).magnitude / targetDistance)); + Vector3 Rdir = (targetPosition - missileVessel.CoM); + float RSqr = Rdir.sqrMagnitude; + leadTime = -RSqr / Vector3.Dot(targetVelocity - currVel, Rdir); + + if (leadTime <= 0f) + leadTime = float.PositiveInfinity; + + //leadTime = targetDistance / (targetVelocity - currVel).magnitude; timeToImpact = leadTime; leadTime = Mathf.Clamp(leadTime, 0f, 8f); return targetPosition + (targetVelocity * leadTime); } - public static Vector3 GetAirToAirTargetModular(Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, Vessel missileVessel, out float timeToImpact) + public static Vector3 GetWeaveTarget(Vector3 targetPosition, Vector3 targetVelocity, Vessel missileVessel, ref float gVert, ref float gHorz, Vector3 gRand, ref float omega, float terminalAngle, float weaveFactor, bool useAGMDescentRatio, float agmDescentRatio, float maneuvergLimit, ref float weaveOffset, ref Vector3 weaveStart, ref float WeaveAlt, out float ttgo, out float gLimit) { - float targetDistance = Vector3.Distance(targetPosition, missileVessel.CoM); + // Based on https://www.sciencedirect.com/science/article/pii/S1474667015333437 - //Basic lead time calculation - Vector3 currVel = ((float)missileVessel.srfSpeed * missileVessel.Velocity().normalized); - timeToImpact = (float)(1 / ((targetVelocity - currVel).magnitude / targetDistance)); + Vector3 missileVel = missileVessel.Velocity(); + float speed = (float)missileVessel.speed; - // Calculate time to CPA to determine target position - float timeToCPA = missileVessel.ClosestTimeToCPA(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); - float easeVel = Mathf.Clamp01(2f - timeToCPA / 8f); - return AIUtils.PredictPosition(targetPosition, targetVelocity * easeVel, targetAcceleration * easeAccel, timeToCPA); + // Time to go calculation according to instantaneous change in range (dR/dt) + Vector3 Rdir = (targetPosition - missileVessel.CoM); + ttgo = -Rdir.sqrMagnitude / Vector3.Dot(targetVelocity - missileVel, Rdir); + + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileGuidance] targetPosition: {targetPosition}, targetVelocity: {targetVelocity}, missileVel: {missileVel}, missileSpeed: {speed}, Rdir.sqrMag: {Rdir.sqrMagnitude}, ttgo: {ttgo}"); + + if (ttgo <= 0f) + { + // Missed target, use PN as backup + return GetPNTarget(targetPosition, targetVelocity, missileVessel, 3, out ttgo, out gLimit); + } + + // Get up direction at missile location + Vector3 upDirection = missileVessel.upAxis; + + // High pass filter + if (targetVelocity.sqrMagnitude > 100f) + Rdir = new Vector3(Rdir.x + targetVelocity.x * ttgo, + Rdir.y + targetVelocity.y * ttgo, + Rdir.z + targetVelocity.z * ttgo); + + Vector3 planarDirToTarget = Rdir.ProjectOnPlanePreNormalized(upDirection).normalized; + + Vector3 right = Vector3.Cross(planarDirToTarget, upDirection); + + float pullUpCos = Vector3.Dot(missileVel.normalized, upDirection); + + float horizontalAngle = VectorUtils.GetAngleOnPlane(missileVel, planarDirToTarget, right); + const float halfPi = Mathf.PI * 0.5f; + float verticalAngle = halfPi - Mathf.Acos(pullUpCos); + + //verticalAngle *= Mathf.Deg2Rad; + horizontalAngle *= Mathf.Deg2Rad; + + /*float verticalAngle = (Mathf.Deg2Rad * Mathf.Sign(pullUpCos)) * VectorUtils.Angle(missileVel.ProjectOnPlanePreNormalized(right), planarDirToTarget); + + float horizontalAngle = (Mathf.Deg2Rad * Mathf.Sign(Vector3.Dot(missileVel, right))) * VectorUtils.Angle(missileVel.ProjectOnPlanePreNormalized(upDirection), planarDirToTarget);*/ + + const float PI2 = 2f * Mathf.PI; + + float weaveDist; + + float ttgoWeave; + if (weaveOffset < 0) + { + weaveOffset = PI2 * omega * ttgo; + weaveStart = VectorUtils.WorldPositionToGeoCoords(missileVessel.CoM, missileVessel.mainBody); + weaveDist = Vector3.Dot(Rdir, planarDirToTarget); + ttgoWeave = ttgo; + if (UnityEngine.Random.value < 0.5) + gHorz = -gHorz; + + if (gHorz != 0.0f) + gHorz += gRand.x * (2f * UnityEngine.Random.value - 1f); + if (gVert != 0.0f) + gVert += gRand.y * (2f * UnityEngine.Random.value - 1f); + if (gRand.z != 0.0f) + omega += gRand.z * (2f * UnityEngine.Random.value - 1f); + } + else + { + Vector3 weaveDir = (targetPosition - VectorUtils.GetWorldSurfacePostion(weaveStart, missileVessel.mainBody)).ProjectOnPlanePreNormalized(upDirection).normalized; + weaveDist = Vector3.Dot(Rdir, weaveDir); + ttgoWeave = weaveFactor * 1.5f * weaveDist / speed; + right = Vector3.Cross(weaveDir, upDirection); + } + + float omegaBeta = PI2 * omega * ttgoWeave; + float sinOmegaBeta = Mathf.Sin(omegaBeta); + float cosOmegaBeta = Mathf.Cos(omegaBeta); + + float sinOmegaBetaOff = Mathf.Sin(omegaBeta - weaveOffset); + float cosOmegaBetaOff = Mathf.Cos(omegaBeta - weaveOffset); + + const float g = 9.80665f; + float ka = 2f * omegaBeta * sinOmegaBeta + 6f * cosOmegaBeta - 6f; + float kj = -2f * omegaBeta * cosOmegaBeta + 6f * sinOmegaBeta - 4f * omegaBeta; + + float ttgoWeaveInv = 1f / ttgoWeave; + float omegaBetaInv = 1f / (Mathf.Max(omegaBeta * omegaBeta, 0.000001f)); + + float gVertTemp = gVert; + float vertGuidanceAngle; + float ttgoWeaveInvVert = ttgoWeaveInv; + bool useA_BPN = (terminalAngle > 0); + + if (useAGMDescentRatio) + { + float currAlt = (float)missileVessel.altitude; + + if (WeaveAlt < 0f) + WeaveAlt = currAlt; + + float altitudeClamp = Mathf.Clamp( + (weaveDist - ((float)missileVessel.srfSpeed * agmDescentRatio)) * 0.22f, 0f, + WeaveAlt);// + Mathf.Max(VectorUtils.AnglePreNormalized(upDirection, missileVel.normalized) - 90f, 0f) * Mathf.Deg2Rad * weaveDist); + + float curvatureCompensation = (1f - Vector3.Dot(upDirection, VectorUtils.GetUpDirection(targetPosition))) * (float) FlightGlobals.currentMainBody.Radius; + + Rdir += (altitudeClamp + curvatureCompensation) * upDirection; + + vertGuidanceAngle = Mathf.Asin(Vector3.Dot(upDirection, Rdir) / Rdir.magnitude); + + /* + // If we have a vertical weave + if (pullUpCos < 0) + { + // Get distance to pull up + float pullUpSin = BDAMath.Sqrt(1f - pullUpCos * pullUpCos); + // Turn radius is mv^2/r = ma -> v^2/r = a -> v^2/a = r, a = 6 g -> v^2 * 1/6 g = r + float pullUpg = gVertTemp > 0.0f ? gVertTemp * 0.8f : (gHorz > 0.0f ? Mathf.Min(gHorz * 0.8f, 6f) : 6f); + float invG = invg / pullUpg; + float pullUpDist = (speed * speed * invG) * (1f - pullUpSin); + float altDiff = currAlt - altitudeClamp; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] speed: {speed}, altitude: {missileVessel.altitude}, altitudeClamp: {altitudeClamp}, pullUpDist: {pullUpDist}, altDiff: {altDiff}, pullUpCos: {pullUpCos}, pullUpSin: {pullUpSin}, verticalAngle: {verticalAngle}, ttgoWeaveInvVert: {ttgoWeaveInvVert}."); + + if (pullUpDist > currAlt) + { + // If we desperately need to pull up + gVertTemp = 0f; + // Calculate the vertical acceleration required to pull up in time + aVert = pullUpDist / currAlt * pullUpg * 9.8066f; + calcaVert = false; + } + // If we're above the target altitude and we need to pull up + if (altDiff > 0 && altDiff < pullUpDist) + gVertTemp = 0f; + } + */ + + float altDiff = currAlt - altitudeClamp; + if (pullUpCos < 0 || altDiff < 0) + { + // Get angle relative to vertical + float pullUpSin = BDAMath.Sqrt(1f - pullUpCos * pullUpCos); + // Turn radius is mv^2/r = ma -> v^2/r = a -> v^2/a = r, a = 6 g -> v^2 * 1/6 g = r + float invG = invg / maneuvergLimit; + float pullUpDist = (speed * speed * invG) * (1f - pullUpSin); + + if (altDiff < 0) + { + gVertTemp = 0f; + float remainingSin = pullUpSin + altDiff / (speed * speed * invG); + float remainingCos = BDAMath.Sqrt(1f - remainingSin * remainingSin); + float remainingAngle = Mathf.Asin(remainingSin); + + Vector3 turnLead = (speed * speed * invG * -0.8f) * ((pullUpCos + remainingCos) * planarDirToTarget) - altDiff * upDirection; + vertGuidanceAngle = Mathf.Asin(Vector3.Dot(upDirection, turnLead) / turnLead.magnitude); + ttgoWeaveInvVert = 1f / (Mathf.Max(-verticalAngle + remainingAngle, 0.00001f) * speed * invG * 0.8f); + //terminalAngle = 0f; + } + else if (altDiff < pullUpDist) + { + gVertTemp = 0f; + Vector3 turnLead = (speed * speed * invG * -0.8f) * (pullUpCos * planarDirToTarget) - altDiff * upDirection; + vertGuidanceAngle = Mathf.Asin(Vector3.Dot(upDirection, turnLead) / turnLead.magnitude); + ttgoWeaveInvVert = 1f / (Mathf.Max(-verticalAngle, 0.00001f) * speed * invG * 0.8f); + terminalAngle = 0f; + useA_BPN = true; + } + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] speed: {speed}, altitude: {missileVessel.altitude}, altitudeClamp: {altitudeClamp}, pullUpDist: {pullUpDist}, altDiff: {altDiff}, curvatureComp: {curvatureCompensation}, leadx: {speed * speed * invG * -0.8f * pullUpCos}, pullUpCos: {pullUpCos}, pullUpSin: {pullUpSin}, verticalAngle: {verticalAngle}, ttgoWeaveInvVert: {ttgoWeaveInvVert}."); + } + + } + else + vertGuidanceAngle = Mathf.Asin(Vector3.Dot(upDirection, Rdir) / Rdir.magnitude); + + float aVert = (useA_BPN ? (speed * (6f * vertGuidanceAngle - 4f * verticalAngle + 2f * terminalAngle * Mathf.Deg2Rad) * ttgoWeaveInvVert) : 0.0f) // A_BPN + + ((gVertTemp != 0.0f) ? (gVertTemp * g * ((ka + omegaBeta * omegaBeta) * sinOmegaBetaOff + kj * cosOmegaBetaOff) * omegaBetaInv) : 0.0f); // A_W + float aHor = (useA_BPN ? (-6f * speed * horizontalAngle) * ttgoWeaveInv : 0.0f) // A_BPN + + ((gHorz != 0.0f) ? (gHorz * g * ((ka + omegaBeta * omegaBeta) * cosOmegaBetaOff + kj * sinOmegaBetaOff) * omegaBetaInv) : 0.0f); // A_W + + Quaternion rotationPitch = Quaternion.AngleAxis(verticalAngle, right); + Quaternion rotationYaw = Quaternion.AngleAxis(horizontalAngle, upDirection); + + Vector3 accel = (aVert * (rotationPitch * rotationYaw * upDirection) + aHor * (rotationYaw * right));// + GetPNAccel(targetPosition, targetVelocity, missileVessel, 3f); + if (useA_BPN) + gLimit = BDAMath.Sqrt(aVert * aVert + aHor * aHor) / (float)PhysicsGlobals.GravitationalAcceleration; + else + { + accel += GetPNAccel(targetPosition, targetVelocity, missileVessel, 3f); + gLimit = accel.magnitude / (float)PhysicsGlobals.GravitationalAcceleration; + } + + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileGuidance] Weave guidance ttgoWeave: {ttgoWeave}, omegaBeta: {omegaBeta}, ka: {ka}, kj: {kj}, vertAngle: {Mathf.Rad2Deg * verticalAngle}, horAngle: {Mathf.Rad2Deg * horizontalAngle}, aVert: {aVert} m/s^2, aHor: {aHor} m/s^2."); + + float leadTime = Mathf.Min(4f, ttgoWeave); + + Vector3 aimPos = missileVessel.CoM + leadTime * missileVel + accel * (0.5f * leadTime * leadTime); + + return aimPos; } - /// - /// Calculate a very accurate time to impact, use the out timeToimpact property if the method returned true. DEPRECIATED, use TimeToCPA. - /// - /// - /// - /// - /// - /// - /// - /// true if it was possible to reach the target, false otherwise - private static bool CalculateAccurateTimeToImpact(float targetDistance, Vector3 targetVelocity, Vessel missileVessel, - Vector3d effectiveMissileAcceleration, Vector3 effectiveTargetAcceleration, out float timeToImpact) + // Kappa/Trajectory Curvature Optimal Guidance + public static Vector3 GetKappaTarget(Vector3 targetPosition, Vector3 targetVelocity, + MissileLauncher ml, float thrust, float shapingAngle, float rangeFac, float vertVelComp, + float targetAlt, float terminalHomingRange, float loftAngle, float loftTermAngle, + float midcourseRange, float maxAltitude, out float ttgo, out float gLimit, + ref MissileBase.LoftStates loftState) { - int iterations = 0; - Vector3d relativeAcceleration = effectiveMissileAcceleration - effectiveTargetAcceleration; - Vector3d relativeVelocity = (float)missileVessel.srfSpeed * missileVessel.Velocity().normalized - - targetVelocity; - Vector3 missileFinalPosition = missileVessel.CoM; - float previousDistanceSqr = 0f; - float currentDistanceSqr; - do + // Get surface velocity direction + Vector3 velDirection = ml.vessel.srf_vel_direction; + + // Get range + float R = Vector3.Distance(targetPosition, ml.vessel.CoM); + // Unfortunately can't be simplified as R is needed later on + + // Kappa Guidance needs an accurate measure of speed to function so no minSpeed application here + float currSpeed = (float)ml.vessel.srfSpeed; + // Set current velocity + Vector3 currVel = currSpeed * velDirection; + + // Old Method + //float leadTime = R / (targetVelocity - currVel).magnitude; + //leadTime = Mathf.Clamp(leadTime, 0f, 16f); + + // Time to go calculation according to instantaneous change in range (dR/dt) + Vector3 Rdir = (targetPosition - ml.vessel.CoM); + Vector3 velDiff = targetVelocity - currVel; + ttgo = -R*R / Vector3.Dot(velDiff, Rdir); + + // Get up direction at missile location + Vector3 upDirection = ml.vessel.upAxis; //VectorUtils.GetUpDirection(ml.vessel.CoM); + + // Get ttgo on the horizontal plane + Vector3 RPlanar = Rdir.ProjectOnPlanePreNormalized(upDirection); + float ttgoPlanar = -RPlanar.sqrMagnitude / Vector3.Dot(velDiff.ProjectOnPlanePreNormalized(upDirection), RPlanar); + + // Average the two values + ttgo = 0.5f * (ttgo + ttgoPlanar); + + // Lead limiting + if (ttgo <= 0f) + ttgo = 60f; + if (ttgo > 60f) + ttgo = 60f; + + float ttgoInv = 1f / ttgo; + + float leadTime = Mathf.Clamp(ttgo, 0f, 8f); + + // Set up PIP vector + Vector3 predictedImpactPoint = AIUtils.PredictPosition(targetPosition, targetVelocity, Vector3.zero, leadTime + TimeWarp.fixedDeltaTime); + + bool boostGuidance = (loftState < MissileBase.LoftStates.Midcourse); + + Vector3 planarDirectionToTarget = Vector3.zero; + + if (boostGuidance) + { + planarDirectionToTarget = ((predictedImpactPoint - ml.vessel.CoM).ProjectOnPlanePreNormalized(upDirection)).normalized; + + // Get angle relative to vertical + float pullDownCos = Vector3.Dot(velDirection, upDirection); + float pullDownSin = BDAMath.Sqrt(1f - pullDownCos * pullDownCos); + // Turn radius is mv^2/r = ma -> v^2/r = a -> v^2/a = r, a = 6 g -> v^2 * 1/6 g = r + float invG = invg / (ml.maneuvergLimit > 0.0f ? ml.maneuvergLimit : 20f); + float turnRadius = currSpeed * currSpeed * invG; + + //float curvatureCompensation = (1f - Vector3.Dot(upDirection, VectorUtils.GetUpDirection(predictedImpactPoint))) * (float)FlightGlobals.currentMainBody.Radius; + float termSin = Mathf.Sin(loftTermAngle * Mathf.Deg2Rad); + + // turnRadius * (pullDownCos + termSin) -> horizontal dist required to go from current orientation to terminal angle + // turnRadius * (1 - pullDownSin) -> vertical dist required to go from current orientation to horizontal + // curvatureCompensation -> dist we must move target up / origin down by if we "unwrap" the world beneath them + // to put them on a flat surface (though in this case we *only* affect the vertical axis) + Vector3 turnLead = (turnRadius * (pullDownCos + termSin)) * planarDirectionToTarget + + (turnRadius * (1f - pullDownSin)) * upDirection; //(currSpeed * currSpeed * 0.0169952698051929473876953125f) * (pullDownSin * planarDirectionToTarget + (1f - pullDownCos) * upDirection); + //float turnTimeOffset = (loftTermAngle * Mathf.Deg2Rad + 0.5f * Mathf.PI - Mathf.Acos(pullDownCos)) * currSpeed * invG; + + float sinTarget = Vector3.Dot((predictedImpactPoint - ml.vessel.CoM - turnLead).normalized, -upDirection); //Vector3.Dot((targetPosition - ml.vessel.CoM), -upDirection) / R; + + boostGuidance = (midcourseRange > 0f) && (R > midcourseRange) && (sinTarget < termSin) && (-sinTarget < Mathf.Sin(loftAngle * Mathf.Deg2Rad)); + } + + // If still in boost phase + if (boostGuidance) { - missileFinalPosition += relativeVelocity * Time.fixedDeltaTime; - relativeVelocity += relativeAcceleration; - currentDistanceSqr = (missileFinalPosition - missileVessel.CoM).sqrMagnitude; + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileGuidance]: Lofting"); + + //float altitudeClamp = Mathf.Clamp(targetAlt + 10f * rangeFac * Mathf.Pow(Vector3.Dot(targetPosition - ml.vessel.CoM, planarDirectionToTarget), Mathf.Abs(vertVelComp)), targetAlt, Mathf.Max(maxAltitude, targetAlt)); + + // Stolen from my AAMloft guidance + // Limit climb angle by turnFactor, turnFactor goes negative when above target alt + float turnFactor = (float)(maxAltitude - ml.vessel.altitude) / (4f * currSpeed); + turnFactor = Mathf.Clamp(turnFactor, -1f, 1f); + + // Limit gs during climb + gLimit = ml.maneuvergLimit; + + return ml.vessel.CoM + currSpeed * ((Mathf.Cos(loftAngle * turnFactor * Mathf.Deg2Rad) * planarDirectionToTarget) + (Mathf.Sin(loftAngle * turnFactor * Mathf.Deg2Rad) * upDirection)); + } + else + { + // Accurately predict impact point + //predictedImpactPoint = AIUtils.PredictPosition(targetPosition, targetVelocity, Vector3.zero, ttgo + TimeWarp.fixedDeltaTime); + + // Final velocity is shaped by shapingAngle, we want the missile to dive onto the target but we don't want to affect the + // horizontal components of velocity + Vector3 vF; + if (shapingAngle == 0f) + { + vF = currVel; + } + else + { + vF = velDirection.ProjectOnPlanePreNormalized(upDirection).normalized; + vF = currSpeed * (Mathf.Cos(shapingAngle * Mathf.Deg2Rad) * vF - Mathf.Sin(shapingAngle * Mathf.Deg2Rad) * upDirection); + } + + // Gains for velocity error and positional error + float K1; + float K2; - if (currentDistanceSqr <= previousDistanceSqr) + // If we're above terminal homing range + if ((loftState < MissileBase.LoftStates.Terminal) && (R > terminalHomingRange) && !ml.vessel.InVacuum()) { - Debug.Log("[BDArmory]: Accurate time to impact failed"); + loftState = MissileBase.LoftStates.Midcourse; + + if (shapingAngle != 0f) + { + // As we get closer to the target we want to focus on the positional error, not the velocity error + float factor = Mathf.Min(0.5f * (R - terminalHomingRange) / terminalHomingRange, 1f); + vF = factor * vF + (1f - factor) * currVel; + } + + // Dynamic pressure times the lift area + float q = (float)(0.5f * ml.vessel.atmDensity * ml.vessel.srfSpeed * ml.vessel.srfSpeed); + + // Needs to be changed if the lift and drag curves are changed + float Lalpha = 2.864788975654117f * q * ml.currLiftArea * BDArmorySettings.GLOBAL_LIFT_MULTIPLIER; // CLmax/AoA(CLmax) * q * S * Lift Multiplier, I.E. linearized Lift/AoA (not CL/AoA) + float D0 = 0.00215f * q * ml.currDragArea * BDArmorySettings.GLOBAL_DRAG_MULTIPLIER; // Drag at 0 AoA + float eta = 0.025f * BDArmorySettings.GLOBAL_DRAG_MULTIPLIER * ml.currDragArea / (BDArmorySettings.GLOBAL_LIFT_MULTIPLIER * ml.currLiftArea); // D = D0 + eta*Lalpha*AoA^2, quadratic approximation of drag. + // eta needs to change if the lift/drag curves are changed. Note this is for small angles + + // Pre-calculation since it's used a lot + float TL = thrust / Lalpha; + + // Ching-Fang Lin's derivation of a missile under thrust. Doesn't work well best I can tell. + //if (thrust > D0) + //{ + // float F2sqr = Lalpha * (thrust - D0) * (TL * TL + 1f) * (TL * TL + 1f) / ((float)(ml.vessel.totalMass * ml.vessel.totalMass * ml.vessel.srfSpeed * ml.vessel.srfSpeed * ml.vessel.srfSpeed * ml.vessel.srfSpeed) * (2 * eta + TL)); + // float F2 = BDAMath.Sqrt(Mathf.Abs(F2sqr)); + + // float sinF2R = Mathf.Sin(F2 * R); + // float cosF2R = Mathf.Cos(F2 * R); + + // K1 = F2 * R * (sinF2R - F2 * R) / (2f - 2f * cosF2R - F2 * R * sinF2R); + // K2 = F2sqr * R * R * (1f - cosF2R) / (2f - 2f * cosF2R - F2 * R * sinF2R); + //} + //else + //{ + // General derivation of aerodynamic constant for Kappa guidance + float Fsqr = D0 * Lalpha * (TL + 1) * (TL + 1) / ((float)(ml.vessel.totalMass * ml.vessel.totalMass * ml.vessel.srfSpeed * ml.vessel.srfSpeed * ml.vessel.srfSpeed * ml.vessel.srfSpeed) * (2 * eta + TL)); + float F = BDAMath.Sqrt(Fsqr); + + float eFR = Mathf.Exp(F * R); + float enFR = Mathf.Exp(-F * R); + + K1 = (2f * Fsqr * R * R - F * R * (eFR - enFR)) / (eFR * (F * R - 2f) - enFR * (F * R + 2f) + 4f); + K2 = (Fsqr * R * R * (eFR + enFR - 2f)) / (eFR * (F * R - 2f) - enFR * (F * R + 2f) + 4f); + //} + } + else + { + loftState = MissileBase.LoftStates.Terminal; + // Optimal gains if we ignore aerodynamic effects. In the terminal phase we can neglect these + K1 = -2f; + K2 = 6f; - timeToImpact = 0; - return false; + // Technically equivalent to setting K1 = 0 + vF = currVel; } - previousDistanceSqr = currentDistanceSqr; - iterations++; - } while (currentDistanceSqr < targetDistance * targetDistance); + // Acceleration per Kappa guidance + Vector3 accel = (K1 * ttgoInv) * (vF - currVel) + (K2 * ttgoInv * ttgoInv) * (predictedImpactPoint - ml.vessel.CoM - currVel * ttgo); + accel = accel.ProjectOnPlanePreNormalized(velDirection); + // gLimit is based solely on acceleration normal to the velocity vector, technically this guidance law gives + // both normal acceleration and tangential acceleration but we can only really manage normal acceleration + gLimit = (accel).magnitude / (float)PhysicsGlobals.GravitationalAcceleration; - timeToImpact = Time.fixedDeltaTime * iterations; - return true; - } + // Debug output, useful for tuning + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: Kappa Guidance K1: {K1}, K2: {K2}, accel: {accel}, vF-currVel: {vF-currVel}, posError: {predictedImpactPoint- ml.vessel.CoM - currVel*ttgo}, g: {gLimit}, ttgo: {ttgo}"); + return ml.vessel.CoM + currVel * Mathf.Min(leadTime, 3f) + accel * Mathf.Min(leadTime * leadTime, 9f); + } + } - public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vessel targetVessel) + 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, float maneuvergLimit, float invManeuvergLimit, ref MissileBase.LoftStates loftState, + out float timeToImpact, out float gLimit, out float targetDistance, + MissileBase.GuidanceModes homingModeTerminal, float N, float optimumAirspeed = 200) { - if (!targetVessel) + Vector3 velDirection = missileVessel.srf_vel_direction; //missileVessel.Velocity().normalized; + + targetDistance = Vector3.Distance(targetPosition, missileVessel.CoM); + + float currSpeed;// = Mathf.Max((float)missileVessel.srfSpeed, minSpeed); + + if (loftState == MissileBase.LoftStates.Boost) + currSpeed = Mathf.Max((float)missileVessel.srfSpeed, optimumAirspeed); + else { - return missile.transform.position + (missile.GetForwardTransform() * 1000); + // If still accelerating + if (Vector3.Dot(missileVessel.acceleration_immediate, velDirection) > 0) + currSpeed = Mathf.Max((float)missileVessel.srfSpeed, optimumAirspeed); + else + currSpeed = (float)missileVessel.srfSpeed; } - Vector3 targetPosition = targetVessel.transform.position; - float leadTime = 0; - float targetDistance = Vector3.Distance(targetVessel.transform.position, missile.transform.position); + - Vector3 simMissileVel = 500 * (targetPosition - missile.transform.position).normalized; + Vector3 currVel = currSpeed * velDirection; - MissileLauncher launcher = missile as MissileLauncher; - float optSpeed = 400; //TODO: Add parameter - if (launcher != null) + //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); + + gLimit = -1f; + + // If loft is not terminal + if ((loftState < MissileBase.LoftStates.Terminal) && (targetDistance > termDist)) { - optSpeed = launcher.optimumAirspeed; + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileGuidance]: Lofting"); + + // Get up direction + Vector3 upDirection = missileVessel.upAxis; //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.CoM; //+ (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, Vector3.zero, 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 + + // Use simple lead compensation to minimize over-compensation + // Get planar direction to target + Vector3 planarDirectionToTarget = + ((AIUtils.PredictPosition(targetPosition, targetVelocity, Vector3.zero, leadTime + TimeWarp.fixedDeltaTime) - missileVessel.CoM).ProjectOnPlanePreNormalized(upDirection)).normalized; + + //float turnTimeOffset = 0f; + + if (loftState == MissileBase.LoftStates.Boost) + { + // Get angle relative to vertical + float pullDownCos = Vector3.Dot(velDirection, upDirection); + // Make sure we're actually pulling down, otherwise our assumptions won't hold, + // Specifically, pullDownSin would require a negative sign in our turnLead + // calculation as it represents distance already covered. Similarly, + // instead of termAngle + turnAngleOffset, it'd be termAngle - turnAngleOffset + // in turnTimeOffset. Either way, this calculation is not required as it is + // expected that the user will have accounted for turn time required from + // horizontal to termAngle in the termAngle trigger point + if (pullDownCos > 0) + { + // If the target isn't above the loft angle + if (Mathf.Cos(loftAngle) * targetDistance > Vector3.Dot(missileRelativePosition, upDirection)) + { + float pullDownSin = BDAMath.Sqrt(1f - pullDownCos * pullDownCos); + // Turn radius is mv^2/r = ma -> v^2/r = a -> v^2/a = r, a = 10 g -> v^2 * 1/(10 g) = r + // We use 1.5f * currSpeed to account for accelerating missiles + float tempSpeed = Mathf.Max(currSpeed * 1.1f, optimumAirspeed); + + float curvatureCompensation = (1f - Vector3.Dot(upDirection, VectorUtils.GetUpDirection(targetPosition))) * (float)FlightGlobals.currentMainBody.Radius; + float turnRadius = (tempSpeed * tempSpeed * invg * invManeuvergLimit); + + Vector3 turnLead = (turnRadius * (pullDownCos + Mathf.Sin(termAngle * Mathf.Deg2Rad))) * planarDirectionToTarget + (turnRadius * (1f - pullDownSin) - curvatureCompensation) * upDirection; + + firePosition += turnLead; + //turnTimeOffset = (termAngle * Mathf.Deg2Rad + (Mathf.PI * 0.5f - Mathf.Acos(pullDownCos))) * tempSpeed * 0.0169952698051929473876953125f; + } + else + loftState = MissileBase.LoftStates.Midcourse; + } + } + + 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, Vector3.zero, Mathf.Min(timeToCPA, 16f)); + missileDropOffset = -0.5f * missileAcceleration * timeToCPA * timeToCPA; + ballisticTarget = targetPredictedPosition + missileDropOffset; + velDirection = (ballisticTarget - firePosition).normalized; + } while (++count < 10 && VectorUtils.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}]"); + + // Check if termination angle agrees with termAngle + if ((loftState < MissileBase.LoftStates.Midcourse) && (angle > -termAngle * Mathf.Deg2Rad)) + { + /*// 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.CoM, 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); + + gLimit = maneuvergLimit; + + //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.CoM + (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 = MissileBase.LoftStates.Midcourse; + + // 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; + */ + + Vector3 finalTargetPos; + + if (velUp > 0f) + { + // If the missile is told to go up, then we either try to go above the target or remain at the current altitude + /*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;*/ + finalTargetPos = missileVessel.CoM + (float)missileVessel.srfSpeed * planarDirectionToTarget + Mathf.Max(targetAlt - (float)missileVessel.altitude, 0f) * upDirection; + } else + { + // Otherwise just fly towards the target according to velUp and velForwards + float spdUp = 0.25f * leadTime * (float)missileVessel.srfSpeed * velUp, spdF = 0.25f * leadTime * (float)missileVessel.srfSpeed * velForwards; + finalTargetPos = new Vector3(missileVessel.CoM.x + spdUp * upDirection.x + spdF * planarDirectionToTarget.x, + missileVessel.CoM.y + spdUp * upDirection.y + spdF * planarDirectionToTarget.y, + missileVessel.CoM.z + spdUp * upDirection.z + spdF * planarDirectionToTarget.z); + } + + // If the target is at < 2 * termDist start mixing + if (targetDistance < 3f * termDist) + { + float blendFac = (targetDistance - termDist) / termDist; + blendFac *= 0.25f * blendFac; + + Vector3 aamTarget; + + if (homingModeTerminal == MissileBase.GuidanceModes.PN) + aamTarget = (1f - blendFac) * GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact, out gLimit) + blendFac * finalTargetPos; + else if (homingModeTerminal == MissileBase.GuidanceModes.APN) + aamTarget = (1f - blendFac) * GetAPNTarget(targetPosition, targetVelocity, targetAcceleration, missileVessel, N, out timeToImpact, out gLimit) + blendFac * finalTargetPos; + else if (homingModeTerminal == MissileBase.GuidanceModes.AAMPure) + return (1f - blendFac) * targetPosition + blendFac * finalTargetPos; + else if (homingModeTerminal == MissileBase.GuidanceModes.AAMLead) + return (1f - blendFac) * AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, leadTime + TimeWarp.fixedDeltaTime) + blendFac * finalTargetPos; + else + aamTarget = (1f - blendFac) * GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact, out gLimit) + blendFac * finalTargetPos; // Default to PN + + gLimit += 10f * blendFac; + + return aamTarget; + } + //else + // gLimit = Mathf.Clamp(20f * (1 - (targetDistance - termDist - 100f) / Mathf.Clamp(termDist * 4f, 5000f, 25000f)), 10f, 20f); + + + // No mixing if targetDistance > 3 * termDist + return finalTargetPos; + + //if (velUp > 0f) + //{ + // // If the missile is told to go up, then we either try to go above the target or remain at the current altitude + // /*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; + //} + + //// Otherwise just fly towards the target according to velUp and velForwards + ////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); + } } - simMissileVel = optSpeed * (targetPosition - missile.transform.position).normalized; + else + { + // If terminal just go straight for target + lead + loftState = MissileBase.LoftStates.Terminal; + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileGuidance]: Terminal"); - leadTime = targetDistance / (float)(targetVessel.Velocity() - simMissileVel).magnitude; + if (targetDistance < 3f * termDist) + { + float blendFac = 0f; + Vector3 targetPos = Vector3.zero; + + if ((targetDistance > termDist) && (homingModeTerminal != MissileBase.GuidanceModes.AAMLead) && (homingModeTerminal != MissileBase.GuidanceModes.AAMPure)) + { + blendFac = (targetDistance - termDist) / termDist; + blendFac *= 0.25f * blendFac; + targetPos = AIUtils.PredictPosition(targetPosition, targetVelocity, Vector3.zero, leadTime + TimeWarp.fixedDeltaTime); + } + + Vector3 aamTarget; + + if (homingModeTerminal == MissileBase.GuidanceModes.PN) + aamTarget = (1f - blendFac) * GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact, out gLimit) + blendFac * targetPos; + else if (homingModeTerminal == MissileBase.GuidanceModes.APN) + aamTarget = (1f - blendFac) * GetAPNTarget(targetPosition, targetVelocity, targetAcceleration, missileVessel, N, out timeToImpact, out gLimit) + blendFac * targetPos; + 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 (1f - blendFac) * GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact, out gLimit) + blendFac * targetPos; // Default to PN + + gLimit += 10f * blendFac; + + return aamTarget; + } + else + { + return AIUtils.PredictPosition(targetPosition, targetVelocity, Vector3.zero, 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); - targetPosition = targetPosition + (targetVessel.Velocity() * leadTime); - if (targetVessel && targetDistance < 800) + if (targetDistance < termDist) { - targetPosition += (Vector3)targetVessel.acceleration * 0.05f * leadTime * leadTime; + 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; + } + }*/ - return targetPosition; + 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 = missileVessel.Velocity(); + timeToImpact = targetDistance / (targetVelocity - currVel).magnitude; + + // Calculate time to CPA to determine target position + 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); + float easeVel = Mathf.Clamp01(2f - timeToCPA / 8f); + 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, out float gLimit) + { + Vector3 missileVel = missileVessel.Velocity(); + Vector3 relVelocity = targetVelocity - missileVel; + Vector3 relRange = targetPosition - missileVessel.CoM; + if (Vector3.Dot(relRange, relVelocity) < 0) + { + gLimit = -1f; + return GetAirToAirTarget(targetPosition, targetVelocity, Vector3.zero, missileVessel, out timeToGo); + } + Vector3 RotVector = Vector3.Cross(relRange, relVelocity) / relRange.sqrMagnitude; + Vector3 RefVector = missileVel.normalized; + Vector3 normalAccel = -N * relVelocity.magnitude * Vector3.Cross(RefVector, RotVector); + gLimit = normalAccel.magnitude / (float)PhysicsGlobals.GravitationalAcceleration; + timeToGo = missileVessel.TimeToCPA(targetPosition, targetVelocity, Vector3.zero, 120f); + return missileVessel.CoM + missileVel * timeToGo + normalAccel * timeToGo * timeToGo; + } + + private static Vector3 GetPNAccel(Vector3 targetPosition, Vector3 targetVelocity, Vessel missileVessel, float N) + { + Vector3 missileVel = missileVessel.Velocity(); + Vector3 relVelocity = targetVelocity - missileVel; + Vector3 relRange = targetPosition - missileVessel.CoM; + Vector3 RotVector = Vector3.Cross(relRange, relVelocity) / relRange.sqrMagnitude; + Vector3 RefVector = missileVel.normalized; + Vector3 normalAccel = -N * relVelocity.magnitude * Vector3.Cross(RefVector, RotVector); + //gLimit = normalAccel.magnitude / (float)PhysicsGlobals.GravitationalAcceleration; + //timeToGo = missileVessel.TimeToCPA(targetPosition, targetVelocity, Vector3.zero, 120f); + return normalAccel; + } + + public static Vector3 GetAPNTarget(Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, Vessel missileVessel, float N, out float timeToGo, out float gLimit) + { + Vector3 missileVel = missileVessel.Velocity(); + 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; + gLimit = normalAccel.magnitude / (float)PhysicsGlobals.GravitationalAcceleration; + 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 = missileVessel.Velocity(); + 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; } - public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vector3 targetPosition, Vector3 targetVelocity) + public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vessel targetVessel) { + float temp; + return GetAirToAirFireSolution(missile, targetVessel, out temp); + } + + /// + /// 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, out float timetogo) + { + if (!targetVessel) + { + timetogo = float.PositiveInfinity; + return missile.vessel.CoM + (missile.GetForwardTransform() * 1000); + } + Vector3 targetPosition = targetVessel.CoM; + Vector3 vel = missile.vessel.Velocity(); + Vector3 startPosition = missile.vessel.CoM; + if (missile.GetWeaponClass() == WeaponClasses.SLW && !missile.vessel.LandedOrSplashed) + { + vel = Vector3.zero; //impact w/ water is going to bring starting torp speed basically down to 0, not whatever plane airspeed was + float torpDropTime = BDAMath.Sqrt(2 * (float)missile.vessel.altitude / (float)FlightGlobals.getGeeForceAtPosition(missile.vessel.CoM).magnitude); + startPosition += missile.vessel.srf_vel_direction * (missile.vessel.horizontalSrfSpeed * torpDropTime); //torp will spend multiple seconds dropping falling at parent vessel speed + startPosition -= (float)FlightGlobals.getAltitudeAtPos(startPosition) * missile.vessel.up; + targetPosition += targetVessel.Velocity() * torpDropTime; //so offset start positions appropriately + } float leadTime = 0; - float targetDistance = Vector3.Distance(targetPosition, missile.transform.position); + float targetDistance = Vector3.Distance(targetPosition, startPosition); - float optSpeed = 400; //TODO: Add parameter MissileLauncher launcher = missile as MissileLauncher; - if (launcher != null) + BDModularGuidance modLauncher = missile as BDModularGuidance; + + float accel = launcher != null ? (launcher.thrust / missile.part.mass) : modLauncher != null ? (modLauncher.thrust/modLauncher.mass) : 10; + + if (missile.vessel.InNearVacuum() && missile.vessel.InOrbit()) // In orbit, use orbital calc { - optSpeed = launcher.optimumAirspeed; + float timeToImpact; + Vector3 relPos = targetVessel.CoM - missile.vessel.CoM; + Vector3 relVel = vel - targetVessel.Velocity(); + Vector3 relAccel = targetVessel.acceleration_immediate - missile.GetForwardTransform() * accel; + + float thrustTime = launcher != null ? launcher.boostTime : modLauncher != null ? (modLauncher.MaxSpeed / accel) : 8; + + timeToImpact = AIUtils.TimeToCPA(relPos, relVel, relAccel, thrustTime); + if (timeToImpact == thrustTime) + { + relPos = AIUtils.PredictPosition(targetPosition, targetVessel.Velocity(), targetVessel.acceleration_immediate, thrustTime) - + AIUtils.PredictPosition(missile.vessel.CoM, vel, missile.GetForwardTransform() * accel, thrustTime); + relVel += relAccel * timeToImpact; + relAccel = targetVessel.acceleration_immediate; + timeToImpact = AIUtils.TimeToCPA(relPos, relVel, relAccel, 60f); + leadTime = thrustTime + timeToImpact; + } + else + leadTime = timeToImpact; + targetPosition += leadTime * (targetVessel.Velocity() - vel) + 0.5f * leadTime * leadTime * targetVessel.acceleration_immediate; } + else // In atmo, use in-atmo calculations + { + Vector3 VelOpt = missile.GetForwardTransform() * (launcher != null ? launcher.optimumAirspeed : 1500); + Vector3 deltaVel = targetVessel.Velocity() - vel; + Vector3 DeltaOptvel = targetVessel.Velocity() - VelOpt; + float T = Mathf.Clamp(Vector3.Project(VelOpt - vel, missile.GetForwardTransform()).magnitude / accel, 0, 8); //time to optimal airspeed + + Vector3 relPosition = targetPosition - startPosition; + Vector3 relAcceleration = targetVessel.acceleration_immediate - 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_immediate, T) - + AIUtils.PredictPosition(startPosition, vel, missile.GetForwardTransform() * accel, T); + relAcceleration = targetVessel.acceleration_immediate; // - missile.MissileReferenceTransform.forward * 0; assume missile is holding steady velocity at optimumAirspeed + leadTime = AIUtils.TimeToCPA(relPosition, DeltaOptvel, relAcceleration, 8 - T) + T; + } - Vector3 simMissileVel = optSpeed * (targetPosition - missile.transform.position).normalized; - leadTime = targetDistance / (targetVelocity - simMissileVel).magnitude; - leadTime = Mathf.Clamp(leadTime, 0f, 8f); + targetPosition += leadTime * targetVessel.Velocity(); - targetPosition = targetPosition + (targetVelocity * leadTime); + if (targetVessel && targetDistance < 800) //TODO - investigate if this would throw off aim accuracy + { + targetPosition += (Vector3)targetVessel.acceleration_immediate * 0.05f * leadTime * leadTime; + } + } + timetogo = leadTime; return targetPosition; } + /// + /// Air-2-Air lead offset calcualtion used for aiming missile turrets + /// + /// + /// + /// + /// + public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vector3 targetPosition, Vector3 targetVelocity, bool turretLoft = false, float turretLoftFac = 0.5f) + { + MissileLauncher launcher = missile as MissileLauncher; + BDModularGuidance modLauncher = missile as BDModularGuidance; + bool inSpace = missile.vessel.InNearVacuum() && missile.vessel.InOrbit(); + float leadTime = 0; + float maxSimTime = 8f; + Vector3 leadPosition = targetPosition; + Vector3 vel = missile.vessel.Velocity(); + Vector3 leadDirection, velOpt; + float accel = launcher != null ? ((Mathf.Clamp01(launcher.boostTime / maxSimTime)) * launcher.thrust + Mathf.Clamp01((maxSimTime - launcher.cruiseDelay - launcher.boostTime) / maxSimTime) * launcher.cruiseThrust) / missile.part.mass + : modLauncher.thrust / modLauncher.mass; + float leadTimeError = 1f; + float missileVelOpt = launcher != null ? launcher.optimumAirspeed : 1500; + int count = 0; + do + { + leadDirection = leadPosition - missile.vessel.CoM; + float targetDistance = leadDirection.magnitude; + leadDirection.Normalize(); + velOpt = (inSpace ? BDAMath.Sqrt(2f * accel * targetDistance) * leadDirection + vel : missileVelOpt * leadDirection); + float deltaVel = Vector3.Dot(targetVelocity - vel, leadDirection); + float deltaVelOpt = Vector3.Dot(targetVelocity - velOpt, leadDirection); + float T = Mathf.Clamp((velOpt - vel).magnitude / accel, 0, maxSimTime); //time to optimal airspeed, clamped to at most 8s + float D = deltaVel * T + 0.5f * 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) + 2f * accel * targetDistance)) / accel; + leadTime = Mathf.Clamp(leadTime, 0f, maxSimTime); + leadTimeError += leadTime; + leadPosition = AIUtils.PredictPosition(targetPosition, targetVelocity - (inSpace ? vel : Vector3.zero), Vector3.zero, leadTime); + } while (++count < 5 && Mathf.Abs(leadTimeError) > 1e-3f); // At most 5 iterations to converge. Also, 1e-2f may be sufficient. + + if (!missile.vessel.InNearVacuum() && turretLoft) + { + Vector3 relPos = leadPosition - missile.vessel.CoM; + float vertDist = Vector3.Dot(relPos, missile.vessel.upAxis); + float horzDist = (float)(relPos - vertDist * missile.vessel.upAxis).magnitude; + float g = (float)missile.vessel.mainBody.GeeASL; + float theta; + + missileVelOpt *= turretLoftFac; + float missileVelOptSqr = missileVelOpt * missileVelOpt; + + float det = missileVelOptSqr * missileVelOptSqr - g * (g * horzDist * horzDist + 2f * vertDist * missileVelOptSqr); + if (det > 0f) + // Regular angle based on projectile motion + theta = Mathf.Atan((missileVelOptSqr - BDAMath.Sqrt(det)) / (g * horzDist)); + else + // Angle to hit the furthest possible target at that elevation + theta = Mathf.Atan(missileVelOpt / (BDAMath.Sqrt(missileVelOptSqr - 2f * g * vertDist))); + theta *= Mathf.Rad2Deg; + + float angle = 90f - VectorUtils.Angle(relPos, missile.vessel.upAxis); + if (theta > angle) + leadPosition = missile.vessel.CoM + Vector3.RotateTowards(relPos, missile.vessel.upAxis, (theta - angle) * Mathf.Deg2Rad, vertDist); + } + + // Don't lead so much you end up leading behind the vehicle... + if (Vector3.Dot(targetPosition - missile.vessel.CoM, leadPosition - missile.vessel.CoM) > 0) + return leadPosition; + else + return targetPosition; + } public static Vector3 GetCruiseTarget(Vector3 targetPosition, Vessel missileVessel, float radarAlt) { - Vector3 upDirection = VectorUtils.GetUpDirection(missileVessel.transform.position); + Vector3 upDirection = missileVessel.upAxis; //VectorUtils.GetUpDirection(missileVessel.transform.position); float currentRadarAlt = GetRadarAltitude(missileVessel); float distanceSqr = - (targetPosition - (missileVessel.transform.position - (currentRadarAlt * upDirection))).sqrMagnitude; + (targetPosition - (missileVessel.CoM - (currentRadarAlt * upDirection))).sqrMagnitude; - Vector3 planarDirectionToTarget = - Vector3.ProjectOnPlane(targetPosition - missileVessel.transform.position, upDirection).normalized; + Vector3 planarDirectionToTarget = (targetPosition - missileVessel.CoM).ProjectOnPlanePreNormalized(upDirection).normalized; float error; @@ -274,13 +1303,13 @@ public static Vector3 GetCruiseTarget(Vector3 targetPosition, Vessel missileVess else { Vector3 tRayDirection = (planarDirectionToTarget * 10) - (10 * upDirection); - Ray terrainRay = new Ray(missileVessel.transform.position, tRayDirection); + Ray terrainRay = new Ray(missileVessel.CoM, tRayDirection); RaycastHit rayHit; - if (Physics.Raycast(terrainRay, out rayHit, 8000, (1 << 15) | (1 << 17))) + if (Physics.Raycast(terrainRay, out rayHit, 8000, (int)(LayerMasks.Scenery | LayerMasks.EVA))) // Why EVA? { float detectedAlt = - Vector3.Project(rayHit.point - missileVessel.transform.position, upDirection).magnitude; + Vector3.Project(rayHit.point - missileVessel.CoM, upDirection).magnitude; error = Mathf.Min(detectedAlt, currentRadarAlt) - radarAlt; } @@ -291,25 +1320,24 @@ public static Vector3 GetCruiseTarget(Vector3 targetPosition, Vessel missileVess } error = Mathf.Clamp(0.05f * error, -5, 3); - return missileVessel.transform.position + (10 * planarDirectionToTarget) - (error * upDirection); + return missileVessel.CoM + (10 * planarDirectionToTarget) - (error * upDirection); } 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 upDirection = missileVessel.upAxis; + Vector3 planarVectorToTarget = (targetPosition - missileVessel.CoM).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, + float sinAmplitude = Mathf.Clamp(Vector3.Distance(targetPosition, missileVessel.CoM) - 850, 0, 4500); Vector3 sinOffset = (Mathf.Sin(1.25f * Time.time) * sinAmplitude * crossAxis); Vector3 targetSin = targetPosition + sinOffset; - Vector3 planarSin = missileVessel.transform.position + planarVectorToTarget + sinOffset; + Vector3 planarSin = missileVessel.CoM + planarVectorToTarget + sinOffset; Vector3 finalTarget; float finalDistance = 2500 + GetRadarAltitude(missileVessel); - if ((targetPosition - missileVessel.transform.position).sqrMagnitude > finalDistance * finalDistance) + if ((targetPosition - missileVessel.CoM).sqrMagnitude > finalDistance * finalDistance) { finalTarget = targetPosition; } @@ -321,103 +1349,769 @@ public static Vector3 GetTerminalManeuveringTarget(Vector3 targetPosition, Vesse return finalTarget; } - public static FloatCurve DefaultLiftCurve = null; - public static FloatCurve DefaultDragCurve = null; + public static FloatCurve DefaultLiftCurve = new([ + new(0, 0, 0.04375f, 0.04375f), + new(8, 0.35f, 0.04801136f, 0.04801136f), + //new(19, 1f), + //new(23, 0.9f), + new(30, 1.5f), + new(65, 0.6f), + new(90, 0.7f) + ]); + + public static FloatCurve DefaultDragCurve = new([ + //new(0, 0.00215f, 0.00014f, 0.00014f), + //new(5, .00285f, 0.0002775f, 0.0002775f), + //new(15, .007f, 0.0003146428f, 0.0003146428f), + //new(29, .01f, 0.0002142857f, 0.01115385f), + //new(55, .3f, 0.008434067f, 0.008434067f), + //new(90, .5f, 0.005714285f, 0.005714285f) + new(0f, 0.00215f, 0f, 0f), + new(5f, 0.00285f, 0.0002775f, 0.0002775f), + new(30f, 0.01f, 0.0002142857f, 0.01115385f), + new(55f, 0.3f, 0.008434067f, 0.008434067f), + new(90f, 0.5f, 0.005714285f, 0.005714285f) + ]); + + // The below curves and constants are derived from the lift and drag curves and will need to be re-calculated + // if these are changed + + const float TRatioInflec1 = 1.181181181181181f; // Thrust to Lift Ratio (at AoA of 30) where the maximum occurs + // after the 65 degree mark + const float TRatioInflec2 = 2.242242242242242f; // Thrust to Lift Ratio (at AoA of 30) where a local maximum no + // longer exists, above this every section must be searched + + public static FloatCurve AoACurve = new([ + new(0.0000000000f, 30.0000000000f, 5.577463f, 5.577463f), + new(0.7107107107f, 33.9639639640f, 6.24605f, 6.24605f), + new(1.5315315315f, 39.6396396396f, 8.396343f, 8.396343f), + new(1.9419419419f, 43.6936936937f, 12.36403f, 12.36403f), + new(2.1421421421f, 46.6666666667f, 19.63926f, 19.63926f), + new(2.2122122122f, 48.3783783784f, 34.71423f, 34.71423f), + new(2.2422422422f, 49.7297297297f, 44.99994f, 44.99994f) + ]); // Floatcurve containing AoA of (local) max acceleration + // for a given thrust to lift (at the max CL of 1.5 at 30 degrees of AoA) ratio. Limited to a max + // of TRatioInflec2 where a local maximum no longer exists + + public static FloatCurve AoAEqCurve = new([ + new(1.1911911912f, 89.6396396396f, -53.40001f, -53.40001f), + new(1.3413413413f, 81.6216216216f, -49.69999f, -49.69999f), + new(1.5215215215f, 73.3333333333f, -37.62499f, -37.62499f), + new(1.7217217217f, 67.4774774775f, -24.31731f, -24.31731f), + new(1.9819819820f, 62.4324324324f, -24.09232f, -24.09232f), + new(2.1821821822f, 56.6666666667f, -48.1499f, -48.1499f), + new(2.2422422422f, 52.6126126126f, -67.49978f, -67.49978f) + ]); // Floatcurve containing AoA after which the acceleration goes above + // that of the local maximums'. Only exists between TRatioInflec1 and TRatioInflec2. + + public static FloatCurve gMaxCurve = new([ + new(0.0000000000f, 1.5000000000f, 0.8248255f, 0.8248255f), + new(1.2012012012f, 2.4907813293f, 0.8942869f, 0.8942869f), + new(1.9119119119f, 3.1757276995f, 1.019205f, 1.019205f), + new(2.2422422422f, 3.5307206802f, 1.074661f, 1.074661f) + ]); // Floatcurve containing max acceleration times the mass (total force) + // normalized by q*S*GLOBAL_LIFT_MULTIPLIER for TRatio between 0 and TRatioInflec2. Note that after TRatioInflec1 + // this becomes a local maxima not a global maxima. This is used to narrow down what part of the curve we should + // solve on. + + // Linearized CL v.s. AoA curve to enable fast solving. Algorithm performs bisection using the fast calculations of the bounds + // and then performs a linear solve + public static float[] linAoA = { 0f, 10f, 24f, 30f, 38f, 57f, 65f, 90f }; + public static float[] linCL = { 0f, 0.454444597111092f, 1.34596044049850f, 1.5f, 1.38043381924198f, 0.719566180758018f, 0.6f, 0.7f }; + // Sin at the points + public static float[] linSin = { 0f, 0.173648177666930f, 0.406736643075800f, 0.5f, 0.615661475325658f, 0.838670567945424f, 0.906307787036650f, 1f }; + // Slope of CL at the intervals + public static float[] linSlope = { 0.0454444597111092f, 0.0636797030991005f, 0.0256732599169169f, -0.0149457725947522f, -0.0347825072886297f, -0.0149457725947522f, 0.004f }; + // y-Intercept of line at those intervals + public static float[] linIntc = { 0f, -0.182352433879912f, 0.729802202492494f, 1.94837317784257f, 2.70216909620991f, 1.57147521865889f, 0.34f }; + + public static float getGLimit(MissileLauncher ml, float thrust, float gLim, float margin, float maxAoA)//, out bool gLimited) + { + if (ml.vessel.InVacuum()) + { + return 180f; // if in vacuum g-limiting should be done via throttle modulation + } + + bool gLimited = false; + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] gLim: {gLim}"); + + // Force required to reach g-limit + gLim *= (float)(ml.vessel.totalMass * PhysicsGlobals.GravitationalAcceleration); + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] force: {gLim}"); + + //float maxAoA = ml.maxAoA; + + float currAoA = maxAoA; + + int interval = 0; - public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, float liftArea, float steerMult, - Vector3 previousTorque, float maxTorque, float maxAoA) + // Factor by which to multiply the lift coefficient to get lift, it's the dynamic pressure times the lift area times + // the global lift multiplier + float qSk = (float) (0.5 * ml.vessel.atmDensity * ml.vessel.srfSpeed * ml.vessel.srfSpeed) * ml.currLiftArea * BDArmorySettings.GLOBAL_LIFT_MULTIPLIER; + + float currG = 0; + + // If we're in the post thrust state + if (thrust == 0) + { + // If the maximum lift achievable is not enough to reach the request accel + // the we turn to the AoA required for max lift + if (gLim > 1.5f*qSk) + { + currAoA = 30f; + } + else + { + // Otherwise, first we calculate the lift in interval 2 (between 24 and 30 AoA) + currG = linCL[2] * qSk; // CL(alpha)*qSk + thrust*sin(alpha) + + // If the resultant g at 24 AoA is < gLim then we're in interval 2 + if (currG < gLim) + { + interval = 2; + } + else + { + // Otherwise check interval 1 + currG = linCL[1] * qSk; + + if (currG > gLim) + { + // If we're still > gLim then we're in interval 0 + interval = 0; + } + else + { + // Otherwise we're in interval 1 + interval = 1; + } + } + + // Calculate AoA for G, since no thrust we can use the faster linear equation + currAoA = calcAoAforGLinear(qSk, gLim, linSlope[interval], linIntc[interval], 0); + } + + // Are we gLimited? + gLimited = currAoA < maxAoA; + return gLimited ? currAoA : maxAoA; + } + else + { + // If we're under thrust, first calculate the ratio of Thrust to lift at max CL + float TRatio = thrust / (1.5f * qSk); + + // Initialize bisection limits + int LHS = 0; + int RHS = 7; + + if (TRatio < TRatioInflec2) + { + // If we're below TRatioInflec2 then we know there's a local max + currG = qSk * gMaxCurve.Evaluate(TRatio); + + if (TRatio > TRatioInflec1) + { + // If we're above TRatioInflec1 then we know it's only a local max + + // First calculate the allowable force margin + // This exists because drag gets very bad above the local max + margin = Mathf.Max(margin, 0f); + margin *= (float)ml.vessel.totalMass; + + if (currG + margin < gLim) + { + // If we're within the margin + if (currG > gLim) + { + // And our local max is > gLim, then we know that + // there is a solution. Calculate the AoAMax + // where the local max occurs + float AoAMax = AoACurve.Evaluate(TRatio); + + // And determine our right hand bound based on + // our AoAMax + if (AoAMax > linAoA[4]) + { + RHS = 5; + } + else if (AoAMax > linAoA[3]) + { + RHS = 4; + } + else + { + RHS = 3; + } + } + else + { + // If our local max is < gLim then we can simply set + // our AoA to be the AoA of the local max + currAoA = AoACurve.Evaluate(TRatio); + gLimited = currAoA < maxAoA; + return gLimited ? currAoA : maxAoA; + } + } + else + { + // If we're not within the margin then we need to consider + // the high AoA section. First calculate the absolute maximum + // g we can achieve + currG = 0.7f * qSk + thrust; + + // If the absolute maximum g we can achieve is not enough, then return + // the local maximum in order to preserve energy + if (currG < gLim) + { + currAoA = AoACurve.Evaluate(TRatio); + gLimited = currAoA < maxAoA; + return gLimited ? currAoA : maxAoA; + } + + // If we're within the limit, then find the AoA where the normal force + // once again reaches the local max value + float AoAEq = AoAEqCurve.Evaluate(TRatio); + + // And determine the left hand bound from there + if (AoAEq > linAoA[6]) + { + // If we're in the final section then just calculate it directly + currAoA = calcAoAforGNonLin(qSk, gLim, linSlope[6], linIntc[6], 0); + gLimited = currAoA < maxAoA; + return gLimited ? currAoA : maxAoA; + } + else if (AoAEq > linAoA[5]) + { + LHS = 5; + } + else + { + LHS = 4; + } + } + } + else + { + // If we're not above TRatioInflec1 then we only have to consider the + // curve up to the local max + float AoAMax = AoACurve.Evaluate(TRatio); + + // Determine the right hand bound for calculation + if (gLim < currG) + { + if (AoAMax > linAoA[3]) + { + RHS = 4; + } + else + { + RHS = 3; + } + } + else + { + gLimited = currAoA < maxAoA; + return gLimited ? currAoA : maxAoA; + } + } + } + else + { + // If we're above TRatioInflec2 then we have to search the whole thing, but past that ratio + // the function is monotonically increasing so it's OK + + // That being said, first calculate the absolute maximum + // g we can achieve + currG = 0.7f * qSk + thrust; + + // If the absolute maximum g we can achieve is not enough, then return + // max AoA + if (currG < gLim) + return maxAoA; + } + + currG = linCL[RHS] * qSk + thrust * linSin[RHS]; + if (currG < gLim) + return maxAoA; + + // Bisection search + while ( (RHS - LHS) > 1) + { + interval = Mathf.FloorToInt(0.5f * (RHS + LHS)); + + currG = linCL[interval] * qSk + thrust * linSin[interval]; + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: LHS: {LHS}, RHS: {RHS}, interval: {interval}, currG: {currG}, gLim: {gLim}"); + + if (currG < gLim) + { + LHS = interval; + } + else + { + RHS = interval; + } + } + + if (LHS == 0) + { + // If we're below 15 (here 10 degrees) then use the linear approximation for sin + currAoA = calcAoAforGLinear(qSk, gLim, linSlope[LHS], linIntc[LHS], thrust); + } + else + { + // Otherwise use the second order approximation centered at pi/2 + currAoA = calcAoAforGNonLin(qSk, gLim, linSlope[LHS], linIntc[LHS], thrust); + } + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: Final Interval: {LHS}, currAoA: {currAoA}, gLim: {gLim}"); + + gLimited = currAoA < maxAoA; + return gLimited ? currAoA : maxAoA; + } + // Pseudocode / logic + // If T = 0 + // We know it's in the first section. If m*gReq > (1.5*q*k*s) then set to min of maxAoA and 30 (margin?). If + // < then we first make linear estimate, then solve by bisection of intervals first -> solve on interval. + // If TRatio < TRatioInflec2 + // First we check the endpoints -> both gMax, and, if TRatio > TRatioInflec1, then 0.7*q*S*k + T (90 degree case). + // If gMax > m*gReq then the answer < AoACurve -> Determine where it is via calculating the pre-calculated points + // then seeing which one has gCalc > m*gReq, using the interval bounded by the point with gCalc > m*gReq on the + // right end. Use bisection -> we know it's bounded at the RHS by the 38 or the 57 section. We can compare the + // AoACurve with 38, if > 38 then use 57 as the bound, otherwise bisection with 38 as the bound. Using this to + // determine which interval we're looking at, we then calc AoACalc. Return the min of maxAoA and AoACalc. + // If gMax < m*gReq, then if TRatio < TRatioInflec1, set to min of AoACurve and maxAoA. If TRatio > TRatioInflec1 + // then we look at the 0.3*q*S*k + T. If < m*gReq then we'll set it to the min of maxAoA and either AoACurve or + // 90, depends on the margin. See below. If > m*gReq then it's in the last two sections, bound by AoAEq on the LHS. + // If AoAEq > 65, then we solve on the last section. If AoAEq < 65 then we check the point at AoA = 65 using the + // pre-calculated values. If > m*gReq then we know that it's in the 57-65 section, otherwise we know it's in the + // 65-90 section. + // Consider adding a margin, if gMax only misses m*gReq by a little we should probably avoid going to the higher + // angles as it adds a lot of drag. Maybe distance based? User settable? + // If TRatio > TRatioInflec2 then we have a continuously monotonically increasing function + // We use the fraction m*gReq/(0.3*q*S*k + T) to determine along which interval we should solve, noting that this + // is an underestimate of the thrust required. (Maybe use arcsin for a more accurate estimate? Costly.) Then simply + // calculate the pre-calculated value at the next point -> bisection and solve on the interval. + + // For all cases, if AoA < 15 then we can use the linear approximation of sin, if an interval includes both AoA < 15 + // and AoA > 15 then try < 15 (interval 2) first, then if > 15 try the non-linear starting from 15. Otherwise we use + // non-linear equation. + } + + // Calculate AoA for a given g loading, given m*g, the dynamic pressure times the lift area times the lift multiplier, + // the linearized approximation of the AoA curve (in slope, y-intercept form) and the thrust. Linear uses a linear + // small angle approximation for sin and non-linear uses a 2nd order approximation of sin about pi/2 + public static float calcAoAforGLinear(float qSk, float mg, float CLalpha, float CLintc, float thrust) + { + //if (BDArmorySettings.DEBUG_MISSILES) + //{ + // float AoA = (mg - CLintc * qSk) / (CLalpha * qSk + thrust * Mathf.Deg2Rad); + // Debug.Log($"[BDArmory.MissileGuidance]: Linear: AoA: {AoA}, thrust: {thrust}, qSk: {qSk}, Predicted CL: {AoA * CLalpha + CLintc}, actual CL: {DefaultLiftCurve.Evaluate(AoA)}, CLa: {CLalpha}, CLintc: {CLintc}, predicted force: {qSk * (AoA * CLalpha + CLintc) + thrust * AoA * Mathf.Deg2Rad}, actual force: {qSk * DefaultLiftCurve.Evaluate(AoA) + thrust * Mathf.Sin(AoA * Mathf.Deg2Rad)}, desired: {mg}"); + //} + return (mg - CLintc * qSk) / (CLalpha * qSk + thrust * Mathf.Deg2Rad); + } + + public static float calcAoAforGNonLin(float qSk, float mg, float CLalpha, float CLintc, float thrust) + { + CLalpha *= qSk; + + //if (BDArmorySettings.DEBUG_MISSILES) + //{ + // float invqSk = 1f / qSk; + // float AoA = (2f * CLalpha + Mathf.PI * thrust * Mathf.Deg2Rad - 2f * BDAMath.Sqrt(CLalpha * CLalpha + Mathf.PI * thrust * Mathf.Deg2Rad * CLalpha + 2f * thrust * (CLintc * qSk + thrust - mg) * Mathf.Deg2Rad * Mathf.Deg2Rad)) / (2f * thrust * Mathf.Deg2Rad * Mathf.Deg2Rad); + // Debug.Log($"[BDArmory.MissileGuidance]: NonLin: AoA: {AoA}, thrust: {thrust}, qSk: {qSk}, Predicted CL: {AoA * CLalpha * invqSk + CLintc}, actual CL: {DefaultLiftCurve.Evaluate(AoA)}, CLa: {CLalpha * invqSk}, CLintc: {CLintc}, predicted force: {qSk * (AoA * CLalpha * invqSk + CLintc) + thrust * (1f - (-Mathf.PI * 0.5f + Mathf.Deg2Rad * AoA) * (-Mathf.PI * 0.5f + Mathf.Deg2Rad * AoA) * 0.5f)}, actual force: {qSk * DefaultLiftCurve.Evaluate(AoA) + thrust * Mathf.Sin(AoA * Mathf.Deg2Rad)}, desired: {mg}"); + //} + return (2f * CLalpha + Mathf.PI * thrust * Mathf.Deg2Rad - 2f * BDAMath.Sqrt(CLalpha * CLalpha + Mathf.PI * thrust * Mathf.Deg2Rad * CLalpha + 2f * thrust * (CLintc * qSk + thrust - mg) * Mathf.Deg2Rad * Mathf.Deg2Rad)) / (2f * thrust * Mathf.Deg2Rad * Mathf.Deg2Rad); + } + + // Linearized curves for cos*CL and sin*CD v.s. AoA for fast solving of the AoA at which maxTorque no longer is sufficient to maintain + // control of the missile. + + public static FloatCurve torqueAoAReturn = new([ + new(2.6496350364963499f, 88.7129999999999939f, -106.9758f, -106.9758f), + new(2.73134328358208922f, 79.9722000000000008f, -70.59726f, -70.59726f), + new(3.14937759336099621f, 65.6675999999999931f, -28.9337f, -28.9337f), + new(3.52488687782805465f, 56.7873000000000019f, -31.87921f, -31.87921f), + new(3.69483568075117441f, 49.9707000000000008f, -61.73428f, -61.73428f), + new(3.76190476190476275f, 44.3798999999999992f, -83.35883f, -18.59649f), + new(3.83091787439613629f, 43.0964999999999989f, -23.74979f, -23.74979f), + new(3.92610837438423754f, 40.3451999999999984f, -28.9031f, -28.9031f) + ]); + + // Note we use linAoA for this as well + public static float[] linLiftTorque = { 0f, 0.449212170675488687f, 1.23071251302548967f, 1.29903810567665712f, 1.08779669420507852f, 0.391903830317496704f, 0.253570957044423284f, 0f }; + public static float[] linDragTorque = { 0f, 0.000748453415988048856f, 0.00346671023416293559f, 0.00499999999999927499f, 0.0656669812489726473f, 0.26524150275361541f, 0.336257675049945692f, 0.5f }; + + // Slope of cos * CL at the intervals + public static float[] linLiftTorqueSlope = { 0.0449212178074f, 0.0558214214286f, 0.0113876666667f, -0.026405125f, -0.0366259562991f, -0.0172916091591f, -0.0101428382818f }; + // y-Intercept of line at those intervals + public static float[] linLiftTorqueIntc = { 0f, -0.109002114286f, 0.957408f, 2.09119175f, 2.47958333937f, 1.37752555239f, 0.91285544536f }; + + // Slope of sin * CD at the intervals + public static float[] linDragTorqueSlope = { 0.000166666666667f, 0.000166666666667f, 0.000166666666667f, 0.00691309375f, 0.0107346842105f, 0.009046f, 0.00653472f }; + // y-Intercept of line at those intervals + public static float[] linDragTorqueIntc = { 0f, 0f, 0f, -0.2023928125f, -0.347613f, -0.251358f, -0.0881248f }; + + const float DLRatioInflec1 = 2.63636363636363624f; + const float DLRatioInflec2 = 3.92610837438423754f; + + // Algorithm is similar to getGLimit, except in this case we only calculate which sections to search in whenever + // the liftArea and dragArea change. We define this using a set of numbers, torqueAoAReturn, the AoA at which torque goes past the local + // maximum which occurs at around 28° AoA, if this is set to -1, then we ONLY search the lower portion of the plot and torqueMaxLocal which + // gives the non-dim torque (which needs to be pre-multiplied by the SUM of liftArea * liftMult and dragArea * dragMult before being saved) + // which is +ve when there's a local maximum, it is set to the negative of the number when a local maximum does not exist, it is not set to + // -1 instead as it still provides a useful bisection point, with which we can determine the first LHS/RHS index. + //public static float[] linAoA = { 0f, 10f, 24f, 30f, 38f, 57f, 65f, 90f }; + public static void setupTorqueAoALimit(MissileLauncher ml, float liftArea, float dragArea) { - if (DefaultLiftCurve == null) + // Drag / Lift ratio + float DL = (BDArmorySettings.GLOBAL_DRAG_MULTIPLIER * dragArea)/(BDArmorySettings.GLOBAL_LIFT_MULTIPLIER * liftArea); + // The % contribution of drag, note that this will error out if there's no drag, + // but that's not supposed to happen. + float SkR = DL / (DL + 1); + + // If we're above DLRationInflec2 then we must search the whole range of AoAs + if (DL < DLRatioInflec2) { - DefaultLiftCurve = new FloatCurve(); - DefaultLiftCurve.Add(0, 0); - DefaultLiftCurve.Add(8, .35f); - // DefaultLiftCurve.Add(19, 1); - // DefaultLiftCurve.Add(23, .9f); - DefaultLiftCurve.Add(30, 1.5f); - DefaultLiftCurve.Add(65, .6f); - DefaultLiftCurve.Add(90, .7f); + // If we're below DLRatioInflec1 then we're bounded on the right by 30° + if (DL < DLRatioInflec1) + ml.torqueBounds = [3, -1]; + else + { + float AoARHS = torqueAoAReturn.Evaluate(DL); + if (AoARHS > linAoA[6]) + ml.torqueBounds = [6, 7]; + else if (AoARHS > linAoA[5]) + ml.torqueBounds = [5, 7]; + else + ml.torqueBounds = [4, 7]; + + ml.torqueAoABounds[2] = AoARHS; + } + // This AoA happens to be a linear function of D/L + ml.torqueAoABounds[0] = 0.0307482f * DL + 28.49333f; + // This non-dimensionalized torque happens to be a + // linear function of SkR + ml.torqueAoABounds[1] = -1.30417f * SkR + 1.30879f; } - if (DefaultDragCurve == null) + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance] TorqueAoALimits for {ml} at SkR: {SkR}, D/L: {DL} are, torqueAoABounds: {ml.torqueAoABounds[0]}, {ml.torqueAoABounds[1]}, {ml.torqueAoABounds[2]}, torqueBounds: {ml.torqueBounds[0]}, {ml.torqueBounds[1]}"); + } + + public static float getTorqueAoALimit(MissileLauncher ml, float liftArea, float dragArea, float maxTorque) + { + // Dynamic pressure + float q = (float)(0.5 * ml.vessel.atmDensity * ml.vessel.srfSpeed * ml.vessel.srfSpeed); + // Technically not required, but in case anyone starts allowing for the CoL to vary + float CoLDist = 1f; + + // Divide out the dynamic pressure and CoLDist components of torque + maxTorque /= q * CoLDist; + maxTorque *= 0.9f; // Let's only go up to 90% of maxTorque to leave some leeway + + int LHS = 0; + int RHS = 7; + int interval = 3; + + // Drag and Lift Area multipliers + float dragSk = dragArea * BDArmorySettings.GLOBAL_DRAG_MULTIPLIER; + float liftSk = liftArea * BDArmorySettings.GLOBAL_LIFT_MULTIPLIER; + + // Here we store the AoA of local max torque, we set it to 180f as for the case + // where the entire range must be searched, this gives the correct AoA + float currAoA = 180f; + + if (ml.torqueBounds[0] > 0) + { + // If we have a left torque bound then we don't need to search the entire range + float torqueMaxLocal = ml.torqueAoABounds[0]; + currAoA = ml.torqueAoABounds[1]; + + if (ml.torqueBounds[1] > 0) + { + // If we have a right torque bound then we need to determine if we're searching + // in the low AoA or the high AoA section, this is decided by if the maxTorque + // is greater than torqueAoABounds times dragSk + liftSk + if (maxTorque > torqueMaxLocal * (dragSk + liftSk)) + { + // If maxTorque exceeds the max aerodynamic torque possible, then just return 180f + if (maxTorque > (liftSk * linLiftTorque[7] + dragSk * linDragTorque[7])) + return 180f; + + LHS = ml.torqueBounds[0]; + RHS = ml.torqueBounds[1]; + } + else + { + RHS = ml.torqueBounds[0]; + } + } + else + { + // If we don't have a right torque bound then we're bound only by the low + // AoA section, and hence can return immediately if torque exceeds the localMax + if (maxTorque > torqueMaxLocal * (dragSk + liftSk)) + return 180f; + + // Otherwise we just search the low AoA portion + RHS = ml.torqueBounds[0]; + } + } + else + { + // If maxTorque exceeds the max aerodynamic torque possible, then just return 180f + if (maxTorque > (liftSk * linLiftTorque[7] + dragSk * linDragTorque[7])) + return 180f; + } + + float currTorque; + + // Bisection search + while ((RHS - LHS) > 1) { - DefaultDragCurve = new FloatCurve(); - DefaultDragCurve.Add(0, 0.00215f); - DefaultDragCurve.Add(5, .00285f); - DefaultDragCurve.Add(15, .007f); - DefaultDragCurve.Add(29, .01f); - DefaultDragCurve.Add(55, .3f); - DefaultDragCurve.Add(90, .5f); + interval = Mathf.FloorToInt(0.5f * (RHS + LHS)); + + currTorque = liftSk * linLiftTorque[interval] + dragSk * linDragTorque[interval]; + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: LHS: {LHS}, RHS: {RHS}, interval: {interval}, currTorque: {currTorque}, maxTorque: {maxTorque}"); + + if (currTorque < maxTorque) + { + LHS = interval; + } + else + { + RHS = interval; + } } + currAoA = (maxTorque - (linLiftTorqueIntc[LHS] * liftSk + linDragTorqueIntc[LHS] * dragSk)) / (linLiftTorqueSlope[LHS] * liftSk + linDragTorqueSlope[LHS] * dragSk); + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: q: {q}, Final Interval: {LHS}, currAoA: {currAoA}, maxTorque: {maxTorque}"); + + return currAoA; + } + + public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, float liftArea, float dragArea, float steerMult, + Vector3 previousTorque, float maxTorque, float maxTorqueAero, float maxAoA) + { + FloatCurve liftCurve = DefaultLiftCurve; FloatCurve dragCurve = DefaultDragCurve; - return DoAeroForces(ml, targetPosition, liftArea, steerMult, previousTorque, maxTorque, maxAoA, liftCurve, - dragCurve); + return DoAeroForces(ml, targetPosition, liftArea, dragArea, steerMult, previousTorque, maxTorque, maxAoA, maxTorqueAero, + liftCurve, dragCurve); } - public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, float liftArea, float steerMult, - Vector3 previousTorque, float maxTorque, float maxAoA, FloatCurve liftCurve, FloatCurve dragCurve) + public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, float liftArea, float dragArea, float steerMult, + Vector3 previousTorque, float maxTorque, float maxTorqueAero, float maxAoA, FloatCurve liftCurve, FloatCurve dragCurve) { Rigidbody rb = ml.part.rb; + if (rb == null || rb.mass == 0) return Vector3.zero; double airDensity = ml.vessel.atmDensity; double airSpeed = ml.vessel.srfSpeed; Vector3d velocity = ml.vessel.Velocity(); + Vector3d velNorm = velocity.normalized; + Vector3 forward = ml.transform.forward; //temp values Vector3 CoL = new Vector3(0, 0, -1f); float liftMultiplier = BDArmorySettings.GLOBAL_LIFT_MULTIPLIER; float dragMultiplier = BDArmorySettings.GLOBAL_DRAG_MULTIPLIER; + double dynamicq = 0.5 * airDensity * airSpeed * airSpeed; + + maxTorque += (float)dynamicq * maxTorqueAero; //lift - float AoA = Mathf.Clamp(Vector3.Angle(ml.transform.forward, velocity.normalized), 0, 90); + float AoA = Mathf.Clamp(VectorUtils.AnglePreNormalized(forward, velNorm), 0, 90); + Vector3 forcePos = ml.transform.TransformPoint(ml.part.CoMOffset + CoL); + Vector3 forceDirection = -velocity.ProjectOnPlanePreNormalized(forward).normalized; + double liftForce = 0.0; + ml.smoothedAoA.Update(AoA); 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; + liftForce = dynamicq * liftArea * liftMultiplier * Mathf.Max(liftCurve.Evaluate(AoA), 0f); rb.AddForceAtPosition((float)liftForce * forceDirection, - ml.transform.TransformPoint(ml.part.CoMOffset + CoL)); + forcePos); } //drag + double dragForce = 0.0; if (airSpeed > 0) { - double dragForce = 0.5 * airDensity * Math.Pow(airSpeed, 2) * liftArea * dragMultiplier * dragCurve.Evaluate(AoA); - rb.AddForceAtPosition((float)dragForce * -velocity.normalized, - ml.transform.TransformPoint(ml.part.CoMOffset + CoL)); + dragForce = dynamicq * dragArea * dragMultiplier * Mathf.Max(dragCurve.Evaluate(AoA), 0f); + rb.AddForceAtPosition((float)dragForce * -velNorm, + forcePos); } //guidance - if (airSpeed > 1 || (ml.vacuumSteerable && ml.Throttle > 0)) + if (airSpeed > 1f || (ml.vacuumSteerable && ml.Throttle > 0)) { - Vector3 targetDirection; + /* This is what the torque on the missile due to aero forces would be + Vector3 aeroTorque = Vector3.Cross(forcePos - ml.vessel.CoM, + new Vector3d(liftForce * forceDirection.x - dragForce * velNorm.x, + liftForce * forceDirection.y - dragForce * velNorm.y, + liftForce * forceDirection.z - dragForce * velNorm.z)); + */ + // So instead we take the opposite cross product to get the negative/opposing torque + Vector3 aeroTorque = Vector3.Cross(new Vector3d(liftForce * forceDirection.x - dragForce * velNorm.x, + liftForce * forceDirection.y - dragForce * velNorm.y, + liftForce * forceDirection.z - dragForce * velNorm.z), + forcePos - ml.vessel.CoM); + //Debug.Log($"[BDArmory.MissileGuidance]: aeroTorque = {aeroTorque}."); + /* Legacy Missile Controller + Vector3 targetDirection; // = (targetPosition - ml.transform.position); float targetAngle; if (AoA < maxAoA) { - targetDirection = (targetPosition - ml.transform.position); - targetAngle = Vector3.Angle(velocity.normalized, targetDirection) * 4; + targetDirection = (targetPosition - ml.vessel.CoM); + targetAngle = Mathf.Min(maxAoA,VectorUtils.Angle(velNorm, targetDirection) * 4f); } else { - targetDirection = velocity.normalized; - targetAngle = AoA; + targetDirection = velNorm; + targetAngle = 0f; //AoA; } - Vector3 torqueDirection = -Vector3.Cross(targetDirection, velocity.normalized).normalized; + Vector3 torqueDirection = -Vector3.Cross(targetDirection, velNorm).normalized; 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); + */ + + float AoALim = Mathf.Min(maxAoA + Mathf.Min(0.1f * maxAoA, 2f), getTorqueAoALimit(ml, liftArea, dragArea, maxTorque)); + //if (ml.torqueAoALimit.x > 0) + // AoALim = Mathf.Min(maxAoA + Mathf.Min(0.1f * maxAoA, 2f), 1.2f * ml.torqueAoALimit.x * ml.torqueAoALimit.y / (float)airSpeed * BDAMath.Sqrt(ml.torqueAoALimit.z / (float)airDensity)); + + Vector3 targetDirection = (targetPosition - ml.vessel.CoM).normalized; + float targetAngle = VectorUtils.AnglePreNormalized(velNorm, targetDirection); + if (targetAngle > AoALim) + targetDirection = Vector3.Slerp(velNorm, targetDirection, AoALim / targetAngle); + float turningAngle = VectorUtils.AnglePreNormalized(forward, targetDirection); + + Vector3 finalTorque; + if (turningAngle * Mathf.Deg2Rad > 0.005f) + { + Vector3 torqueDirection = Vector3.Cross(forward, targetDirection) / Mathf.Sin(turningAngle * Mathf.Deg2Rad); + //Debug.Log($"[BDArmory.MissileGuidance]: torqueDirection = {torqueDirection}, sqrMagnitude = {torqueDirection.sqrMagnitude}."); + + if (turningAngle < 1f) + turningAngle *= turningAngle; + + float torque = Mathf.Clamp(Mathf.Min(turningAngle, AoALim) * 4f * steerMult, 0f, maxTorque); + + float aeroTorqueSqr = aeroTorque.sqrMagnitude; + + // If aeroTorque < maxTorque we're not yet saturated + if (aeroTorqueSqr < maxTorque * maxTorque) + { + float temp = Vector3.Dot(aeroTorque, torqueDirection); + //Debug.Log($"[BDArmory.MissileGuidance]: aeroTorque not saturated, torque = {torque}."); + // If torque drives us over maxTorque, then using the quadratic formula, we determine the value that gets us maxTorque + if ((aeroTorque + torqueDirection * torque).sqrMagnitude > maxTorque * maxTorque) + { + // Solution to the quadratic formula for the intersection of a line with a sphere, note we use the +ve solution + // There is no need to check the determinant as any line that originates within the sphere will always intersect the sphere + torque = BDAMath.Sqrt(temp * temp - (aeroTorque.sqrMagnitude - maxTorque * maxTorque)) - temp; + //Debug.Log($"[BDArmory.MissileGuidance]: torque saturation! torque = {torque}."); + } + //// If aeroTorque is within 50% of maxTorque then tone down torque marginally + //if (aeroTorqueSqr > 0.25f * maxTorque * maxTorque)// && temp > 0f) + //{ + // //Debug.Log($"[BDArmory.MissileGuidance] torque limiter: {(1f - (aeroTorqueSqr / (maxTorque * maxTorque) - 0.49f) * 1.96078f)}"); + // //torque *= (1f - (aeroTorqueSqr / (maxTorque * maxTorque) - 0.49f) * 1.96078f); + // float aeroTorqueMag = BDAMath.Sqrt(aeroTorqueSqr); + // float x = 1.7f - aeroTorqueMag / maxTorque; + // //Debug.Log($"[BDArmory.MissileGuidance] torque limiter: {(x*x*x*x - 0.0625f) * 1.066f}"); + // torque *= (x * x * x * x - 0.0625f) * 1.066f; + //} + + if (temp < 0) + torque *= 0.5f; + + // If we're approaching the limit (90% of maxTorque) and we're faster than the last time we reached it, + // recalculate the torqueAoALimit as the estimate is a bit more restrictive when going faster + //if (ml.torqueAoALimit.x > 0f && aeroTorqueSqr > 0.81f * maxTorque * maxTorque && airSpeed > 1.5f * ml.torqueAoALimit.y) + //{ + // // Here we assume the torqueAoALimit has more or less a quadratic relationship with AoA + // ml.torqueAoALimit = new Vector3(1.2f * BDAMath.Sqrt(BDAMath.Sqrt(aeroTorqueSqr / (maxTorque * maxTorque))) * AoA, (float)airSpeed, (float)airDensity); + //} + + // Otherwise we just use torque unmodified + } + else + { + //ml.torqueAoALimit = new Vector3(AoA, (float)airSpeed, (float)airDensity); + //Debug.Log($"[BDArmory.MissileGuidance]: aeroTorque saturated! torque = {torque}."); + // If we're saturated, then as long as torqueDirection somewhat opposes aeroTorque we can look + // at how much torque we can apply + float temp = Vector3.Dot(aeroTorque, torqueDirection); + // We check the determinant of the quadratic as well to ensure we actually intersect with the sphere + float det = temp * temp - (aeroTorque.sqrMagnitude - maxTorque * maxTorque); + if (temp < 0f && det > 0f) + { + float temp2 = BDAMath.Sqrt(det); + // temp2 > 0 and temp < 0 so LHS < RHS + float LHS = -temp2 - temp; + float RHS = temp2 - temp; + //Debug.Log($"[BDArmory.MissileGuidance]: Possible to unsaturate! LHS: {LHS}, {RHS}, torque = {torque}."); + // There are three cases here, first is the case is if torque is insufficient to drive us under saturation, + // in which case we'll just apply enough to saturate, but only if LHS is < 2f * torque and maxTorque + if (torque < LHS) + { + // This unsaturation method lead to some pretty poor results so I'm just disabling it + //if (LHS < 2f * torque && LHS < maxTorque) + // torque = LHS; + //else + //{ + torque = 0f; + aeroTorque = (maxTorque / aeroTorque.magnitude) * aeroTorque; + //} + } + // The second case is where we've gone over in the opposite direction, in which case we must reduce our torque + else if (torque > RHS) + torque = RHS; + // A special case occurs if |temp| < Mathf.Epsilon, which is the single point intersection solution, where + // torque can potentially approx. equal the single point solution but in that case we wouldn't have to modify + // the torque. If torque is not approx. equal one of these two cases should catch it + // The third case is where we're perfectly within bounds so we don't modify torque + } + else + { + // If all previous checks fail, we're saturated, so we limit the aeroTorque + torque = 0f; + aeroTorque = (maxTorque / aeroTorque.magnitude) * aeroTorque; + //Debug.Log($"[BDArmory.MissileGuidance]: Cannot unsaturate! aeroTorque = {aeroTorque}."); + } + } + + finalTorque = torque > 0f ? (torque * torqueDirection + aeroTorque) : aeroTorque; + } + else + { + if (aeroTorque.sqrMagnitude > maxTorque * maxTorque) + { + aeroTorque = (maxTorque / aeroTorque.magnitude) * aeroTorque; + } + + finalTorque = aeroTorque; + } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) ml.debugString.AppendLine($"achieved g: {(ml.vessel.acceleration.ProjectOnPlanePreNormalized(velNorm).magnitude) * (1f / 9.81f):F5}, lift: {liftForce / ml.part.mass * (1f / 9.81f):F5}, CL: {liftCurve.Evaluate(AoA):F5}\nAoA: {AoA:F5}, AoALim: {AoALim:F5}, MaxAoA: {maxAoA:F5}\nTargetAngle: {targetAngle:F5}, TurningAngle: {turningAngle:F5}\nmaxTorque: {maxTorque}, maxTorqueAero: {maxTorqueAero * dynamicq}, currTorque: {finalTorque.magnitude}, liftArea: {liftArea}, dragArea: {dragArea}"); + + finalTorque = ml.transform.InverseTransformDirection(finalTorque).ProjectOnPlanePreNormalized(Vector3.forward); + + //Debug.Log($"[BDArmory.MissileGuidance]: torque = {torque}, torqueDirection = {torqueDirection}, aeroTorque = {aeroTorque}, finalTorque = {finalTorque}."); 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; } @@ -444,7 +2138,7 @@ public static float GetRadarAltitudeAtPos(Vector3 position) public static float GetRaycastRadarAltitude(Vector3 position) { - Vector3 upDirection = -FlightGlobals.getGeeForceAtPosition(position).normalized; + Vector3 upDirection = VectorUtils.GetUpDirection(position); float altAtPos = FlightGlobals.getAltitudeAtPos(position); if (altAtPos < 0) @@ -461,7 +2155,7 @@ public static float GetRaycastRadarAltitude(Vector3 position) } RaycastHit rayHit; - if (Physics.Raycast(ray, out rayHit, rayDistance, (1 << 15) | (1 << 17))) + if (Physics.Raycast(ray, out rayHit, rayDistance, (int)(LayerMasks.Scenery | LayerMasks.EVA))) // Why EVA? { return rayHit.distance; } diff --git a/BDArmory/Guidances/_description b/BDArmory/Guidances/_description new file mode 100644 index 000000000..09274c411 --- /dev/null +++ b/BDArmory/Guidances/_description @@ -0,0 +1,2 @@ +Guidance modes for missiles. +- FIXME This could be part of Control or a Missiles folder. \ No newline at end of file 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 91% rename from BDArmory.Core/Dependencies.cs rename to BDArmory/Initialization/Dependencies.cs index 465dcfb28..e1272cd97 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 { @@ -30,8 +30,7 @@ public static void Register(object obj) public static T Get() where T : class { Type type = typeof(T); - Object instance; - Systems.TryGetValue(type, out instance); + Systems.TryGetValue(type, out object instance); if (instance == null) { diff --git a/BDArmory/Misc/Misc.cs b/BDArmory/Misc/Misc.cs deleted file mode 100644 index d0b527f8e..000000000 --- a/BDArmory/Misc/Misc.cs +++ /dev/null @@ -1,433 +0,0 @@ -using BDArmory.Core.Extension; -using BDArmory.Core; -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 Misc - { - 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("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; - } - - 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, 9076737)) - { - 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, 9076737)) - { - 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) - { - double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); - double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); - float altitude = (float)(FlightGlobals.currentMainBody.GetAltitude(position)); - return Mathf.Clamp(altitude - (float)FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos), 0, altitude); - } - - 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] GM Killed Vessel " + v.GetDisplayName()); - foreach (MissileFire missileFire in v.FindPartModulesImplementing()) - { - PartExploderSystem.AddPartToExplode(missileFire.part); - ExplosionFx.CreateExplosion(missileFire.part.transform.position, 0.2f, explModelPath, explSoundPath, ExplosionSourceType.Missile, 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); - } - } - - } -} diff --git a/BDArmory/Misc/MissileLaunchParams.cs b/BDArmory/Misc/MissileLaunchParams.cs deleted file mode 100644 index 9bd295da9..000000000 --- a/BDArmory/Misc/MissileLaunchParams.cs +++ /dev/null @@ -1,71 +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 rangeAddMin = 0; - float rangeAddMax = 0; - float relSpeed; - - 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; - - rangeAddMin += relSpeed * 2; - rangeAddMax += relSpeed * 8; - rangeAddMin += launcherSpeed * 2; - rangeAddMax += launcherSpeed * 2; - - 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/VectorUtils.cs b/BDArmory/Misc/VectorUtils.cs deleted file mode 100644 index ef25f66e0..000000000 --- a/BDArmory/Misc/VectorUtils.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using UnityEngine; - -namespace BDArmory.Misc -{ - public static class VectorUtils - { - private static System.Random RandomGen = new System.Random(); - - /// Right compared to fromDirection, make sure it's not orthogonal to toDirection, or you'll get unstable signs - 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; - } - - /// - /// Same as SignedAngle, just using double precision for the cosine calculation. - /// For very small angles the floating point precision starts to matter, as the cosine is close to 1, not to 0. - /// - public static float SignedAngleDP(Vector3 fromDirection, Vector3 toDirection, Vector3 referenceRight) - { - float angle = (float)Vector3d.Angle(fromDirection, toDirection); - float sign = Mathf.Sign(Vector3.Dot(toDirection, referenceRight)); - float finalAngle = sign * angle; - return finalAngle; - } - - /// - /// Convert an angle to be between -180 and 180. - /// - public static float ToAngle(this float angle) - { - angle = (angle + 180) % 360; - return angle > 0 ? angle - 180 : angle + 180; - } - - //from howlingmoonsoftware.com - //calculates how long it will take for a target to be where it will be when a bullet fired now can reach it. - //delta = initial relative position, vr = relative velocity, muzzleV = bullet velocity. - public static float CalculateLeadTime(Vector3 delta, Vector3 vr, float muzzleV) - { - // Quadratic equation coefficients a*t^2 + b*t + c = 0 - float a = Vector3.Dot(vr, vr) - muzzleV * muzzleV; - float b = 2f * Vector3.Dot(vr, delta); - float c = Vector3.Dot(delta, delta); - - float det = b * b - 4f * a * c; - - // If the determinant is negative, then there is no solution - if (det > 0f) - { - return 2f * c / (Mathf.Sqrt(det) - b); - } - else - { - return -1f; - } - } - - /// - /// Returns a value between -1 and 1 via Perlin noise. - /// - /// Returns a value between -1 and 1 via Perlin noise. - /// The x coordinate. - /// The y coordinate. - public static float FullRangePerlinNoise(float x, float y) - { - float perlin = Mathf.PerlinNoise(x, y); - - perlin -= 0.5f; - perlin *= 2; - - return perlin; - } - - public static Vector3 RandomDirectionDeviation(Vector3 direction, float maxAngle) - { - return Vector3.RotateTowards(direction, UnityEngine.Random.rotation * direction, UnityEngine.Random.Range(0, maxAngle * Mathf.Deg2Rad), 0).normalized; - } - - public static Vector3 WeightedDirectionDeviation(Vector3 direction, float maxAngle) - { - 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; - } - - /// - /// Returns the original vector rotated in a random direction using the give standard deviation. - /// - /// mean direction - /// standard deviation in degrees - /// Randomly adjusted Vector3 - /// - /// Technically, this is calculated using the chi-squared distribution in polar coordinates, - /// which, incidentally, makes the math easier too. - /// However a chi-squared (k=2) distance from center distribution produces a vector distributed normally - /// on any chosen axis orthogonal to the original vector, which is exactly what we want. - /// - public static Vector3 GaussianDirectionDeviation(Vector3 direction, float standardDeviation) - { - return Quaternion.AngleAxis(UnityEngine.Random.Range(-180f, 180f), direction) - * Quaternion.AngleAxis(Rayleigh() * standardDeviation, - new Vector3(-1 / direction.x, -1 / direction.y, 2 / direction.z)) // orthogonal vector - * direction; - } - - /// Random float distributed with an approximated standard normal distribution - /// https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform - /// Note a standard normal variable is technically unbounded - public static float Gaussian() - { - // Technically this will raise an exception if the first random produces a zero - try - { - return Mathf.Sqrt(-2 * Mathf.Log(UnityEngine.Random.value)) * Mathf.Cos(Mathf.PI * UnityEngine.Random.value); - } - catch (Exception) - { // I have no idea what exception Mathf.Log raises when it gets a zero - return 0; - } - } - - /// - /// Random float distributed with the chi-squared distribution with two degrees of freedom - /// aka the Rayleigh distribution. - /// Multiply by deviation for best results. - /// - /// https://en.wikipedia.org/wiki/Rayleigh_distribution - /// Note a chi-square distributed variable is technically unbounded - 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)); - } - catch (Exception) - { // I have no idea what exception Mathf.Log raises when it gets a zero - return 0; - } - } - - /// - /// Converts world position to Lat,Long,Alt form. - /// - /// The position in geo coords. - /// World position. - /// Body. - public static Vector3d WorldPositionToGeoCoords(Vector3d worldPosition, CelestialBody body) - { - if (!body) - { - //Debug.Log ("BahaTurret.VectorUtils.WorldPositionToGeoCoords body is null"); - return Vector3d.zero; - } - - double lat = body.GetLatitude(worldPosition); - double longi = body.GetLongitude(worldPosition); - double alt = body.GetAltitude(worldPosition); - return new Vector3d(lat, longi, alt); - } - - /// - /// Calculates the coordinates of a point a certain distance away in a specified direction. - /// - /// Starting point coordinates, in Lat,Long,Alt form - /// The body on which the movement is happening - /// Bearing to move in, in degrees, where 0 is north and 90 is east - /// Distance to move, in meters - /// Ending point coordinates, in Lat,Long,Alt form - public static Vector3 GeoCoordinateOffset(Vector3 start, CelestialBody body, float bearing, float distance) - { - //https://stackoverflow.com/questions/2637023/how-to-calculate-the-latlng-of-a-point-a-certain-distance-away-from-another - float lat1 = start.x * Mathf.Deg2Rad; - float lon1 = start.y * Mathf.Deg2Rad; - bearing *= Mathf.Deg2Rad; - distance /= ((float)body.Radius + start.z); - - float lat2 = Mathf.Asin(Mathf.Sin(lat1) * Mathf.Cos(distance) + Mathf.Cos(lat1) * Mathf.Sin(distance) * Mathf.Cos(bearing)); - float lon2 = lon1 + Mathf.Atan2(Mathf.Sin(bearing) * Mathf.Sin(distance) * Mathf.Cos(lat1), Mathf.Cos(distance) - Mathf.Sin(lat1) * Mathf.Sin(lat2)); - - return new Vector3(lat2 * Mathf.Rad2Deg, lon2 * Mathf.Rad2Deg, start.z); - } - - /// - /// Calculate the bearing going from one point to another - /// - /// Starting point coordinates, in Lat,Long,Alt form - /// Destination point coordinates, in Lat,Long,Alt form - /// Bearing when looking at destination from start, in degrees, where 0 is north and 90 is east - public static float GeoForwardAzimuth(Vector3 start, Vector3 destination) - { - //http://www.movable-type.co.uk/scripts/latlong.html - float lat1 = start.x * Mathf.Deg2Rad; - float lon1 = start.y * Mathf.Deg2Rad; - float lat2 = destination.x * Mathf.Deg2Rad; - float lon2 = destination.y * Mathf.Deg2Rad; - return Mathf.Atan2(Mathf.Sin(lon2 - lon1) * Mathf.Cos(lat2), Mathf.Cos(lat1) * Mathf.Sin(lat2) - Mathf.Sin(lat1) * Mathf.Cos(lat2) * Mathf.Cos(lon2 - lon1)) * Mathf.Rad2Deg; - } - - /// - /// Calculate the distance from one point to another on a globe - /// - /// Starting point coordinates, in Lat,Long,Alt form - /// Destination point coordinates, in Lat,Long,Alt form - /// The body on which the distance is calculated - /// distance between the two points - public static float GeoDistance(Vector3 start, Vector3 destination, CelestialBody body) - { - //http://www.movable-type.co.uk/scripts/latlong.html - float lat1 = start.x * Mathf.Deg2Rad; - float lat2 = destination.x * Mathf.Deg2Rad; - 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)); - } - - public static Vector3 RotatePointAround(Vector3 pointToRotate, Vector3 pivotPoint, Vector3 axis, float angle) - { - Vector3 line = pointToRotate - pivotPoint; - line = Quaternion.AngleAxis(angle, axis) * line; - return pivotPoint + line; - } - - 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; - } - - public static Vector3 GetWorldSurfacePostion(Vector3d geoPosition, CelestialBody body) - { - if (!body) - { - return Vector3.zero; - } - return body.GetWorldSurfacePosition(geoPosition.x, geoPosition.y, geoPosition.z); - } - - public static Vector3 GetUpDirection(Vector3 position) - { - if (FlightGlobals.currentMainBody == null) return Vector3.up; - return (position - FlightGlobals.currentMainBody.transform.position).normalized; - } - - public static bool SphereRayIntersect(Ray ray, Vector3 sphereCenter, double sphereRadius, out double distance) - { - Vector3 o = ray.origin; - Vector3 l = ray.direction; - Vector3d c = sphereCenter; - double r = sphereRadius; - - double d; - - d = -(Vector3.Dot(l, o - c) + Math.Sqrt(Mathf.Pow(Vector3.Dot(l, o - c), 2) - (o - c).sqrMagnitude + (r * r))); - - if (double.IsNaN(d)) - { - distance = 0; - return false; - } - else - { - distance = d; - return true; - } - } - } -} diff --git a/BDArmory/Misc/ViewScanResults.cs b/BDArmory/Misc/ViewScanResults.cs deleted file mode 100644 index a4f5a3b5a..000000000 --- a/BDArmory/Misc/ViewScanResults.cs +++ /dev/null @@ -1,19 +0,0 @@ -using BDArmory.Modules; -using UnityEngine; - -namespace BDArmory.Misc -{ - public struct ViewScanResults - { - public bool foundMissile; - public bool foundHeatMissile; - public bool foundRadarMissile; - public bool foundAGM; - public bool firingAtMe; - public float missDistance; - public float missileThreatDistance; - public Vector3 threatPosition; - public Vessel threatVessel; - public MissileFire threatWeaponManager; - } -} diff --git a/BDArmory/ModIntegration/BDATeamIcons.cs b/BDArmory/ModIntegration/BDATeamIcons.cs new file mode 100644 index 000000000..84ec366c6 --- /dev/null +++ b/BDArmory/ModIntegration/BDATeamIcons.cs @@ -0,0 +1,19 @@ +// Legacy BDA Team Icons + +using System; +using System.Linq; + +namespace BDArmory.ModIntegration +{ + public static class LegacyTeamIcons + { + public static bool CheckForLegacyTeamIcons() + { + using var a = AppDomain.CurrentDomain.GetAssemblies().ToList().GetEnumerator(); + while (a.MoveNext()) + if (a.Current.FullName.Split([','])[0] == "BDATeamIcons") + return true; + return false; + } + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/CameraTools.cs b/BDArmory/ModIntegration/CameraTools.cs new file mode 100644 index 000000000..cbcc7f7b5 --- /dev/null +++ b/BDArmory/ModIntegration/CameraTools.cs @@ -0,0 +1,38 @@ +using BDArmory.Extensions; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using UnityEngine; + +namespace BDArmory.ModIntegration +{ + class CameraTools + { + // Instead of directly affecting CameraTools, this provides the fields and properties that CameraTools can look for and interact with. + public static bool InhibitCameraTools => VesselSpawnerStatus.vesselsSpawning; // Flag for CameraTools (currently just checks for vessels being spawned). + public static float RestoreDistanceLimit { get; private set; } = 50f; // Limit to how far away to set the camera when restoring it due to BDA automatically enabling the camera. + public static Vessel MissileTargetVessel // Get the current target of a missile that is the active vessel. + { + get + { + var vessel = FlightGlobals.ActiveVessel; + if (vessel == null || !vessel.IsMissile()) return null; + var mb = VesselModuleRegistry.GetMissileBase(vessel); + if (mb == null) return null; + var ti = mb.targetVessel; + if (ti == null) return null; + return ti.Vessel; + } + } + public static Vector3 MissileTargetPosition // Get the current target position of a missile that is the active vessel (if the target isn't a vessel). + { + get + { + var vessel = FlightGlobals.ActiveVessel; + if (vessel == null || !vessel.IsMissile()) return default; + var mb = VesselModuleRegistry.GetMissileBase(vessel); + if (mb == null) return default; + return mb.TargetPosition; + } + } + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/ConformalDecals.cs b/BDArmory/ModIntegration/ConformalDecals.cs new file mode 100644 index 000000000..372a8d849 --- /dev/null +++ b/BDArmory/ModIntegration/ConformalDecals.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; +using BDArmory.Settings; + +namespace BDArmory.ModIntegration +{ + [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] + public class ConformalDecals : MonoBehaviour + { + public static ConformalDecals Instance; + public static bool hasConformalDecals = false; + static Assembly CDAssembly = null; + Type MCDModType = null; + Func CDisAttachedFieldGetter = null; + Action CDisAttachedFieldSetter = null; + + void Awake() + { + if (Instance is not null) Destroy(Instance); + Instance = this; + } + + void Start() + { + CheckForConformalDecals(); + if (hasConformalDecals) + { + GetMCDModType(); + GetMCDIsAttachedField(); + } + else + { + Destroy(this); // Destroy ourselves to not take up any further CPU cycles. + } + } + + public static void CheckForConformalDecals() + { + if (hasConformalDecals) return; // Already checked and found. + using var a = AppDomain.CurrentDomain.GetAssemblies().ToList().GetEnumerator(); + while (a.MoveNext()) + { + if (a.Current.FullName.StartsWith("ConformalDecals")) + { + CDAssembly = a.Current; + hasConformalDecals = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ModIntegration.ConformalDecals]: Conformal Decals mod detected: {CDAssembly.FullName}."); + return; + } + } + } + + public void GetMCDModType() + { + if (!hasConformalDecals) return; + foreach (var t in CDAssembly.GetTypes()) + { + if (t == null) continue; + if (t.Name == "ModuleConformalDecal") + { + MCDModType = t; + return; + } + } + Debug.LogError($"[BDArmory.ModIntegration.ConformalDecals]: Failed to find ModuleConformalDecal despite ConformalDecals mod being detected!"); + } + public void GetMCDIsAttachedField() + { + if (MCDModType == null) return; + try + { + var fieldInfo = MCDModType.GetField("_isAttached", BindingFlags.NonPublic | BindingFlags.Instance); + CDisAttachedFieldGetter = ReflectionUtils.CreateGetter(fieldInfo); + CDisAttachedFieldSetter = ReflectionUtils.CreateSetter(fieldInfo); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.ConformalDecals]: Failed to find ModuleConformalDecals._isAttached. Has ConformalDecals changed? {e.Message}"); + } + } + + public object GetMCDComponent(Part p) + { + if (MCDModType == null) return null; + return p.GetComponent(MCDModType); + } + public bool GetMCDIsAttached(object MCDComponent) + { + if (MCDComponent == null) return false; + return CDisAttachedFieldGetter(MCDComponent); + } + public void SetMCDIsAttached(object MCDComponent, bool value) + { + CDisAttachedFieldSetter(MCDComponent, value); + } + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/ModuleManager.cs b/BDArmory/ModIntegration/ModuleManager.cs new file mode 100644 index 000000000..66dd01953 --- /dev/null +++ b/BDArmory/ModIntegration/ModuleManager.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; + +namespace BDArmory.ModIntegration +{ + public static class ModuleManager + { + public static bool CheckForModuleManager() + { + using var a = AppDomain.CurrentDomain.GetAssemblies().ToList().GetEnumerator(); + while (a.MoveNext()) + if (a.Current.FullName.Split([','])[0] == "ModuleManager") + return true; + return false; + } + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/MouseAimFlight.cs b/BDArmory/ModIntegration/MouseAimFlight.cs new file mode 100644 index 000000000..64f44e2aa --- /dev/null +++ b/BDArmory/ModIntegration/MouseAimFlight.cs @@ -0,0 +1,129 @@ +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; + Func targetPositionFieldGetter = null; + Vector3 lastTarget = default; + 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 + { + bool foundMouseAimActive = false; + bool foundMouseAimTarget = false; + 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); + foundMouseAimActive = true; + } + else if (fieldInfo.Name == "targetPosition") + { + targetPositionFieldGetter = ReflectionUtils.CreateGetter(fieldInfo); + foundMouseAimTarget = true; + } + if (foundMouseAimActive && foundMouseAimTarget) return; + } + } + } + } + } + if (hasMouseAimFlight && (!foundMouseAimActive || !foundMouseAimTarget)) + { + Debug.LogWarning($"[BDArmory.ModIntegration.MouseAimFlight]: MouseAimFlight mod found, but failed to locate the required fields: mouseAimActive: {foundMouseAimActive}, : targetPosition: {foundMouseAimTarget}"); + hasMouseAimFlight = false; + } + } + 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 Vector3 GetCurrentMouseAimTarget() + { + if (!IsMouseAimActive) return lastTarget; + lastTarget = targetPositionFieldGetter(mouseAimFlightInstance); + return lastTarget; + } + + public static bool IsMouseAimActive => hasMouseAimFlight && Instance != null && Instance.IsMouseAimFlightActive(); + public static Vector3 GetMouseAimTarget => Instance.GetCurrentMouseAimTarget(); + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/PhysicsRangeExtender.cs b/BDArmory/ModIntegration/PhysicsRangeExtender.cs new file mode 100644 index 000000000..7202fcc90 --- /dev/null +++ b/BDArmory/ModIntegration/PhysicsRangeExtender.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace BDArmory.ModIntegration +{ + public static class PhysicsRangeExtender + { + static bool havePRE = false; + static PropertyInfo modEnabled = null; // bool property + static PropertyInfo PRERange = null; // int property + static MethodInfo UpdateRanges = null; // Method for updating ranges after setting PRERange. + public static bool CheckForPhysicsRangeExtender() + { + using var a = AppDomain.CurrentDomain.GetAssemblies().ToList().GetEnumerator(); + while (a.MoveNext()) + { + if (a.Current.FullName.Split([','])[0] == "PhysicsRangeExtender") + { + havePRE = true; + foreach (var t in a.Current.GetTypes()) + { + if (t == null) continue; + if (t.Name == "PreSettings") + { + modEnabled = t.GetProperty("ModEnabled", BindingFlags.Public | BindingFlags.Static); + PRERange = t.GetProperty("GlobalRange", BindingFlags.Public | BindingFlags.Static); + } + if (t.Name == "PhysicsRangeExtender") + { + UpdateRanges = t.GetMethod("UpdateRanges", BindingFlags.Public | BindingFlags.Static); + } + } + break; + } + } + return havePRE; + } + public static bool IsPREEnabled => modEnabled != null && (bool)modEnabled.GetValue(null); + public static float GetPRERange() + { + if (PRERange == null) return 0; + return (int)PRERange.GetValue(null) * 1000f; + } + public static bool SetPRERange(int range) + { + if (PRERange == null) return false; + try + { + PRERange.SetValue(null, range / 1000); + UpdateRanges.Invoke(null, [false]); + } + catch (Exception e) + { + Debug.LogError($"Failed to update PRE range: {e.Message}"); + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/ReflectionUtils.cs b/BDArmory/ModIntegration/ReflectionUtils.cs new file mode 100644 index 000000000..4afd3aba9 --- /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(T)); + + 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/BDGenericAIBase.cs b/BDArmory/Modules/BDGenericAIBase.cs deleted file mode 100644 index 3e5a4cd74..000000000 --- a/BDArmory/Modules/BDGenericAIBase.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System; -using System.Text; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Targeting; -using BDArmory.UI; -using UnityEngine; -using KSP.Localization; - -namespace BDArmory.Modules -{ - /// - /// A base class for implementing AI. - /// Note: You do not have to use it, it is just for convenience, all the game cares about is that you implement the IBDAIControl interface. - /// - public abstract class BDGenericAIBase : PartModule, IBDAIControl, IBDWMModule - { - #region declarations - - public bool pilotEnabled => pilotOn; - - // separate private field for pilot On, because properties cannot be KSPFields - [KSPField(isPersistant = true)] - public bool pilotOn; - protected Vessel activeVessel; - - public MissileFire weaponManager { get; protected set; } - - /// - /// The default is BDAirspeedControl. If you want to use something else, just override ActivatePilot (and, potentially, DeactivatePilot), and make it use something else. - /// - protected BDAirspeedControl speedController; - - protected Transform vesselTransform => vessel.ReferenceTransform; - - protected StringBuilder debugString = new StringBuilder(); - - protected Vessel targetVessel; - - protected virtual Vector3d assignedPositionGeo { get; set; } - - public Vector3d assignedPositionWorld - { - get - { - return VectorUtils.GetWorldSurfacePostion(assignedPositionGeo, vessel.mainBody); - } - protected set - { - assignedPositionGeo = VectorUtils.WorldPositionToGeoCoords(value, vessel.mainBody); - } - } - - //wing commander - public ModuleWingCommander commandLeader { get; protected set; } - - protected PilotCommands command; - public string currentStatus { get; protected set; } = "Free"; - protected int commandFollowIndex; - - public PilotCommands currentCommand => command; - public virtual Vector3d commandGPS => assignedPositionGeo; - - #endregion declarations - - public abstract bool CanEngage(); - - public abstract bool IsValidFixedWeaponTarget(Vessel target); - - /// - /// This will be called every update and should run the autopilot logic. - /// - /// For simple use cases: - /// 1. Engage your target (get in position to engage, shooting is done by guard mode) - /// 2. If no target, check command, and follow it - /// Do this by setting s.pitch, s.yaw and s.roll. - /// - /// For advanced use cases you probably know what you're doing :P - /// - /// current flight control state - protected abstract void AutoPilot(FlightCtrlState s); - - // A small wrapper to make sure the autopilot does not do anything when it shouldn't - private void autoPilot(FlightCtrlState s) - { - if (!weaponManager || !vessel || !vessel.transform || vessel.packed || !vessel.mainBody) - return; - // nobody is controlling any more possibly due to G forces? - if (!vessel.isCommandable) - { - s.NeutralizeStick(); - vessel.Autopilot.Disable(); - return; - } - debugString.Length = 0; - - // generally other AI and guard mode expects this target to be engaged - GetGuardTarget(); // get the guard target from weapon manager - GetNonGuardTarget(); // if guard mode is off, get the UI target - GetGuardNonTarget(); // pick a target if guard mode is on, but no target is selected, - // though really targeting should be managed by the weaponManager, what if we pick an airplane while having only abrams cannons? :P - // (this is another reason why target selection is hardcoded into the base class, so changing this later is less of a mess :) ) - - AutoPilot(s); - } - - #region Pilot on/off - - public virtual void ActivatePilot() - { - pilotOn = true; - if (activeVessel) - activeVessel.OnFlyByWire -= autoPilot; - activeVessel = vessel; - activeVessel.OnFlyByWire += autoPilot; - - if (!speedController) - { - speedController = gameObject.AddComponent(); - speedController.vessel = vessel; - } - - speedController.Activate(); - - GameEvents.onVesselDestroy.Remove(RemoveAutopilot); - 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); - RefreshPartWindow(); - } - - public virtual void DeactivatePilot() - { - pilotOn = false; - if (activeVessel) - activeVessel.OnFlyByWire -= autoPilot; - RefreshPartWindow(); - - if (speedController) - { - speedController.Deactivate(); - } - } - - protected void RemoveAutopilot(Vessel v) - { - if (v == vessel) - { - v.OnFlyByWire -= autoPilot; - } - } - - protected void RefreshPartWindow() - { - Events["TogglePilot"].guiName = pilotEnabled ? Localizer.Format("#LOC_BDArmory_DeactivatePilot") : Localizer.Format("#LOC_BDArmory_ActivatePilot");//"Deactivate Pilot""Activate Pilot" - } - - [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_TogglePilot", active = true)]//Toggle Pilot - public void TogglePilot() - { - if (pilotEnabled) - { - DeactivatePilot(); - } - else - { - ActivatePilot(); - } - } - - [KSPAction("Activate Pilot")] - public void AGActivatePilot(KSPActionParam param) => ActivatePilot(); - - [KSPAction("Deactivate Pilot")] - public void AGDeactivatePilot(KSPActionParam param) => DeactivatePilot(); - - [KSPAction("Toggle Pilot")] - public void AGTogglePilot(KSPActionParam param) => TogglePilot(); - - public virtual string Name { get; } = "AI Control"; - public bool Enabled => pilotEnabled; - - public void Toggle() => TogglePilot(); - - #endregion Pilot on/off - - #region events - - protected virtual void Start() - { - if (HighLogic.LoadedSceneIsFlight) - { - part.OnJustAboutToBeDestroyed += DeactivatePilot; - vessel.OnJustAboutToBeDestroyed += DeactivatePilot; - GameEvents.onVesselWasModified.Add(onVesselWasModified); - MissileFire.OnChangeTeam += OnToggleTeam; - - activeVessel = vessel; - UpdateWeaponManager(); - - if (pilotEnabled) - { - ActivatePilot(); - } - } - - RefreshPartWindow(); - } - - protected virtual void OnDestroy() - { - MissileFire.OnChangeTeam -= OnToggleTeam; - } - - protected virtual void OnGUI() - { - if (!pilotEnabled || !vessel.isActiveVessel) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - GUI.Label(new Rect(200, Screen.height - 200, 400, 400), $"{vessel.name}: {debugString.ToString()}"); - } - } - - protected virtual void OnToggleTeam(MissileFire mf, BDTeam team) - { - if (mf.vessel == vessel || (commandLeader && commandLeader.vessel == mf.vessel)) - { - ReleaseCommand(); - } - } - - protected virtual void onVesselWasModified(Vessel v) - { - if (v != activeVessel) - return; - - if (vessel != activeVessel) - { - if (activeVessel) - activeVessel.OnJustAboutToBeDestroyed -= DeactivatePilot; - if (vessel) - vessel.OnJustAboutToBeDestroyed += DeactivatePilot; - if (weaponManager != null && weaponManager.vessel == activeVessel) - { - if (this.Equals(weaponManager.AI)) - weaponManager.AI = null; - UpdateWeaponManager(); - } - } - - activeVessel = vessel; - } - - #endregion events - - #region utilities - - protected void UpdateWeaponManager() - { - weaponManager = vessel.FindPartModuleImplementing(); - if (weaponManager != null) - weaponManager.AI = this; - } - - protected void GetGuardTarget() - { - if (weaponManager == null || weaponManager.vessel != vessel) - UpdateWeaponManager(); - if (weaponManager != null && weaponManager.vessel == vessel) - { - if (weaponManager.guardMode && weaponManager.currentTarget != null) - { - targetVessel = weaponManager.currentTarget.Vessel; - } - else - { - targetVessel = null; - } - weaponManager.AI = this; - return; - } - } - - /// - /// If guard mode is set but no target is selected, pick something - /// - protected virtual void GetGuardNonTarget() - { - if (weaponManager && weaponManager.guardMode && !targetVessel) - { - // select target based on competition style - TargetInfo potentialTarget = BDArmorySettings.DEFAULT_FFA_TARGETING ? BDATargetManager.GetClosestTargetWithBiasAndHysteresis(weaponManager) : BDATargetManager.GetLeastEngagedTarget(weaponManager); - if (potentialTarget && potentialTarget.Vessel) - { - targetVessel = potentialTarget.Vessel; - } - } - } - - /// - /// If guard mode off, and UI target is of the opposing team, set it as target - /// - protected void GetNonGuardTarget() - { - if (weaponManager != null && !weaponManager.guardMode) - { - if (weaponManager.Team.IsEnemy(vessel.targetObject?.GetVessel()?.FindPartModuleImplementing()?.Team)) - targetVessel = (Vessel)vessel.targetObject; - } - } - - /// - /// Write some text to the debug field (the one on lower left when debug labels are on), followed by a newline. - /// - /// text to write - protected void DebugLine(string text) - { - debugString.Append(text); - debugString.Append(Environment.NewLine); - } - - protected void SetStatus(string text) - { - currentStatus = text; - DebugLine(text); - } - - #endregion utilities - - #region WingCommander - - public virtual void ReleaseCommand() - { - if (!vessel || command == PilotCommands.Free) return; - if (command == PilotCommands.Follow && commandLeader) - { - commandLeader = null; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDGenericAIBase]:" + vessel.vesselName + " was released from command."); - command = PilotCommands.Free; - - assignedPositionWorld = vesselTransform.position; - } - - public virtual void CommandFollow(ModuleWingCommander leader, int followerIndex) - { - if (!pilotEnabled) return; - if (leader == vessel || followerIndex < 0) return; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDGenericAIBase]:" + vessel.vesselName + " was commanded to follow."); - command = PilotCommands.Follow; - commandLeader = leader; - commandFollowIndex = followerIndex; - } - - public virtual void CommandAG(KSPActionGroup ag) - { - if (!pilotEnabled) return; - vessel.ActionGroups.ToggleGroup(ag); - } - - public virtual void CommandFlyTo(Vector3 gpsCoords) - { - if (!pilotEnabled) return; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDGenericAIBase]:" + vessel.vesselName + " was commanded to go to."); - assignedPositionGeo = gpsCoords; - command = PilotCommands.FlyTo; - } - - public virtual void CommandAttack(Vector3 gpsCoords) - { - if (!pilotEnabled) return; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDGenericAIBase]:" + vessel.vesselName + " was commanded to attack."); - assignedPositionGeo = gpsCoords; - command = PilotCommands.Attack; - } - - public virtual void CommandTakeOff() - { - ActivatePilot(); - } - - #endregion WingCommander - } -} diff --git a/BDArmory/Modules/BDModularGuidance.cs b/BDArmory/Modules/BDModularGuidance.cs deleted file mode 100644 index 9c41271f8..000000000 --- a/BDArmory/Modules/BDModularGuidance.cs +++ /dev/null @@ -1,1427 +0,0 @@ -using System; -using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Radar; -using BDArmory.Targeting; -using BDArmory.UI; -using KSP.UI.Screens; -using SaveUpgradePipeline; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class BDModularGuidance : MissileBase - { - private bool _missileIgnited; - private int _nextStage = 1; - - private PartModule _targetDecoupler; - - private readonly Vessel _targetVessel = new Vessel(); - - private Transform _velocityTransform; - - public Vessel LegacyTargetVessel; - - private MissileFire weaponManager = null; - private bool mfChecked = false; - - private readonly List _vesselParts = new List(); - - #region KSP FIELDS - - [KSPField] - public string ForwardTransform = "ForwardNegative"; - - [KSPField] - public string UpTransform = "RightPositive"; - - [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 WeaponName; - - [KSPField(isPersistant = false, guiActive = true, guiName = "#LOC_BDArmory_GuidanceType", guiActiveEditor = true)]//Guidance Type - public string GuidanceLabel = "AGM/STS"; - - [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_TargetingMode", guiActiveEditor = true), UI_Label(affectSymCounterparts = UI_Scene.All, scene = UI_Scene.All)]//Targeting Mode - private string _targetingLabel = TargetingModes.Radar.ToString(); - - [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 - public float ActiveRadarRange = 6000; - - [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; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StagesNumber"), UI_FloatRange(minValue = 1f, maxValue = 9f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Stages Number - public float StagesNumber = 1; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StageToTriggerOnProximity"), UI_FloatRange(minValue = 0f, maxValue = 6f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Stage to Trigger On Proximity - public float StageToTriggerOnProximity = 0; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerDamping"), UI_FloatRange(minValue = 0f, maxValue = 20f, stepIncrement = .05f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Steer Damping - public float SteerDamping = 5; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerFactor"), UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = .1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Steer Factor - public float SteerMult = 10; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_RollCorrection"), UI_Toggle(controlEnabled = true, enabledText = "#LOC_BDArmory_RollCorrection_enabledText", disabledText = "#LOC_BDArmory_RollCorrection_disabledText", scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Roll Correction--Roll enabled--Roll disabled - public bool RollCorrection = false; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_TimeBetweenStages"),//Time Between Stages - UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.5f, scene = UI_Scene.Editor)] - public float timeBetweenStages = 1f; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinSpeedGuidance"),//Min Speed before guidance - UI_FloatRange(minValue = 0f, maxValue = 1000f, stepIncrement = 50f, scene = UI_Scene.Editor)] - public float MinSpeedGuidance = 200f; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ClearanceRadius", advancedTweakable = true),//Clearance radius - UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.05f, scene = UI_Scene.Editor)] - public float clearanceRadius = 0.14f; - - public override float ClearanceRadius => clearanceRadius; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ClearanceLength", advancedTweakable = true),//Clearance length - UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.05f, scene = UI_Scene.Editor)] - public float clearanceLength = 0.14f; - - public override float ClearanceLength => clearanceLength; - - private Vector3 initialMissileRollPlane; - private Vector3 initialMissileForward; - - - private bool _minSpeedAchieved = false; - private double lastRollAngle; - private double angularVelocity; - - - #endregion KSP FIELDS - - public TransformAxisVectors ForwardTransformAxis { get; set; } - public TransformAxisVectors UpTransformAxis { get; set; } - - public float Mass => (float)vessel.totalMass; - - public enum TransformAxisVectors - { - UpPositive, - UpNegative, - ForwardPositive, - ForwardNegative, - RightPositive, - RightNegative - } - - private void RefreshGuidanceMode() - { - switch (GuidanceIndex) - { - case 1: - GuidanceMode = GuidanceModes.AAMPure; - GuidanceLabel = "AAM"; - break; - - case 2: - GuidanceMode = GuidanceModes.AGM; - GuidanceLabel = "AGM/STS"; - break; - - case 3: - GuidanceMode = GuidanceModes.Cruise; - GuidanceLabel = "Cruise"; - break; - - case 4: - GuidanceMode = GuidanceModes.AGMBallistic; - GuidanceLabel = "Ballistic"; - break; - } - - if (Fields["CruiseAltitude"] != null) - { - CruiseAltitudeRange(); - Fields["CruiseAltitude"].guiActive = GuidanceMode == GuidanceModes.Cruise; - Fields["CruiseAltitude"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; - Fields["CruiseSpeed"].guiActive = GuidanceMode == GuidanceModes.Cruise; - Fields["CruiseSpeed"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; - Events["CruiseAltitudeRange"].guiActive = GuidanceMode == GuidanceModes.Cruise; - Events["CruiseAltitudeRange"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; - Fields["CruisePredictionTime"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; - } - - if (Fields["BallisticOverShootFactor"] != null) - { - Fields["BallisticOverShootFactor"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; - Fields["BallisticOverShootFactor"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; - Fields["BallisticAngle"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; - Fields["BallisticAngle"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; - } - if (Fields["SoftAscent"] != null) - { - Fields["SoftAscent"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; - Fields["SoftAscent"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; - } - Misc.Misc.RefreshAssociatedWindows(part); - } - - public override void OnFixedUpdate() - { - if (HasFired && !HasExploded) - { - UpdateGuidance(); - CheckDetonationState(); - CheckDetonationDistance(); - CheckDelayedFired(); - CheckNextStage(); - - if (isTimed && TimeIndex > detonationTime) - { - AutoDestruction(); - } - } - - if (HasExploded && StageToTriggerOnProximity == 0) - { - AutoDestruction(); - } - } - - void Update() - { - if (!HasFired) - CheckDetonationState(); - } - - private void CheckNextStage() - { - if (ShouldExecuteNextStage()) - { - if (!nextStageCountdownStart) - { - this.nextStageCountdownStart = true; - this.stageCutOfftime = Time.time; - } - else - { - if ((Time.time - stageCutOfftime) >= timeBetweenStages) - { - ExecuteNextStage(); - nextStageCountdownStart = false; - } - } - } - } - - public bool nextStageCountdownStart { get; set; } = false; - - public float stageCutOfftime { get; set; } = 0f; - - private void CheckDelayedFired() - { - if (_missileIgnited) return; - if (TimeIndex > dropTime) - { - MissileIgnition(); - } - } - - private void DisableRecursiveFlow(List children) - { - List.Enumerator child = children.GetEnumerator(); - while (child.MoveNext()) - { - if (child.Current == null) continue; - - DisablingExplosives(child.Current); - - IEnumerator resource = child.Current.Resources.GetEnumerator(); - while (resource.MoveNext()) - { - if (resource.Current == null) continue; - if (resource.Current.flowState) - { - resource.Current.flowState = false; - } - } - resource.Dispose(); - - if (child.Current.children.Count > 0) - { - DisableRecursiveFlow(child.Current.children); - } - if (!_vesselParts.Contains(child.Current)) _vesselParts.Add(child.Current); - } - child.Dispose(); - } - - private void EnableResourceFlow(List children) - { - List.Enumerator child = children.GetEnumerator(); - while (child.MoveNext()) - { - if (child.Current == null) continue; - - SetupExplosive(child.Current); - - IEnumerator resource = child.Current.Resources.GetEnumerator(); - while (resource.MoveNext()) - { - if (resource.Current == null) continue; - if (!resource.Current.flowState) - { - resource.Current.flowState = true; - } - } - resource.Dispose(); - if (child.Current.children.Count > 0) - { - EnableResourceFlow(child.Current.children); - } - } - child.Dispose(); - } - - private void DisableResourcesFlow() - { - if (_targetDecoupler != null) - { - if (_targetDecoupler.part.children.Count == 0) return; - _vesselParts.Clear(); - DisableRecursiveFlow(_targetDecoupler.part.children); - } - } - - private void MissileIgnition() - { - EnableResourceFlow(_vesselParts); - GameObject velocityObject = new GameObject("velObject"); - velocityObject.transform.position = vessel.transform.position; - velocityObject.transform.parent = vessel.transform; - _velocityTransform = velocityObject.transform; - - MissileState = MissileStates.Boost; - - ExecuteNextStage(); - - MissileState = MissileStates.Cruise; - - _missileIgnited = true; - RadarWarningReceiver.WarnMissileLaunch(MissileReferenceTransform.position, GetForwardTransform()); - } - - private bool ShouldExecuteNextStage() - { - if (!_missileIgnited) return false; - if (TimeIndex < 1) return false; - - // Replaced Linq expression... - using (List.Enumerator parts = vessel.parts.GetEnumerator()) - while (parts.MoveNext()) - { - if (parts.Current == null || !IsEngine(parts.Current)) continue; - if (EngineIgnitedAndHasFuel(parts.Current)) - { - return false; - } - } - - //If the next stage is greater than the number defined of stages the missile is done - if (_nextStage > StagesNumber) - { - MissileState = MissileStates.PostThrust; - return false; - } - - return true; - } - - public bool IsEngine(Part p) - { - using (List.Enumerator m = p.Modules.GetEnumerator()) - while (m.MoveNext()) - { - if (m.Current == null) continue; - if (m.Current is ModuleEngines) return true; - } - return false; - } - - public static bool EngineIgnitedAndHasFuel(Part p) - { - using (List.Enumerator m = p.Modules.GetEnumerator()) - while (m.MoveNext()) - { - PartModule pm = m.Current; - ModuleEngines eng = pm as ModuleEngines; - if (eng != null) - { - return (eng.EngineIgnited && (!eng.getFlameoutState || eng.flameoutBar == 0 || eng.status == "Nominal")); - } - } - return false; - } - - public override void OnStart(StartState state) - { - base.OnStart(state); - SetupsFields(); - - if (string.IsNullOrEmpty(GetShortName())) - { - shortName = "Unnamed"; - } - part.force_activate(); - RefreshGuidanceMode(); - - UpdateTargetingMode((TargetingModes)Enum.Parse(typeof(TargetingModes), _targetingLabel)); - - _targetDecoupler = FindFirstDecoupler(part.parent, null); - - DisableResourcesFlow(); - - weaponClass = WeaponClasses.Missile; - WeaponName = GetShortName(); - - activeRadarRange = ActiveRadarRange; - - //TODO: BDModularGuidance should be configurable? - heatThreshold = 50; - lockedSensorFOV = 5; - radarLOAL = true; - - // fill activeRadarLockTrackCurve with default values if not set by part config: - if ((TargetingMode == TargetingModes.Radar || TargetingModeTerminal == TargetingModes.Radar) && activeRadarRange > 0 && activeRadarLockTrackCurve.minTime == float.MaxValue) - { - activeRadarLockTrackCurve.Add(0f, 0f); - activeRadarLockTrackCurve.Add(activeRadarRange, RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); // TODO: tune & balance constants! - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDModularGuidance]: OnStart missile " + shortName + ": setting default locktrackcurve with maxrange/minrcs: " + activeRadarLockTrackCurve.maxTime + "/" + RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); - } - - } - - private void SetupsFields() - { - Events["HideUI"].active = false; - Events["ShowUI"].active = true; - - if (isTimed) - { - Fields["detonationTime"].guiActive = true; - Fields["detonationTime"].guiActiveEditor = true; - } - else - { - Fields["detonationTime"].guiActive = false; - Fields["detonationTime"].guiActiveEditor = false; - } - - if (HighLogic.LoadedSceneIsEditor) - { - WeaponNameWindow.OnActionGroupEditorOpened.Add(OnActionGroupEditorOpened); - WeaponNameWindow.OnActionGroupEditorClosed.Add(OnActionGroupEditorClosed); - Fields["CruiseAltitude"].guiActiveEditor = true; - Fields["CruiseSpeed"].guiActiveEditor = false; - Events["SwitchTargetingMode"].guiActiveEditor = true; - Events["SwitchGuidanceMode"].guiActiveEditor = true; - } - else - { - Fields["CruiseAltitude"].guiActiveEditor = false; - Fields["CruiseSpeed"].guiActiveEditor = false; - Events["SwitchTargetingMode"].guiActiveEditor = false; - Events["SwitchGuidanceMode"].guiActiveEditor = false; - SetMissileTransform(); - } - - UI_FloatRange staticMin = (UI_FloatRange)Fields["minStaticLaunchRange"].uiControlEditor; - UI_FloatRange staticMax = (UI_FloatRange)Fields["maxStaticLaunchRange"].uiControlEditor; - UI_FloatRange radarMax = (UI_FloatRange)Fields["ActiveRadarRange"].uiControlEditor; - - staticMin.onFieldChanged += OnStaticRangeUpdated; - staticMax.onFieldChanged += OnStaticRangeUpdated; - staticMax.maxValue = BDArmorySettings.MAX_ENGAGEMENT_RANGE; - staticMax.stepIncrement = BDArmorySettings.MAX_ENGAGEMENT_RANGE / 100; - radarMax.maxValue = BDArmorySettings.MAX_ENGAGEMENT_RANGE; - radarMax.stepIncrement = BDArmorySettings.MAX_ENGAGEMENT_RANGE / 100; - - UI_FloatRange stageOnProximity = (UI_FloatRange)Fields["StageToTriggerOnProximity"].uiControlEditor; - stageOnProximity.onFieldChanged = OnStageOnProximity; - - OnStageOnProximity(Fields["StageToTriggerOnProximity"], null); - InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); - } - - private void OnStageOnProximity(BaseField baseField, object o) - { - UI_FloatRange detonationDistance = (UI_FloatRange)Fields["DetonationDistance"].uiControlEditor; - - if (StageToTriggerOnProximity != 0) - { - detonationDistance = (UI_FloatRange)Fields["DetonationDistance"].uiControlEditor; - - detonationDistance.maxValue = 8000; - - detonationDistance.stepIncrement = 50; - } - else - { - detonationDistance.maxValue = 100; - - detonationDistance.stepIncrement = 1; - } - } - - private void OnStaticRangeUpdated(BaseField baseField, object o) - { - InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); - } - - private void UpdateTargetingMode(TargetingModes newTargetingMode) - { - if (newTargetingMode == TargetingModes.Radar) - { - Fields["ActiveRadarRange"].guiActive = true; - Fields["ActiveRadarRange"].guiActiveEditor = true; - } - else - { - Fields["ActiveRadarRange"].guiActive = false; - Fields["ActiveRadarRange"].guiActiveEditor = false; - } - TargetingMode = newTargetingMode; - _targetingLabel = newTargetingMode.ToString(); - - Misc.Misc.RefreshAssociatedWindows(part); - } - - private void OnDestroy() - { - WeaponNameWindow.OnActionGroupEditorOpened.Remove(OnActionGroupEditorOpened); - WeaponNameWindow.OnActionGroupEditorClosed.Remove(OnActionGroupEditorClosed); - GameEvents.onPartDie.Remove(PartDie); - } - - private void SetMissileTransform() - { - MissileReferenceTransform = part.transform; - ForwardTransformAxis = (TransformAxisVectors)Enum.Parse(typeof(TransformAxisVectors), ForwardTransform); - UpTransformAxis = (TransformAxisVectors)Enum.Parse(typeof(TransformAxisVectors), UpTransform); - } - - void UpdateGuidance() - { - if (guidanceActive) - { - switch (TargetingMode) - { - case TargetingModes.None: - if (_targetVessel != null) - { - TargetPosition = _targetVessel.CurrentCoM; - TargetVelocity = _targetVessel.Velocity(); - TargetAcceleration = _targetVessel.acceleration; - } - break; - - case TargetingModes.Radar: - UpdateRadarTarget(); - break; - - case TargetingModes.Heat: - UpdateHeatTarget(); - break; - - case TargetingModes.Laser: - UpdateLaserTarget(); - break; - - case TargetingModes.Gps: - UpdateGPSTarget(); - break; - - case TargetingModes.AntiRad: - UpdateAntiRadiationTarget(); - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - private Vector3 AAMGuidance() - { - Vector3 aamTarget; - if (TargetAcquired) - { - float timeToImpact; - 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("[BDModularGuidance]: Missile with Name={0} has exceeded the max off boresight, checking missed target ", vessel.vesselName); - aamTarget = TargetPosition; - } - DrawDebugLine(vessel.CoM, aamTarget); - } - else - { - aamTarget = vessel.CoM + (20 * vessel.srfSpeed * vessel.Velocity().normalized); - } - - return aamTarget; - } - - private Vector3 AGMGuidance() - { - if (TargetingMode != TargetingModes.Gps) - { - if (TargetAcquired) - { - //lose lock if seeker reaches gimbal limit - float targetViewAngle = Vector3.Angle(vessel.transform.forward, TargetPosition - vessel.CoM); - - if (targetViewAngle > maxOffBoresight) - { - Debug.Log("[BDModularGuidance]: AGM Missile guidance failed - target out of view"); - guidanceActive = false; - } - } - else - { - if (TargetingMode == TargetingModes.Laser) - { - //keep going straight until found laser point - TargetPosition = laserStartPosition + (20000 * startDirection); - } - } - } - Vector3 agmTarget = MissileGuidance.GetAirToGroundTarget(TargetPosition, vessel, 1.85f); - return agmTarget; - } - - private Vector3 CruiseGuidance() - { - if (this._guidance == null) - { - this._guidance = new CruiseGuidance(this); - } - - return this._guidance.GetDirection(this, TargetPosition); - } - - private void CheckMiss(Vector3 targetPosition) - { - if (HasMissed) 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 ((vessel.CoM - targetPosition).sqrMagnitude > - (vessel.CoM + (vessel.Velocity() * Time.fixedDeltaTime) - (targetPosition + (TargetVelocity * Time.fixedDeltaTime))).sqrMagnitude) return; - - if (MissileState != MissileStates.PostThrust) return; - - Debug.Log("[BDModularGuidance]: Missile CheckMiss showed miss for " + vessel.vesselName + " with target at " + (targetPosition - vessel.CoM).ToString("0.0")); - - var pilotAI = vessel.FindPartModuleImplementing(); // Get the pilot AI if the missile has one. - if (pilotAI != null) - { - ResetMissile(); - pilotAI.ActivatePilot(); - return; - } - - HasMissed = true; - guidanceActive = false; - TargetMf = null; - isTimed = true; - detonationTime = TimeIndex + 1.5f; - } - - private void ResetMissile() - { - Debug.Log("[BDModularGuidance]: Resetting missile " + vessel.vesselName); - heatTarget = TargetSignatureData.noTarget; - vrd = null; - radarTarget = TargetSignatureData.noTarget; - HasFired = false; - StagesNumber = 1; - _nextStage = 1; - TargetAcquired = false; - TargetMf = null; - TimeFired = -1; - _missileIgnited = false; - lockFailTimer = -1; - guidanceActive = false; - HasMissed = false; - HasExploded = false; - DetonationDistanceState = DetonationDistanceStates.Cruising; - BDATargetManager.FiredMissiles.Remove(this); - MissileState = MissileStates.Idle; - if (mfChecked && weaponManager != null) - { - Debug.Log("[BDModularGuidance]: disabling target lock for " + vessel.vesselName); - weaponManager.guardFiringMissile = false; // Disable target lock. - mfChecked = false; - } - } - - private void CheckMiss() - { - if (HasMissed) return; - - if (MissileState == MissileStates.PostThrust && (vessel.LandedOrSplashed || vessel.Velocity().magnitude < 10f)) - { - Debug.Log("[BDModularGuidance]: Missile CheckMiss showed miss for " + vessel.vesselName); - - var pilotAI = vessel.FindPartModuleImplementing(); // Get the pilot AI if the missile has one. - if (pilotAI != null) - { - ResetMissile(); - pilotAI.ActivatePilot(); - return; - } - - HasMissed = true; - guidanceActive = false; - TargetMf = null; - isTimed = true; - detonationTime = TimeIndex + 1.5f; - } - } - - - public void GuidanceSteer(FlightCtrlState s) - { - debugString.Length = 0; - if (guidanceActive && MissileReferenceTransform != null && _velocityTransform != null) - { - if (!mfChecked) - { - weaponManager = vessel.FindPartModuleImplementing(); - mfChecked = true; - } - if (mfChecked && weaponManager != null && !weaponManager.guardFiringMissile) - { - Debug.Log("[BDModularGuidance]: enabling target lock for " + vessel.vesselName); - weaponManager.guardFiringMissile = true; // Enable target lock. - } - - if (vessel.Velocity().magnitude < MinSpeedGuidance) - { - if (!_minSpeedAchieved) - { - s.mainThrottle = 1; - return; - } - } - else - { - _minSpeedAchieved = true; - } - - Vector3 newTargetPosition = new Vector3(); - switch (GuidanceIndex) - { - case 1: - newTargetPosition = AAMGuidance(); - break; - - case 2: - newTargetPosition = AGMGuidance(); - break; - - case 3: - newTargetPosition = CruiseGuidance(); - break; - - case 4: - newTargetPosition = BallisticGuidance(); - break; - } - CheckMiss(newTargetPosition); - - //Updating aero surfaces - if (TimeIndex > dropTime + 0.5f) - { - _velocityTransform.rotation = Quaternion.LookRotation(vessel.Velocity(), -vessel.transform.forward); - Vector3 targetDirection = _velocityTransform.InverseTransformPoint(newTargetPosition).normalized; - targetDirection = Vector3.RotateTowards(Vector3.forward, targetDirection, 15 * Mathf.Deg2Rad, 0); - - Vector3 localAngVel = vessel.angularVelocity; - float steerYaw = SteerMult * targetDirection.x - SteerDamping * -localAngVel.z; - float steerPitch = SteerMult * targetDirection.y - SteerDamping * -localAngVel.x; - - s.yaw = Mathf.Clamp(steerYaw, -MaxSteer, MaxSteer); - s.pitch = Mathf.Clamp(steerPitch, -MaxSteer, MaxSteer); - - if (RollCorrection) - { - SetRoll(); - s.roll = Roll; - } - } - s.mainThrottle = Throttle; - - CheckMiss(); - } - } - - private void SetRoll() - { - var vesselTransform = vessel.transform.position; - - Vector3 gravityVector = FlightGlobals.getGeeForceAtPosition(vesselTransform).normalized; - Vector3 rollVessel = -vessel.transform.right.normalized; - - var currentAngle = Vector3.SignedAngle(rollVessel, gravityVector, Vector3.Cross(rollVessel, gravityVector)) - 90f; - - debugString.Append($"Roll angle: {currentAngle}"); - debugString.Append(Environment.NewLine); - this.angularVelocity = currentAngle - this.lastRollAngle; - //this.angularAcceleration = angularVelocity - this.lasAngularVelocity; - - var futureAngle = currentAngle + angularVelocity / Time.fixedDeltaTime * 1f; - - debugString.Append($"future Roll angle: {futureAngle}"); - - if (futureAngle > 0.5f || currentAngle > 0.5f) - { - this.Roll = Mathf.Clamp(Roll - 0.001f, -1f, 0f); - } - else if (futureAngle < -0.5f || currentAngle < -0.5f) - { - this.Roll = Mathf.Clamp(Roll + 0.001f, 0, 1f); - } - debugString.Append($"Roll value: {this.Roll}"); - - lastRollAngle = currentAngle; - //lasAngularVelocity = angularVelocity; - } - - public float Roll { get; set; } - - private Vector3 BallisticGuidance() - { - return CalculateAGMBallisticGuidance(this, TargetPosition); - } - - private void UpdateMenus(bool visible) - { - Events["HideUI"].active = visible; - Events["ShowUI"].active = !visible; - } - - private void OnActionGroupEditorOpened() - { - Events["HideUI"].active = false; - Events["ShowUI"].active = false; - } - - private void OnActionGroupEditorClosed() - { - Events["HideUI"].active = false; - Events["ShowUI"].active = true; - } - - /// - /// Recursive method to find the top decoupler that should be used to jettison the missile. - /// - /// - /// - /// - private PartModule FindFirstDecoupler(Part parent, PartModule last) - { - if (parent == null || !parent) return last; - - PartModule newModuleDecouple = parent.FindModuleImplementing(); - if (newModuleDecouple == null) - { - newModuleDecouple = parent.FindModuleImplementing(); - } - if (newModuleDecouple != null && newModuleDecouple) - { - return FindFirstDecoupler(parent.parent, newModuleDecouple); - } - return FindFirstDecoupler(parent.parent, last); - } - - /// - /// This method will execute the next ActionGroup. Due to StageManager is designed to work with an active vessel - /// And a missile is not an active vessel. I had to use a different way handle stages. And action groups works perfect! - /// - public void ExecuteNextStage() - { - Debug.LogFormat("[BDModularGuidance]: Executing next stage {0} for {1}", _nextStage, vessel.vesselName); - vessel.ActionGroups.ToggleGroup( - (KSPActionGroup)Enum.Parse(typeof(KSPActionGroup), "Custom0" + (int)_nextStage)); - - _nextStage++; - - vessel.OnFlyByWire += GuidanceSteer; - - //todo: find a way to fly by wire vessel decoupled - } - - void OnGUI() - { - if (HighLogic.LoadedSceneIsFlight) - { - drawLabels(); - } - } - - #region KSP ACTIONS - - [KSPAction("Fire Missile")] - public void AgFire(KSPActionParam param) - { - FireMissile(); - } - - /// - /// Reset the missile if it has a pilot AI. - /// - [KSPAction("Reset Missile")] - public void AGReset(KSPActionParam param) - { - var pilotAI = vessel.FindPartModuleImplementing(); // Get the pilot AI if the missile has one. - if (pilotAI != null) - { - ResetMissile(); - pilotAI.ActivatePilot(); - } - } - - #endregion KSP ACTIONS - - #region KSP EVENTS - - [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_FireMissile", active = true)]//Fire Missile - public void GuiFire() - { - FireMissile(); - } - - [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_FireMissile", active = true)]//Fire Missile - public override void FireMissile() - { - if (BDArmorySetup.Instance.ActiveWeaponManager != null && - BDArmorySetup.Instance.ActiveWeaponManager.vessel == vessel) - { - BDArmorySetup.Instance.ActiveWeaponManager.SendTargetDataToMissile(this); - } - - if (!HasFired) - { - GameEvents.onPartDie.Add(PartDie); - BDATargetManager.FiredMissiles.Add(this); - - List.Enumerator wpm = vessel.FindPartModulesImplementing().GetEnumerator(); - while (wpm.MoveNext()) - { - if (wpm.Current == null) continue; - Team = wpm.Current.Team; - break; - } - wpm.Dispose(); - - SourceVessel = vessel; - SetTargeting(); - Jettison(); - AddTargetInfoToVessel(); - IncreaseTolerance(); - - this.initialMissileRollPlane = -this.vessel.transform.up; - this.initialMissileForward = this.vessel.transform.forward; - vessel.vesselName = GetShortName(); - vessel.vesselType = VesselType.Plane; - - if (!vessel.ActionGroups[KSPActionGroup.SAS]) - { - vessel.ActionGroups.ToggleGroup(KSPActionGroup.SAS); - } - - TimeFired = Time.time; - guidanceActive = true; - MissileState = MissileStates.Drop; - - Misc.Misc.RefreshAssociatedWindows(part); - - HasFired = true; - DetonationDistanceState = DetonationDistanceStates.NotSafe; - } - if (BDArmorySetup.Instance.ActiveWeaponManager != null) - { - BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); - } - } - - private void IncreaseTolerance() - { - foreach (var vesselPart in this.vessel.parts) - { - vesselPart.crashTolerance = 99; - vesselPart.breakingForce = 99; - vesselPart.breakingTorque = 99; - } - } - - private void SetTargeting() - { - startDirection = GetForwardTransform(); - SetLaserTargeting(); - SetAntiRadTargeting(); - } - - void OnDisable() - { - if (TargetingMode == TargetingModes.AntiRad) - { - RadarWarningReceiver.OnRadarPing -= ReceiveRadarPing; - } - } - - public Vector3 StartDirection { get; set; } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_GuidanceMode", active = true)]//Guidance Mode - public void SwitchGuidanceMode() - { - GuidanceIndex++; - if (GuidanceIndex > 4) - { - GuidanceIndex = 1; - } - - RefreshGuidanceMode(); - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetingMode", active = true)]//Targeting Mode - public void SwitchTargetingMode() - { - string[] targetingModes = Enum.GetNames(typeof(TargetingModes)); - - int currentIndex = targetingModes.IndexOf(TargetingMode.ToString()); - - if (currentIndex < targetingModes.Length - 1) - { - UpdateTargetingMode((TargetingModes)Enum.Parse(typeof(TargetingModes), targetingModes[currentIndex + 1])); - } - else - { - UpdateTargetingMode((TargetingModes)Enum.Parse(typeof(TargetingModes), targetingModes[0])); - } - } - - [KSPEvent(guiActive = true, guiActiveEditor = false, active = true, guiName = "#LOC_BDArmory_Jettison")]//Jettison - public override void Jettison() - { - if (_targetDecoupler == null || !_targetDecoupler || !(_targetDecoupler is IStageSeparator)) return; - - ModuleDecouple decouple = _targetDecoupler as ModuleDecouple; - if (decouple != null) - { - decouple.ejectionForce *= 5; - decouple.Decouple(); - } - else - { - ((ModuleAnchoredDecoupler)_targetDecoupler).ejectionForce *= 5; - ((ModuleAnchoredDecoupler)_targetDecoupler).Decouple(); - } - - if (BDArmorySetup.Instance.ActiveWeaponManager != null) - BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); - } - - public override float GetBlastRadius() - { - if (vessel.FindPartModulesImplementing().Count > 0) - { - return vessel.FindPartModulesImplementing().Max(x => x.blastRadius); - } - else - { - return 5; - } - } - - protected override void PartDie(Part p) - { - if (p != part) return; - AutoDestruction(); - BDATargetManager.FiredMissiles.Remove(this); - GameEvents.onPartDie.Remove(PartDie); - } - - private void AutoDestruction() - { - var parts = this.vessel.Parts.ToArray(); - for (int i = parts.Length - 1; i >= 0; i--) - { - parts[i]?.explode(); - } - - parts = null; - } - - public override void Detonate() - { - if (HasExploded || !HasFired) return; - if (SourceVessel == null) SourceVessel = vessel; - - if (StageToTriggerOnProximity != 0) - { - vessel.ActionGroups.ToggleGroup((KSPActionGroup)Enum.Parse(typeof(KSPActionGroup), "Custom0" + (int)StageToTriggerOnProximity)); - HasExploded = true; - } - else - { - vessel.FindPartModulesImplementing().ForEach(explosivePart => { if (!explosivePart.manualOverride) explosivePart.DetonateIfPossible(); }); - if (vessel.FindPartModulesImplementing().Any(explosivePart => explosivePart.hasDetonated)) - { - HasExploded = true; - AutoDestruction(); - } - } - } - - public override Vector3 GetForwardTransform() - { - return GetTransform(ForwardTransformAxis); - } - - public Vector3 GetTransform(TransformAxisVectors transformAxis) - { - switch (transformAxis) - { - case TransformAxisVectors.UpPositive: - return MissileReferenceTransform.up; - - case TransformAxisVectors.UpNegative: - return -MissileReferenceTransform.up; - - case TransformAxisVectors.ForwardPositive: - return MissileReferenceTransform.forward; - - case TransformAxisVectors.ForwardNegative: - return -MissileReferenceTransform.forward; - - case TransformAxisVectors.RightNegative: - return -MissileReferenceTransform.right; - - case TransformAxisVectors.RightPositive: - return MissileReferenceTransform.right; - - default: - return MissileReferenceTransform.forward; - } - } - - [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_HideUI", active = false)]//Hide Weapon Name UI - public void HideUI() - { - WeaponNameWindow.HideGUI(); - UpdateMenus(false); - } - - [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_ShowUI", active = false)]//Set Weapon Name UI - public void ShowUI() - { - WeaponNameWindow.ShowGUI(this); - UpdateMenus(true); - } - - void OnCollisionEnter(Collision col) - { - base.CollisionEnter(col); - } - - #endregion KSP EVENTS - } - - #region UI - - [KSPAddon(KSPAddon.Startup.EditorAny, false)] - public class WeaponNameWindow : MonoBehaviour - { - internal static EventVoid OnActionGroupEditorOpened = new EventVoid("OnActionGroupEditorOpened"); - internal static EventVoid OnActionGroupEditorClosed = new EventVoid("OnActionGroupEditorClosed"); - - private static GUIStyle unchanged; - private static GUIStyle changed; - private static GUIStyle greyed; - private static GUIStyle overfull; - - private static WeaponNameWindow instance; - private static Vector3 mousePos = Vector3.zero; - - private bool ActionGroupMode; - - private Rect guiWindowRect = new Rect(0, 0, 0, 0); - - private BDModularGuidance missile_module; - - [KSPField] public int offsetGUIPos = -1; - - 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 - - private bool styleSetup; - - private string txtName = string.Empty; - - public static void HideGUI() - { - if (instance != null && instance.missile_module != null) - { - instance.missile_module.WeaponName = instance.missile_module.shortName; - instance.missile_module = null; - instance.UpdateGUIState(); - } - EditorLogic editor = EditorLogic.fetch; - if (editor != null) - editor.Unlock("BD_MN_GUILock"); - } - - public static void ShowGUI(BDModularGuidance missile_module) - { - if (instance != null) - { - instance.missile_module = missile_module; - instance.UpdateGUIState(); - } - } - - private void UpdateGUIState() - { - enabled = missile_module != null; - EditorLogic editor = EditorLogic.fetch; - if (!enabled && editor != null) - editor.Unlock("BD_MN_GUILock"); - } - - private IEnumerator CheckActionGroupEditor() - { - while (EditorLogic.fetch == null) - { - yield return null; - } - EditorLogic editor = EditorLogic.fetch; - while (EditorLogic.fetch != null) - { - if (editor.editorScreen == EditorScreen.Actions) - { - if (!ActionGroupMode) - { - HideGUI(); - OnActionGroupEditorOpened.Fire(); - } - EditorActionGroups age = EditorActionGroups.Instance; - if (missile_module && !age.GetSelectedParts().Contains(missile_module.part)) - { - HideGUI(); - } - ActionGroupMode = true; - } - else - { - if (ActionGroupMode) - { - HideGUI(); - OnActionGroupEditorClosed.Fire(); - } - ActionGroupMode = false; - } - yield return null; - } - } - - private void Awake() - { - enabled = false; - instance = this; - StartCoroutine(CheckActionGroupEditor()); - } - - private void OnDestroy() - { - instance = null; - } - - public void OnGUI() - { - if (!styleSetup) - { - styleSetup = true; - Styles.InitStyles(); - } - - EditorLogic editor = EditorLogic.fetch; - if (!HighLogic.LoadedSceneIsEditor || !editor) - { - return; - } - bool cursorInGUI = false; // nicked the locking code from Ferram - mousePos = Input.mousePosition; //Mouse location; based on Kerbal Engineer Redux code - mousePos.y = Screen.height - mousePos.y; - - int posMult = 0; - if (offsetGUIPos != -1) - { - posMult = offsetGUIPos; - } - if (ActionGroupMode) - { - if (guiWindowRect.width == 0) - { - guiWindowRect = new Rect(430 * posMult, 365, 438, 50); - } - new Rect(guiWindowRect.xMin + 440, mousePos.y - 5, 300, 20); - } - else - { - if (guiWindowRect.width == 0) - { - //guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, (Screen.height - 365)); - guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, 50); - } - new Rect(guiWindowRect.xMin - (230 - 8), mousePos.y - 5, 220, 20); - } - cursorInGUI = guiWindowRect.Contains(mousePos); - if (cursorInGUI) - { - editor.Lock(false, false, false, "BD_MN_GUILock"); - //if (EditorTooltip.Instance != null) - // EditorTooltip.Instance.HideToolTip(); - } - else - { - editor.Unlock("BD_MN_GUILock"); - } - guiWindowRect = GUILayout.Window(GetInstanceID(), guiWindowRect, GUIWindow, "Weapon Name GUI", Styles.styleEditorPanel); - } - - public void GUIWindow(int windowID) - { - InitializeStyles(); - - GUILayout.BeginVertical(); - GUILayout.Space(20); - - GUILayout.BeginHorizontal(); - - GUILayout.Label("Weapon Name: "); - - txtName = GUILayout.TextField(txtName); - - if (GUILayout.Button("Save & Close")) - { - missile_module.WeaponName = txtName; - missile_module.shortName = txtName; - instance.missile_module.HideUI(); - } - - GUILayout.EndHorizontal(); - - scrollPos = GUILayout.BeginScrollView(scrollPos); - - GUILayout.EndScrollView(); - - GUILayout.EndVertical(); - - GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref guiWindowRect); - } - - private static void InitializeStyles() - { - if (unchanged == null) - { - if (GUI.skin == null) - { - unchanged = new GUIStyle(); - changed = new GUIStyle(); - greyed = new GUIStyle(); - overfull = new GUIStyle(); - } - else - { - unchanged = new GUIStyle(GUI.skin.textField); - changed = new GUIStyle(GUI.skin.textField); - greyed = new GUIStyle(GUI.skin.textField); - overfull = new GUIStyle(GUI.skin.label); - } - - unchanged.normal.textColor = Color.white; - unchanged.active.textColor = Color.white; - unchanged.focused.textColor = Color.white; - unchanged.hover.textColor = Color.white; - - changed.normal.textColor = Color.yellow; - changed.active.textColor = Color.yellow; - changed.focused.textColor = Color.yellow; - changed.hover.textColor = Color.yellow; - - greyed.normal.textColor = Color.gray; - - overfull.normal.textColor = Color.red; - } - } - } - - internal class Styles - { - // Base styles - public static GUIStyle styleEditorTooltip; - public static GUIStyle styleEditorPanel; - - /// - /// This one sets up the styles we use - /// - internal static void InitStyles() - { - styleEditorTooltip = new GUIStyle(); - styleEditorTooltip.name = "Tooltip"; - styleEditorTooltip.fontSize = 12; - styleEditorTooltip.normal.textColor = new Color32(207, 207, 207, 255); - styleEditorTooltip.stretchHeight = true; - styleEditorTooltip.wordWrap = true; - styleEditorTooltip.normal.background = CreateColorPixel(new Color32(7, 54, 66, 200)); - styleEditorTooltip.border = new RectOffset(3, 3, 3, 3); - styleEditorTooltip.padding = new RectOffset(4, 4, 6, 4); - styleEditorTooltip.alignment = TextAnchor.MiddleLeft; - - styleEditorPanel = new GUIStyle(); - styleEditorPanel.normal.background = CreateColorPixel(new Color32(7, 54, 66, 200)); - styleEditorPanel.border = new RectOffset(27, 27, 27, 27); - styleEditorPanel.padding = new RectOffset(10, 10, 10, 10); - styleEditorPanel.normal.textColor = new Color32(147, 161, 161, 255); - styleEditorPanel.fontSize = 12; - } - - /// - /// Creates a 1x1 texture - /// - /// Color of the texture - /// - internal static Texture2D CreateColorPixel(Color32 Background) - { - Texture2D retTex = new Texture2D(1, 1); - retTex.SetPixel(0, 0, Background); - retTex.Apply(); - return retTex; - } - } - - #endregion UI -} diff --git a/BDArmory/Modules/BDModulePilotAI.cs b/BDArmory/Modules/BDModulePilotAI.cs deleted file mode 100644 index 5425fed25..000000000 --- a/BDArmory/Modules/BDModulePilotAI.cs +++ /dev/null @@ -1,2609 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Targeting; -using BDArmory.UI; -using Expansions.Missions; -using KSP.UI.Screens; -using Smooth.Algebraics; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class BDModulePilotAI : BDGenericAIBase, IBDAIControl - { - public enum SteerModes - { NormalFlight, Aiming } - - SteerModes steerMode = SteerModes.NormalFlight; - - bool extending; - double startedExtendingAt = 0; - // string extendingReason = ""; - - bool requestedExtend; - Vector3 requestedExtendTpos; - - public bool IsExtending - { - get { return extending || requestedExtend; } - } - - public void StopExtending() - { - extending = false; - // extendingReason = ""; - startedExtendingAt = 0; - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG Stop extending due to request"); - } - - public void RequestExtend(Vector3 tPosition) - { - requestedExtend = true; - requestedExtendTpos = tPosition; - } - - public override bool CanEngage() - { - return !vessel.LandedOrSplashed; - } - - GameObject vobj; - - Transform velocityTransform - { - get - { - if (!vobj) - { - vobj = new GameObject("velObject"); - vobj.transform.position = vessel.ReferenceTransform.position; - vobj.transform.parent = vessel.ReferenceTransform; - } - - return vobj.transform; - } - } - - Vector3 upDirection = Vector3.up; - - #region Pilot AI Settings GUI - - [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 = 6.6f; - //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), - UI_FloatRange(minValue = 0.01f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] - public float steerKiAdjust = 0.25f; - - [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)] - public float steerDamping = 2.5f; - - #region Dynamic Damping - // 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)] - public string DynamicDampingLabel = ""; - - [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)] - public float DynamicDampingMin = 1.5f; - - [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)] - public float DynamicDampingMax = 4f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicDampingFactor", advancedTweakable = true, - 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 dynamicSteerDampingFactor = 7f; - - // Dynamic Pitch - [KSPField(guiName = "#LOC_BDArmory_DynamicDampingPitch", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] - public string PitchLabel = ""; - - [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingPitch", advancedTweakable = true, - groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_Toggle(scene = UI_Scene.All, enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] - public bool dynamicDampingPitch = true; - - [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)] - public float DynamicDampingPitchMin = 1.5f; - - [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)] - public float DynamicDampingPitchMax = 4f; - - [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingPitchFactor", advancedTweakable = true, - 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 dynamicSteerDampingPitchFactor = 7f; - - // Dynamic Yaw - [KSPField(guiName = "#LOC_BDArmory_DynamicDampingYaw", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] - public string YawLabel = ""; - - [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingYaw", advancedTweakable = true, - groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_Toggle(scene = UI_Scene.All, enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] - public bool dynamicDampingYaw = true; - - [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)] - public float DynamicDampingYawMin = 1.5f; - - [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)] - public float DynamicDampingYawMax = 4f; - - [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingYawFactor", advancedTweakable = true, - 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 dynamicSteerDampingYawFactor = 7f; - - // Dynamic Roll - [KSPField(guiName = "#LOC_BDArmory_DynamicDampingRoll", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] - public string RollLabel = ""; - - [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingRoll", advancedTweakable = true, - groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_Toggle(scene = UI_Scene.All, enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] - public bool dynamicDampingRoll = true; - - [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)] - public float DynamicDampingRollMin = 1.5f; - - [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)] - public float DynamicDampingRollMax = 4f; - - [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 = 7f; - - //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 = false; - #endregion - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DefaultAltitude", //Default Alt. - groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_PilotAI_Altitudes", groupStartCollapsed = true), - UI_FloatRange(minValue = 150f, maxValue = 15000f, stepIncrement = 25f, scene = UI_Scene.All)] - public float defaultAltitude = 1500; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinAltitude", //Min Altitude - groupName = "pilotAI_Altitudes", groupDisplayName = "#LOC_BDArmory_PilotAI_Altitudes", groupStartCollapsed = true), - UI_FloatRange(minValue = 25f, maxValue = 6000, stepIncrement = 25f, scene = UI_Scene.All)] - public float minAltitude = 500f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxSpeed", //Max Speed - groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_PilotAI_Speeds", groupStartCollapsed = true), - UI_FloatRange(minValue = 20f, maxValue = 800f, stepIncrement = 1.0f, scene = UI_Scene.All)] - public float maxSpeed = 325; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TakeOffSpeed", //TakeOff Speed - groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_PilotAI_Speeds", groupStartCollapsed = true), - UI_FloatRange(minValue = 10f, maxValue = 200f, stepIncrement = 1.0f, scene = UI_Scene.All)] - public float takeOffSpeed = 70; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinSpeed", //MinCombatSpeed - groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_PilotAI_Speeds", groupStartCollapsed = true), - UI_FloatRange(minValue = 10f, maxValue = 200, stepIncrement = 1.0f, scene = UI_Scene.All)] - public float minSpeed = 60f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_IdleSpeed", //Idle Speed - groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_PilotAI_Speeds", groupStartCollapsed = true), - UI_FloatRange(minValue = 10f, maxValue = 200f, stepIncrement = 1.0f, scene = UI_Scene.All)] - public float idleSpeed = 120f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerLimiter", advancedTweakable = true, //Steer Limiter - groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), - UI_FloatRange(minValue = .1f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.All)] - public float maxSteer = 1; - - //[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)] - //public float maxAttitude = 90f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BankLimiter", advancedTweakable = true, //Bank Angle Limiter - groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), - UI_FloatRange(minValue = 10f, maxValue = 180f, stepIncrement = 5f, scene = UI_Scene.All)] - public float maxBank = 180f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_maxAllowedGForce", //Max G - groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), - UI_FloatRange(minValue = 2f, maxValue = 45f, stepIncrement = 0.25f, scene = UI_Scene.All)] - public float maxAllowedGForce = 10; - - [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)] - public float maxAllowedAoA = 35; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinEvasionTime", advancedTweakable = true, // Minimum Evasion Time - groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), - 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_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)] - public float evasionThreshold = 50f; - - [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)] - public float evasionTimeThreshold = 0f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CollisionAvoidanceThreshold", advancedTweakable = true, //Vessel collision avoidance threshold - groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 50f, stepIncrement = 1f, scene = UI_Scene.All)] - float collisionAvoidanceThreshold = 30f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CollisionAvoidancePeriod", advancedTweakable = true, //Vessel collision avoidance period - groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 3f, stepIncrement = 0.1f, scene = UI_Scene.All)] - float vesselCollisionAvoidancePeriod = 1.5f; // Avoid for 1.5s. - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ExtendMultiplier", advancedTweakable = true, //Extend Distance Multiplier - groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = .1f, scene = UI_Scene.All)] - public float extendMult = 1f; - - [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),] - public bool canExtend = true; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, category = "DoubleSlider", guiName = "#LOC_BDArmory_TurnRadiusTwiddleFactorMin", advancedTweakable = true,//Turn radius twiddle factors (category seems to have no effect) - groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_PilotAI_Terrain", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 5f, stepIncrement = 0.5f, scene = UI_Scene.All)] - public float turnRadiusTwiddleFactorMin = 2.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, category = "DoubleSlider", guiName = "#LOC_BDArmory_TurnRadiusTwiddleFactorMax", advancedTweakable = true,//Turn radius twiddle factors (category seems to have no effect) - groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_PilotAI_Terrain", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 5f, stepIncrement = 0.5f, scene = UI_Scene.All)] - public float turnRadiusTwiddleFactorMax = 4.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_AllowRamming", advancedTweakable = true, //Toggle Allow Ramming - groupName = "pilotAI_Ramming", groupDisplayName = "#LOC_BDArmory_PilotAI_Ramming", groupStartCollapsed = true), - UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] - public bool allowRamming = true; // Allow switching to ramming mode. - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ControlSurfaceLag", advancedTweakable = true,//Control surface lag (for getting an accurate intercept for ramming). - groupName = "pilotAI_Ramming", groupDisplayName = "#LOC_BDArmory_PilotAI_Ramming", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 0.2f, stepIncrement = 0.01f, scene = UI_Scene.All)] - public float controlSurfaceLag = 0.01f; // Lag time in response of control surfaces. - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Orbit", advancedTweakable = true),//Orbit - UI_Toggle(enabledText = "#LOC_BDArmory_Orbit_enabledText", disabledText = "#LOC_BDArmory_Orbit_disabledText", scene = UI_Scene.All),]//Starboard (CW)--Port (CCW) - public bool ClockwiseOrbit = true; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_UnclampTuning", advancedTweakable = true),//Unclamp tuning - UI_Toggle(enabledText = "#LOC_BDArmory_UnclampTuning_enabledText", disabledText = "#LOC_BDArmory_UnclampTuning_disabledText", scene = UI_Scene.All),]//Unclamped--Clamped - public bool UpToEleven = false; - - Dictionary altMaxValues = new Dictionary - { - { nameof(defaultAltitude), 100000f }, - { nameof(minAltitude), 60000f }, - { nameof(steerMult), 200f }, - { nameof(steerKiAdjust), 20f }, - { nameof(steerDamping), 100f }, - { nameof(maxSteer), 1f}, - { nameof(maxSpeed), 3000f }, - { nameof(takeOffSpeed), 2000f }, - { nameof(minSpeed), 2000f }, - { nameof(idleSpeed), 3000f }, - { nameof(maxAllowedGForce), 1000f }, - { nameof(maxAllowedAoA), 180f }, - { nameof(extendMult), 200f }, - { nameof(minEvasionTime), 10f }, - { nameof(evasionThreshold), 300f }, - { nameof(evasionTimeThreshold), 3f }, - { nameof(turnRadiusTwiddleFactorMin), 10f}, - { nameof(turnRadiusTwiddleFactorMax), 10f}, - { nameof(controlSurfaceLag), 1f}, - { nameof(DynamicDampingMin), 100f }, - { nameof(DynamicDampingMax), 100f }, - { nameof(dynamicSteerDampingFactor), 100f }, - { nameof(DynamicDampingPitchMin), 100f }, - { nameof(DynamicDampingPitchMax), 100f }, - { nameof(dynamicSteerDampingPitchFactor), 100f }, - { nameof(DynamicDampingYawMin), 100f }, - { nameof(DynamicDampingYawMax), 100f }, - { nameof(dynamicSteerDampingYawFactor), 100f }, - { nameof(DynamicDampingRollMin), 100f }, - { nameof(DynamicDampingRollMax), 100f }, - { nameof(dynamicSteerDampingRollFactor), 100f } - - }; - - [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; - - #endregion - - #region AI Internal Parameters - bool toEleven = false; - - //manueuverability and g loading data - // float maxDynPresGRecorded; - float dynDynPresGRecorded = 1.0f; // Start at reasonable non-zero value. - float dynMaxVelocityMagSqr = 1.0f; // Start at reasonable non-zero value. - - float maxAllowedCosAoA; - float lastAllowedAoA; - - float maxPosG; - float cosAoAAtMaxPosG; - - float maxNegG; - float cosAoAAtMaxNegG; - - float[] gLoadMovingAvgArray = new float[32]; - float[] cosAoAMovingAvgArray = new float[32]; - int movingAvgIndex; - - float gLoadMovingAvg; - float cosAoAMovingAvg; - - 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 lastPitchInput; - - //Controller Integral - float pitchIntegral; - float yawIntegral; - - //instantaneous turn radius and possible acceleration from lift - //properties can be used so that other AI modules can read this for future maneuverability comparisons between craft - float turnRadius; - float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration; - - public float TurnRadius - { - get { return turnRadius; } - private set { turnRadius = value; } - } - - float maxLiftAcceleration; - - public float MaxLiftAcceleration - { - get { return maxLiftAcceleration; } - private set { maxLiftAcceleration = value; } - } - - float turningTimer; - float evasiveTimer; - float threatRating; - Vector3 lastTargetPosition; - - LineRenderer lr; - Vector3 flyingToPosition; - Vector3 rollTarget; - Vector3 angVelRollTarget; - - //speed controller - bool useAB = true; - bool useBrakes = true; - bool regainEnergy = false; - - //collision detection (for other vessels). Look ahead period is vesselCollisionAvoidancePeriod + vesselCollisionAvoidanceTickerFreq * Time.fixedDeltaTime - int vesselCollisionAvoidanceTickerFreq = 10; // Number of fixedDeltaTime steps between vessel-vessel collision checks. - int collisionDetectionTicker = 0; - float collisionDetectionTimer = 0; - Vector3 collisionAvoidDirection; - - // Terrain avoidance and below minimum altitude globals. - int terrainAlertTicker = 0; // A ticker to reduce the frequency of terrain alert checks. - bool belowMinAltitude; // True when below minAltitude or avoiding terrain. - bool gainAltInhibited = false; // Inhibit gain altitude to minimum altitude when chasing or evading someone as long as we're pointing upwards. - bool avoidingTerrain = false; // True when avoiding terrain. - bool initialTakeOff = true; // False after the initial take-off. - float terrainAlertDetectionRadius = 30.0f; // Sphere radius that the vessel occupies. Should cover most vessels. FIXME This could be based on the vessel's maximum width/height. - float terrainAlertThreatRange; // The distance to the terrain to consider (based on turn radius). - 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; - - // Ramming - public bool ramming = false; // Whether or not we're currently trying to ram someone. - - //Dynamic Steer Damping - private bool dynamicDamping = false; - private bool CustomDynamicAxisField = false; - public float dynSteerDampingValue; - public float dynSteerDampingPitchValue; - public float dynSteerDampingYawValue; - public float dynSteerDampingRollValue; - - //wing command - bool useRollHint; - private Vector3d debugFollowPosition; - - double commandSpeed; - Vector3d commandHeading; - - float finalMaxSteer = 1; - - #endregion - - #region RMB info in editor - - // Yes - public override string GetInfo() - { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("Available settings:"); - sb.AppendLine($"- Default Alt. - altitude to fly at when cruising/idle"); - sb.AppendLine($"- Min Altitude - below this altitude AI will prioritize gaining altitude over combat"); - sb.AppendLine($"- Steer Factor - higher will make the AI apply more control input for the same desired rotation"); - sb.AppendLine($"- Steer Ki - higher will make the AI apply control trim faster"); - sb.AppendLine($"- Steer Damping - higher will make the AI apply more control input when it wants to stop rotation"); - if (GameSettings.ADVANCED_TWEAKABLES) - sb.AppendLine($"- Steer Limiter - limit AI from applying full control input"); - sb.AppendLine($"- Max Speed - AI will not fly faster than this"); - sb.AppendLine($"- TakeOff Speed - speed at which to start pitching up when taking off"); - sb.AppendLine($"- MinCombat Speed - AI will prioritize regaining speed over combat below this"); - sb.AppendLine($"- Idle Speed - Cruising speed when not in combat"); - sb.AppendLine($"- Max G - AI will try not to perform maneuvers at higher G than this"); - sb.AppendLine($"- Max AoA - AI will try not to exceed this angle of attack"); - if (GameSettings.ADVANCED_TWEAKABLES) - { - sb.AppendLine($"- Extend Multiplier - scale the time spent extending"); - sb.AppendLine($"- Evasion Multiplier - scale the time spent evading"); - sb.AppendLine($"- Dynamic Steer Damping (min/max) - Dynamically adjust the steer damping factor based on angle to target"); - sb.AppendLine($"- Dyn Steer Damping Factor - Strength of dynamic steer damping adjustment"); - sb.AppendLine($"- Turn Radius Tuning (min/max) - Compensating factor for not being able to perform the perfect turn when oriented correctly/incorrectly"); - sb.AppendLine($"- Control Surface Lag - Lag time in response of control surfaces"); - sb.AppendLine($"- Orbit - Which direction to orbit when idling over a location"); - sb.AppendLine($"- Extend Toggle - Toggle extending multiplier behaviour"); - sb.AppendLine($"- Dynamic Steer Damping - Toggle dynamic steer damping"); - sb.AppendLine($"- Allow Ramming - Toggle ramming behaviour when out of guns/ammo"); - sb.AppendLine($"- Unclamp tuning - Increases variable limits, no direct effect on behaviour"); - } - sb.AppendLine($"- Standby Mode - AI will not take off until an enemy is detected"); - - return sb.ToString(); - } - - #endregion RMB info in editor - - protected void SetSliderClamps(string fieldNameMin, string fieldNameMax) - { - // Enforce min <= max for pairs of sliders - UI_FloatRange field = (UI_FloatRange)Fields[fieldNameMin].uiControlEditor; - field.onFieldChanged = OnMinUpdated; - field = (UI_FloatRange)Fields[fieldNameMin].uiControlFlight; - field.onFieldChanged = OnMinUpdated; - field = (UI_FloatRange)Fields[fieldNameMax].uiControlEditor; - field.onFieldChanged = OnMaxUpdated; - field = (UI_FloatRange)Fields[fieldNameMax].uiControlFlight; - field.onFieldChanged = OnMaxUpdated; - } - public void OnMinUpdated(BaseField field, object obj) - { - if (turnRadiusTwiddleFactorMax < turnRadiusTwiddleFactorMin) { turnRadiusTwiddleFactorMax = turnRadiusTwiddleFactorMin; } // Enforce min < max for turn radius twiddle factor. - // if (DynamicDampingMax < DynamicDampingMin) { DynamicDampingMax = DynamicDampingMin; } // Enforce min < max for dynamic steer damping. - // if (DynamicDampingPitchMax < DynamicDampingPitchMin) { DynamicDampingPitchMax = DynamicDampingPitchMin; } - // if (DynamicDampingYawMax < DynamicDampingYawMin) { DynamicDampingYawMax = DynamicDampingYawMin; } - // if (DynamicDampingRollMax < DynamicDampingRollMin) { DynamicDampingRollMax = DynamicDampingRollMin; } // reversed roll dynamic damp behavior - } - - public void OnMaxUpdated(BaseField field, object obj) - { - if (turnRadiusTwiddleFactorMin > turnRadiusTwiddleFactorMax) { turnRadiusTwiddleFactorMin = turnRadiusTwiddleFactorMax; } // Enforce min < max for turn radius twiddle factor. - // if (DynamicDampingMin > DynamicDampingMax) { DynamicDampingMin = DynamicDampingMax; } // Enforce min < max for dynamic steer damping. - // if (DynamicDampingPitchMin > DynamicDampingPitchMax) { DynamicDampingPitchMin = DynamicDampingPitchMax; } - // if (DynamicDampingYawMin > DynamicDampingYawMax) { DynamicDampingYawMin = DynamicDampingYawMax; } - // if (DynamicDampingRollMin > DynamicDampingRollMax) { DynamicDampingRollMin = DynamicDampingRollMax; } // reversed roll dynamic damp behavior - } - - public void ToggleDynamicDampingFields() - { - // Dynamic damping - var DynamicDampingLabel = Fields["DynamicDampingLabel"]; - var DampingMin = Fields["DynamicDampingMin"]; - var DampingMax = Fields["DynamicDampingMax"]; - var DampingFactor = Fields["dynamicSteerDampingFactor"]; - - DynamicDampingLabel.guiActive = dynamicSteerDamping && !CustomDynamicAxisFields; - DynamicDampingLabel.guiActiveEditor = dynamicSteerDamping && !CustomDynamicAxisFields; - DampingMin.guiActive = dynamicSteerDamping && !CustomDynamicAxisFields; - DampingMin.guiActiveEditor = dynamicSteerDamping && !CustomDynamicAxisFields; - DampingMax.guiActive = dynamicSteerDamping && !CustomDynamicAxisFields; - DampingMax.guiActiveEditor = dynamicSteerDamping && !CustomDynamicAxisFields; - DampingFactor.guiActive = dynamicSteerDamping && !CustomDynamicAxisFields; - DampingFactor.guiActiveEditor = dynamicSteerDamping && !CustomDynamicAxisFields; - - // 3-axis dynamic damping - var DynamicPitchLabel = Fields["PitchLabel"]; - var DynamicDampingPitch = Fields["dynamicDampingPitch"]; - var DynamicDampingPitchMaxField = Fields["DynamicDampingPitchMax"]; - var DynamicDampingPitchMinField = Fields["DynamicDampingPitchMin"]; - var DynamicDampingPitchFactorField = Fields["dynamicSteerDampingPitchFactor"]; - - var DynamicYawLabel = Fields["YawLabel"]; - var DynamicDampingYaw = Fields["dynamicDampingYaw"]; - var DynamicDampingYawMaxField = Fields["DynamicDampingYawMax"]; - var DynamicDampingYawMinField = Fields["DynamicDampingYawMin"]; - var DynamicDampingYawFactorField = Fields["dynamicSteerDampingYawFactor"]; - - var DynamicRollLabel = Fields["RollLabel"]; - var DynamicDampingRoll = Fields["dynamicDampingRoll"]; - var DynamicDampingRollMaxField = Fields["DynamicDampingRollMax"]; - var DynamicDampingRollMinField = Fields["DynamicDampingRollMin"]; - var DynamicDampingRollFactorField = Fields["dynamicSteerDampingRollFactor"]; - - DynamicPitchLabel.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicPitchLabel.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitch.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitch.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitchMinField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitchMinField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitchMaxField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitchMaxField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitchFactorField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingPitchFactorField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - - DynamicYawLabel.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicYawLabel.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYaw.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYaw.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYawMinField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYawMinField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYawMaxField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYawMaxField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYawFactorField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingYawFactorField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - - DynamicRollLabel.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicRollLabel.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingRoll.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingRoll.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingRollMinField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingRollMinField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingRollMaxField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; - DynamicDampingRollMaxField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - 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; - } - - protected override void Start() - { - base.Start(); - - if (HighLogic.LoadedSceneIsFlight) - { - maxAllowedCosAoA = (float)Math.Cos(maxAllowedAoA * Math.PI / 180.0); - lastAllowedAoA = maxAllowedAoA; - } - - SetSliderClamps("turnRadiusTwiddleFactorMin", "turnRadiusTwiddleFactorMax"); - // SetSliderClamps("DynamicDampingMin", "DynamicDampingMax"); - // SetSliderClamps("DynamicDampingPitchMin", "DynamicDampingPitchMax"); - // SetSliderClamps("DynamicDampingYawMin", "DynamicDampingYawMax"); - // SetSliderClamps("DynamicDampingRollMin", "DynamicDampingRollMax"); - dynamicDamping = dynamicSteerDamping; - CustomDynamicAxisField = CustomDynamicAxisFields; - ToggleDynamicDampingFields(); - // InitSteerDamping(); - } - - public override void ActivatePilot() - { - base.ActivatePilot(); - - belowMinAltitude = vessel.LandedOrSplashed; - prevTargetDir = vesselTransform.up; - if (initialTakeOff && !vessel.LandedOrSplashed) // In case we activate pilot after taking off manually. - initialTakeOff = false; - - bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)vessel.orbit.referenceBody.GeeASL; // Set gravity for calculations; - } - - void Update() - { - if (BDArmorySettings.DRAW_DEBUG_LINES && pilotEnabled) - { - if (lr) - { - lr.enabled = true; - lr.SetPosition(0, vessel.ReferenceTransform.position); - lr.SetPosition(1, flyingToPosition); - } - else - { - lr = gameObject.AddComponent(); - lr.positionCount = 2; - lr.startWidth = 0.5f; - lr.endWidth = 0.5f; - } - - minSpeed = Mathf.Clamp(minSpeed, 0, idleSpeed - 20); - minSpeed = Mathf.Clamp(minSpeed, 0, maxSpeed - 20); - } - else - { - if (lr) - { - 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; - } - - //hide dynamic steer damping fields if dynamic damping isn't toggled - if (dynamicSteerDamping != dynamicDamping) - { - // InitSteerDamping(); - dynamicDamping = dynamicSteerDamping; - ToggleDynamicDampingFields(); - } - //hide custom dynamic axis fields when it isn't toggled - if (CustomDynamicAxisFields != CustomDynamicAxisField) - { - CustomDynamicAxisField = CustomDynamicAxisFields; - ToggleDynamicDampingFields(); - } - } - - IEnumerator setVar(string name, float value) - { - yield return new WaitForFixedUpdate(); - typeof(BDModulePilotAI).GetField(name).SetValue(this, value); - } - - void FixedUpdate() - { - //floating origin and velocity offloading corrections - if (lastTargetPosition != null && (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero())) - { - lastTargetPosition -= FloatingOrigin.OffsetNonKrakensbane; - } - } - - // This is triggered every Time.fixedDeltaTime. - protected override void AutoPilot(FlightCtrlState s) - { - finalMaxSteer = maxSteer; - - if (terrainAlertCoolDown > 0) - terrainAlertCoolDown -= Time.fixedDeltaTime; - - //default brakes off full throttle - //s.mainThrottle = 1; - - //vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); - AdjustThrottle(maxSpeed, true); - useAB = true; - useBrakes = true; - vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); - - steerMode = SteerModes.NormalFlight; - useVelRollTarget = false; - - // landed and still, chill out - if (vessel.LandedOrSplashed && standbyMode && weaponManager && (BDATargetManager.GetClosestTarget(this.weaponManager) == null || BDArmorySettings.PEACE_MODE)) //TheDog: replaced querying of targetdatabase with actual check if a target can be detected - { - //s.mainThrottle = 0; - //vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - AdjustThrottle(0, true); - return; - } - - //upDirection = -FlightGlobals.getGeeForceAtPosition(transform.position).normalized; - upDirection = VectorUtils.GetUpDirection(vessel.transform.position); - - CalculateAccelerationAndTurningCircle(); - - 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. - gainAltInhibited = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG " + 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. - gainAltInhibited = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG " + vessel.vesselName + " was " + currentStatus + " and went below min altitude, inhibiting gain alt."); - } - - if (vessel.srfSpeed < minSpeed) - { regainEnergy = true; } - else if (!belowMinAltitude && vessel.srfSpeed > Mathf.Min(minSpeed + 20f, idleSpeed)) - { regainEnergy = false; } - - - UpdateVelocityRelativeDirections(); - CheckLandingGear(); - if (!vessel.LandedOrSplashed && (FlyAvoidTerrain(s) || (!ramming && FlyAvoidOthers(s)))) - { turningTimer = 0; } - else if (belowMinAltitude && !(gainAltInhibited && Vector3.Dot(vessel.Velocity() / vessel.srfSpeed, vessel.upAxis) > 0)) // If we're below minimum altitude, gain altitude unless we're being inhibited and gaining altitude. - { - if (initialTakeOff || command != PilotCommands.Follow) - { - TakeOff(s); - turningTimer = 0; - } - } - else - { - if (command != PilotCommands.Free) - { UpdateCommand(s); } - else - { UpdateAI(s); } - } - UpdateGAndAoALimits(s); - AdjustPitchForGAndAoALimits(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.srfSpeed, 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; - } - } - - void UpdateAI(FlightCtrlState s) - { - currentStatus = "Free"; - - if (requestedExtend) - { - requestedExtend = false; - if (!extending) startedExtendingAt = Planetarium.GetUniversalTime(); - extending = true; - lastTargetPosition = requestedExtendTpos; - } - - // Calculate threat rating from any threats - float minimumEvasionTime = minEvasionTime; - threatRating = evasionThreshold + 1f; // Don't evade by default - if (weaponManager && (weaponManager.missileIsIncoming || weaponManager.isChaffing || weaponManager.isFlaring)) - { - threatRating = 0f; // Allow entering evasion code if we're under missile fire - minimumEvasionTime = minEvasionTime * 2f + 1f; // Longer minimum evasion time for missiles, so we don't turn into them - } - else if (weaponManager.underFire && !ramming) // If we're ramming, ignore gunfire. - { - if (weaponManager.incomingMissTime >= evasionTimeThreshold) // If we haven't been under fire long enough, ignore gunfire - threatRating = weaponManager.incomingMissDistance; - } - - debugString.Append($"Threat Rating: {threatRating}"); - debugString.Append(Environment.NewLine); - - // If we're currently evading or a threat is significant and we're not ramming. - if ((evasiveTimer < minimumEvasionTime && evasiveTimer != 0) || threatRating < evasionThreshold) - { - if (evasiveTimer < minimumEvasionTime) - { - threatRelativePosition = vessel.Velocity().normalized + vesselTransform.right; - - if (weaponManager) - { - if (weaponManager.rwr?.rwrEnabled ?? false) //use rwr to check missile threat direction - { - 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 == 4) - { - 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 (weaponManager.underFire) - { - threatRelativePosition = weaponManager.incomingThreatPosition - vesselTransform.position; - } - } - } - Evasive(s); - evasiveTimer += Time.fixedDeltaTime; - turningTimer = 0; - - if (evasiveTimer >= minimumEvasionTime) - { - evasiveTimer = 0; - collisionDetectionTicker = vesselCollisionAvoidanceTickerFreq + 1; //check for collision again after exiting evasion routine - } - } - else if (!extending && weaponManager && targetVessel != null && targetVessel.transform != null) - { - evasiveTimer = 0; - 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. - { - //dangerous if low altitude and target is far below you - don't dive into ground! - if (!extending) startedExtendingAt = Planetarium.GetUniversalTime(); - extending = true; - // extendingReason = "Too steeply below"; - lastTargetPosition = targetVessel.vesselTransform.position; - } - - if (Vector3.Angle(targetVessel.vesselTransform.position - vesselTransform.position, vesselTransform.up) > 35) // If target is outside of 35° cone ahead of us then keep flying straight. - { - turningTimer += Time.fixedDeltaTime; - } - else - { - turningTimer = 0; - } - - debugString.Append($"turningTimer: {turningTimer}"); - debugString.Append(Environment.NewLine); - - float targetForwardDot = Vector3.Dot(targetVesselRelPos.normalized, vesselTransform.up); - 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 - - if (canExtend && targetVelFrac < 0.8f && targetForwardDot < 0.2f && targetVesselRelPos.magnitude < 400) // Target is outside of ~78° cone ahead, closer than 400m and slower than us, so we won't be able to turn to attack it now. - { - if (!extending) startedExtendingAt = Planetarium.GetUniversalTime(); - extending = true; - // extendingReason = "Can't turn fast enough"; - lastTargetPosition = targetVessel.vesselTransform.position - vessel.Velocity(); //we'll set our last target pos based on the enemy vessel and where we were 1 seconds ago - weaponManager.ForceScan(); - } - if (canExtend && turningTimer > 15) - { - //extend if turning circles for too long - RequestExtend(targetVessel.vesselTransform.position); - // extendingReason = "Turning too long"; - turningTimer = 0; - weaponManager.ForceScan(); - } - } - else //extend if too close for agm attack (agm = air-to-ground missiles, target is on the ground/water) - { - float extendDistance = Mathf.Clamp(weaponManager.guardRange - 1800, 2500, 4000); - float srfDist = (GetSurfacePosition(targetVessel.transform.position) - GetSurfacePosition(vessel.transform.position)).sqrMagnitude; - - if (srfDist < extendDistance * extendDistance && Vector3.Angle(vesselTransform.up, targetVessel.transform.position - vessel.transform.position) > 45) - { - if (!extending) startedExtendingAt = Planetarium.GetUniversalTime(); - extending = true; - // extendingReason = "Surface target"; - lastTargetPosition = targetVessel.transform.position; - weaponManager.ForceScan(); - } - } - - if (!extending) - { - if (weaponManager.HasWeaponsAndAmmo() || !RamTarget(s, targetVessel)) // If we're out of ammo, see if we can ram someone, otherwise, behave as normal. - { - ramming = false; - currentStatus = "Engaging"; - debugString.Append($"Flying to target"); - debugString.Append(Environment.NewLine); - FlyToTargetVessel(s, targetVessel); - } - } - } - else - { - evasiveTimer = 0; - if (!extending && !(terrainAlertCoolDown > 0)) - { - currentStatus = "Orbiting"; - FlyOrbit(s, assignedPositionGeo, 2000, idleSpeed, ClockwiseOrbit); - } - } - - if (extending) - { - evasiveTimer = 0; - currentStatus = "Extending"; - debugString.Append($"Extending"); - debugString.Append(Environment.NewLine); - FlyExtend(s, lastTargetPosition); - } - } - - bool PredictCollisionWithVessel(Vessel v, float maxTime, out Vector3 badDirection) - { - if (vessel == null || v == null || v == weaponManager?.incomingMissileVessel - || v.rootPart.FindModuleImplementing() != null) //evasive will handle avoiding missiles - { - badDirection = Vector3.zero; - return false; - } - - // 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. - if (timeToCPA > 0 && timeToCPA < maxTime) - { - Vector3 tPos = AIUtils.PredictPosition(v, timeToCPA); - Vector3 myPos = AIUtils.PredictPosition(vessel, timeToCPA); - if (Vector3.SqrMagnitude(tPos - myPos) < collisionAvoidanceThreshold * collisionAvoidanceThreshold) // Within collisionAvoidanceThreshold of each other. Danger Will Robinson! - { - badDirection = tPos - vesselTransform.position; - return true; - } - } - - badDirection = Vector3.zero; - return false; - } - - bool RamTarget(FlightCtrlState s, Vessel v) - { - if (BDArmorySettings.DISABLE_RAMMING || !allowRamming) return false; // Override from BDArmory settings and local config. - if (v == null) return false; // We don't have a target. - if (Vector3.Dot(vessel.srf_vel_direction, v.srf_vel_direction) * (float)v.srfSpeed / (float)vessel.srfSpeed > 0.95f) return false; // We're not approaching them fast enough. - 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); - - // Let's try to ram someone! - if (!ramming) - ramming = true; - currentStatus = "Ramming speed!"; - - // 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); - float easeVel = Mathf.Clamp01(2f - timeToCPA / 8f); - Vector3 predictedPosition = AIUtils.PredictPosition(v.transform.position, v.Velocity() * easeVel, v.acceleration * easeAccel, timeToCPA); - - // Set steer mode to aiming for less than 8s left - if (timeToCPA < 8f) - steerMode = SteerModes.Aiming; - else - steerMode = SteerModes.NormalFlight; - - if (controlSurfaceLag > 0) - predictedPosition += -1 * controlSurfaceLag * controlSurfaceLag * (timeToCPA / controlSurfaceLag - 1f + Mathf.Exp(-timeToCPA / controlSurfaceLag)) * vessel.acceleration * easeAccel; // Compensation for control surface lag. - FlyToPosition(s, predictedPosition); - AdjustThrottle(maxSpeed, false, true); // Ramming speed! - - return true; - } - - void FlyToTargetVessel(FlightCtrlState s, Vessel v) - { - Vector3 target = v.CoM; - MissileBase missile = null; - Vector3 vectorToTarget = v.transform.position - vesselTransform.position; - float distanceToTarget = vectorToTarget.magnitude; - float planarDistanceToTarget = Vector3.ProjectOnPlane(vectorToTarget, upDirection).magnitude; - float angleToTarget = Vector3.Angle(target - vesselTransform.position, vesselTransform.up); - if (weaponManager) - { - missile = weaponManager.CurrentMissile; - if (missile != null) - { - if (missile.GetWeaponClass() == WeaponClasses.Missile) - { - if (distanceToTarget > 5500f) - { - finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - } - - if (missile.TargetingMode == MissileBase.TargetingModes.Heat && !weaponManager.heatTarget.exists) - { - debugString.Append($"Attempting heat lock"); - debugString.Append(Environment.NewLine); - target += v.srf_velocity.normalized * 10; - } - else - { - target = MissileGuidance.GetAirToAirFireSolution(missile, v); - } - - if (angleToTarget < 20f) - { - steerMode = SteerModes.Aiming; - } - } - else //bombing - { - if (distanceToTarget > 4500f) - { - finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - } - - if (angleToTarget < 45f) - { - target = target + (Mathf.Max(defaultAltitude - 500f, minAltitude) * upDirection); - Vector3 tDir = (target - vesselTransform.position).normalized; - tDir = (1000 * tDir) - (vessel.Velocity().normalized * 600); - target = vesselTransform.position + tDir; - } - else - { - target = target + (Mathf.Max(defaultAltitude - 500f, minAltitude) * upDirection); - } - } - } - else if (weaponManager.currentGun) - { - ModuleWeapon weapon = weaponManager.currentGun; - if (weapon != null) - { - Vector3 leadOffset = weapon.GetLeadOffset(); - - float targetAngVel = Vector3.Angle(v.transform.position - vessel.transform.position, v.transform.position + (vessel.Velocity()) - vessel.transform.position); - debugString.Append($"targetAngVel: {targetAngVel}"); - debugString.Append(Environment.NewLine); - float magnifier = Mathf.Clamp(targetAngVel, 1f, 2f); - magnifier += ((magnifier - 1f) * Mathf.Sin(Time.time * 0.75f)); - target -= magnifier * leadOffset; - - angleToTarget = Vector3.Angle(vesselTransform.up, target - vesselTransform.position); - if (distanceToTarget < weaponManager.gunRange && angleToTarget < 20) - { - steerMode = SteerModes.Aiming; //steer to aim - } - else - { - if (distanceToTarget > 3500f || angleToTarget > 90f || vessel.srfSpeed < takeOffSpeed) - { - finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - } - else - { - //figuring how much to lead the target's movement to get there after its movement assuming we can manage a constant speed turn - //this only runs if we're not aiming and not that far from the target and the target is in front of us - float curVesselMaxAccel = Math.Min(dynDynPresGRecorded * (float)vessel.dynamicPressurekPa, maxAllowedGForce * bodyGravity); - if (curVesselMaxAccel > 0) - { - float timeToTurn = (float)vessel.srfSpeed * angleToTarget * Mathf.Deg2Rad / curVesselMaxAccel; - target += v.Velocity() * timeToTurn; - target += 0.5f * v.acceleration * timeToTurn * timeToTurn; - } - } - } - - if (v.LandedOrSplashed) - { - if (distanceToTarget > defaultAltitude * 2.2f) - { - target = FlightPosition(target, defaultAltitude); - } - else - { - steerMode = SteerModes.Aiming; - } - } - else if (distanceToTarget > weaponManager.gunRange * 1.5f || Vector3.Dot(target - vesselTransform.position, vesselTransform.up) < 0) - { - target = v.CoM; - } - } - } - else if (planarDistanceToTarget > weaponManager.gunRange * 1.25f && (vessel.altitude < targetVessel.altitude || (float)vessel.radarAltitude < defaultAltitude)) //climb to target vessel's altitude if lower and still too far for guns - { - finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - target = vesselTransform.position + GetLimitedClimbDirectionForSpeed(vectorToTarget); - } - else - { - finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - } - } - - float targetDot = Vector3.Dot(vesselTransform.up, v.transform.position - vessel.transform.position); - - //manage speed when close to enemy - float finalMaxSpeed = maxSpeed; - if (targetDot > 0) - { - finalMaxSpeed = Mathf.Max((distanceToTarget - 100) / 8, 0) + (float)v.srfSpeed; - finalMaxSpeed = Mathf.Max(finalMaxSpeed, minSpeed); - } - AdjustThrottle(finalMaxSpeed, true); - - 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.Append($"Enemy on tail. Braking!"); - debugString.Append(Environment.NewLine); - AdjustThrottle(minSpeed, true); - } - if (missile != null - && targetDot > 0 - && distanceToTarget < MissileLaunchParams.GetDynamicLaunchParams(missile, v.Velocity(), v.transform.position).minLaunchRange - && vessel.srfSpeed > idleSpeed) - { - RequestExtend(lastTargetPosition); // Get far enough away to use the missile. - // extendingReason = "Missile"; - } - - if (regainEnergy && angleToTarget > 30f) - { - RegainEnergy(s, target - vesselTransform.position); - return; - } - else - { - useVelRollTarget = true; - FlyToPosition(s, target); - return; - } - } - - void RegainEnergy(FlightCtrlState s, Vector3 direction, float throttleOverride = -1f) - { - debugString.Append($"Regaining energy"); - debugString.Append(Environment.NewLine); - - steerMode = SteerModes.Aiming; - Vector3 planarDirection = Vector3.ProjectOnPlane(direction, 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; - - if (throttleOverride >= 0) - AdjustThrottle(maxSpeed, false, true, throttleOverride); - else - AdjustThrottle(maxSpeed, false, true); - - FlyToPosition(s, vesselTransform.position + (targetDirection * 100), true); - } - - float GetSteerLimiterForSpeedAndPower() - { - float possibleAccel = speedController.GetPossibleAccel(); - float speed = (float)vessel.srfSpeed; - - debugString.Append($"possibleAccel: {possibleAccel}"); - debugString.Append(Environment.NewLine); - - float limiter = ((speed - minSpeed) / 2 / minSpeed) + possibleAccel / 15f; // FIXME The calculation for possibleAccel needs further investigation. - debugString.Append($"unclamped limiter: { limiter}"); - debugString.Append(Environment.NewLine); - - return Mathf.Clamp01(limiter); - } - - Vector3 prevTargetDir; - Vector3 debugPos; - bool useVelRollTarget; - - void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrottle = false) - { - if (!belowMinAltitude) // Includes avoidingTerrain - { - if (weaponManager && Time.time - weaponManager.timeBombReleased < 1.5f) - { - targetPosition = vessel.transform.position + vessel.Velocity(); - } - - targetPosition = FlightPosition(targetPosition, minAltitude); - targetPosition = vesselTransform.position + ((targetPosition - vesselTransform.position).normalized * 100); - } - - Vector3d srfVel = vessel.Velocity(); - if (srfVel != Vector3d.zero) - { - velocityTransform.rotation = Quaternion.LookRotation(srfVel, -vesselTransform.forward); - } - velocityTransform.rotation = Quaternion.AngleAxis(90, velocityTransform.right) * velocityTransform.rotation; - - //ang vel - Vector3 localAngVel = vessel.angularVelocity; - //test - Vector3 currTargetDir = (targetPosition - vesselTransform.position).normalized; - if (steerMode == SteerModes.NormalFlight) - { - float gRotVel = ((10f * maxAllowedGForce) / ((float)vessel.srfSpeed)); - //currTargetDir = Vector3.RotateTowards(prevTargetDir, currTargetDir, gRotVel*Mathf.Deg2Rad, 0); - } - Vector3 targetAngVel = Vector3.Cross(prevTargetDir, currTargetDir) / Time.fixedDeltaTime; - Vector3 localTargetAngVel = vesselTransform.InverseTransformVector(targetAngVel); - prevTargetDir = currTargetDir; - targetPosition = vessel.transform.position + (currTargetDir * 100); - - flyingToPosition = targetPosition; - - //test poststall - float AoA = Vector3.Angle(vessel.ReferenceTransform.up, vessel.Velocity()); - if (AoA > 30f) - { - steerMode = SteerModes.Aiming; - } - - //slow down for tighter turns - float velAngleToTarget = Mathf.Clamp(Vector3.Angle(targetPosition - vesselTransform.position, vessel.Velocity()), 0, 90); - float speedReductionFactor = 1.25f; - float finalSpeed = Mathf.Min(speedController.targetSpeed, Mathf.Clamp(maxSpeed - (speedReductionFactor * velAngleToTarget), idleSpeed, maxSpeed)); - debugString.Append($"Final Target Speed: {finalSpeed}"); - debugString.Append(Environment.NewLine); - - if (!overrideThrottle) - { - AdjustThrottle(finalSpeed, useBrakes, useAB); - } - - if (steerMode == SteerModes.Aiming) - { - localAngVel -= localTargetAngVel; - } - - Vector3 targetDirection; - Vector3 targetDirectionYaw; - float yawError; - float pitchError; - //float postYawFactor; - //float postPitchFactor; - if (steerMode == SteerModes.NormalFlight) - { - targetDirection = velocityTransform.InverseTransformDirection(targetPosition - velocityTransform.position).normalized; - targetDirection = Vector3.RotateTowards(Vector3.up, targetDirection, 45 * Mathf.Deg2Rad, 0); - - targetDirectionYaw = vesselTransform.InverseTransformDirection(vessel.Velocity()).normalized; - targetDirectionYaw = Vector3.RotateTowards(Vector3.up, targetDirectionYaw, 45 * Mathf.Deg2Rad, 0); - } - else//(steerMode == SteerModes.Aiming) - { - targetDirection = vesselTransform.InverseTransformDirection(targetPosition - vesselTransform.position).normalized; - targetDirection = Vector3.RotateTowards(Vector3.up, targetDirection, 25 * Mathf.Deg2Rad, 0); - targetDirectionYaw = targetDirection; - } - debugPos = vessel.transform.position + (targetPosition - vesselTransform.position) * 5000; - - //// Adjust targetDirection based on ATTITUDE limits - //var horizonUp = Vector3.ProjectOnPlane(vesselTransform.up, 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); - //} - //debugString.Append($"Attitude: " + attitude); - //debugString.Append(Environment.NewLine); - - pitchError = VectorUtils.SignedAngle(Vector3.up, Vector3.ProjectOnPlane(targetDirection, Vector3.right), Vector3.back); - yawError = VectorUtils.SignedAngle(Vector3.up, Vector3.ProjectOnPlane(targetDirectionYaw, Vector3.forward), Vector3.right); - - //test - debugString.Append($"finalMaxSteer: {finalMaxSteer}"); - debugString.Append(Environment.NewLine); - - //roll - Vector3 currentRoll = -vesselTransform.forward; - float rollUp = (steerMode == SteerModes.Aiming ? 5f : 10f); - if (steerMode == SteerModes.NormalFlight) - { - rollUp += (1 - finalMaxSteer) * 10f; - } - rollTarget = (targetPosition + (rollUp * upDirection)) - vesselTransform.position; - - //test - if (steerMode == SteerModes.Aiming && !belowMinAltitude) - { - angVelRollTarget = -140 * vesselTransform.TransformVector(Quaternion.AngleAxis(90f, Vector3.up) * localTargetAngVel); - rollTarget += angVelRollTarget; - } - - if (command == PilotCommands.Follow && useRollHint) - { - rollTarget = -commandLeader.vessel.ReferenceTransform.forward; - } - - // - if (belowMinAltitude) - { - if (avoidingTerrain) - rollTarget = terrainAlertNormal * 100; - else - rollTarget = vessel.upAxis * 100; - } - if (useVelRollTarget && !belowMinAltitude) - { - rollTarget = Vector3.ProjectOnPlane(rollTarget, vessel.Velocity()); - currentRoll = Vector3.ProjectOnPlane(currentRoll, vessel.Velocity()); - } - else - { - rollTarget = Vector3.ProjectOnPlane(rollTarget, vesselTransform.up); - } - - //ramming - if (ramming) - rollTarget = Vector3.ProjectOnPlane(targetPosition - vesselTransform.position + rollUp * Mathf.Clamp((targetPosition - vesselTransform.position).magnitude / 500f, 0f, 1f) * upDirection, vesselTransform.up); - - // 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); - float bankAngle = Vector3.SignedAngle(horizonNormal, rollTarget, vesselTransform.up); - - // FlightGlobals.ActiveVessel.mainBody.transform.position - this.vessel.transform.position; - 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); - // debugString.Append($"Bank Angle: " + bankAngle); - // debugString.Append(Environment.NewLine); - - //v/q - float dynamicAdjustment = Mathf.Clamp(16 * (float)(vessel.srfSpeed / vessel.dynamicPressurekPa), 0, 1.2f); - - float rollError = Misc.Misc.SignedAngle(currentRoll, rollTarget, vesselTransform.right); - float steerRoll = (steerMult * 0.0015f * rollError); - float rollDamping = (.10f * SteerDamping(Mathf.Abs(rollError), Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up), 3) * -localAngVel.y); - steerRoll -= rollDamping; - steerRoll *= dynamicAdjustment; - - if (steerMode == SteerModes.NormalFlight) - { - //premature dive fix - pitchError = pitchError * Mathf.Clamp01((21 - Mathf.Exp(Mathf.Abs(rollError) / 30)) / 20); - } - - float steerPitch = (0.015f * steerMult * pitchError) - (SteerDamping(Mathf.Abs(Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up)), Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up), 1) * -localAngVel.x * (1 + steerKiAdjust)); - float steerYaw = (0.005f * steerMult * yawError) - (SteerDamping(Mathf.Abs(yawError * (steerMode == SteerModes.Aiming ? (180f / 25f) : 4f)), Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up), 2) * 0.2f * -localAngVel.z * (1 + steerKiAdjust)); - - pitchIntegral += pitchError; - yawIntegral += yawError; - - steerPitch *= dynamicAdjustment; - steerYaw *= dynamicAdjustment; - - float pitchKi = 0.1f * (steerKiAdjust / 5); //This is what should be allowed to be tweaked by the player, just like the steerMult, it is very low right now - pitchIntegral = Mathf.Clamp(pitchIntegral, -0.2f / (pitchKi * dynamicAdjustment), 0.2f / (pitchKi * dynamicAdjustment)); //0.2f is the limit of the integral variable, making it bigger increases overshoot - steerPitch += pitchIntegral * pitchKi * dynamicAdjustment; //Adds the integral component to the mix - - float yawKi = 0.1f * (steerKiAdjust / 15); - yawIntegral = Mathf.Clamp(yawIntegral, -0.2f / (yawKi * dynamicAdjustment), 0.2f / (yawKi * dynamicAdjustment)); - steerYaw += yawIntegral * yawKi * dynamicAdjustment; - - float roll = Mathf.Clamp(steerRoll, -maxSteer, maxSteer); - s.roll = roll; - s.yaw = Mathf.Clamp(steerYaw, -finalMaxSteer, finalMaxSteer); - s.pitch = Mathf.Clamp(steerPitch, Mathf.Min(-finalMaxSteer, -0.2f), finalMaxSteer); - } - - void FlyExtend(FlightCtrlState s, Vector3 tPosition) - { - if (weaponManager) - { - if (weaponManager.TargetOverride) - { - extending = false; - // extendingReason = ""; - startedExtendingAt = 0; - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG Stop extending due to target override"); - } - - float extendDistance = Mathf.Clamp(weaponManager.guardRange - 1800, 500, 4000) * extendMult; // General extending distance. - float desiredMinAltitude = (float)vessel.radarAltitude + (defaultAltitude - (float)vessel.radarAltitude) * extendMult; // Desired minimum altitude after extending. - - if (weaponManager.CurrentMissile && weaponManager.CurrentMissile.GetWeaponClass() == WeaponClasses.Bomb) // Run away from the bomb! - { - extendDistance = 4500; - desiredMinAltitude = defaultAltitude; - } - - if (targetVessel != null && !targetVessel.LandedOrSplashed) // We have a flying target, only extend a short distance and don't climb. - { - extendDistance = 300 * extendMult; // The effect of this is generally to extend for only 1 frame. - desiredMinAltitude = minAltitude; - } - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG " + vessel.vesselName + " extending for " + (Planetarium.GetUniversalTime() - startedExtendingAt) + "s due to \"" + extendingReason + "\" for distance " + extendDistance + ", expected time " + (extendDistance / vessel.srfSpeed) + "s"); - - Vector3 srfVector = Vector3.ProjectOnPlane(vessel.transform.position - tPosition, upDirection); - float srfDist = srfVector.magnitude; - if (srfDist < extendDistance) // Extend from position is closer (horizontally) than the extend distance. - { - Vector3 targetDirection = srfVector.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 = FlightPosition(target, desiredMinAltitude); // Further adjustments for speed, situation, etc. and desired minimum altitude after extending. - if (regainEnergy) - { - RegainEnergy(s, target - vesselTransform.position); - return; - } - else - { - FlyToPosition(s, target); - } - } - else // We're far enough away, stop extending. - { - extending = false; - // extendingReason = ""; - startedExtendingAt = 0; - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG Stop extending due to gone far enough (" + srfDist + " of " + extendDistance + ")"); - } - } - else // No weapon manager. - { - extending = false; - // extendingReason = ""; - startedExtendingAt = 0; - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG Stop extending due to no weapon manager"); - } - } - - void FlyOrbit(FlightCtrlState s, Vector3d centerGPS, float radius, float speed, bool clockwise) - { - if (regainEnergy) - { - RegainEnergy(s, vessel.Velocity()); - return; - } - - finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - - debugString.Append($"Flying orbit"); - debugString.Append(Environment.NewLine); - Vector3 flightCenter = GetTerrainSurfacePosition(VectorUtils.GetWorldSurfacePostion(centerGPS, vessel.mainBody)) + (defaultAltitude * upDirection); - - Vector3 myVectorFromCenter = Vector3.ProjectOnPlane(vessel.transform.position - flightCenter, upDirection); - Vector3 myVectorOnOrbit = myVectorFromCenter.normalized * radius; - - Vector3 targetVectorFromCenter = Quaternion.AngleAxis(clockwise ? 15f : -15f, upDirection) * myVectorOnOrbit; - - Vector3 verticalVelVector = Vector3.Project(vessel.Velocity(), upDirection); //for vv damping - - Vector3 targetPosition = flightCenter + targetVectorFromCenter - (verticalVelVector * 0.25f); - - Vector3 vectorToTarget = targetPosition - vesselTransform.position; - //Vector3 planarVel = Vector3.ProjectOnPlane(vessel.Velocity(), 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) - { - Debug.Log("[BDArmory]: AI Pilot reached command destination."); - command = PilotCommands.Free; - } - - useVelRollTarget = true; - - AdjustThrottle(speed, false); - FlyToPosition(s, targetPosition); - } - - //sends target speed to speedController - void AdjustThrottle(float targetSpeed, bool useBrakes, bool allowAfterburner = true, float throttleOverride = -1f) - { - speedController.targetSpeed = targetSpeed; - speedController.useBrakes = useBrakes; - speedController.allowAfterburner = allowAfterburner; - speedController.throttleOverride = throttleOverride; - } - - Vector3 threatRelativePosition; - - void Evasive(FlightCtrlState s) - { - if (s == null) return; - if (vessel == null) return; - if (weaponManager == null) return; - - currentStatus = "Evading"; - debugString.Append($"Evasive"); - debugString.Append(Environment.NewLine); - debugString.Append($"Threat Distance: {weaponManager.incomingMissileDistance}"); - debugString.Append(Environment.NewLine); - - bool hasABEngines = (speedController.multiModeEngines.Count > 0); - - collisionDetectionTicker += 2; - - if (weaponManager) - { - if (weaponManager.isFlaring) - { - useAB = vessel.srfSpeed < minSpeed; - useBrakes = false; - float targetSpeed = minSpeed; - AdjustThrottle(targetSpeed, false, useAB); - } - - if ((weaponManager.isChaffing || weaponManager.isFlaring) && (weaponManager.incomingMissileDistance > 2000)) - { - debugString.Append($"Breaking from missile threat!"); - debugString.Append(Environment.NewLine); - - Vector3 axis = -Vector3.Cross(vesselTransform.up, threatRelativePosition); - Vector3 breakDirection = Quaternion.AngleAxis(90, axis) * threatRelativePosition; - //Vector3 breakTarget = vesselTransform.position + breakDirection; - - if (hasABEngines) - RegainEnergy(s, breakDirection); - else - RegainEnergy(s, breakDirection, 0.66f); - return; - } - else if ((weaponManager.incomingMissileVessel) && (weaponManager.incomingMissileDistance <= 2000)) - { - float mSqrDist = Vector3.SqrMagnitude(weaponManager.incomingMissileVessel.transform.position - vesselTransform.position); - if (mSqrDist < 810000) //900m - { - debugString.Append($"Missile about to impact! pull away!"); - debugString.Append(Environment.NewLine); - - AdjustThrottle(maxSpeed, false, false); - - Vector3 cross = Vector3.Cross(weaponManager.incomingMissileVessel.transform.position - vesselTransform.position, vessel.Velocity()).normalized; - if (Vector3.Dot(cross, -vesselTransform.forward) < 0) - { - cross = -cross; - } - FlyToPosition(s, vesselTransform.position + (50 * vessel.Velocity() / vessel.srfSpeed) + (100 * cross)); - return; - } - } - else if (weaponManager.underFire) - { - debugString.Append($"Dodging gunfire"); - float threatDirectionFactor = Vector3.Dot(vesselTransform.up, threatRelativePosition.normalized); - //Vector3 axis = -Vector3.Cross(vesselTransform.up, threatRelativePosition); - - 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.Append($" from directly ahead!"); - } - 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.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.Append($" from behind afar; engaging barrel roll"); - else - { - debugString.Append($" from behind moderate distance; engaging aggressvie barrel roll and braking"); - steerMode = SteerModes.Aiming; - AdjustThrottle(minSpeed, true, false); - } - } - 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.Append($" 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; - - steerMode = SteerModes.Aiming; - } - 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.Append($" from far side; engaging barrel roll"); - } - } - - 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; - } - - FlyToPosition(s, breakTarget); - return; - } - } - - 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)); - //+ (Mathf.Sin (Time.time/3) * upDirection * minAltitude/3); - - FlyToPosition(s, target); - } - - void UpdateVelocityRelativeDirections() // Vectors that are used in TakeOff and FlyAvoidTerrain. - { - relativeVelocityRightDirection = Vector3.Cross(upDirection, vessel.srf_vel_direction).normalized; - relativeVelocityDownDirection = Vector3.Cross(relativeVelocityRightDirection, vessel.srf_vel_direction).normalized; - } - - void CheckLandingGear() - { - if (!vessel.LandedOrSplashed) - { - if (vessel.radarAltitude > 50.0f) - vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, false); - else - vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, true); - } - } - - void TakeOff(FlightCtrlState s) - { - debugString.Append($"Taking off/Gaining altitude"); - debugString.Append(Environment.NewLine); - - if (vessel.LandedOrSplashed && vessel.srfSpeed < takeOffSpeed) - { - currentStatus = initialTakeOff ? "Taking off" : vessel.Splashed ? "Splashed" : "Landed"; - if (vessel.Splashed) - { vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, false); } - assignedPositionWorld = vessel.transform.position; - return; - } - currentStatus = "Gain Alt. (" + (int)minAltitude + "m)"; - - steerMode = SteerModes.Aiming; - - float radarAlt = (float)vessel.radarAltitude; - - if (initialTakeOff && radarAlt > terrainAlertDetectionRadius) - initialTakeOff = false; - - // Get surface normal relative to our velocity direction below the vessel and where the vessel is heading. - RaycastHit rayHit; - Vector3 forwardDirection = (vessel.horizontalSrfSpeed < 10 ? vesselTransform.up : (Vector3)vessel.srf_vel_direction) * 100; // Forward direction not adjusted for terrain. - Vector3 forwardPoint = vessel.transform.position + forwardDirection * 100; // Forward point not adjusted for terrain. - Ray ray = new Ray(forwardPoint, relativeVelocityDownDirection); // Check ahead and below. - Vector3 terrainBelowAheadNormal = (Physics.Raycast(ray, out rayHit, minAltitude + 1.0f, 1 << 15)) ? rayHit.normal : upDirection; // Terrain normal below point ahead. - ray = new Ray(vessel.transform.position, relativeVelocityDownDirection); // Check here below. - Vector3 terrainBelowNormal = (Physics.Raycast(ray, out rayHit, minAltitude + 1.0f, 1 << 15)) ? 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); - } - - 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. - - ++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. - if (terrainAlertTicker >= terrainAlertTickerThreshold) - { - terrainAlertTicker = 0; - - // Reset/initialise some variables. - 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. - 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 = 150.0f + turnRadiusTwiddleFactor * turnRadius + (float)vessel.srfSpeed * controlLagTime; // 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); - RaycastHit rayHit; - if (Physics.Raycast(rayForwardDown, out rayHit, 1.5f * terrainAlertDetectionRadius, 1 << 15)) // sqrt(2) should be sufficient, so 1.5 will cover it. - { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; - } - if (Physics.Raycast(rayForwardUp, out rayHit, 1.5f * terrainAlertDetectionRadius, 1 << 15) && (terrainAlertDistance < 0.0f || rayHit.distance < terrainAlertDistance)) - { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; - } - if (Physics.Raycast(rayForwardLeft, out rayHit, 1.5f * terrainAlertDetectionRadius, 1 << 15) && (terrainAlertDistance < 0.0f || rayHit.distance < terrainAlertDistance)) - { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; - } - if (Physics.Raycast(rayForwardRight, out rayHit, 1.5f * terrainAlertDetectionRadius, 1 << 15) && (terrainAlertDistance < 0.0f || rayHit.distance < terrainAlertDistance)) - { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; - } - if (terrainAlertDistance > 0) - { - terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, 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, 1 << 15)) // Found something. - { - // 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, 1 << 15)) // 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. - float terrainAlertThreshold = 150.0f + 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, 1 << 15)) - { - if (rayHit.distance < terrainAlertDistance / Mathf.Sin(phi)) // Hit terrain closer than expected => terrain slope is increasing relative to our velocity direction. - { - if (BDArmorySettings.DRAW_DEBUG_LINES) - { - terrainAlertDebugDraw2 = true; - terrainAlertDebugPos2 = rayHit.point; - terrainAlertDebugDir2 = rayHit.normal; - } - terrainAlertNormal = rayHit.normal; // Use the normal of the steeper terrain (relative to our velocity). - terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, terrainAlertNormal).normalized; - } - } - } - } - } - // Finally, check the distance to sea-level as water doesn't act like a collider, so it's getting ignored. - if (vessel.mainBody.ocean) - { - float sinTheta = Vector3.Dot(vessel.srf_vel_direction, 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)); - 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 terrainAlertThreshold = 150.0f + 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. - { - terrainAlertDistance = (float)vessel.altitude; - terrainAlertNormal = upDirection; - terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, upDirection).normalized; - avoidingTerrain = true; - - if (BDArmorySettings.DRAW_DEBUG_LINES) - { - terrainAlertDebugPos = vessel.transform.position + vessel.srf_vel_direction * (float)vessel.altitude / -sinTheta; - terrainAlertDebugDir = upDirection; - } - } - } - } - } - - if (avoidingTerrain) - { - 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 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. - 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; - correctionDirection = Vector3.RotateTowards(correctionDirection, horizontalCorrectionDirection, Mathf.Max(0.0f, (1.0f - (float)vessel.srfSpeed / 120.0f) / 2.0f * maxAngle * Mathf.Deg2Rad) * adjustmentFactor, 0.0f); // Rotate up to maxAngle/2 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 ? terrainAlertCorrectionDirection : (beta * terrainAlertCorrectionDirection + (1.0f - beta) * correctionDirection).normalized; // Update our target direction over several frames (if it's not the initial correction). (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); - - // Update status and book keeping. - currentStatus = "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; - } - - // Hurray, we've avoided the terrain! - avoidingTerrain = false; - return false; - } - - bool FlyAvoidOthers(FlightCtrlState s) // Check for collisions with other vessels and try to avoid them. - { // Mostly a re-hash of FlyAvoidCollision, but with terrain detection removed. - if (collisionDetectionTimer > vesselCollisionAvoidancePeriod) - { - collisionDetectionTimer = 0; - collisionDetectionTicker = vesselCollisionAvoidanceTickerFreq + 1; - } - if (collisionDetectionTimer > 0) - { - //fly avoid - currentStatus = "AvoidCollision"; - debugString.Append($"Avoiding Collision"); - debugString.Append(Environment.NewLine); - collisionDetectionTimer += Time.fixedDeltaTime; - - Vector3 target = vesselTransform.position + collisionAvoidDirection; - FlyToPosition(s, target); - return true; - } - else if (collisionDetectionTicker > vesselCollisionAvoidanceTickerFreq) // Only check every vesselCollisionAvoidanceTickerFreq frames. - { - collisionDetectionTicker = 0; - - // Check for collisions with other vessels. - bool vesselCollision = false; - collisionAvoidDirection = vessel.srf_vel_direction; - using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) - while (vs.MoveNext()) - { - if (vs.Current == null) continue; - if (vs.Current == vessel || vs.Current.Landed || !(Vector3.Dot(vs.Current.transform.position - vesselTransform.position, vesselTransform.up) > 0)) continue; - if (!PredictCollisionWithVessel(vs.Current, vesselCollisionAvoidancePeriod + vesselCollisionAvoidanceTickerFreq * Time.fixedDeltaTime, out collisionAvoidDirection)) continue; - if (vs.Current.FindPartModuleImplementing()?.commandLeader?.vessel == vessel) continue; - vesselCollision = true; - break; // Early exit on first detected vessel collision. Chances of multiple vessel collisions are low. - } - if (vesselCollision) - { - Vector3 axis = -Vector3.Cross(vesselTransform.up, collisionAvoidDirection); - collisionAvoidDirection = Quaternion.AngleAxis(25, axis) * collisionAvoidDirection; //don't need to change the angle that much to avoid, and it should prevent stupid suicidal manuevers as well - collisionDetectionTimer += Time.fixedDeltaTime; - return FlyAvoidOthers(s); // Call ourself again to trigger the actual avoidance. - } - } - else - { ++collisionDetectionTicker; } - return false; - } - - Vector3 GetLimitedClimbDirectionForSpeed(Vector3 direction) - { - if (Vector3.Dot(direction, upDirection) < 0) - { - debugString.Append($"climb limit angle: unlimited"); - debugString.Append(Environment.NewLine); - return direction; //only use this if climbing - } - - Vector3 planarDirection = Vector3.ProjectOnPlane(direction, upDirection).normalized * 100; - - float angle = Mathf.Clamp((float)vessel.srfSpeed * 0.13f, 5, 90); - - debugString.Append($"climb limit angle: {angle}"); - debugString.Append(Environment.NewLine); - 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 (lastAllowedAoA != maxAllowedAoA) - { - lastAllowedAoA = maxAllowedAoA; - maxAllowedCosAoA = (float)Math.Cos(lastAllowedAoA * Math.PI / 180.0); - } - 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); - - //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; - - ++movingAvgIndex; - if (movingAvgIndex == gLoadMovingAvgArray.Length) - movingAvgIndex = 0; - - if (gLoadMovingAvg < maxNegG || Math.Abs(cosAoAMovingAvg - cosAoAAtMaxNegG) < 0.005f) - { - maxNegG = gLoadMovingAvg; - cosAoAAtMaxNegG = cosAoAMovingAvg; - } - if (gLoadMovingAvg > maxPosG || Math.Abs(cosAoAMovingAvg - cosAoAAtMaxPosG) < 0.005f) - { - maxPosG = gLoadMovingAvg; - cosAoAAtMaxPosG = cosAoAMovingAvg; - } - - if (cosAoAAtMaxNegG >= cosAoAAtMaxPosG) - { - cosAoAAtMaxNegG = cosAoAAtMaxPosG = maxNegG = maxPosG = 0; - gOffsetPerDynPres = gaoASlopePerDynPres = 0; - return; - } - - // if (maxPosG > maxDynPresGRecorded) - // maxDynPresGRecorded = maxPosG; - - dynDynPresGRecorded *= 0.999615f; // Decay the highest observed G-force from dynamic pressure (we want a fairly recent value in case the planes dynamics have changed). Half-life of about 30s. - if (!vessel.LandedOrSplashed && Math.Abs(gLoadMovingAvg) > dynDynPresGRecorded) - dynDynPresGRecorded = Math.Abs(gLoadMovingAvg); - - dynMaxVelocityMagSqr *= 0.999615f; // Decay the max recorded squared velocity at the same rate as the dynamic pressure G-force decays to keep the turnRadius constant if they otherwise haven't changed. - if (!vessel.LandedOrSplashed && (float)vessel.Velocity().sqrMagnitude > dynMaxVelocityMagSqr) - dynMaxVelocityMagSqr = (float)vessel.Velocity().sqrMagnitude; - - float aoADiff = cosAoAAtMaxPosG - cosAoAAtMaxNegG; - - //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 - } - - void AdjustPitchForGAndAoALimits(FlightCtrlState s) - { - float minCosAoA, maxCosAoA; - //debugString += "\nMax Pos G: " + maxPosG + " @ " + cosAoAAtMaxPosG; - //debugString += "\nMax Neg G: " + maxNegG + " @ " + cosAoAAtMaxNegG; - - if (vessel.LandedOrSplashed || vessel.srfSpeed < Math.Min(minSpeed, takeOffSpeed)) //if we're going too slow, don't use this - { - float speed = Math.Max(takeOffSpeed, minSpeed); - negPitchDynPresLimitIntegrator = -1f * 0.001f * 0.5f * 1.225f * speed * speed; - posPitchDynPresLimitIntegrator = 1f * 0.001f * 0.5f * 1.225f * speed * speed; - return; - } - - 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.Append($"Updating Neg Gs"); - debugString.Append(Environment.NewLine); - 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.Append($"Updating Pos Gs"); - debugString.Append(Environment.NewLine); - 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; - } - - float currentG = -Vector3.Dot(vessel.acceleration, vessel.ReferenceTransform.forward); - float negLim, posLim; - negLim = negPitchDynPresLimitIntegrator * invVesselDynPreskPa + negPitchDynPresLimit; - if (negLim > s.pitch) - { - if (currentG > -(maxAllowedGForce * 0.97f * bodyGravity)) - { - negPitchDynPresLimitIntegrator -= (float)(0.15 * vessel.dynamicPressurekPa); //jsut an override in case things break - - maxNegG = currentG * invVesselDynPreskPa; - cosAoAAtMaxNegG = curCosAoA; - - negPitchDynPresLimit = 0; - - //maxPosG = 0; - //cosAoAAtMaxPosG = 0; - } - - s.pitch = negLim; - debugString.Append($"Limiting Neg Gs"); - debugString.Append(Environment.NewLine); - } - posLim = posPitchDynPresLimitIntegrator * invVesselDynPreskPa + posPitchDynPresLimit; - if (posLim < s.pitch) - { - if (currentG < (maxAllowedGForce * 0.97f * bodyGravity)) - { - posPitchDynPresLimitIntegrator += (float)(0.15 * vessel.dynamicPressurekPa); //jsut an override in case things break - - maxPosG = currentG * invVesselDynPreskPa; - cosAoAAtMaxPosG = curCosAoA; - - posPitchDynPresLimit = 0; - - //maxNegG = 0; - //cosAoAAtMaxNegG = 0; - } - - s.pitch = posLim; - debugString.Append($"Limiting Pos Gs"); - debugString.Append(Environment.NewLine); - } - - lastPitchInput = s.pitch; - lastCosAoA = curCosAoA; - - debugString.Append($"Neg Pitch Lim: {negLim}"); - debugString.Append(Environment.NewLine); - debugString.Append($"Pos Pitch Lim: {posLim}"); - debugString.Append(Environment.NewLine); - } - - void CalculateAccelerationAndTurningCircle() - { - maxLiftAcceleration = dynDynPresGRecorded * (float)vessel.dynamicPressurekPa; //maximum acceleration from lift that the vehicle can provide - - 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 = dynMaxVelocityMagSqr / 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!) - } - - Vector3 DefaultAltPosition() - { - return (vessel.transform.position + (-(float)vessel.altitude * upDirection) + (defaultAltitude * upDirection)); - } - - Vector3 GetSurfacePosition(Vector3 position) - { - return position - ((float)FlightGlobals.getAltitudeAtPos(position) * upDirection); - } - - Vector3 GetTerrainSurfacePosition(Vector3 position) - { - return position - (MissileGuidance.GetRaycastRadarAltitude(position) * upDirection); - } - - Vector3 FlightPosition(Vector3 targetPosition, float minAlt) - { - Vector3 forwardDirection = vesselTransform.up; - Vector3 targetDirection = (targetPosition - vesselTransform.position).normalized; - - float vertFactor = 0; - vertFactor += (((float)vessel.srfSpeed / minSpeed) - 2f) * 0.3f; //speeds greater than 2x minSpeed encourage going upwards; below encourages downwards - vertFactor += (((targetPosition - vesselTransform.position).magnitude / 1000f) - 1f) * 0.3f; //distances greater than 1000m encourage going upwards; closer encourages going downwards - vertFactor -= Mathf.Clamp01(Vector3.Dot(vesselTransform.position - targetPosition, upDirection) / 1600f - 1f) * 0.5f; //being higher than 1600m above a target encourages going downwards - if (targetVessel) - vertFactor += Vector3.Dot(targetVessel.Velocity() / targetVessel.srfSpeed, (targetVessel.ReferenceTransform.position - vesselTransform.position).normalized) * 0.3f; //the target moving away from us encourages upward motion, moving towards us encourages downward motion - else - vertFactor += 0.4f; - vertFactor -= weaponManager.underFire ? 0.5f : 0; //being under fire encourages going downwards as well, to gain energy - - float alt = (float)vessel.radarAltitude; - - if (vertFactor > 2) - vertFactor = 2; - if (vertFactor < -2) - vertFactor = -2; - - 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) - { - 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; - } - - targetPosition = vesselTransform.position + Vector3.Cross(Vector3.Cross(forwardDirection, targetDirection), forwardDirection).normalized * 200; - } - else if (steerMode != SteerModes.Aiming) - { - float distance = (targetPosition - vesselTransform.position).magnitude; - if (vertFactor < 0) - distance = Math.Min(distance, Math.Abs((alt - minAlt) / vertFactor)); - - targetPosition += upDirection * Math.Min(distance, 1000) * vertFactor * Mathf.Clamp01(0.7f - Math.Abs(Vector3.Dot(projectedTargetDirection, projectedDirection))); - } - - if ((float)vessel.radarAltitude > minAlt * 1.1f) - { - return targetPosition; - } - - float pointRadarAlt = MissileGuidance.GetRaycastRadarAltitude(targetPosition); - if (pointRadarAlt < minAlt) - { - float adjustment = (minAlt - pointRadarAlt); - debugString.Append($"Target position is below minAlt. Adjusting by {adjustment}"); - debugString.Append(Environment.NewLine); - return targetPosition + (adjustment * upDirection); - } - else - { - return targetPosition; - } - } - - 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"; - return steerDamping; - } - else if (angleToTarget >= 180 || angleToTarget < 0) // Check for valid angle to target. - { - 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; - } - - if (CustomDynamicAxisFields) - { - switch (axis) - { - case 1: - if (dynamicDampingPitch) - { - dynSteerDampingPitchValue = GetDampeningFactor(angleToTarget, dynamicSteerDampingPitchFactor, DynamicDampingPitchMin, DynamicDampingPitchMax); - PitchLabel = dynSteerDampingPitchValue.ToString(); - return dynSteerDampingPitchValue; - } - break; - case 2: - if (dynamicDampingYaw) - { - dynSteerDampingYawValue = GetDampeningFactor(angleToTarget, dynamicSteerDampingYawFactor, DynamicDampingYawMin, DynamicDampingYawMax); - YawLabel = dynSteerDampingYawValue.ToString(); - return dynSteerDampingYawValue; - } - break; - case 3: - if (dynamicDampingRoll) - { - dynSteerDampingRollValue = GetDampeningFactor(angleToTarget, dynamicSteerDampingRollFactor, DynamicDampingRollMin, DynamicDampingRollMax); - RollLabel = dynSteerDampingRollValue.ToString(); - return dynSteerDampingRollValue; - } - break; - } - // The specific axis wasn't enabled, use the global value - dynSteerDampingValue = steerDamping; - 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 = GetDampeningFactor(defaultTargetPosition, dynamicSteerDampingFactor, DynamicDampingMin, DynamicDampingMax); - DynamicDampingLabel = dynSteerDampingValue.ToString(); - return dynSteerDampingValue; - } - } - - private float GetDampeningFactor(float angleToTarget, float dynamicSteerDampingFactorAxis, float DynamicDampingMinAxis, float DynamicDampingMaxAxis) - { - return Mathf.Clamp((float)(Math.Pow((180 - angleToTarget) / 180, dynamicSteerDampingFactorAxis) * (DynamicDampingMaxAxis - DynamicDampingMinAxis) + DynamicDampingMinAxis), Mathf.Min(DynamicDampingMinAxis, DynamicDampingMaxAxis), Mathf.Max(DynamicDampingMinAxis, DynamicDampingMaxAxis)); - } - - public override bool IsValidFixedWeaponTarget(Vessel target) - { - if (!vessel) return false; - // aircraft can aim at anything - return true; - } - - bool DetectCollision(Vector3 direction, out Vector3 badDirection) - { - badDirection = Vector3.zero; - if ((float)vessel.radarAltitude < 20) return false; - - direction = direction.normalized; - int layerMask = 1 << 15; - Ray ray = new Ray(vesselTransform.position + (50 * vesselTransform.up), direction); - float distance = Mathf.Clamp((float)vessel.srfSpeed * 4f, 125f, 2500); - RaycastHit hit; - if (!Physics.SphereCast(ray, 10, out hit, distance, layerMask)) return false; - Rigidbody otherRb = hit.collider.attachedRigidbody; - if (otherRb) - { - if (!(Vector3.Dot(otherRb.velocity, vessel.Velocity()) < 0)) return false; - badDirection = hit.point - ray.origin; - return true; - } - badDirection = hit.point - ray.origin; - return true; - } - - void UpdateCommand(FlightCtrlState s) - { - if (command == PilotCommands.Follow && !commandLeader) - { - ReleaseCommand(); - return; - } - - if (command == PilotCommands.Follow) - { - currentStatus = "Follow"; - UpdateFollowCommand(s); - } - else if (command == PilotCommands.FlyTo) - { - currentStatus = "Fly To"; - FlyOrbit(s, assignedPositionGeo, 2500, idleSpeed, ClockwiseOrbit); - } - else if (command == PilotCommands.Attack) - { - if ((BDArmorySettings.RUNWAY_PROJECT) && (targetVessel != null) && ((targetVessel.vesselTransform.position - vessel.vesselTransform.position).sqrMagnitude <= (weaponManager.guardRange * weaponManager.guardRange))) // If the vessel has a target within range, let it fight! - { - ReleaseCommand(); - return; - } - else if (weaponManager.underAttack || weaponManager.underFire) - { - ReleaseCommand(); - return; - } - else - { - currentStatus = "Attack"; - FlyOrbit(s, assignedPositionGeo, 4500, maxSpeed, ClockwiseOrbit); - } - } - } - - void UpdateFollowCommand(FlightCtrlState s) - { - steerMode = SteerModes.NormalFlight; - vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); - - commandSpeed = commandLeader.vessel.srfSpeed; - commandHeading = commandLeader.vessel.Velocity().normalized; - - //formation position - Vector3d commandPosition = GetFormationPosition(); - debugFollowPosition = commandPosition; - - float distanceToPos = Vector3.Distance(vesselTransform.position, commandPosition); - - float dotToPos = Vector3.Dot(vesselTransform.up, commandPosition - vesselTransform.position); - Vector3 flyPos; - useRollHint = false; - - float ctrlModeThresh = 1000; - - if (distanceToPos < ctrlModeThresh) - { - flyPos = commandPosition + (ctrlModeThresh * commandHeading); - - Vector3 vectorToFlyPos = flyPos - vessel.ReferenceTransform.position; - Vector3 projectedPosOffset = Vector3.ProjectOnPlane(commandPosition - vessel.ReferenceTransform.position, commandHeading); - float posOffsetMag = projectedPosOffset.magnitude; - float adjustAngle = (Mathf.Clamp(posOffsetMag * 0.27f, 0, 25)); - Vector3 projVel = Vector3.Project(vessel.Velocity() - commandLeader.vessel.Velocity(), projectedPosOffset); - adjustAngle -= Mathf.Clamp(Mathf.Sign(Vector3.Dot(projVel, projectedPosOffset)) * projVel.magnitude * 0.12f, -10, 10); - - adjustAngle *= Mathf.Deg2Rad; - - vectorToFlyPos = Vector3.RotateTowards(vectorToFlyPos, projectedPosOffset, adjustAngle, 0); - - flyPos = vessel.ReferenceTransform.position + vectorToFlyPos; - - if (distanceToPos < 400) - { - steerMode = SteerModes.Aiming; - } - else - { - steerMode = SteerModes.NormalFlight; - } - - if (distanceToPos < 10) - { - useRollHint = true; - } - } - else - { - steerMode = SteerModes.NormalFlight; - flyPos = commandPosition; - } - - double finalMaxSpeed = commandSpeed; - if (dotToPos > 0) - { - finalMaxSpeed += (distanceToPos / 8); - } - else - { - finalMaxSpeed -= (distanceToPos / 2); - } - - AdjustThrottle((float)finalMaxSpeed, true); - - FlyToPosition(s, flyPos); - } - - Vector3d GetFormationPosition() - { - Quaternion origVRot = velocityTransform.rotation; - Vector3 origVLPos = velocityTransform.localPosition; - - velocityTransform.position = commandLeader.vessel.ReferenceTransform.position; - if (commandLeader.vessel.Velocity() != Vector3d.zero) - { - velocityTransform.rotation = Quaternion.LookRotation(commandLeader.vessel.Velocity(), upDirection); - velocityTransform.rotation = Quaternion.AngleAxis(90, velocityTransform.right) * velocityTransform.rotation; - } - else - { - velocityTransform.rotation = commandLeader.vessel.ReferenceTransform.rotation; - } - - Vector3d pos = velocityTransform.TransformPoint(this.GetLocalFormationPosition(commandFollowIndex));// - lateralVelVector - verticalVelVector; - - velocityTransform.localPosition = origVLPos; - velocityTransform.rotation = origVRot; - - return pos; - } - - public override void CommandTakeOff() - { - base.CommandTakeOff(); - standbyMode = false; - } - - protected override void OnGUI() - { - base.OnGUI(); - - if (!pilotEnabled || !vessel.isActiveVessel) return; - - if (!BDArmorySettings.DRAW_DEBUG_LINES) return; - if (command == PilotCommands.Follow) - { - BDGUIUtils.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 + rollTarget, 2, Color.blue); - BDGUIUtils.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 + 100 * terrainAlertDebugDir, 2, Color.cyan); - if (terrainAlertDebugDraw2) - { - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, terrainAlertDebugPos2, 2, Color.yellow); - BDGUIUtils.DrawLineBetweenWorldPositions(terrainAlertDebugPos2, terrainAlertDebugPos2 + 100 * terrainAlertDebugDir2, 2, Color.yellow); - } - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 100 * (vessel.srf_vel_direction - relativeVelocityDownDirection).normalized, 1, Color.grey); - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 100 * (vessel.srf_vel_direction + relativeVelocityDownDirection).normalized, 1, Color.grey); - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 100 * (vessel.srf_vel_direction - relativeVelocityRightDirection).normalized, 1, Color.grey); - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 100 * (vessel.srf_vel_direction + relativeVelocityRightDirection).normalized, 1, Color.grey); - } - } - } -} diff --git a/BDArmory/Modules/BDModuleSurfaceAI.cs b/BDArmory/Modules/BDModuleSurfaceAI.cs deleted file mode 100644 index 4dd3660ac..000000000 --- a/BDArmory/Modules/BDModuleSurfaceAI.cs +++ /dev/null @@ -1,700 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.UI; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class BDModuleSurfaceAI : BDGenericAIBase, IBDAIControl - { - #region Declarations - - Vessel extendingTarget = null; - Vessel bypassTarget = null; - Vector3 bypassTargetPos; - - Vector3 targetDirection; - float targetVelocity; // the velocity the ship should target, not the velocity of its target - bool aimingMode = false; - - int collisionDetectionTicker = 0; - Vector3? dodgeVector; - float weaveAdjustment = 0; - float weaveDirection = 1; - const float weaveLimit = 15; - const float weaveFactor = 6.5f; - - Vector3 upDir; - - AIUtils.TraversabilityMatrix pathingMatrix; - List waypoints = new List(); - bool leftPath = false; - - protected override Vector3d assignedPositionGeo - { - get { return intermediatePositionGeo; } - set - { - finalPositionGeo = value; - leftPath = true; - } - } - - Vector3d finalPositionGeo; - Vector3d intermediatePositionGeo; - public override Vector3d commandGPS => finalPositionGeo; - - private BDLandSpeedControl motorControl; - - //settings - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_VehicleType"),//Vehicle type - UI_ChooseOption(options = new string[3] { "Land", "Amphibious", "Water" })] - public string SurfaceTypeName = "Land"; - - public AIUtils.VehicleMovementType SurfaceType - => (AIUtils.VehicleMovementType)Enum.Parse(typeof(AIUtils.VehicleMovementType), SurfaceTypeName); - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxSlopeAngle"),//Max slope angle - UI_FloatRange(minValue = 1f, maxValue = 30f, stepIncrement = 1f, scene = UI_Scene.All)] - public float MaxSlopeAngle = 10f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CruiseSpeed"),//Cruise speed - UI_FloatRange(minValue = 5f, maxValue = 60f, stepIncrement = 1f, scene = UI_Scene.All)] - public float CruiseSpeed = 20; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxSpeed"),//Max speed - UI_FloatRange(minValue = 5f, maxValue = 80f, stepIncrement = 1f, scene = UI_Scene.All)] - public float MaxSpeed = 30; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxDrift"),//Max drift - UI_FloatRange(minValue = 1f, maxValue = 180f, stepIncrement = 1f, scene = UI_Scene.All)] - public float MaxDrift = 10; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPitch"),//Moving pitch - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] - public float TargetPitch = 0; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BankAngle"),//Bank angle - UI_FloatRange(minValue = -45f, maxValue = 45f, stepIncrement = 1f, scene = UI_Scene.All)] - public float BankAngle = 0; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerFactor"),//Steer Factor - UI_FloatRange(minValue = 0.2f, maxValue = 20f, stepIncrement = .1f, scene = UI_Scene.All)] - public float steerMult = 6; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerDamping"),//Steer Damping - UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] - public float steerDamping = 3; - - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Steering"), - // UI_Toggle(enabledText = "Powered", disabledText = "Passive")] - public bool PoweredSteering = true; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BroadsideAttack"),//Attack vector - UI_Toggle(enabledText = "#LOC_BDArmory_BroadsideAttack_enabledText", disabledText = "#LOC_BDArmory_BroadsideAttack_disabledText")]//Broadside--Bow - public bool BroadsideAttack = false; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinEngagementRange"),//Min engagement range - UI_FloatRange(minValue = 0f, maxValue = 6000f, stepIncrement = 100f, scene = UI_Scene.All)] - public float MinEngagementRange = 500; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxEngagementRange"),//Max engagement range - UI_FloatRange(minValue = 500f, maxValue = 8000f, stepIncrement = 100f, scene = UI_Scene.All)] - public float MaxEngagementRange = 4000; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ManeuverRCS"),//RCS active - UI_Toggle(enabledText = "#LOC_BDArmory_ManeuverRCS_enabledText", disabledText = "#LOC_BDArmory_ManeuverRCS_disabledText", scene = UI_Scene.All),]//Maneuvers--Combat - public bool ManeuverRCS = false; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinObstacleMass", advancedTweakable = true),//Min obstacle mass - UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All),] - public float AvoidMass = 0f; - - [KSPField(isPersistant = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_PreferredBroadsideDirection", advancedTweakable = true),//Preferred broadside direction - UI_ChooseOption(options = new string[3] { "Starboard", "Whatever", "Port" }, scene = UI_Scene.All),] - public string OrbitDirectionName = "Whatever"; - readonly string[] orbitDirections = new string[3] { "Starboard", "Whatever", "Port" }; - - [KSPField(isPersistant = true)] - int sideSlipDirection = 0; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_GoesUp", advancedTweakable = true),//Goes up to - UI_Toggle(enabledText = "#LOC_BDArmory_GoesUp_enabledText", disabledText = "#LOC_BDArmory_GoesUp_disabledText", scene = UI_Scene.All),]//eleven--ten - public bool UpToEleven = false; - bool toEleven = false; - - const float AttackAngleAtMaxRange = 30f; - - Dictionary altMaxValues = new Dictionary - { - { nameof(MaxSlopeAngle), 90f }, - { nameof(CruiseSpeed), 300f }, - { nameof(MaxSpeed), 400f }, - { nameof(steerMult), 200f }, - { nameof(steerDamping), 100f }, - { nameof(MinEngagementRange), 20000f }, - { nameof(MaxEngagementRange), 30000f }, - { nameof(AvoidMass), 1000000f }, - }; - - #endregion Declarations - - #region RMB info in editor - - // Yes - public override string GetInfo() - { - // known bug - the game caches the RMB info, changing the variable after checking the info - // does not update the info. :( No idea how to force an update. - StringBuilder sb = new StringBuilder(); - sb.AppendLine("Available settings:"); - sb.AppendLine($"- Vehicle type - can this vessel operate on land/sea/both"); - sb.AppendLine($"- Max slope angle - what is the steepest slope this vessel can negotiate"); - sb.AppendLine($"- Cruise speed - the default speed at which it is safe to maneuver"); - sb.AppendLine($"- Max speed - the maximum combat speed"); - sb.AppendLine($"- Max drift - maximum allowed angle between facing and velocity vector"); - sb.AppendLine($"- Moving pitch - the pitch level to maintain when moving at cruise speed"); - sb.AppendLine($"- Bank angle - the limit on roll when turning, positive rolls into turns"); - sb.AppendLine($"- Steer Factor - higher will make the AI apply more control input for the same desired rotation"); - sb.AppendLine($"- Steer Damping - higher will make the AI apply more control input when it wants to stop rotation"); - sb.AppendLine($"- Attack vector - does the vessel attack from the front or the sides"); - sb.AppendLine($"- Min engagement range - AI will try to move away from oponents if closer than this range"); - sb.AppendLine($"- Max engagement range - AI will prioritize getting closer over attacking when beyond this range"); - sb.AppendLine($"- RCS active - Use RCS during any maneuvers, or only in combat "); - if (GameSettings.ADVANCED_TWEAKABLES) - { - sb.AppendLine($"- Min obstacle mass - Obstacles of a lower mass than this will be ignored instead of avoided"); - sb.AppendLine($"- Goes up to - Increases variable limits, no direct effect on behaviour"); - } - - return sb.ToString(); - } - - #endregion RMB info in editor - - #region events - - public override void ActivatePilot() - { - base.ActivatePilot(); - - pathingMatrix = new AIUtils.TraversabilityMatrix(); - - if (!motorControl) - { - motorControl = gameObject.AddComponent(); - motorControl.vessel = vessel; - } - motorControl.Activate(); - - if (BroadsideAttack && sideSlipDirection == 0) - { - sideSlipDirection = orbitDirections.IndexOf(OrbitDirectionName); - if (sideSlipDirection == 0) - sideSlipDirection = UnityEngine.Random.Range(0, 2) > 1 ? 1 : -1; - } - - leftPath = true; - extendingTarget = null; - bypassTarget = null; - collisionDetectionTicker = 6; - } - - public override void DeactivatePilot() - { - base.DeactivatePilot(); - - if (motorControl) - motorControl.Deactivate(); - } - - void Update() - { - // 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(BDModuleSurfaceAI).GetField(s.Current).GetValue(this))); - } - toEleven = UpToEleven; - } - } - - IEnumerator setVar(string name, float value) - { - yield return new WaitForFixedUpdate(); - typeof(BDModuleSurfaceAI).GetField(name).SetValue(this, value); - } - - protected override void OnGUI() - { - base.OnGUI(); - - if (!pilotEnabled || !vessel.isActiveVessel) return; - - if (!BDArmorySettings.DRAW_DEBUG_LINES) return; - if (command == PilotCommands.Follow) - { - BDGUIUtils.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); - - pathingMatrix.DrawDebug(vessel.CoM, waypoints); - } - - #endregion events - - #region Actual AI Pilot - - protected override void AutoPilot(FlightCtrlState s) - { - if (!vessel.Autopilot.Enabled) - vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); - - targetVelocity = 0; - targetDirection = vesselTransform.up; - aimingMode = false; - upDir = VectorUtils.GetUpDirection(vesselTransform.position); - DebugLine(""); - - // check if we should be panicking - if (!PanicModes()) - { - // pilot logic figures out what we're supposed to be doing, and sets the base state - PilotLogic(); - // situational awareness modifies the base as best as it can (evasive mainly) - Tactical(); - } - - AttitudeControl(s); // move according to our targets - AdjustThrottle(targetVelocity); // set throttle according to our targets and movement - } - - void PilotLogic() - { - // 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) continue; - if (!vs.Current.LandedOrSplashed || vs.Current.FindPartModuleImplementing()?.commandLeader?.vessel == vessel - || vs.Current.GetTotalMass() < AvoidMass) - 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 : CruiseSpeed; - 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?.vessel) - || (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?.selectedWeapon 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, MaxSlopeAngle, 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) / (CruiseSpeed * 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; - 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 - && (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; - 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 = CruiseSpeed / 10 + (MaxSpeed - CruiseSpeed / 10) * (distance - MinEngagementRange) / (MaxEngagementRange - MinEngagementRange); //slow down if inside engagement range to extend shooting opportunities - 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 ? CruiseSpeed / 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 : 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; - } - - cycleWaypoint(); - - SetStatus($"Not doing anything in particular"); - targetDirection = vesselTransform.up; - } - - void Tactical() - { - // enable RCS if we're in combat - vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, weaponManager && targetVessel && !BDArmorySettings.PEACE_MODE - && (weaponManager.selectedWeapon != null || (vessel.CoM - targetVessel.CoM).sqrMagnitude < MaxEngagementRange * MaxEngagementRange) - || weaponManager.underFire || weaponManager.missileIsIncoming); - - // if weaponManager thinks we're under fire, do the evasive dance - if (weaponManager.underFire || weaponManager.missileIsIncoming) - { - targetVelocity = MaxSpeed; - if (weaponManager.underFire || weaponManager.incomingMissileDistance < 2500) - { - if (Mathf.Abs(weaveAdjustment) + Time.deltaTime * weaveFactor > weaveLimit) weaveDirection *= -1; - weaveAdjustment += weaveFactor * weaveDirection * Time.deltaTime; - } - else - { - weaveAdjustment = 0; - } - } - else - { - weaveAdjustment = 0; - } - DebugLine($"underFire {weaponManager.underFire}, weaveAdjustment {weaveAdjustment}"); - } - - bool PanicModes() - { - if (!vessel.LandedOrSplashed) - { - targetVelocity = 0; - targetDirection = Vector3.ProjectOnPlane(vessel.srf_velocity, upDir); - SetStatus("Airtime!"); - return true; - } - else if (vessel.Landed - && !vessel.Splashed // I'm looking at you, Kerbal Konstructs. (When launching directly into water, KK seems to set both vessel.Landed and vessel.Splashed to true.) - && (SurfaceType & AIUtils.VehicleMovementType.Land) == 0) - { - targetVelocity = 0; - SetStatus("Stranded"); - return true; - } - else if (vessel.Splashed && (SurfaceType & AIUtils.VehicleMovementType.Water) == 0) - { - targetVelocity = 0; - SetStatus("Floating"); - return true; - } - return false; - } - - void AdjustThrottle(float targetSpeed) - { - targetVelocity = Mathf.Clamp(targetVelocity, 0, MaxSpeed); - - 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."); - } - else - DebugLine($"Target velocity: {targetVelocity}"); - DebugLine($"engine thrust: {speedController.debugThrust}, motor zero: {motorControl.zeroPoint}"); - - speedController.targetSpeed = motorControl.targetSpeed = targetSpeed; - speedController.useBrakes = motorControl.preventNegativeZeroPoint = speedController.debugThrust > 0; - } - - void AttitudeControl(FlightCtrlState s) - { - const float terrainOffset = 5; - - Vector3 yawTarget = Vector3.ProjectOnPlane(targetDirection, vesselTransform.forward); - - // limit "aoa" if we're moving - float driftMult = 1; - if (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}"); - - 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)); - - if (ManeuverRCS && (Mathf.Abs(s.roll) >= 1 || Mathf.Abs(s.pitch) >= 1 || Mathf.Abs(s.yaw) >= 1)) - vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); - } - - #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"); - else if (vessel.Landed && (SurfaceType & AIUtils.VehicleMovementType.Land) == 0) - DebugLine(vessel.vesselName + " cannot engage: vehicle not on land"); - else if (!vessel.LandedOrSplashed) - 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 - return true; - return false; - } - - public override bool IsValidFixedWeaponTarget(Vessel target) - => !BroadsideAttack && - (((target?.Splashed ?? false) && (SurfaceType & AIUtils.VehicleMovementType.Water) != 0) - || ((target?.Landed ?? false) && (SurfaceType & AIUtils.VehicleMovementType.Land) != 0)) - ; //valid if can traverse the same medium and using bow fire - - /// null if no collision, dodge vector if one detected - Vector3? PredictCollisionWithVessel(Vessel v, float maxTime, float interval) - { - //evasive will handle avoiding missiles - if (v == weaponManager.incomingMissileVessel - || v.rootPart.FindModuleImplementing() != null) - return null; - - float time = Mathf.Min(0.5f, maxTime); - while (time < maxTime) - { - Vector3 tPos = v.PredictPosition(time); - Vector3 myPos = vessel.PredictPosition(time); - if (Vector3.SqrMagnitude(tPos - myPos) < 2500f) - { - return Vector3.Dot(tPos - myPos, vesselTransform.right) > 0 ? -vesselTransform.right : vesselTransform.right; - } - - time = Mathf.MoveTowards(time, maxTime, interval); - } - - return null; - } - - void checkBypass(Vessel target) - { - if (!pathingMatrix.TraversableStraightLine( - VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), - VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), - vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass)) - { - bypassTarget = target; - bypassTargetPos = VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody); - waypoints = 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]; - else - bypassTarget = null; - } - } - - private void Pathfind(Vector3 destination) - { - waypoints = pathingMatrix.Pathfind( - VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), - destination, vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass); - intermediatePositionGeo = waypoints[0]; - } - - void cycleWaypoint() - { - if (waypoints.Count > 1) - { - waypoints.RemoveAt(0); - intermediatePositionGeo = waypoints[0]; - } - else if (bypassTarget != null) - { - waypoints.Clear(); - bypassTarget = null; - leftPath = true; - } - } - - #endregion Autopilot helper functions - - #region WingCommander - - Vector3 GetFormationPosition() - { - return commandLeader.vessel.CoM + Quaternion.LookRotation(commandLeader.vessel.up, upDir) * this.GetLocalFormationPosition(commandFollowIndex); - } - - #endregion WingCommander - } -} diff --git a/BDArmory/Modules/KerbalSafety.cs b/BDArmory/Modules/KerbalSafety.cs new file mode 100644 index 000000000..1e5958928 --- /dev/null +++ b/BDArmory/Modules/KerbalSafety.cs @@ -0,0 +1,847 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Modules +{ + public enum KerbalSafetyLevel { Off, Partial, Full }; + // A class to manage the safety of kerbals in BDA. + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class KerbalSafetyManager : MonoBehaviour + { + #region Definitions + static public KerbalSafetyManager Instance; // static instance for dealing with global stuff. + + public Dictionary kerbals = new Dictionary(); // The kerbals being managed. + List evaKerbalsToMonitor = new List(); + bool isEnabled = false; + public Vessel activeVesselBeforeEject = null; + public KerbalSafetyLevel safetyLevel { get { return (KerbalSafetyLevel)BDArmorySettings.KERBAL_SAFETY; } } + #endregion + + public void Awake() + { + if (Instance != null) + Destroy(Instance); + Instance = this; + } + + public void Start() + { + Debug.Log($"[BDArmory.KerbalSafety]: Safety manager started with level {safetyLevel}, but currently disabled."); + } + + public void OnDestroy() + { + DisableKerbalSafety(); + } + + public void HandleSceneChange(GameEvents.FromToAction fromTo) + { + if (fromTo.from == GameScenes.FLIGHT) + { + DisableKerbalSafety(); + } + } + + public void EnableKerbalSafety() + { + if (safetyLevel == KerbalSafetyLevel.Off) return; + if (isEnabled) return; + isEnabled = true; + Debug.Log("[BDArmory.KerbalSafety]: Enabling kerbal safety."); + foreach (var ks in kerbals.Values) + ks.AddHandlers(); + GameEvents.onVesselSOIChanged.Add(EatenByTheKraken); + GameEvents.onGameSceneSwitchRequested.Add(HandleSceneChange); + GameEvents.onVesselGoOffRails.Add(CheckVesselForKerbals); + GameEvents.onVesselSwitching.Add(OnVesselSwitch); + CheckAllVesselsForKerbals(); // Check for new vessels that were added while we weren't active. + } + + public void DisableKerbalSafety() + { + if (!isEnabled) return; + isEnabled = false; + Debug.Log("[BDArmory.KerbalSafety]: Disabling kerbal safety."); + foreach (var ks in kerbals.Values.ToList()) StopManagingKerbal(ks); + kerbals.Clear(); + GameEvents.onVesselSOIChanged.Remove(EatenByTheKraken); + GameEvents.onGameSceneSwitchRequested.Remove(HandleSceneChange); + GameEvents.onVesselGoOffRails.Remove(CheckVesselForKerbals); + GameEvents.onVesselSwitching.Remove(OnVesselSwitch); + } + + public void CheckAllVesselsForKerbals() + { + if (isEnabled) + { + newKerbalsAwaitingCheck.Clear(); + evaKerbalsToMonitor.Clear(); + foreach (var vessel in FlightGlobals.Vessels) + { + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.vesselType)) continue; + CheckVesselForKerbals(vessel); + } + } + else + { + EnableKerbalSafety(); + } + } + + public void CheckVesselForKerbals(Vessel vessel) + { + if (safetyLevel == KerbalSafetyLevel.Off) return; + if (vessel == null) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Checking {vessel.vesselName} for kerbals."); + 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; + if (kerbals.ContainsKey(crew.displayName)) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {crew.displayName} is already managed."); + continue; // Already managed. + } + KerbalSafety ks = null; + var ksList = part.gameObject.GetComponents(); + foreach (var k in ksList) { if (k.kerbalName == crew.name) { ks = k; break; } } + if (ks == null) { ks = part.gameObject.AddComponent(); } + StartCoroutine(ks.Configure(crew, part)); + } + } + } + + public void StopManagingKerbal(KerbalSafety ks) + { + if (ks == null || !kerbals.ContainsKey(ks.kerbalName)) return; + ks.recovered = true; + kerbals.Remove(ks.kerbalName); + Destroy(ks); + } + + HashSet newKerbalsAwaitingCheck = new HashSet(); + public void ManageNewlyEjectedKerbal(KerbalEVA kerbal, Vector3 velocity) + { + if (newKerbalsAwaitingCheck.Contains(kerbal)) return; + newKerbalsAwaitingCheck.Add(kerbal); + StartCoroutine(ManageNewlyEjectedKerbalCoroutine(kerbal)); + StartCoroutine(ManuallyMoveKerbalEVACoroutine(kerbal, velocity, 2f)); + if (activeVesselBeforeEject != null && activeVesselBeforeEject != FlightGlobals.ActiveVessel) { LoadedVesselSwitcher.Instance.ForceSwitchVessel(activeVesselBeforeEject); } + } + + IEnumerator ManageNewlyEjectedKerbalCoroutine(KerbalEVA kerbal) + { + var kerbalName = kerbal.vessel.vesselName; + var wait = new WaitForFixedUpdate(); + while (kerbal != null && !kerbal.Ready) yield return wait; + if (kerbal != null && kerbal.vessel != null) + { + CheckVesselForKerbals(kerbal.vessel); + newKerbalsAwaitingCheck.Remove(kerbal); + } + else + { + Debug.LogWarning("[BDArmory.KerbalSafety]: " + kerbalName + " disappeared before we could start managing them."); + } + } + + /// + /// The flight integrator doesn't seem to update the EVA kerbal's position or velocity for about 0.95s of real-time for some unknown reason (this seems fairly constant regardless of time-control or FPS). + /// + /// The kerbal on EVA. + /// The amount of real-time to manually update for. + IEnumerator ManuallyMoveKerbalEVACoroutine(KerbalEVA kerbal, Vector3 velocity, float realTime = 1f) + { + 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); + float verticalSpeedAdjustment = 0f; + var wait = new WaitForFixedUpdate(); + var position = kerbal.vessel.GetWorldPos3D(); + if (kerbal.vessel.radarAltitude + verticalSpeed * Time.fixedDeltaTime < 2f) // Crashed into terrain, explode upwards. + { + 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; + } + else + { + velocity += 1.5f * -(gee + UnityEngine.Random.onUnitSphere * 0.3f * 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.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) + { + // Note: 0.968f gives a reduction in speed to ~20% over 1s. + if (verticalSpeed < 0f && kerbal.vessel.radarAltitude + verticalSpeed * (realTime - (Time.realtimeSinceStartup - startTime)) < 100f) + { + velocity = velocity * 0.968f + 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.DEBUG_OTHER) verticalSpeedAdjustment = Vector3.Dot(-gee.normalized, gee * Time.fixedDeltaTime); + } + verticalSpeed = Vector3.Dot(-gee.normalized, velocity); + position += velocity * Time.fixedDeltaTime; + if (BDKrakensbane.IsActive) + { + position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + kerbal.vessel.IgnoreGForces(1); + kerbal.vessel.IgnoreSpeed(1); + kerbal.vessel.SetPosition(position); + kerbal.vessel.SetWorldVelocity(velocity); + yield return wait; + if (activeVesselBeforeEject != null && activeVesselBeforeEject != FlightGlobals.ActiveVessel) { LoadedVesselSwitcher.Instance.ForceSwitchVessel(activeVesselBeforeEject); } + 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.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: " + !BDKrakensbane.FloatingOriginOffset.IsZero() + ", frameVel: " + !Krakensbane.GetFrameVelocity().IsZero() + ")" + " " + BDKrakensbane.FrameVelocityV3f.ToString("0.0") + ", corr: " + Krakensbane.GetLastCorrection().ToString("0.0")); + } + } + } + + /// + /// Register all the crew members as recovered, then recover the vessel. + /// + /// The vessel to recover. + public void RecoverVesselNow(Vessel vessel) + { + foreach (var part in vessel.parts.ToList()) + { + foreach (var crew in part.protoModuleCrew.ToList()) + { + if (kerbals.ContainsKey(crew.displayName)) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: Recovering " + kerbals[crew.displayName].kerbalName + "."); + StopManagingKerbal(kerbals[crew.displayName]); + } + } + } + if (vessel.protoVessel != null) + { + try + { + if (vessel != null) + foreach (var part in vessel.Parts.Where(p => p != null).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}"); + } + } + } + + /// + /// Vessels leaving the SoI can cause the Kraken to break KSP if they're switched to, so we detect and remove such vessels. + /// + /// + void EatenByTheKraken(GameEvents.HostedFromToAction fromTo) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return; + string vesselName = fromTo.host.vesselName; + if (fromTo.host.isActiveVessel) + { + foreach (var wm in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value)) + { + if (wm == null || wm.vessel == null) continue; + if (wm.vessel.mainBody == fromTo.from) + { + Debug.LogWarning($"[BDArmory.KerbalSafety]: {vesselName} was the active vessel, switching away to avoid the Kraken. Force-switching to {wm.vessel.vesselName}."); + LoadedVesselSwitcher.Instance.ForceSwitchVessel(wm.vessel); // Switch to the first vessel in the SoI that this vessel came from. + break; + } + } + } + if (evaKerbalsToMonitor.Where(k => k != null).Select(k => k.vessel).Contains(fromTo.host)) + { + var message = $"{vesselName} got eaten by the Kraken!"; + Debug.LogWarning("[BDArmory.KerbalSafety]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + fromTo.host.gameObject.SetActive(false); + evaKerbalsToMonitor.Remove(evaKerbalsToMonitor.Find(k => k.vessel == fromTo.host)); + fromTo.host.Die(); + } + else + { + if (fromTo.host != null && fromTo.host.loaded && !GameModes.AsteroidUtils.IsManagedAsteroid(fromTo.host)) + { + Debug.LogWarning($"[BDArmory.KerbalSafety]: {fromTo.host.GetName()} got eaten by the Kraken!"); + fromTo.host.gameObject.SetActive(false); + fromTo.host.Die(); + } + } + StartCoroutine(FeedTheKraken()); + } + + /// + /// During this frame and the next, remove debris that's left the SoI. + /// + IEnumerator FeedTheKraken() + { + CheckForDebrisBeyondTheSoI(); + yield return new WaitForFixedUpdate(); + CheckForDebrisBeyondTheSoI(); + } + + /// + /// Clean up debris that's left the current SoI. + /// + /// Note: Occasionally, a "won't fix" bug in Unity's reuse of Transforms causes a series of errors of the form + /// Infinity or NaN floating point numbers appear when calculating the transform matrix for a Collider. + /// to appear in the logs due to performing raycasting operations. These usually disappear once the part with the broken collider gets destroyed. + /// + void CheckForDebrisBeyondTheSoI() + { + foreach (var vessel in FlightGlobals.Vessels.ToList()) // Look for and remove any debris that might also have been Kraken'd. + { + if (vessel.vesselType == VesselType.Debris && vessel.mainBody != FlightGlobals.currentMainBody) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Feeding {vessel.vesselName} ({(vessel.rootPart != null ? vessel.rootPart.partInfo.name : "")}) to the Kraken."); + RecoverVesselNow(vessel); + } + else + { + List badColliders = []; + foreach (var part in vessel.Parts) + { + foreach (var collider in part.GetPartColliders()) + { + if (collider.enabled && collider.bounds.size.sqrMagnitude == 0) + { + // Debug.Log($"DEBUG Part collider {collider} on {part.partInfo.name} on {vessel.vesselName} has bounds of magnitude 0!"); + badColliders.Add(collider); + } + } + foreach (var collider in badColliders) Destroy(collider); + badColliders.Clear(); + } + } + } + } + + void OnVesselSwitch(Vessel from, Vessel to) + { + var weaponManagers = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList(); + if (to != null && weaponManagers.Contains(to.ActiveController().WM)) // New vessel is an active competitor. + { + activeVesselBeforeEject = to; + } + else if (from != null && weaponManagers.Contains(from.ActiveController().WM)) // Old vessel is an active competitor. + { + activeVesselBeforeEject = from; + } + } + + public void ReconfigureInventories() + { + if (isEnabled) + { + foreach (var kerbal in kerbals.Values) + { kerbal.ReconfigureInventory(); } + } + } + } + + public class KerbalSafety : MonoBehaviour + { + #region Definitions + public string kerbalName; // The name of the kerbal/crew member. + public KerbalEVA kerbalEVA; // For kerbals that have ejected or are sitting in command seats. + public ProtoCrewMember crew; // For kerbals that are in cockpits. + public Part part; // The part the proto crew member is in. + public KerbalSeat seat; // The seat the kerbalEVA is in (if they're in one). + public ModuleEvaChute chute; // The chute of the crew member. + // public BDModulePilotAI ai; // The pilot AI. + public bool recovering = false; // Whether they're scheduled for recovery or not. + public bool recovered = false; // Whether they've been recovered or not. + public bool deployingChute = false; // Whether they're scheduled for deploying their chute or not. + public bool ejected = false; // Whether the kerbal has ejected or not. + public bool leavingSeat = false; // Whether the kerbal is about to leave their seat. + private string message; + #endregion + + #region Field definitions + // [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_EjectOnImpendingDoom", // Eject if doomed + // groupName = "pilotAI_Ejection", groupDisplayName = "#LOC_BDArmory_AI_Ejection", groupStartCollapsed = true), + // UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.02f, scene = UI_Scene.All)] + // public float ejectOnImpendingDoom = 0.2f; // Time to impact at which to eject. + #endregion + + /// + /// Begin managing a crew member in a part. + /// + /// The proto crew member. + /// The part. + public IEnumerator Configure(ProtoCrewMember c, Part p) + { + if (c == null) + { + Debug.LogError("[BDArmory.KerbalSafety]: Cannot manage null crew."); + Destroy(this); + yield break; + } + if (p == null) + { + Debug.LogError("[BDArmory.KerbalSafety]: Crew cannot exist outside of a part."); + Destroy(this); + yield break; + } + 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 || 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; + } + crew = c; + switch (BDArmorySettings.KERBAL_SAFETY_INVENTORY) + { + case 1: + crew.ResetInventory(true); // Reset the inventory to the default of a chute and a jetpack. + break; + case 2: + crew.ResetInventory(false); // Reset the inventory to just a chute. + break; + } + part = p; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Configuring KerbalSafety for {kerbalName} in {part.partInfo.name}"); + if (p.IsKerbalEVA()) + { + kerbalEVA = p.GetComponent(); + if (kerbalEVA.IsSeated()) + { + bool found = false; + foreach (var s in VesselModuleRegistry.GetModules(p.vessel)) + { + if (s.Occupant == p) + { + seat = s; + found = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} in part {part.partInfo.name} of {part.vessel.vesselName} found in seat {seat.part.partInfo.name}"); + break; + } + } + if (!found) + { + Debug.LogWarning("[BDArmory.KerbalSafety]: Failed to find the kerbal seat that " + kerbalName + " occupies."); + ejected = true; + StartCoroutine(DelayedChuteDeployment()); + StartCoroutine(RecoverWhenPossible()); + } + } + 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()); + } + ConfigureKerbalEVA(kerbalEVA); + } + AddHandlers(); + KerbalSafetyManager.Instance.kerbals.Add(kerbalName, this); + 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 (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() + { + if (BDArmorySettings.KERBAL_SAFETY_INVENTORY == 0) return; + if (crew != null) crew.ResetInventory(BDArmorySettings.KERBAL_SAFETY_INVENTORY == 1); + if (kerbalEVA != null) ConfigureKerbalEVA(kerbalEVA); + } + + private void DisableConstructionMode(KerbalEVA kerbalEVA) + { + if (kerbalEVA.InConstructionMode) + kerbalEVA.InConstructionMode = false; + } + + 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.DEBUG_OTHER) + { + Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} is MIA. Ejected: {ejected}, deployed chute: {deployingChute}."); + } + if (KerbalSafetyManager.Instance) KerbalSafetyManager.Instance.StopManagingKerbal(this); + RemoveHandlers(); // Make sure the handlers get removed. + } + + /// + /// Add various event handlers. + /// + public void AddHandlers() + { + if (kerbalEVA) + { + if (seat && seat.part) + seat.part.OnJustAboutToDie += Eject; + } + else + { + if (part) + part.OnJustAboutToDie += Eject; + } + GameEvents.onVesselPartCountChanged.Add(OnVesselModified); + GameEvents.onVesselCreate.Add(OnVesselModified); + GameEvents.onVesselGoOnRails.Add(OnGoOnRails); + } + + /// + /// Remove the event handlers. + /// + public void RemoveHandlers() + { + if (part) part.OnJustAboutToDie -= Eject; + if (seat && seat.part) seat.part.OnJustAboutToDie -= Eject; + GameEvents.onVesselPartCountChanged.Remove(OnVesselModified); + GameEvents.onVesselCreate.Remove(OnVesselModified); + GameEvents.onVesselGoOnRails.Remove(OnGoOnRails); + } + + public void OnGoOnRails(Vessel vessel) + { + if (vessel != part.vessel) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {vessel.vesselName} went on rails, no longer managing {kerbalName}."); + KerbalSafetyManager.Instance.StopManagingKerbal(this); + } + + // FIXME to be part of an update loop (maybe) + // void EjectOnImpendingDoom() + // { + // if (!ejected && ejectOnImpendingDoom * (float)vessel.srfSpeed > ai.terrainAlertDistance) + // { + // KerbalSafety.Instance.Eject(vessel, this); // Abandon ship! + // ai.avoidingTerrain = false; + // } + // } + + #region Ejection + /// + /// Eject from a vessel. + /// + 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.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.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.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} has already left their seat."); + } + StartCoroutine(DelayedChuteDeployment()); + StartCoroutine(RecoverWhenPossible()); + } + } + else if (crew != null && part.protoModuleCrew.Contains(crew) && !FlightEVA.hatchInsideFairing(part)) // Eject from a cockpit. + { + if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Full) return; + if (!ProcessEjection(part)) // All exits were blocked by something. + { + // if (!EjectFromOtherPart()) // Look for other airlocks to spawn from. + // { + message = kerbalName + " failed to eject from " + part.vessel.vesselName + ", all exits were blocked. R.I.P."; + // BDACompetitionMode.Instance.competitionStatus.Add(message); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: " + message); + // } + } + } + else + { + Debug.LogWarning("[BDArmory.KerbalSafety]: Ejection called without a kerbal present."); + } + ejected = true; + } + + private bool EjectFromOtherPart() + { + Part fromPart = part; + foreach (var toPart in part.vessel.parts) + { + if (toPart == part) continue; + if (toPart.CrewCapacity > 0 && !FlightEVA.hatchInsideFairing(toPart) && !FlightEVA.HatchIsObstructed(toPart, toPart.airlock)) + { + var crewTransfer = CrewTransfer.Create(fromPart, crew, OnDialogDismiss); + if (crewTransfer != null && crewTransfer.validParts.Contains(toPart)) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Transferring {kerbalName} from {fromPart} to {toPart} then ejecting."); + crewTransfer.MoveCrewTo(toPart); + if (ProcessEjection(toPart)) + return true; + fromPart = toPart; + } + } + } + return false; + } + + private void OnDialogDismiss(PartItemTransfer.DismissAction arg1, Part arg2) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(arg1); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(arg2); + } + + private bool ProcessEjection(Part fromPart) + { + kerbalEVA = FlightEVA.fetch.spawnEVA(crew, fromPart, fromPart.airlock, true); + if (KerbalSafetyManager.Instance.activeVesselBeforeEject != null && KerbalSafetyManager.Instance.activeVesselBeforeEject != FlightGlobals.ActiveVessel) { LoadedVesselSwitcher.Instance.ForceSwitchVessel(KerbalSafetyManager.Instance.activeVesselBeforeEject); } + if (kerbalEVA != null && kerbalEVA.vessel != null) + { + CameraManager.Instance.SetCameraFlight(); + if (crew != null && crew.KerbalRef != null) + { + crew.KerbalRef.state = Kerbal.States.BAILED_OUT; + fromPart.vessel.RemoveCrew(crew); + } + 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); + KerbalSafetyManager.Instance.ManageNewlyEjectedKerbal(kerbalEVA, fromPart.vessel.Velocity()); + recovered = true; + OnDestroy(); + return true; + } + else + { + return false; + } + } + + /// + /// Check various conditions when this vessel gets modified. + /// + /// The vessel that was modified. + public void OnVesselModified(Vessel vessel) + { + if (this == null) return; + if (part == null || vessel == null || !vessel.loaded || part.vessel != vessel) return; + if (kerbalEVA != null) + { + if (kerbalEVA.isActiveAndEnabled) + { + switch (vessel.parts.Count) + { + 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.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} was not active (probably dead and being cleaned up by KSP already)."); + KerbalSafetyManager.Instance.StopManagingKerbal(this); + } + } + else // It's a crew. + { + // FIXME Check if the crew needs to eject. + ejected = false; // Reset ejected flag as failure to eject may have changed due to the vessel modification. + } + } + + /// + /// Parachute deployment. + /// + /// Delay before deploying the chute + IEnumerator DelayedChuteDeployment(float delay = 1f) + { + if (deployingChute) + { + 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 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.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(); + } + 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); + } + + /// + /// Leave seat after a short delay. + /// + /// Delay before leaving seat. + IEnumerator DelayedLeaveSeat(float delay = 3f) + { + if (leavingSeat) + { + yield break; + } + leavingSeat = true; + 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.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()); + StartCoroutine(RecoverWhenPossible()); + } + } + + /// + /// Recover the kerbal when possible (has landed and isn't the active vessel). + /// + /// Don't wait until the kerbal has landed. + public IEnumerator RecoverWhenPossible(bool asap = false) + { + if (asap) + { + if (KerbalSafetyManager.Instance.kerbals.ContainsKey(kerbalName)) + KerbalSafetyManager.Instance.kerbals.Remove(kerbalName); // Stop managing this kerbal. + } + if (recovering) + { + yield break; + } + recovering = true; + if (!asap) + { + 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 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.DEBUG_OTHER) Debug.LogError($"[BDArmory.KerbalSafety]: {kerbalName} on EVA is MIA."); + yield break; + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Recovering {kerbalName}."); + recovered = 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 + } +} \ No newline at end of file diff --git a/BDArmory/Modules/MissileBase.cs b/BDArmory/Modules/MissileBase.cs deleted file mode 100644 index 48d0bc048..000000000 --- a/BDArmory/Modules/MissileBase.cs +++ /dev/null @@ -1,1126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.CounterMeasure; -using BDArmory.Control; -using BDArmory.FX; -using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Radar; -using BDArmory.Targeting; -using BDArmory.UI; -using UnityEngine; - -namespace BDArmory.Modules -{ - public abstract class MissileBase : EngageableWeapon, IBDWeapon - { - // High Speed missile fix - /// ////////////////////////////////// - [KSPField(isPersistant = true)] - public float DetonationOffset = 0.1f; - - [KSPField(isPersistant = true)] - public bool autoDetCalc = false; - /// ////////////////////////////////// - - protected WeaponClasses weaponClass; - - public WeaponClasses GetWeaponClass() - { - return weaponClass; - } - - public string GetMissileType() - { - return missileType; - } - - [KSPField] - public string missileType = "missile"; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxStaticLaunchRange"), UI_FloatRange(minValue = 5000f, maxValue = 50000f, stepIncrement = 1000f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Max Static Launch Range - public float maxStaticLaunchRange = 5000; - - [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; - - [KSPField] - public float minLaunchSpeed = 0; - - public virtual float ClearanceRadius => 0.14f; - - public virtual float ClearanceLength => 0.14f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxOffBoresight"),//Max Off Boresight - UI_FloatRange(minValue = 0f, maxValue = 360f, stepIncrement = 5f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)] - public float maxOffBoresight = 360; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DetonationDistanceOverride"), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Detonation distance override - public float DetonationDistance = -1; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DetonateAtMinimumDistance"), // Detonate At Minumum Distance - UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] - public bool DetonateAtMinimumDistance = false; - - //[KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "SLW Offset"), UI_FloatRange(minValue = -1000f, maxValue = 0f, stepIncrement = 100f, affectSymCounterparts = UI_Scene.All)] - public float SLWOffset = 0; - - public float getSWLWOffset - { - get - { - return SLWOffset; - } - } - - [KSPField] - public bool guidanceActive = true; - - [KSPField] - public float lockedSensorFOV = 2.5f; - - [KSPField] - public float heatThreshold = 150; - - [KSPField] - public bool allAspect = false; - - [KSPField] - public bool isTimed = false; - - [KSPField] - public bool radarLOAL = false; - - [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)] - public float dropTime = 0.5f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_InCargoBay"),//In Cargo Bay: - UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true", affectSymCounterparts = UI_Scene.All)]//False--True - public bool inCargoBay = false; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_DetonationTime"),//Detonation Time - UI_FloatRange(minValue = 2f, maxValue = 30f, stepIncrement = 0.5f, scene = UI_Scene.Editor)] - public float detonationTime = 2; - - [KSPField] - public float activeRadarRange = 6000; - - [Obsolete("Use activeRadarLockTrackCurve!")] - [KSPField] - public float activeRadarMinThresh = 140; - - [KSPField] - public FloatCurve activeRadarLockTrackCurve = new FloatCurve(); // floatcurve to define min/max range and lockable radar cross section - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BallisticOvershootFactor"),//Ballistic Overshoot factor - UI_FloatRange(minValue = 0.5f, maxValue = 1.5f, stepIncrement = 0.01f, scene = UI_Scene.Editor)] - public float BallisticOverShootFactor = 0.7f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BallisticAnglePath"),//Ballistic Angle path - UI_FloatRange(minValue = 5f, maxValue = 60f, stepIncrement = 5f, scene = UI_Scene.Editor)] - public float BallisticAngle = 45.0f; - - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CruiseAltitude"), UI_FloatRange(minValue = 1f, maxValue = 500f, stepIncrement = 10f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Cruise Altitude - public float CruiseAltitude = 500; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CruiseSpeed"), UI_FloatRange(minValue = 100f, maxValue = 6000f, stepIncrement = 50f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Cruise speed - public float CruiseSpeed = 300; - - [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] - public float missileRadarCrossSection = RadarUtils.RCS_MISSILES; // radar cross section of this missile for detection purposes - - public enum MissileStates { Idle, Drop, Boost, Cruise, PostThrust } - - public enum DetonationDistanceStates { NotSafe, Cruising, CheckingProximity, Detonate } - - public enum TargetingModes { None, Radar, Heat, Laser, Gps, AntiRad } - - public MissileStates MissileState { get; set; } = MissileStates.Idle; - - public DetonationDistanceStates DetonationDistanceState { get; set; } = DetonationDistanceStates.NotSafe; - - public enum GuidanceModes { None, AAMLead, AAMPure, AGM, AGMBallistic, Cruise, STS, Bomb, RCS, BeamRiding, SLW } - - public GuidanceModes GuidanceMode; - - public bool HasFired { get; set; } = false; - - public BDTeam Team { get; set; } - - public bool HasMissed { get; set; } = false; - - public Vector3 TargetPosition { get; set; } = Vector3.zero; - - public Vector3 TargetVelocity { get; set; } = Vector3.zero; - - public Vector3 TargetAcceleration { get; set; } = Vector3.zero; - - public float TimeIndex => Time.time - TimeFired; - - public TargetingModes TargetingMode { get; set; } - - public TargetingModes TargetingModeTerminal { get; set; } - - public float TimeToImpact { get; set; } - - public bool TargetAcquired { get; set; } - - public bool ActiveRadar { get; set; } - - public Vessel SourceVessel { get; set; } = null; - - public bool HasExploded { get; set; } = false; - - protected IGuidance _guidance; - - private double _lastVerticalSpeed; - private double _lastHorizontalSpeed; - - public double HorizontalAcceleration - { - get - { - var result = (vessel.horizontalSrfSpeed - _lastHorizontalSpeed); - _lastHorizontalSpeed = vessel.horizontalSrfSpeed; - return result; - - } - } - - public double VerticalAcceleration - { - get - { - var result = (vessel.horizontalSrfSpeed - _lastHorizontalSpeed); - _lastVerticalSpeed = vessel.verticalSpeed; - return result; - } - } - - - - public float Throttle - { - get - { - return _throttle; - } - - set - { - _throttle = Mathf.Clamp01(value); - } - } - - public float TimeFired = -1; - - protected float lockFailTimer = -1; - - public Vessel legacyTargetVessel; - - public Transform MissileReferenceTransform; - - protected ModuleTargetingCamera targetingPod; - - //laser stuff - public ModuleTargetingCamera lockedCamera; - protected Vector3 lastLaserPoint; - protected Vector3 laserStartPosition; - protected Vector3 startDirection; - - //GPS stuff - public Vector3d targetGPSCoords; - - //heat stuff - public TargetSignatureData heatTarget; - private TargetSignatureData predictedHeatTarget; - - //radar stuff - public VesselRadarData vrd; - public TargetSignatureData radarTarget; - private TargetSignatureData[] scannedTargets; - public MissileFire TargetMf = null; - private LineRenderer LR; - - private int snapshotTicker; - private int locksCount = 0; - private float _radarFailTimer = 0; - private float maxRadarFailTime = 5; - private float lastRWRPing = 0; - private bool radarLOALSearching = false; - protected bool checkMiss = false; - public StringBuilder debugString = new StringBuilder(); - - private float _throttle = 1f; - Vector3 previousPos; - - public string Sublabel; - public int missilecount = 0; //#191 - - 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()) - 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++; - } - } - - public string GetSubLabel() - { - return Sublabel = "Guidance: " + Enum.GetName(typeof(TargetingModes), TargetingMode) + "; Remaining: " + missilecount; // - } - - public Part GetPart() - { - return part; - } - - public abstract void FireMissile(); - - public abstract void Jettison(); - - public abstract float GetBlastRadius(); - - protected abstract void PartDie(Part p); - - protected void DisablingExplosives(Part p) - { - if (p == null) return; - - var explosive = p.FindModuleImplementing(); - if (explosive != null) - { - p.FindModuleImplementing().Armed = false; - } - } - - protected void SetupExplosive(Part p) - { - if (p == null) return; - - var explosive = p.FindModuleImplementing(); - if (explosive != null) - { - p.FindModuleImplementing().Armed = true; - if (GuidanceMode == GuidanceModes.AGM || GuidanceMode == GuidanceModes.AGMBallistic) - { - p.FindModuleImplementing().Shaped = true; - } - } - } - - public abstract void Detonate(); - - public abstract Vector3 GetForwardTransform(); - - protected void AddTargetInfoToVessel() - { - TargetInfo info = vessel.gameObject.AddComponent(); - info.Team = Team; - info.isMissile = true; - info.MissileBaseModule = this; - } - - [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_GPSTarget", active = true, name = "GPSTarget")]//GPS Target - public void assignGPSTarget() - { - if (HighLogic.LoadedSceneIsFlight) - PickGPSTarget(); - } - - [KSPField(isPersistant = true)] - public bool gpsSet = false; - - [KSPField(isPersistant = true)] - public Vector3 assignedGPSCoords; - - [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_GPSTarget")]//GPS Target - public string gpsTargetName = ""; - - - - void PickGPSTarget() - { - gpsSet = true; - Fields["gpsTargetName"].guiActive = true; - gpsTargetName = BDArmorySetup.Instance.ActiveWeaponManager.designatedGPSInfo.name; - assignedGPSCoords = BDArmorySetup.Instance.ActiveWeaponManager.designatedGPSCoords; - } - - public Vector3d UpdateGPSTarget() - { - Vector3 gpsTargetCoords_; - - if (gpsSet && assignedGPSCoords != null) - { - gpsTargetCoords_ = assignedGPSCoords; - } - else - { - gpsTargetCoords_ = targetGPSCoords; - } - - if (TargetAcquired) - { - TargetPosition = VectorUtils.GetWorldSurfacePostion(gpsTargetCoords_, vessel.mainBody); - TargetVelocity = Vector3.zero; - TargetAcceleration = Vector3.zero; - } - else - { - guidanceActive = false; - } - - return gpsTargetCoords_; - } - - protected void UpdateHeatTarget() - { - - if (lockFailTimer > 1) - { - legacyTargetVessel = null; - TargetAcquired = false; - predictedHeatTarget.exists = false; - predictedHeatTarget.signalStrength = 0; - return; - } - - if (heatTarget.exists && lockFailTimer < 0) - { - lockFailTimer = 0; - predictedHeatTarget = heatTarget; - } - if (lockFailTimer >= 0) - { - // Decide where to point seeker - Ray lookRay; - float targetHeatScore = 0; - if (predictedHeatTarget.exists) // We have an active target we've been seeking, or a prior target that went stale - { - lookRay = new Ray(transform.position, predictedHeatTarget.position - transform.position); - targetHeatScore = predictedHeatTarget.signalStrength; - } - else if (heatTarget.exists) // We have a new active target and no prior target - { - lookRay = new Ray(transform.position, heatTarget.position + (heatTarget.velocity * Time.fixedDeltaTime) - transform.position); - targetHeatScore = heatTarget.signalStrength; - } - else // No target, look straight ahead - { - lookRay = new Ray(transform.position, vessel.srf_vel_direction); - } - - if (BDArmorySettings.DRAW_DEBUG_LINES) - DrawDebugLine(lookRay.origin, lookRay.origin + lookRay.direction * 10000, Color.magenta); - - // Update heat target - heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, lookRay, targetHeatScore, lockedSensorFOV / 2, heatThreshold, allAspect, (SourceVessel != null ? SourceVessel.gameObject?.GetComponent() : null)); // Unity messes with fake nulls and breaks ?. operators sometimes. - - if (heatTarget.exists) - { - TargetAcquired = true; - TargetPosition = heatTarget.position + (2 * heatTarget.velocity * Time.fixedDeltaTime); // Not sure why this is 2* - TargetVelocity = heatTarget.velocity; - TargetAcceleration = heatTarget.acceleration; - lockFailTimer = 0; - - // Update target information - predictedHeatTarget = heatTarget; - } - else - { - TargetAcquired = false; - if (FlightGlobals.ready) - { - lockFailTimer += Time.fixedDeltaTime; - } - } - - // Update predicted values based on target information - if (predictedHeatTarget.exists) - { - float currentFactor = (1400 * 1400) / Mathf.Clamp((predictedHeatTarget.position - transform.position).sqrMagnitude, 90000, 36000000); - Vector3 currVel = (float)vessel.srfSpeed * vessel.Velocity().normalized; - predictedHeatTarget.position = predictedHeatTarget.position + predictedHeatTarget.velocity * Time.fixedDeltaTime; - float futureFactor = (1400 * 1400) / Mathf.Clamp((predictedHeatTarget.position - (transform.position + (currVel * Time.fixedDeltaTime))).sqrMagnitude, 90000, 36000000); - predictedHeatTarget.signalStrength *= futureFactor / currentFactor; - } - - } - } - - protected void SetAntiRadTargeting() - { - if (TargetingMode == TargetingModes.AntiRad && TargetAcquired) - { - RadarWarningReceiver.OnRadarPing += ReceiveRadarPing; - } - } - - protected void SetLaserTargeting() - { - if (TargetingMode == TargetingModes.Laser) - { - laserStartPosition = MissileReferenceTransform.position; - if (lockedCamera) - { - TargetAcquired = true; - TargetPosition = lastLaserPoint = lockedCamera.groundTargetPosition; - targetingPod = lockedCamera; - } - } - } - - protected void UpdateLaserTarget() - { - if (TargetAcquired) - { - if (lockedCamera && lockedCamera.groundStabilized && !lockedCamera.gimbalLimitReached && lockedCamera.surfaceDetected) //active laser target - { - TargetPosition = lockedCamera.groundTargetPosition; - TargetVelocity = (TargetPosition - lastLaserPoint) / Time.fixedDeltaTime; - TargetAcceleration = Vector3.zero; - lastLaserPoint = TargetPosition; - - if (GuidanceMode == GuidanceModes.BeamRiding && TimeIndex > 0.25f && Vector3.Dot(GetForwardTransform(), part.transform.position - lockedCamera.transform.position) < 0) - { - TargetAcquired = false; - lockedCamera = null; - } - } - else //lost active laser target, home on last known position - { - if (CMSmoke.RaycastSmoke(new Ray(transform.position, lastLaserPoint - transform.position))) - { - //Debug.Log("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; - TargetAcceleration = Vector3.zero; - lastLaserPoint = TargetPosition; - } - else - { - TargetPosition = lastLaserPoint; - } - } - } - else - { - ModuleTargetingCamera foundCam = null; - bool parentOnly = (GuidanceMode == GuidanceModes.BeamRiding); - foundCam = BDATargetManager.GetLaserTarget(this, parentOnly); - if (foundCam != null && foundCam.cameraEnabled && foundCam.groundStabilized && BDATargetManager.CanSeePosition(foundCam.groundTargetPosition, vessel.transform.position, MissileReferenceTransform.position)) - { - Debug.Log("[BDArmory]: Laser guided missileBase actively found laser point. Enabling guidance."); - lockedCamera = foundCam; - TargetAcquired = true; - } - } - } - - protected void UpdateRadarTarget() - { - TargetAcquired = false; - - float angleToTarget = Vector3.Angle(radarTarget.predictedPosition - transform.position, GetForwardTransform()); - - 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 (vrd) - { - TargetSignatureData t = TargetSignatureData.noTarget; - List possibleTargets = vrd.GetLockedTargets(); - for (int i = 0; i < possibleTargets.Count; i++) - { - if (possibleTargets[i].vessel == radarTarget.vessel) - { - t = possibleTargets[i]; - } - } - - if (t.exists) - { - TargetAcquired = true; - radarTarget = t; - TargetPosition = radarTarget.predictedPositionWithChaffFactor; - TargetVelocity = radarTarget.velocity; - TargetAcceleration = radarTarget.acceleration; - _radarFailTimer = 0; - return; - } - else - { - if (_radarFailTimer > maxRadarFailTime) - { - Debug.Log("[BDArmory]: Semi-Active Radar guidance failed. Parent radar lost target."); - radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; - return; - } - else - { - if (_radarFailTimer == 0) - { - Debug.Log("[BDArmory]: Semi-Active Radar guidance failed - waiting for data"); - } - _radarFailTimer += Time.fixedDeltaTime; - radarTarget.timeAcquired = Time.time; - radarTarget.position = radarTarget.predictedPosition; - TargetPosition = radarTarget.predictedPositionWithChaffFactor; - TargetVelocity = radarTarget.velocity; - TargetAcceleration = Vector3.zero; - TargetAcquired = true; - } - } - } - else - { - Debug.Log("[BDArmory]: Semi-Active Radar guidance failed. Out of range and no data feed."); - radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; - return; - } - } - else - { - // active radar with target locked: - vrd = null; - - if (angleToTarget > maxOffBoresight) - { - Debug.Log("[BDArmory]: Active Radar guidance failed. Target is out of active seeker gimbal limits."); - radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; - return; - } - else - { - if (scannedTargets == null) scannedTargets = new TargetSignatureData[5]; - TargetSignatureData.ResetTSDArray(ref scannedTargets); - Ray ray = new Ray(transform.position, radarTarget.predictedPosition - transform.position); - bool pingRWR = Time.time - lastRWRPing > 0.4f; - if (pingRWR) lastRWRPing = Time.time; - bool radarSnapshot = (snapshotTicker > 10); - if (radarSnapshot) - { - snapshotTicker = 0; - } - else - { - snapshotTicker++; - } - - //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); - - if (radarLOAL && radarLOALSearching && !radarSnapshot) - { - //only scan on snapshot interval - } - else - { - for (int i = 0; i < scannedTargets.Length; i++) - { - if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - radarTarget.predictedPosition).sqrMagnitude < sqrThresh) - { - //re-check engagement envelope, only lock appropriate targets - if (CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo)) - { - radarTarget = scannedTargets[i]; - TargetAcquired = true; - radarLOALSearching = false; - TargetPosition = radarTarget.predictedPositionWithChaffFactor + (radarTarget.velocity * Time.fixedDeltaTime); - TargetVelocity = radarTarget.velocity; - TargetAcceleration = radarTarget.acceleration; - _radarFailTimer = 0; - if (!ActiveRadar && Time.time - TimeFired > 1) - { - if (locksCount == 0) - { - if (weaponClass == WeaponClasses.SLW) - RadarWarningReceiver.PingRWR(ray, lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); - else - RadarWarningReceiver.PingRWR(ray, lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); - Debug.Log("[BDArmory]: Pitbull! Radar missilebase has gone active. Radar sig strength: " + radarTarget.signalStrength.ToString("0.0")); - } - else if (locksCount > 2) - { - guidanceActive = false; - checkMiss = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Active Radar guidance failed. Radar missileBase reached max re-lock attempts."); - } - } - locksCount++; - } - ActiveRadar = true; - return; - } - } - } - } - - if (radarLOAL) - { - radarLOALSearching = true; - TargetAcquired = true; - TargetPosition = radarTarget.predictedPositionWithChaffFactor + (radarTarget.velocity * Time.fixedDeltaTime); - TargetVelocity = radarTarget.velocity; - TargetAcceleration = Vector3.zero; - ActiveRadar = false; - _radarFailTimer = 0; - } - else - { - Debug.Log("[BDArmory]: Active Radar guidance failed. No target locked."); - radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; - radarLOALSearching = false; - TargetAcquired = false; - ActiveRadar = false; - } - } - } - } - else if (radarLOAL && radarLOALSearching) - { - // not locked on before launch, trying lock-on after launch: - - if (scannedTargets == null) scannedTargets = new TargetSignatureData[5]; - TargetSignatureData.ResetTSDArray(ref scannedTargets); - Ray ray = new Ray(transform.position, GetForwardTransform()); - bool pingRWR = Time.time - lastRWRPing > 0.4f; - if (pingRWR) lastRWRPing = Time.time; - bool radarSnapshot = (snapshotTicker > 5); - if (radarSnapshot) - { - snapshotTicker = 0; - } - else - { - snapshotTicker++; - } - - //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 smallestAngle = 360; - TargetSignatureData lockedTarget = TargetSignatureData.noTarget; - - for (int i = 0; i < scannedTargets.Length; i++) - { - if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - radarTarget.predictedPosition).sqrMagnitude < sqrThresh) - { - //re-check engagement envelope, only lock appropriate targets - if (CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo)) - { - float angle = Vector3.Angle(scannedTargets[i].predictedPosition - transform.position, GetForwardTransform()); - if (angle < smallestAngle) - { - lockedTarget = scannedTargets[i]; - smallestAngle = angle; - } - - ActiveRadar = true; - return; - } - } - } - - if (lockedTarget.exists) - { - radarTarget = lockedTarget; - TargetAcquired = true; - radarLOALSearching = false; - TargetPosition = radarTarget.predictedPositionWithChaffFactor + (radarTarget.velocity * Time.fixedDeltaTime); - TargetVelocity = radarTarget.velocity; - TargetAcceleration = radarTarget.acceleration; - - if (!ActiveRadar && Time.time - TimeFired > 1) - { - if (weaponClass == WeaponClasses.SLW) - RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); - else - RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); - - Debug.Log("[BDArmory]: Pitbull! Radar missileBase has gone active. Radar sig strength: " + radarTarget.signalStrength.ToString("0.0")); - } - return; - } - else - { - TargetAcquired = true; - TargetPosition = transform.position + (startDirection * 500); - TargetVelocity = Vector3.zero; - TargetAcceleration = Vector3.zero; - radarLOALSearching = true; - _radarFailTimer += Time.fixedDeltaTime; - if (_radarFailTimer > maxRadarFailTime) - { - Debug.Log("[BDArmory]: Active Radar guidance failed. LOAL could not lock a target."); - radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; - radarLOALSearching = false; - TargetAcquired = false; - ActiveRadar = false; - } - return; - } - } - - if (!radarTarget.exists) - { - legacyTargetVessel = null; - } - } - - protected bool CheckTargetEngagementEnvelope(TargetInfo ti) - { - return (ti.isMissile && engageMissile) || - (!ti.isMissile && ti.isFlying && engageAir) || - ((ti.isLandedOrSurfaceSplashed || ti.isSplashed) && engageGround) || - (ti.isUnderwater && engageSLW); - } - - protected void ReceiveRadarPing(Vessel v, Vector3 source, RadarWarningReceiver.RWRThreatTypes type, float persistTime) - { - 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) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[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); - TargetVelocity = Vector3.zero; - TargetAcceleration = Vector3.zero; - lockFailTimer = 0; - } - } - } - - protected void UpdateAntiRadiationTarget() - { - if (!TargetAcquired) - { - guidanceActive = false; - return; - } - - if (FlightGlobals.ready) - { - if (lockFailTimer < 0) - { - lockFailTimer = 0; - } - lockFailTimer += Time.fixedDeltaTime; - } - - if (lockFailTimer > 8) - { - guidanceActive = false; - TargetAcquired = false; - } - else - { - TargetPosition = VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody); - } - } - - public void DrawDebugLine(Vector3 start, Vector3 end, Color color = default(Color)) - { - if (BDArmorySettings.DRAW_DEBUG_LINES) - { - if (!gameObject.GetComponent()) - { - LR = gameObject.AddComponent(); - LR.material = new Material(Shader.Find("KSP/Emissive/Diffuse")); - LR.material.SetColor("_EmissiveColor", color); - } - else - { - LR = gameObject.GetComponent(); - } - LR.positionCount = 2; - LR.SetPosition(0, start); - LR.SetPosition(1, end); - } - } - - protected void CheckDetonationDistance() - { - if (DetonationDistanceState == DetonationDistanceStates.Detonate) - { - Debug.Log("[BDArmory]: Target detected inside sphere - detonating"); - - Detonate(); - } - } - - protected Vector3 CalculateAGMBallisticGuidance(MissileBase missile, Vector3 targetPosition) - { - if (this._guidance == null) - { - _guidance = new BallisticGuidance(); - } - - return _guidance.GetDirection(this, targetPosition); - } - - - - - - protected void drawLabels() - { - if (vessel == null || !vessel.isActiveVessel) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - GUI.Label(new Rect(200, Screen.height - 200, 400, 400), this.shortName + ":" + debugString.ToString()); - } - } - - public float GetTntMass() - { - return vessel.FindPartModulesImplementing().Max(x => x.tntMass); - } - - public void CheckDetonationState() - { - //Guard clauses - if (!TargetAcquired) return; - - var targetDistancePerFrame = TargetVelocity * Time.fixedDeltaTime; - var missileDistancePerFrame = vessel.Velocity() * Time.fixedDeltaTime; - - var futureTargetPosition = (TargetPosition + targetDistancePerFrame); - var futureMissilePosition = (vessel.CoM + missileDistancePerFrame); - - var relativeSpeed = (TargetVelocity - vessel.Velocity()).magnitude * Time.fixedDeltaTime; - - 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, 557057).AsEnumerable().GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - if (hitsEnu.Current == null) continue; - try - { - Part partHit = hitsEnu.Current.GetComponentInParent(); - - if (partHit?.vessel != vessel && partHit?.vessel == SourceVessel) // Not ourselves, but the source vessel. - { - //We found a hit to the vessel - return; - } - } - catch - { - // ignored - } - } - } - - //We are safe and we can continue with the cruising phase - DetonationDistanceState = DetonationDistanceStates.Cruising; - 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 (bdModularGuidance == null) return; - - if (Vector3.Distance(futureMissilePosition, futureTargetPosition) > this.DetonationDistance) return; - - DetonationDistanceState = DetonationDistanceStates.CheckingProximity; - } - break; - - case DetonationDistanceStates.CheckingProximity: - if (DetonationDistance == 0) - { - if (weaponClass == WeaponClasses.Bomb) return; - - if (TimeIndex > 1f) - { - //Vector3 floatingorigin_current = FloatingOrigin.Offset; - - Ray rayFuturePosition = new Ray(vessel.CoM, futureMissilePosition); - - var hitsFuture = Physics.RaycastAll(rayFuturePosition, (float)missileDistancePerFrame.magnitude, 557057).AsEnumerable(); - - using (var hitsEnu = hitsFuture.GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - RaycastHit hit = hitsEnu.Current; - - try - { - var hitPart = hit.collider.gameObject.GetComponentInParent(); - - if (hitPart?.vessel != SourceVessel && hitPart?.vessel != vessel) - { - //We found a hit to other vessel - vessel.SetPosition(hit.point); - DetonationDistanceState = DetonationDistanceStates.Detonate; - Detonate(); - return; - } - } - catch - { - // ignored - } - } - } - } - - previousPos = part.transform.position; - } - else - { - float optimalDistance = (float)(Math.Max(DetonationDistance, relativeSpeed)); - using (var hitsEnu = Physics.OverlapSphere(vessel.CoM, optimalDistance, 557057).AsEnumerable().GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - if (hitsEnu.Current == null) continue; - - try - { - Part partHit = hitsEnu.Current.GetComponentInParent(); - - if (partHit?.vessel == vessel || partHit?.vessel == SourceVessel) continue; - if (partHit?.vessel.vesselType == VesselType.Debris) continue; // Ignore debris - - Debug.Log("[BDArmory]: Missile proximity sphere hit | Distance overlap = " + optimalDistance + "| Part name = " + partHit.name); - - //We found a hit a different vessel than ours - if (DetonateAtMinimumDistance) - { - 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; - } - DetonationDistanceState = DetonationDistanceStates.Detonate; - return; - } - catch - { - // ignored - } - } - } - } - - break; - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: DetonationDistanceState = : " + DetonationDistanceState); - } - } - - protected void SetInitialDetonationDistance() - { - if (this.DetonationDistance == -1) - { - if (GuidanceMode == GuidanceModes.AAMLead || GuidanceMode == GuidanceModes.AAMPure) - { - DetonationDistance = GetBlastRadius() * 0.25f; - } - else - { - //DetonationDistance = GetBlastRadius() * 0.05f; - DetonationDistance = 0f; - } - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: DetonationDistance = : " + DetonationDistance); - } - } - - protected void CollisionEnter(Collision col) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Missile Collided"); - - if (TimeIndex > 2 && HasFired && col.collider.gameObject.GetComponentInParent().GetFireFX()) - { - ContactPoint contact = col.contacts[0]; - Vector3 pos = contact.point; - BulletHitFX.AttachFlames(pos, col.collider.gameObject.GetComponentInParent()); - } - - if (HasExploded || !HasFired) return; - - if (DetonationDistanceState != DetonationDistanceStates.CheckingProximity) return; - - Debug.Log("[BDArmory]: Missile Collided - Triggering Detonation"); - Detonate(); - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ChangetoLowAltitudeRange", active = true)]//Change to Low Altitude Range - public void CruiseAltitudeRange() - { - if (Events["CruiseAltitudeRange"].guiName == "Change to Low Altitude Range") - { - Events["CruiseAltitudeRange"].guiName = "Change to High Altitude Range"; - - UI_FloatRange cruiseAltitudField = (UI_FloatRange)Fields["CruiseAltitude"].uiControlEditor; - cruiseAltitudField.maxValue = 500f; - cruiseAltitudField.minValue = 1f; - cruiseAltitudField.stepIncrement = 5f; - } - else - { - Events["CruiseAltitudeRange"].guiName = "Change to Low Altitude Range"; - UI_FloatRange cruiseAltitudField = (UI_FloatRange)Fields["CruiseAltitude"].uiControlEditor; - cruiseAltitudField.maxValue = 25000f; - cruiseAltitudField.minValue = 500; - cruiseAltitudField.stepIncrement = 500f; - } - this.part.RefreshAssociatedWindows(); - } - } -} diff --git a/BDArmory/Modules/MissileFire.cs b/BDArmory/Modules/MissileFire.cs deleted file mode 100644 index 3fc3d9b9e..000000000 --- a/BDArmory/Modules/MissileFire.cs +++ /dev/null @@ -1,4768 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using KSP.Localization; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.CounterMeasure; -using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Parts; -using BDArmory.Radar; -using BDArmory.Targeting; -using BDArmory.UI; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class MissileFire : PartModule - { - #region Declarations - - //weapons - private const int LIST_CAPACITY = 100; - private List weaponTypes = new List(LIST_CAPACITY); - public IBDWeapon[] weaponArray; - - // extension for feature_engagementenvelope: specific lists by weapon engagement type - private List weaponTypesAir = new List(LIST_CAPACITY); - private List weaponTypesMissile = new List(LIST_CAPACITY); - private List weaponTypesGround = new List(LIST_CAPACITY); - private List weaponTypesSLW = new List(LIST_CAPACITY); - - [KSPField(guiActiveEditor = false, isPersistant = true, guiActive = false)] public int weaponIndex; - - //ScreenMessage armedMessage; - ScreenMessage selectionMessage; - string selectionText = ""; - - Transform cameraTransform; - - float startTime; - int missilesAway; - - public float totalHP; - - public bool hasLoadedRippleData; - float rippleTimer; - - public TargetSignatureData heatTarget; - - //[KSPField(isPersistant = true)] - public float rippleRPM - { - get - { - if (selectedWeapon != null) - { - return rippleDictionary[selectedWeapon.GetShortName()].rpm; - } - else - { - return 0; - } - } - set - { - if (selectedWeapon != null) - { - if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) - { - rippleDictionary[selectedWeapon.GetShortName()].rpm = value; - } - else - { - return; - } - } - else - { - return; - } - } - } - - float triggerTimer; - int rippleGunCount; - int _gunRippleIndex; - public float gunRippleRpm; - - public int gunRippleIndex - { - get { return _gunRippleIndex; } - set - { - _gunRippleIndex = value; - if (_gunRippleIndex >= rippleGunCount) - { - _gunRippleIndex = 0; - } - } - } - - //ripple stuff - string rippleData = string.Empty; - Dictionary rippleDictionary; //weapon name, ripple option - public bool canRipple; - - //public float triggerHoldTime = 0.3f; - - //[KSPField(isPersistant = true)] - - public bool rippleFire - { - get - { - if (selectedWeapon == null) return false; - if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) - { - return rippleDictionary[selectedWeapon.GetShortName()].rippleFire; - } - //rippleDictionary.Add(selectedWeapon.GetShortName(), new RippleOption(false, 650)); - return false; - } - } - - public void ToggleRippleFire() - { - if (selectedWeapon != null) - { - RippleOption ro; - if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) - { - ro = rippleDictionary[selectedWeapon.GetShortName()]; - } - else - { - ro = new RippleOption(false, 650); //default to true ripple fire for guns, otherwise, false - if (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) - { - ro.rippleFire = currentGun.useRippleFire; - } - rippleDictionary.Add(selectedWeapon.GetShortName(), ro); - } - - ro.rippleFire = !ro.rippleFire; - - if (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) - { - using (List.Enumerator w = vessel.FindPartModulesImplementing().GetEnumerator()) - while (w.MoveNext()) - { - if (w.Current == null) continue; - if (w.Current.GetShortName() == selectedWeapon.GetShortName()) - w.Current.useRippleFire = ro.rippleFire; - } - } - } - } - - public void AGToggleRipple(KSPActionParam param) - { - ToggleRippleFire(); - } - - void ParseRippleOptions() - { - rippleDictionary = new Dictionary(); - //Debug.Log("[BDArmory]: Parsing ripple options"); - if (!string.IsNullOrEmpty(rippleData)) - { - //Debug.Log("[BDArmory]: Ripple data: " + rippleData); - try - { - using (IEnumerator weapon = rippleData.Split(new char[] { ';' }).AsEnumerable().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == string.Empty) continue; - - string[] options = weapon.Current.Split(new char[] { ',' }); - string wpnName = options[0]; - bool rf = bool.Parse(options[1]); - float rpm = float.Parse(options[2]); - RippleOption ro = new RippleOption(rf, rpm); - rippleDictionary.Add(wpnName, ro); - } - } - catch (Exception) - { - //Debug.Log("[BDArmory]: Ripple data was invalid."); - rippleData = string.Empty; - } - } - else - { - //Debug.Log("[BDArmory]: Ripple data is empty."); - } - hasLoadedRippleData = true; - } - - void SaveRippleOptions(ConfigNode node) - { - if (rippleDictionary != null) - { - rippleData = string.Empty; - using (Dictionary.KeyCollection.Enumerator wpnName = rippleDictionary.Keys.GetEnumerator()) - while (wpnName.MoveNext()) - { - if (wpnName.Current == null) continue; - rippleData += $"{wpnName},{rippleDictionary[wpnName.Current].rippleFire},{rippleDictionary[wpnName.Current].rpm};"; - } - node.SetValue("RippleData", rippleData, true); - } - //Debug.Log("[BDArmory]: Saved ripple data"); - } - - public bool hasSingleFired; - - //bomb aimer - Part bombPart; - Vector3 bombAimerPosition = Vector3.zero; - Texture2D bombAimerTexture = GameDatabase.Instance.GetTexture("BDArmory/Textures/grayCircle", false); - bool showBombAimer; - - //targeting - private List loadedVessels = new List(); - float targetListTimer; - - //sounds - AudioSource audioSource; - public AudioSource warningAudioSource; - AudioSource targetingAudioSource; - AudioClip clickSound; - AudioClip warningSound; - AudioClip armOnSound; - AudioClip armOffSound; - AudioClip heatGrowlSound; - bool warningSounding; - - //missile warning - public bool missileIsIncoming; - public float incomingMissileDistance = float.MaxValue; - public Vessel incomingMissileVessel; - - //guard mode vars - float targetScanTimer; - Vessel guardTarget; - public TargetInfo currentTarget; - TargetInfo overrideTarget; //used for setting target next guard scan for stuff like assisting teammates - float overrideTimer; - - public bool TargetOverride - { - get { return overrideTimer > 0; } - } - - //AIPilot - public IBDAIControl AI; - - // some extending related code still uses pilotAI, which is implementation specific and does not make sense to include in the interface - private BDModulePilotAI pilotAI { get { return AI as BDModulePilotAI; } } - - public float timeBombReleased; - - //targeting pods - public ModuleTargetingCamera mainTGP = null; - public List targetingPods = new List(); - - //radar - public List radars = new List(); - public VesselRadarData vesselRadarData; - - //jammers - public List jammers = new List(); - - //other modules - public List wmModules = new List(); - - //wingcommander - public ModuleWingCommander wingCommander; - - //RWR - private RadarWarningReceiver radarWarn; - - public RadarWarningReceiver rwr - { - get - { - if (!radarWarn || radarWarn.vessel != vessel) - { - return null; - } - return radarWarn; - } - set { radarWarn = value; } - } - - //GPS - public GPSTargetInfo designatedGPSInfo; - - public Vector3d designatedGPSCoords => designatedGPSInfo.gpsCoordinates; - - //weapon slaving - public bool slavingTurrets = false; - public Vector3 slavedPosition; - public Vector3 slavedVelocity; - public Vector3 slavedAcceleration; - public TargetSignatureData slavedTarget; - - //current weapon ref - public MissileBase CurrentMissile; - - public ModuleWeapon currentGun - { - get - { - if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) - { - return selectedWeapon.GetPart().FindModuleImplementing(); - } - else - { - return null; - } - } - } - - public bool underAttack; - public bool underFire; - Coroutine ufRoutine; - - public Vector3 incomingThreatPosition; - public Vessel incomingThreatVessel; - public MissileFire incomingWeaponManager; - public float incomingMissDistance; - public float incomingMissTime; - public Vessel priorThreatVessel = null; - - public bool debilitated = false; - - public bool guardFiringMissile; - bool antiRadTargetAcquired; - Vector3 antiRadiationTarget; - bool laserPointDetected; - - ModuleTargetingCamera foundCam; - - #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)] - public float targetScanInterval = 3; - - // 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)] - public float fireBurstLength = 0; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringTolerance"),//Firing Tolerance - UI_FloatRange(minValue = 0f, maxValue = 2f, stepIncrement = 0.05f, scene = UI_Scene.All)] - public float AutoFireCosAngleAdjustment = 1f; //tune Autofire angle in WM GUI - - public float adjustedAutoFireCosAngle = 0.999484f; - - [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)] - public float - guardAngle = 360; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_VisualRange"),//Visual Range - UI_FloatRange(minValue = 100f, maxValue = 5000, stepIncrement = 100f, scene = UI_Scene.All)] - public float - guardRange = 10000; - - [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)] - public float - gunRange = 2500f; - - 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 - public float maxMissilesOnTarget = 1; - - #region Target Priority - // Target priority variables - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Priority Toggle - UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] - public bool targetPriorityEnabled = true; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_CurrentTarget", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] - public string TargetLabel = ""; - - [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"); - [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)] - public float targetBias = 1.3f; - - private string targetRangeLabel = Localizer.Format("#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)] - public float targetWeightRange = 0f; - - private string targetATALabel = Localizer.Format("#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)] - public float targetWeightATA = 0f; - - private string targetAoDLabel = Localizer.Format("#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)] - public float targetWeightAoD = 2f; - - private string targetAccelLabel = Localizer.Format("#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)] - public float targetWeightAccel = 0; - - private string targetClosureTimeLabel = Localizer.Format("#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)] - public float targetWeightClosureTime = 0f; - - private string targetWeaponNumberLabel = Localizer.Format("#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)] - public float targetWeightWeaponNumber = 0; - - private string targetMassLabel = Localizer.Format("#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)] - public float targetWeightMass = 0; - - private string targetFriendliesEngagingLabel = Localizer.Format("#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)] - public float targetWeightFriendliesEngaging = 1f; - - private string targetThreatLabel = Localizer.Format("#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),//Number Friendlies Engaging - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] - public float targetWeightThreat = 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 repetition - 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),// Countermeasure dispensing repetition - UI_FloatRange(minValue = 1f, maxValue = 20f, stepIncrement = 1f, scene = UI_Scene.All)] - public float cmRepetition = 5f; // 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),// Countermeasure dispensing interval - 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),// Countermeasure dispensing interval - UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] - public float cmWaitTime = 1.0f; // Works well - #endregion - - public void ToggleGuardMode() - { - guardMode = !guardMode; - - if (!guardMode) - { - //disable turret firing and guard mode - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - weapon.Current.visualTargetVessel = null; - weapon.Current.autoFire = false; - weapon.Current.aiControlled = false; - } - weaponIndex = 0; - selectedWeapon = null; - } - } - - [KSPAction("Toggle Guard Mode")] - public void AGToggleGuardMode(KSPActionParam param) - { - ToggleGuardMode(); - } - - //[KSPField(isPersistant = true)] public bool guardMode; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_GuardMode"),//Guard Mode: - UI_Toggle(disabledText = "OFF", enabledText = "ON")] - public bool guardMode; - - //[KSPField(isPersistant = false, guiActive = false, guiActiveEditor = false, guiName = "Target Type: "), UI_Toggle(disabledText = "Vessels", enabledText = "Missiles")] - public bool targetMissiles = false; - - [KSPAction("Toggle Target Type")] - public void AGToggleTargetType(KSPActionParam param) - { - ToggleTargetType(); - } - - public void ToggleTargetType() - { - targetMissiles = !targetMissiles; - audioSource.PlayOneShot(clickSound); - } - - [KSPAction("Jettison Weapon")] - public void AGJettisonWeapon(KSPActionParam param) - { - if (CurrentMissile) - { - using (List.Enumerator missile = vessel.FindPartModulesImplementing().GetEnumerator()) - while (missile.MoveNext()) - { - if (missile.Current == null) continue; - if (missile.Current.GetShortName() == CurrentMissile.GetShortName()) - { - missile.Current.Jettison(); - } - } - } - else if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket) - { - using (List.Enumerator rocket = vessel.FindPartModulesImplementing().GetEnumerator()) - while (rocket.MoveNext()) - { - if (rocket.Current == null) continue; - rocket.Current.Jettison(); - } - } - } - - [KSPAction("Deploy Kerbal's Parachute")] // If there's an EVAing kerbal. - public void AGDeployKerbalsParachute(KSPActionParam param) - { - var EVAChutes = vessel.FindPartModulesImplementing(); - foreach (var chute in EVAChutes) - { - if (chute == null) continue; - chute.deployAltitude = (float)vessel.radarAltitude + 100f; // Current height + 100 so that it deploys immediately. - chute.deploymentState = ModuleParachute.deploymentStates.ACTIVE; - chute.Deploy(); - } - } - - public BDTeam Team - { - get - { - return BDTeam.Get(teamString); - } - set - { - if (!team_loaded) return; - if (!BDArmorySetup.Instance.Teams.ContainsKey(value.Name)) - BDArmorySetup.Instance.Teams.Add(value.Name, value); - teamString = value.Name; - team = value.Serialize(); - } - } - - // Team name - [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Team")]//Team - public string teamString = "Neutral"; - - // Serialized team - [KSPField(isPersistant = true)] - public string team; - private bool team_loaded = false; - - [KSPAction("Next Team")] - public void AGNextTeam(KSPActionParam param) - { - NextTeam(); - } - - public delegate void ChangeTeamDelegate(MissileFire wm, BDTeam team); - - public static event ChangeTeamDelegate OnChangeTeam; - - public void SetTeam(BDTeam team) - { - if (HighLogic.LoadedSceneIsFlight) - { - SetTarget(null); // Without this, friendliesEngaging never gets updated - using (var wpnMgr = vessel.FindPartModulesImplementing().GetEnumerator()) - while (wpnMgr.MoveNext()) - { - if (wpnMgr.Current == null) continue; - wpnMgr.Current.Team = team; - } - - if (vessel.gameObject.GetComponent()) - { - BDATargetManager.RemoveTarget(vessel.gameObject.GetComponent()); - Destroy(vessel.gameObject.GetComponent()); - } - OnChangeTeam?.Invoke(this, Team); - ResetGuardInterval(); - } - else if (HighLogic.LoadedSceneIsEditor) - { - using (var editorPart = EditorLogic.fetch.ship.Parts.GetEnumerator()) - while (editorPart.MoveNext()) - using (var wpnMgr = editorPart.Current.FindModulesImplementing().GetEnumerator()) - while (wpnMgr.MoveNext()) - { - if (wpnMgr.Current == null) continue; - wpnMgr.Current.Team = team; - } - } - } - - public void SetTeamByName(string teamName) - { - - } - - [KSPEvent(active = true, guiActiveEditor = true, guiActive = false)] - public void NextTeam() - { - var teamList = new List { "A", "B" }; - using (var teams = BDArmorySetup.Instance.Teams.GetEnumerator()) - while (teams.MoveNext()) - if (!teamList.Contains(teams.Current.Key) && !teams.Current.Value.Neutral) - teamList.Add(teams.Current.Key); - teamList.Sort(); - SetTeam(BDTeam.Get(teamList[(teamList.IndexOf(Team.Name) + 1) % teamList.Count])); - } - - - [KSPEvent(guiActive = false, guiActiveEditor = true, active = true, guiName = "#LOC_BDArmory_SelectTeam")]//Select Team - public void SelectTeam() - { - BDTeamSelector.Instance.Open(this, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); - } - - [KSPField(isPersistant = true)] - public bool isArmed = false; - - [KSPAction("Arm/Disarm")] - public void AGToggleArm(KSPActionParam param) - { - ToggleArm(); - } - - public void ToggleArm() - { - isArmed = !isArmed; - if (isArmed) audioSource.PlayOneShot(armOnSound); - else audioSource.PlayOneShot(armOffSound); - } - - [KSPField(isPersistant = false, guiActive = true, guiName = "#LOC_BDArmory_Weapon")]//Weapon - public string selectedWeaponString = - "None"; - - IBDWeapon sw; - - public IBDWeapon selectedWeapon - { - get - { - if ((sw != null && sw.GetPart().vessel == vessel) || weaponIndex <= 0) return sw; - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (weapon.Current.GetShortName() != selectedWeaponString) continue; - sw = weapon.Current; - break; - } - return sw; - } - set - { - if (sw == value) return; - sw = value; - selectedWeaponString = GetWeaponName(value); - UpdateSelectedWeaponState(); - } - } - - [KSPAction("Fire Missile")] - public void AGFire(KSPActionParam param) - { - FireMissile(); - } - - [KSPAction("Fire Guns (Hold)")] - public void AGFireGunsHold(KSPActionParam param) - { - if (weaponIndex <= 0 || (selectedWeapon.GetWeaponClass() != WeaponClasses.Gun && - selectedWeapon.GetWeaponClass() != WeaponClasses.Rocket && - selectedWeapon.GetWeaponClass() != WeaponClasses.DefenseLaser)) return; - using (List.Enumerator weap = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weap.MoveNext()) - { - if (weap.Current == null) continue; - if (weap.Current.weaponState != ModuleWeapon.WeaponStates.Enabled || - weap.Current.GetShortName() != selectedWeapon.GetShortName()) continue; - weap.Current.AGFireHold(param); - } - } - - [KSPAction("Fire Guns (Toggle)")] - public void AGFireGunsToggle(KSPActionParam param) - { - if (weaponIndex <= 0 || (selectedWeapon.GetWeaponClass() != WeaponClasses.Gun && - selectedWeapon.GetWeaponClass() != WeaponClasses.Rocket && - selectedWeapon.GetWeaponClass() != WeaponClasses.DefenseLaser)) return; - using (List.Enumerator weap = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weap.MoveNext()) - { - if (weap.Current == null) continue; - if (weap.Current.weaponState != ModuleWeapon.WeaponStates.Enabled || - weap.Current.GetShortName() != selectedWeapon.GetShortName()) continue; - weap.Current.AGFireToggle(param); - } - } - - [KSPAction("Next Weapon")] - public void AGCycle(KSPActionParam param) - { - CycleWeapon(true); - } - - [KSPAction("Previous Weapon")] - public void AGCycleBack(KSPActionParam param) - { - CycleWeapon(false); - } - - [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_OpenGUI", active = true)]//Open GUI - public void ToggleToolbarGUI() - { - BDArmorySetup.windowBDAToolBarEnabled = !BDArmorySetup.windowBDAToolBarEnabled; - } - - public void SetAFCAA() - { - UI_FloatRange field = (UI_FloatRange)Fields["AutoFireCosAngleAdjustment"].uiControlEditor; - field.onFieldChanged = OnAFCAAUpdated; - // field = (UI_FloatRange)Fields["AutoFireCosAngleAdjustment"].uiControlFlight; // Not visible in flight mode, use the guard menu instead. - // field.onFieldChanged = OnAFCAAUpdated; - OnAFCAAUpdated(null, null); - } - - public void OnAFCAAUpdated(BaseField field, object obj) - { - adjustedAutoFireCosAngle = Mathf.Cos((AutoFireCosAngleAdjustment * Mathf.Deg2Rad)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[MissileFire]: Setting AFCAA to " + adjustedAutoFireCosAngle); - } - #endregion KSPFields,events,actions - - #endregion Declarations - - #region KSP Events - - public override void OnSave(ConfigNode node) - { - base.OnSave(node); - - if (HighLogic.LoadedSceneIsFlight) - { - SaveRippleOptions(node); - } - } - - public override void OnLoad(ConfigNode node) - { - base.OnLoad(node); - if (HighLogic.LoadedSceneIsFlight) - { - rippleData = string.Empty; - if (node.HasValue("RippleData")) - { - rippleData = node.GetValue("RippleData"); - } - ParseRippleOptions(); - } - } - - 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"); - - //HEAT LOCKING - heatTarget = TargetSignatureData.noTarget; - } - - public void Start() - { - team_loaded = true; - Team = BDTeam.Deserialize(team); - - UpdateMaxGuardRange(); - SetAFCAA(); - - startTime = Time.time; - - if (HighLogic.LoadedSceneIsFlight) - { - part.force_activate(); - - selectionMessage = new ScreenMessage("", 2.0f, ScreenMessageStyle.LOWER_CENTER); - - UpdateList(); - if (weaponArray.Length > 0) selectedWeapon = weaponArray[weaponIndex]; - //selectedWeaponString = GetWeaponName(selectedWeapon); - - cameraTransform = part.FindModelTransform("BDARPMCameraTransform"); - - part.force_activate(); - rippleTimer = Time.time; - targetListTimer = Time.time; - - wingCommander = part.FindModuleImplementing(); - - audioSource = gameObject.AddComponent(); - audioSource.minDistance = 1; - audioSource.maxDistance = 500; - audioSource.dopplerLevel = 0; - audioSource.spatialBlend = 1; - - warningAudioSource = gameObject.AddComponent(); - warningAudioSource.minDistance = 1; - warningAudioSource.maxDistance = 500; - warningAudioSource.dopplerLevel = 0; - warningAudioSource.spatialBlend = 1; - - targetingAudioSource = gameObject.AddComponent(); - targetingAudioSource.minDistance = 1; - targetingAudioSource.maxDistance = 250; - targetingAudioSource.dopplerLevel = 0; - targetingAudioSource.loop = true; - targetingAudioSource.spatialBlend = 1; - - StartCoroutine(MissileWarningResetRoutine()); - - if (vessel.isActiveVessel) - { - BDArmorySetup.Instance.ActiveWeaponManager = this; - } - - UpdateVolume(); - BDArmorySetup.OnVolumeChange += UpdateVolume; - BDArmorySetup.OnSavedSettings += ClampVisualRange; - - StartCoroutine(StartupListUpdater()); - missilesAway = 0; - - GameEvents.onVesselCreate.Add(OnVesselCreate); - GameEvents.onPartJointBreak.Add(OnPartJointBreak); - GameEvents.onPartDie.Add(OnPartDie); - - GetTotalHP(); - - using (List.Enumerator aipilot = vessel.FindPartModulesImplementing().GetEnumerator()) - while (aipilot.MoveNext()) - { - if (aipilot.Current == null) continue; - AI = aipilot.Current; - break; - } - - RefreshModules(); - } - } - - void OnPartDie() - { - OnPartDie(part); - } - - void OnPartDie(Part p) - { - if (p == part) - { - try - { - GameEvents.onPartDie.Remove(OnPartDie); - GameEvents.onPartJointBreak.Remove(OnPartJointBreak); - GameEvents.onVesselCreate.Remove(OnVesselCreate); - } - catch (Exception e) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Error OnPartDie: " + e.Message); - Debug.Log("[BDArmory]: Error OnPartDie: " + e.Message); - } - } - RefreshModules(); - UpdateList(); - } - - void OnVesselCreate(Vessel v) - { - RefreshModules(); - } - - void OnPartJointBreak(PartJoint j, float breakForce) - { - if (!part) - { - GameEvents.onPartJointBreak.Remove(OnPartJointBreak); - } - - if ((j.Parent && j.Parent.vessel == vessel) || (j.Child && j.Child.vessel == vessel)) - { - RefreshModules(); - UpdateList(); - } - } - - public void GetTotalHP() // get total craft HP - { - using (List.Enumerator p = vessel.parts.GetEnumerator()) - while (p.MoveNext()) - { - if (p.Current == null) continue; - if (p.Current.Modules.GetModule()) continue; // don't grab missiles - if (p.Current.Modules.GetModule()) continue; // don't grab bits that are going to fall off - if (p.Current.FindParentModuleImplementing()) continue; // should grab ModularMissiles too - /* - if (p.Current.Modules.GetModule() != null) - { - var hp = p.Current.Modules.GetModule(); - totalHP += hp.Hitpoints; - } - */ - ++totalHP; - //Debug.Log(vessel.vesselName + " part count: " + totalHP); - } - } - - public override void OnUpdate() - { - if (!HighLogic.LoadedSceneIsFlight) - { - return; - } - - base.OnUpdate(); - if (!vessel.packed) - { - if (weaponIndex >= weaponArray.Length) - { - hasSingleFired = true; - triggerTimer = 0; - - weaponIndex = Mathf.Clamp(weaponIndex, 0, weaponArray.Length - 1); - - 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 (!CheckMouseIsOnGui() && isArmed && BDInputUtils.GetKey(BDInputSettingsFields.WEAP_FIRE_KEY)) - { - triggerTimer += Time.fixedDeltaTime; - } - else - { - triggerTimer = 0; - hasSingleFired = false; - } - - //firing missiles and rockets=== - if (!guardMode && - 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; - } - } - } - else if (!guardMode && - selectedWeapon != null && - ((selectedWeapon.GetWeaponClass() == WeaponClasses.Gun - || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket - || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) && currentGun.roundsPerMinute < 1500)) - { - canRipple = true; - } - else - { - canRipple = false; - } - } - } - - private void CalculateMissilesAway() - { - int tempMissilesAway = 0; - using (List.Enumerator firedMissiles = BDATargetManager.FiredMissiles.GetEnumerator()) - while (firedMissiles.MoveNext()) - { - if (firedMissiles.Current == null) continue; - - var missileBase = firedMissiles.Current as MissileBase; - - if (missileBase.SourceVessel != this.vessel) continue; - - if (missileBase.MissileState != MissileBase.MissileStates.PostThrust && !missileBase.HasMissed && !missileBase.HasExploded) - { - tempMissilesAway++; - } - } - - this.missilesAway = tempMissilesAway; - } - - public override void OnFixedUpdate() - { - if (guardMode && vessel.IsControllable) - { - GuardMode(); - } - else - { - targetScanTimer = -100; - } - BombAimer(); - } - - void OnDestroy() - { - BDArmorySetup.OnVolumeChange -= UpdateVolume; - BDArmorySetup.OnSavedSettings -= ClampVisualRange; - GameEvents.onVesselCreate.Remove(OnVesselCreate); - GameEvents.onPartJointBreak.Remove(OnPartJointBreak); - GameEvents.onPartDie.Remove(OnPartDie); - } - - void ClampVisualRange() - { - guardRange = Mathf.Clamp(guardRange, 0, BDArmorySettings.MAX_GUARD_VISUAL_RANGE); - } - - void OnGUI() - { - if (HighLogic.LoadedSceneIsFlight && vessel == FlightGlobals.ActiveVessel && - BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled) - { - if (BDArmorySettings.DRAW_DEBUG_LINES) - { - if (incomingMissileVessel) - { - BDGUIUtils.DrawLineBetweenWorldPositions(part.transform.position, - incomingMissileVessel.transform.position, 5, Color.cyan); - } - } - - if (showBombAimer) - { - MissileBase ml = CurrentMissile; - if (ml) - { - float size = 128; - Texture2D texture = BDArmorySetup.Instance.greenCircleTexture; - - if ((ml is MissileLauncher && ((MissileLauncher)ml).guidanceActive) || ml is BDModularGuidance) - { - texture = BDArmorySetup.Instance.largeGreenCircleTexture; - size = 256; - } - BDGUIUtils.DrawTextureOnWorldPos(bombAimerPosition, texture, new Vector2(size, size), 0); - } - } - - //MISSILE LOCK HUD - MissileBase missile = CurrentMissile; - if (missile) - { - if (missile.TargetingMode == MissileBase.TargetingModes.Laser) - { - if (laserPointDetected && foundCam) - { - BDGUIUtils.DrawTextureOnWorldPos(foundCam.groundTargetPosition, BDArmorySetup.Instance.greenCircleTexture, new Vector2(48, 48), 1); - } - - 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); - } - } - } - else if (missile.TargetingMode == MissileBase.TargetingModes.Heat) - { - MissileBase ml = CurrentMissile; - if (heatTarget.exists) - { - BDGUIUtils.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); - 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); - } - 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); - } - } - else if (missile.TargetingMode == MissileBase.TargetingModes.Radar) - { - MissileBase ml = CurrentMissile; - //if(radar && radar.locked) - 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); - //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); - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - string dynRangeDebug = string.Empty; - MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(missile, vesselRadarData.lockedTargetData.targetData.velocity, vesselRadarData.lockedTargetData.targetData.predictedPosition); - dynRangeDebug += "MaxDLZ: " + dlz.maxLaunchRange; - dynRangeDebug += "\nMinDLZ: " + dlz.minLaunchRange; - GUI.Label(new Rect(800, 600, 200, 200), dynRangeDebug); - } - } - } - else if (missile.TargetingMode == MissileBase.TargetingModes.AntiRad) - { - if (rwr && rwr.rwrEnabled && rwr.displayRWR) - { - for (int i = 0; i < rwr.pingsData.Length; i++) - { - if (rwr.pingsData[i].exists && (rwr.pingsData[i].signalStrength == 0 || rwr.pingsData[i].signalStrength == 5) && Vector3.Dot(rwr.pingWorldPositions[i] - missile.transform.position, missile.GetForwardTransform()) > 0) - { - BDGUIUtils.DrawTextureOnWorldPos(rwr.pingWorldPositions[i], BDArmorySetup.Instance.greenDiamondTexture, new Vector2(22, 22), 0); - } - } - } - - if (antiRadTargetAcquired) - { - BDGUIUtils.DrawTextureOnWorldPos(antiRadiationTarget, - BDArmorySetup.Instance.openGreenSquare, new Vector2(22, 22), 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); - } - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - GUI.Label(new Rect(600, 900, 100, 100), "Missiles away: " + missilesAway); - } - } - } - - bool CheckMouseIsOnGui() - { - return Misc.Misc.CheckMouseIsOnGui(); - } - - #endregion KSP Events - - #region Enumerators - - IEnumerator StartupListUpdater() - { - while (vessel.packed || !FlightGlobals.ready) - { - yield return null; - if (vessel.isActiveVessel) - { - BDArmorySetup.Instance.ActiveWeaponManager = this; - } - } - UpdateList(); - } - - IEnumerator MissileWarningResetRoutine() - { - while (enabled) - { - missileIsIncoming = false; - yield return new WaitForSeconds(1); - } - } - - IEnumerator UnderFireRoutine() - { - underFire = true; - yield return new WaitForSeconds(3); - underFire = false; - } - - IEnumerator UnderAttackRoutine() - { - underAttack = true; - yield return new WaitForSeconds(3); - underAttack = false; - } - - IEnumerator GuardTurretRoutine() - { - if (gameObject.activeInHierarchy) - //target is out of visual range, try using sensors - { - if (guardTarget.LandedOrSplashed) - { - if (targetingPods.Count > 0) - { - using (List.Enumerator tgp = targetingPods.GetEnumerator()) - while (tgp.MoveNext()) - { - if (tgp.Current == null) continue; - if (!tgp.Current.enabled || (tgp.Current.cameraEnabled && tgp.Current.groundStabilized && - !((tgp.Current.groundTargetPosition - - guardTarget.transform.position).sqrMagnitude > 20 * 20))) continue; - tgp.Current.EnableCamera(); - yield return StartCoroutine(tgp.Current.PointToPositionRoutine(guardTarget.CoM)); - //yield return StartCoroutine(tgp.Current.PointToPositionRoutine(TargetInfo.TargetCOMDispersion(guardTarget))); - if (!tgp.Current) continue; - if (tgp.Current.groundStabilized && guardTarget && - (tgp.Current.groundTargetPosition - guardTarget.transform.position).sqrMagnitude < 20 * 20) - { - tgp.Current.slaveTurrets = true; - StartGuardTurretFiring(); - yield break; - } - tgp.Current.DisableCamera(); - } - } - - if (!guardTarget || (guardTarget.transform.position - transform.position).sqrMagnitude > guardRange * guardRange) - { - SetTarget(null); //disengage, sensors unavailable. - yield break; - } - } - else - { - // DISABLE RADAR - /* - if (!vesselRadarData || !(vesselRadarData.radarCount > 0)) - { - List.Enumerator rd = radars.GetEnumerator(); - while (rd.MoveNext()) - { - if (rd.Current == null) continue; - if (!rd.Current.canLock) continue; - rd.Current.EnableRadar(); - break; - } - rd.Dispose(); - } - */ - - if (vesselRadarData && - (!vesselRadarData.locked || - (vesselRadarData.lockedTargetData.targetData.predictedPosition - guardTarget.transform.position) - .sqrMagnitude > 40 * 40)) - { - //vesselRadarData.TryLockTarget(guardTarget.transform.position); - vesselRadarData.TryLockTarget(guardTarget); - yield return new WaitForSeconds(0.5f); - if (guardTarget && vesselRadarData && vesselRadarData.locked && - vesselRadarData.lockedTargetData.vessel == guardTarget) - { - vesselRadarData.SlaveTurrets(); - StartGuardTurretFiring(); - yield break; - } - } - - if (!guardTarget || (guardTarget.transform.position - transform.position).sqrMagnitude > guardRange * guardRange) - { - SetTarget(null); //disengage, sensors unavailable. - yield break; - } - } - } - - StartGuardTurretFiring(); - yield break; - } - - IEnumerator ResetMissileThreatDistanceRoutine() - { - yield return new WaitForSeconds(8); - incomingMissileDistance = float.MaxValue; - } - - IEnumerator GuardMissileRoutine() - { - MissileBase ml = CurrentMissile; - - if (ml && !guardFiringMissile) - { - guardFiringMissile = true; - - if (ml.TargetingMode == MissileBase.TargetingModes.Radar && vesselRadarData) - { - if (SetCargoBays()) - { - yield return new WaitForSeconds(1f); - } - - float attemptLockTime = Time.time; - while ((!vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != guardTarget)) && Time.time - attemptLockTime < 2) - { - if (vesselRadarData.locked) - { - vesselRadarData.SwitchActiveLockedTarget(guardTarget); - yield return null; - } - //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); - // } - - //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) - { - vesselRadarData.SlaveTurrets(); - float turretStartTime = Time.time; - while (Time.time - turretStartTime < 5) - { - 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 null; - - // if (ml && guardTarget && vesselRadarData.locked && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) - if (ml && guardTarget && vesselRadarData.locked && GetLaunchAuthorization(guardTarget, this)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("Firing on target: " + guardTarget.GetName()); - } - FireCurrentMissile(true); - //StartCoroutine(MissileAwayRoutine(mlauncher)); - } - } - else if (ml.TargetingMode == MissileBase.TargetingModes.Heat) - { - if (vesselRadarData && vesselRadarData.locked) // FIXME Why does heat guidance use the radar data structures? This wipes radar guided missiles' targeting data when switching to a heat guided missile. - { - vesselRadarData.UnlockAllTargets(); - vesselRadarData.UnslaveTurrets(); - } - - if (SetCargoBays()) - { - yield return new WaitForSeconds(1f); - } - - float attemptStartTime = Time.time; - float attemptDuration = Mathf.Max(targetScanInterval * 0.75f, 5f); - MissileLauncher mlauncher; - while (ml && guardTarget && Time.time - attemptStartTime < attemptDuration && (!heatTarget.exists || (heatTarget.predictedPosition - guardTarget.transform.position).sqrMagnitude > 40 * 40)) - { - //TODO BDModularGuidance: add turret - //try using missile turret to lock target - 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(); - } - - //try uncaged IR lock with radar - if (guardTarget && !heatTarget.exists && vesselRadarData && vesselRadarData.radarCount > 0) - { - if (!vesselRadarData.locked || - (vesselRadarData.lockedTargetData.targetData.predictedPosition - - guardTarget.transform.position).sqrMagnitude > 40 * 40) - { - //vesselRadarData.TryLockTarget(guardTarget.transform.position); - vesselRadarData.TryLockTarget(guardTarget); - yield return new WaitForSeconds(Mathf.Min(1, (targetScanInterval * 0.25f))); - } - } - - // if (AIMightDirectFire() && ml && heatTarget.exists) - // { - // float LAstartTime = Time.time; - // while (Time.time - LAstartTime < 3 && AIMightDirectFire() && GetLaunchAuthorization(guardTarget, this)) - // { - // yield return new WaitForFixedUpdate(); - // } - // yield return new WaitForSeconds(0.5f); - // } - - //wait for missile turret to point at target - mlauncher = ml as MissileLauncher; - if (mlauncher != null) - { - if (ml && mlauncher.missileTurret && heatTarget.exists) - { - float turretStartTime = attemptStartTime; - while (heatTarget.exists && 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, heatTarget.predictedPosition, heatTarget.velocity); - mlauncher.missileTurret.SlavedAim(); - - if (angle < mlauncher.missileTurret.fireFOV) - { - break; - // turretStartTime -= 3 * Time.fixedDeltaTime; - } - yield return new WaitForFixedUpdate(); - } - } - } - - yield return null; - - // if (guardTarget && ml && heatTarget.exists && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) - if (guardTarget && ml && heatTarget.exists && GetLaunchAuthorization(guardTarget, this)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Firing on target: " + guardTarget.GetName()); - } - - FireCurrentMissile(true); - //StartCoroutine(MissileAwayRoutine(mlauncher)); - } - } - 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(1f); - } - - float attemptStartTime = Time.time; - float attemptDuration = targetScanInterval * 0.75f; - while (Time.time - attemptStartTime < attemptDuration && - (!antiRadTargetAcquired || (antiRadiationTarget - guardTarget.CoM).sqrMagnitude > 20 * 20)) - { - yield return new WaitForFixedUpdate(); - } - - if (ml && antiRadTargetAcquired && (antiRadiationTarget - guardTarget.CoM).sqrMagnitude < 20 * 20) - { - FireCurrentMissile(true); - //StartCoroutine(MissileAwayRoutine(ml)); - } - } - else if (ml.TargetingMode == MissileBase.TargetingModes.Laser) - { - if (SetCargoBays()) - { - yield return new WaitForSeconds(1f); - } - - 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(); - yield return StartCoroutine(tgp.Current.PointToPositionRoutine(guardTarget.CoM)); - if (tgp.Current.groundStabilized && (tgp.Current.groundTargetPosition - guardTarget.transform.position).sqrMagnitude < 20 * 20) - { - tgp.Current.CoMLock = true; // make the designator continue to paint target - break; - } - } - } - - //search for a laser point that corresponds with target vessel - float attemptStartTime = Time.time; - float attemptDuration = targetScanInterval * 0.75f; - while (Time.time - attemptStartTime < attemptDuration && (!laserPointDetected || (foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude > 20 * 20))) - { - yield return new WaitForFixedUpdate(); - } - - if (ml && laserPointDetected && foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude < 20 * 20) - { - FireCurrentMissile(true); - //StartCoroutine(MissileAwayRoutine(ml)); - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Laser Target Error"); - } - } - - guardFiringMissile = false; - } - } - - IEnumerator GuardBombRoutine() - { - guardFiringMissile = true; - bool hasSetCargoBays = false; - 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()) - { - //check database for target first - float twoxsqrRad = 4f * radius * radius; - bool foundTargetInDatabase = false; - using (List.Enumerator gps = BDATargetManager.GPSTargetList(Team).GetEnumerator()) - while (gps.MoveNext()) - { - if (!((gps.Current.worldPos - guardTarget.CoM).sqrMagnitude < twoxsqrRad)) continue; - designatedGPSInfo = gps.Current; - foundTargetInDatabase = true; - break; - } - - //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) - { - tgp.EnableCamera(); - yield return StartCoroutine(tgp.PointToPositionRoutine(guardTarget.CoM)); - - if (tgp) - { - if (guardTarget && tgp.groundStabilized && (tgp.groundTargetPosition - guardTarget.transform.position).sqrMagnitude < CurrentMissile.GetBlastRadius() * CurrentMissile.GetBlastRadius()) - { - radius = 500; - designatedGPSInfo = new GPSTargetInfo(tgp.bodyRelativeGTP, "Guard Target"); - bombStartTime = Time.time; - } - else//failed to acquire target via tgp, cancel. - { - tgp.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. - { - guardFiringMissile = false; - yield break; - } - } - } - - bool doProxyCheck = true; - - float prevDist = 2 * radius; - radius = Mathf.Max(radius, 50f); - while (guardTarget && Time.time - bombStartTime < bombAttemptDuration && weaponIndex > 0 && - weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb && missilesAway < maxMissilesOnTarget) - { - float targetDist = Vector3.Distance(bombAimerPosition, guardTarget.CoM); - - if (targetDist < (radius * 20f) && !hasSetCargoBays) - { - SetCargoBays(); - hasSetCargoBays = true; - } - - if (targetDist > radius - || Vector3.Dot(VectorUtils.GetUpDirection(vessel.CoM), vessel.transform.forward) > 0) // roll check - { - if (targetDist < Mathf.Max(radius * 2, 800f) && - Vector3.Dot(guardTarget.CoM - bombAimerPosition, guardTarget.CoM - transform.position) < 0) - { - pilotAI.RequestExtend(guardTarget.CoM); - break; - } - yield return null; - } - else - { - if (doProxyCheck) - { - if (targetDist - prevDist > 0) - { - doProxyCheck = false; - } - else - { - prevDist = targetDist; - } - } - - if (!doProxyCheck) - { - FireCurrentMissile(true); - timeBombReleased = Time.time; - yield return new WaitForSeconds(rippleFire ? 60f / rippleRPM : 0.06f); - if (missilesAway >= maxMissilesOnTarget) - { - yield return new WaitForSeconds(1f); - if (pilotAI) - { - pilotAI.RequestExtend(guardTarget.CoM); - } - } - } - else - { - yield return null; - } - } - } - - designatedGPSInfo = new GPSTargetInfo(); - guardFiringMissile = false; - } - - //IEnumerator MissileAwayRoutine(MissileBase ml) - //{ - // missilesAway++; - - // MissileLauncher launcher = ml as MissileLauncher; - // if (launcher != null) - // { - // float timeStart = Time.time; - // float timeLimit = Mathf.Max(launcher.dropTime + launcher.cruiseTime + launcher.boostTime + 4, 10); - // while (ml) - // { - // if (ml.guidanceActive && Time.time - timeStart < timeLimit) - // { - // yield return null; - // } - // else - // { - // break; - // } - - // } - // } - // else - // { - // while (ml) - // { - // if (ml.MissileState != MissileBase.MissileStates.PostThrust) - // { - // yield return null; - - // } - // else - // { - // break; - // } - // } - // } - - // missilesAway--; - //} - - //IEnumerator BombsAwayRoutine(MissileBase ml) - //{ - // missilesAway++; - // float timeStart = Time.time; - // float timeLimit = 3; - // while (ml) - // { - // if (Time.time - timeStart < timeLimit) - // { - // yield return null; - // } - // else - // { - // break; - // } - // } - // missilesAway--; - //} - #endregion Enumerators - - #region Audio - - void UpdateVolume() - { - if (audioSource) - { - audioSource.volume = BDArmorySettings.BDARMORY_UI_VOLUME; - } - if (warningAudioSource) - { - warningAudioSource.volume = BDArmorySettings.BDARMORY_UI_VOLUME; - } - if (targetingAudioSource) - { - targetingAudioSource.volume = BDArmorySettings.BDARMORY_UI_VOLUME; - } - } - - void UpdateTargetingAudio() - { - if (BDArmorySetup.GameIsPaused) - { - if (targetingAudioSource.isPlaying) - { - targetingAudioSource.Stop(); - } - return; - } - - if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Missile && vessel.isActiveVessel) - { - MissileBase ml = CurrentMissile; - if (ml.TargetingMode == MissileBase.TargetingModes.Heat) - { - if (targetingAudioSource.clip != heatGrowlSound) - { - targetingAudioSource.clip = heatGrowlSound; - } - - if (heatTarget.exists) - { - targetingAudioSource.pitch = Mathf.MoveTowards(targetingAudioSource.pitch, 2, 8 * Time.deltaTime); - } - else - { - targetingAudioSource.pitch = Mathf.MoveTowards(targetingAudioSource.pitch, 1, 8 * Time.deltaTime); - } - - if (!targetingAudioSource.isPlaying) - { - targetingAudioSource.Play(); - } - } - else - { - if (targetingAudioSource.isPlaying) - { - targetingAudioSource.Stop(); - } - } - } - else - { - targetingAudioSource.pitch = 1; - if (targetingAudioSource.isPlaying) - { - targetingAudioSource.Stop(); - } - } - } - - IEnumerator WarningSoundRoutine(float distance, MissileBase ml)//give distance parameter - { - if (distance < this.guardRange) - { - warningSounding = true; - BDArmorySetup.Instance.missileWarningTime = Time.time; - BDArmorySetup.Instance.missileWarning = true; - warningAudioSource.pitch = distance < 800 ? 1.45f : 1f; - warningAudioSource.PlayOneShot(warningSound); - - float waitTime = distance < 800 ? .25f : 1.5f; - - yield return new WaitForSeconds(waitTime); - - if (ml.vessel && CanSeeTarget(ml.vessel)) - { - BDATargetManager.ReportVessel(ml.vessel, this); - } - } - warningSounding = false; - } - - #endregion Audio - - #region CounterMeasure - - public bool isChaffing; - public bool isFlaring; - public bool isECMJamming; - - bool isLegacyCMing; - - int cmCounter; - int cmAmount = 5; - - public void FireAllCountermeasures(int count) - { - if (!isChaffing && !isFlaring - && ThreatClosingTime(incomingMissileVessel) > cmThreshold) - { - StartCoroutine(AllCMRoutine(count)); - } - } - - public void FireECM() - { - if (!isECMJamming) - { - StartCoroutine(ECMRoutine()); - } - } - - public void FireChaff() - { - if (!isChaffing - && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) - { - StartCoroutine(ChaffRoutine((int)cmRepetition, cmInterval)); - } - } - - public void FireFlares() - { - if (!isFlaring - && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) - { - StartCoroutine(FlareRoutine((int)cmRepetition, cmInterval)); - StartCoroutine(ResetMissileThreatDistanceRoutine()); - } - } - - IEnumerator ECMRoutine() - { - isECMJamming = true; - //yield return new WaitForSeconds(UnityEngine.Random.Range(0.2f, 1f)); - using (List.Enumerator ecm = vessel.FindPartModulesImplementing().GetEnumerator()) - while (ecm.MoveNext()) - { - if (ecm.Current == null) continue; - if (ecm.Current.jammerEnabled) continue; - ecm.Current.EnableJammer(); - } - yield return new WaitForSeconds(10.0f); - isECMJamming = false; - - using (List.Enumerator ecm1 = vessel.FindPartModulesImplementing().GetEnumerator()) - while (ecm1.MoveNext()) - { - if (ecm1.Current == null) continue; - ecm1.Current.DisableJammer(); - } - } - - IEnumerator ChaffRoutine(int repetition, float interval) - { - isChaffing = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Starting chaff routine"); - // yield return new WaitForSeconds(0.2f); // Reaction time delay - for (int i = 0; i < repetition; i++) - { - using (List.Enumerator cm = vessel.FindPartModulesImplementing().GetEnumerator()) - while (cm.MoveNext()) - { - if (cm.Current == null) continue; - if (cm.Current.cmType == CMDropper.CountermeasureTypes.Chaff) - { - cm.Current.DropCM(); - } - } - - yield return new WaitForSeconds(interval); - } - yield return new WaitForSeconds(cmWaitTime); - isChaffing = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Ending chaff routine"); - } - - IEnumerator FlareRoutine(int repetition, float interval) - { - isFlaring = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Starting flare routine"); - // yield return new WaitForSeconds(0.2f); // Reaction time delay - for (int i = 0; i < repetition; i++) - { - using (List.Enumerator cm = vessel.FindPartModulesImplementing().GetEnumerator()) - while (cm.MoveNext()) - { - if (cm.Current == null) continue; - if (cm.Current.cmType == CMDropper.CountermeasureTypes.Flare) - { - cm.Current.DropCM(); - } - } - yield return new WaitForSeconds(interval); - } - yield return new WaitForSeconds(cmWaitTime); - isFlaring = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Ending flare routine"); - } - - 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]: Starting All CM routine"); - for (int i = 0; i < count; i++) - { - using (List.Enumerator cm = vessel.FindPartModulesImplementing().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); - } - isFlaring = false; - isChaffing = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory]: Ending All CM routine"); - } - - IEnumerator LegacyCMRoutine() - { - isLegacyCMing = true; - yield return new WaitForSeconds(UnityEngine.Random.Range(.2f, 1f)); - if (incomingMissileDistance < 2500) - { - cmAmount = Mathf.RoundToInt((2500 - incomingMissileDistance) / 400); - using (List.Enumerator cm = vessel.FindPartModulesImplementing().GetEnumerator()) - while (cm.MoveNext()) - { - if (cm.Current == null) continue; - cm.Current.DropCM(); - } - cmCounter++; - if (cmCounter < cmAmount) - { - yield return new WaitForSeconds(0.15f); - } - else - { - cmCounter = 0; - yield return new WaitForSeconds(UnityEngine.Random.Range(.5f, 1f)); - } - } - isLegacyCMing = false; - } - - public void MissileWarning(float distance, MissileBase ml)//take distance parameter - { - if (vessel.isActiveVessel && !warningSounding) - { - StartCoroutine(WarningSoundRoutine(distance, ml)); - } - - missileIsIncoming = true; - incomingMissileDistance = distance; - } - - #endregion CounterMeasure - - #region Fire - - bool FireCurrentMissile(bool checkClearance) - { - MissileBase missile = CurrentMissile; - if (missile == null) return false; - - if (missile is MissileBase) - { - MissileBase ml = missile; - if (checkClearance && (!CheckBombClearance(ml) || (ml is MissileLauncher && ((MissileLauncher)ml).rotaryRail && !((MissileLauncher)ml).rotaryRail.readyMissile == ml))) - { - using (List.Enumerator otherMissile = vessel.FindPartModulesImplementing().GetEnumerator()) - while (otherMissile.MoveNext()) - { - if (otherMissile.Current == null) continue; - if (otherMissile.Current == ml || otherMissile.Current.GetShortName() != ml.GetShortName() || - !CheckBombClearance(otherMissile.Current)) continue; - CurrentMissile = otherMissile.Current; - selectedWeapon = otherMissile.Current; - FireCurrentMissile(false); - return true; - } - CurrentMissile = ml; - selectedWeapon = ml; - return false; - } - - if (ml is MissileLauncher && ((MissileLauncher)ml).missileTurret) - { - ((MissileLauncher)ml).missileTurret.FireMissile(((MissileLauncher)ml)); - } - else if (ml is MissileLauncher && ((MissileLauncher)ml).rotaryRail) - { - ((MissileLauncher)ml).rotaryRail.FireMissile(((MissileLauncher)ml)); - } - else - { - SendTargetDataToMissile(ml); - ml.FireMissile(); - } - - if (guardMode) - { - if (ml.GetWeaponClass() == WeaponClasses.Bomb) - { - //StartCoroutine(BombsAwayRoutine(ml)); - } - } - else - { - if (vesselRadarData && vesselRadarData.autoCycleLockOnFire) - { - vesselRadarData.CycleActiveLock(); - } - } - } - else - { - SendTargetDataToMissile(missile); - missile.FireMissile(); - } - - CalculateMissilesAway(); // Immediately update missiles away. - UpdateList(); - return true; - } - - void FireMissile() - { - if (weaponIndex == 0) - { - return; - } - - if (selectedWeapon == null) - { - return; - } - - if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || - selectedWeapon.GetWeaponClass() == WeaponClasses.SLW || - selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) - { - FireCurrentMissile(true); - } - UpdateList(); - } - - #endregion Fire - - #region Weapon Info - - void DisplaySelectedWeaponMessage() - { - if (BDArmorySetup.GAME_UI_ENABLED && vessel == FlightGlobals.ActiveVessel) - { - ScreenMessages.RemoveMessage(selectionMessage); - selectionMessage.textInstance = null; - - selectionText = "Selected Weapon: " + (GetWeaponName(weaponArray[weaponIndex])).ToString(); - selectionMessage.message = selectionText; - selectionMessage.style = ScreenMessageStyle.UPPER_CENTER; - - ScreenMessages.PostScreenMessage(selectionMessage); - } - } - - string GetWeaponName(IBDWeapon weapon) - { - if (weapon == null) - { - return "None"; - } - else - { - return weapon.GetShortName(); - } - } - - public void UpdateList() - { - weaponTypes.Clear(); - // extension for feature_engagementenvelope: also clear engagement specific weapon lists - weaponTypesAir.Clear(); - weaponTypesMissile.Clear(); - weaponTypesGround.Clear(); - weaponTypesSLW.Clear(); - - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - string weaponName = weapon.Current.GetShortName(); - bool alreadyAdded = false; - using (List.Enumerator weap = weaponTypes.GetEnumerator()) - while (weap.MoveNext()) - { - if (weap.Current == null) continue; - if (weap.Current.GetShortName() == weaponName) - { - alreadyAdded = true; - //break; - } - } - - //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) - { - continue; - } - - if (!alreadyAdded) - { - weaponTypes.Add(weapon.Current); - } - - EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; - - if (engageableWeapon != null) - { - if (engageableWeapon.GetEngageAirTargets()) weaponTypesAir.Add(weapon.Current); - if (engageableWeapon.GetEngageMissileTargets()) weaponTypesMissile.Add(weapon.Current); - if (engageableWeapon.GetEngageGroundTargets()) weaponTypesGround.Add(weapon.Current); - if (engageableWeapon.GetEngageSLWTargets()) weaponTypesSLW.Add(weapon.Current); - } - else - { - weaponTypesAir.Add(weapon.Current); - weaponTypesMissile.Add(weapon.Current); - weaponTypesGround.Add(weapon.Current); - weaponTypesSLW.Add(weapon.Current); - } - - if (weapon.Current.GetWeaponClass() == WeaponClasses.Bomb || - weapon.Current.GetWeaponClass() == WeaponClasses.Missile || - weapon.Current.GetWeaponClass() == WeaponClasses.SLW) - { - weapon.Current.GetPart().FindModuleImplementing().GetMissileCount(); // #191, Do it this way so the GetMissileCount only updates when missile fired - } - } - - //weaponTypes.Sort(); - weaponTypes = weaponTypes.OrderBy(w => w.GetShortName()).ToList(); - - List tempList = new List { null }; - tempList.AddRange(weaponTypes); - - weaponArray = tempList.ToArray(); - - if (weaponIndex >= weaponArray.Length) - { - hasSingleFired = true; - triggerTimer = 0; - } - PrepareWeapons(); - } - - 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; - } - - if (vessel.isActiveVessel && weaponIndex != 0) - { - DisplaySelectedWeaponMessage(); - } - } - - if (weaponIndex == 0) - { - selectedWeapon = null; - hasSingleFired = true; - } - - MissileBase aMl = GetAsymMissile(); - if (aMl) - { - selectedWeapon = aMl; - } - - MissileBase rMl = GetRotaryReadyMissile(); - if (rMl) - { - selectedWeapon = rMl; - } - - UpdateSelectedWeaponState(); - } - - private void UpdateSelectedWeaponState() - { - if (vessel == null) return; - - MissileBase aMl = GetAsymMissile(); - if (aMl) - { - CurrentMissile = aMl; - } - - MissileBase rMl = GetRotaryReadyMissile(); - if (rMl) - { - CurrentMissile = rMl; - } - - if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb || selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW)) - { - //Debug.Log("[BDArmory]: =====selected weapon: " + selectedWeapon.GetPart().name); - if (!CurrentMissile || CurrentMissile.part.name != selectedWeapon.GetPart().name) - { - CurrentMissile = selectedWeapon.GetPart().FindModuleImplementing(); - } - } - else - { - CurrentMissile = null; - } - - //selectedWeapon = weaponArray[weaponIndex]; - - //bomb stuff - if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) - { - bombPart = selectedWeapon.GetPart(); - } - else - { - bombPart = null; - } - - //gun ripple stuff - if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) && - currentGun.roundsPerMinute < 1500) - { - float counter = 0; // Used to get a count of the ripple weapons. a float version of rippleGunCount. - gunRippleIndex = 0; - // This value will be incremented as we set the ripple weapons - rippleGunCount = 0; - float weaponRpm = 0; // used to set the rippleGunRPM - - // JDK: this looks like it can be greatly simplified... - - #region Old Code (for reference. remove when satisfied new code works as expected. - - //List tempListModuleWeapon = vessel.FindPartModulesImplementing(); - //foreach (ModuleWeapon weapon in tempListModuleWeapon) - //{ - // if (selectedWeapon.GetShortName() == weapon.GetShortName()) - // { - // weapon.rippleIndex = Mathf.RoundToInt(counter); - // weaponRPM = weapon.roundsPerMinute; - // ++counter; - // rippleGunCount++; - // } - //} - //gunRippleRpm = weaponRPM * counter; - //float timeDelayPerGun = 60f / (weaponRPM * counter); - ////number of seconds between each gun firing; will reduce with increasing RPM or number of guns - //foreach (ModuleWeapon weapon in tempListModuleWeapon) - //{ - // if (selectedWeapon.GetShortName() == weapon.GetShortName()) - // { - // weapon.initialFireDelay = timeDelayPerGun; //set the time delay for moving to next index - // } - //} - - //RippleOption ro; //ripplesetup and stuff - //if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) - //{ - // ro = rippleDictionary[selectedWeapon.GetShortName()]; - //} - //else - //{ - // ro = new RippleOption(currentGun.useRippleFire, 650); //take from gun's persistant value - // rippleDictionary.Add(selectedWeapon.GetShortName(), ro); - //} - - //foreach (ModuleWeapon w in vessel.FindPartModulesImplementing()) - //{ - // if (w.GetShortName() == selectedWeapon.GetShortName()) - // w.useRippleFire = ro.rippleFire; - //} - - #endregion Old Code (for reference. remove when satisfied new code works as expected. - - // TODO: JDK verify new code works as expected. - // New code, simplified. - - //First lest set the Ripple Option. Doing it first eliminates a loop. - RippleOption ro; //ripplesetup and stuff - if (rippleDictionary.ContainsKey(selectedWeapon.GetShortName())) - { - ro = rippleDictionary[selectedWeapon.GetShortName()]; - } - else - { - ro = new RippleOption(currentGun.useRippleFire, 650); //take from gun's persistant value - rippleDictionary.Add(selectedWeapon.GetShortName(), ro); - } - - //Get ripple weapon count, so we don't have to enumerate the whole list again. - List rippleWeapons = new List(); - using (List.Enumerator weapCnt = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapCnt.MoveNext()) - { - if (weapCnt.Current == null) continue; - if (selectedWeapon.GetShortName() != weapCnt.Current.GetShortName()) continue; - weaponRpm = weapCnt.Current.roundsPerMinute; - 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. - using (List.Enumerator weapon = rippleWeapons.GetEnumerator()) - while (weapon.MoveNext()) - { - 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++; - } - } - - ToggleTurret(); - SetMissileTurrets(); - SetRotaryRails(); - } - - private HashSet baysOpened = new HashSet(); - private bool SetCargoBays() - { - if (!guardMode) return false; - bool openingBays = false; - - if (weaponIndex > 0 && CurrentMissile && guardTarget && Vector3.Dot(guardTarget.transform.position - CurrentMissile.transform.position, CurrentMissile.GetForwardTransform()) > 0) - { - if (CurrentMissile.part.ShieldedFromAirstream) - { - using (List.Enumerator ml = vessel.FindPartModulesImplementing().GetEnumerator()) - while (ml.MoveNext()) - { - if (ml.Current == null) continue; - if (ml.Current.part.ShieldedFromAirstream) ml.Current.inCargoBay = true; - } - } - - if (CurrentMissile.inCargoBay) - { - using (List.Enumerator bay = vessel.FindPartModulesImplementing().GetEnumerator()) - while (bay.MoveNext()) - { - if (bay.Current == null) continue; - if (CurrentMissile.part.airstreamShields.Contains(bay.Current)) - { - ModuleAnimateGeneric anim = bay.Current.part.Modules.GetModule(bay.Current.DeployModuleIndex) as ModuleAnimateGeneric; - if (anim == null) continue; - - string toggleOption = anim.Events["Toggle"].guiName; - if (toggleOption == "Open") - { - if (anim) - { - anim.Toggle(); - openingBays = true; - baysOpened.Add(bay.Current.GetPersistentId()); - } - } - } - else - { - if (!baysOpened.Contains(bay.Current.GetPersistentId())) continue; // Only close bays we've opened. - ModuleAnimateGeneric anim = bay.Current.part.Modules.GetModule(bay.Current.DeployModuleIndex) as ModuleAnimateGeneric; - if (anim == null) continue; - - string toggleOption = anim.Events["Toggle"].guiName; - if (toggleOption == "Close") - { - if (anim) - { - anim.Toggle(); - } - } - } - } - } - else - { - using (List.Enumerator bay = vessel.FindPartModulesImplementing().GetEnumerator()) - while (bay.MoveNext()) - { - if (bay.Current == null) continue; - if (!baysOpened.Contains(bay.Current.GetPersistentId())) continue; // Only close bays we've opened. - ModuleAnimateGeneric anim = bay.Current.part.Modules.GetModule(bay.Current.DeployModuleIndex) as ModuleAnimateGeneric; - if (anim == null) continue; - - string toggleOption = anim.Events["Toggle"].guiName; - if (toggleOption == "Close") - { - if (anim) - { - anim.Toggle(); - } - } - } - } - } - else - { - using (List.Enumerator bay = vessel.FindPartModulesImplementing().GetEnumerator()) - while (bay.MoveNext()) - { - if (bay.Current == null) continue; - if (!baysOpened.Contains(bay.Current.GetPersistentId())) continue; // Only close bays we've opened. - ModuleAnimateGeneric anim = bay.Current.part.Modules.GetModule(bay.Current.DeployModuleIndex) as ModuleAnimateGeneric; - if (anim == null) continue; - - string toggleOption = anim.Events["Toggle"].guiName; - if (toggleOption == "Close") - { - if (anim) - { - anim.Toggle(); - } - } - } - } - - return openingBays; - } - - void SetRotaryRails() - { - if (weaponIndex == 0) return; - - if (selectedWeapon == null) return; - - if ( - !(selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || - selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb || - selectedWeapon.GetWeaponClass() == WeaponClasses.SLW)) return; - - if (!CurrentMissile) return; - - //TODO BDModularGuidance: Rotatory Rail? - MissileLauncher cm = CurrentMissile as MissileLauncher; - if (cm == null) return; - using (List.Enumerator rotRail = vessel.FindPartModulesImplementing().GetEnumerator()) - while (rotRail.MoveNext()) - { - if (rotRail.Current == null) continue; - if (rotRail.Current.missileCount == 0) - { - //Debug.Log("SetRotaryRails(): rail has no missiles"); - continue; - } - - //Debug.Log("[BDArmory]: SetRotaryRails(): rotRail.Current.readyToFire: " + rotRail.Current.readyToFire + ", rotRail.Current.readyMissile: " + ((rotRail.Current.readyMissile != null) ? rotRail.Current.readyMissile.part.name : "null") + ", rotRail.Current.nextMissile: " + ((rotRail.Current.nextMissile != null) ? rotRail.Current.nextMissile.part.name : "null")); - - //Debug.Log("[BDArmory]: current missile: " + cm.part.name); - - if (rotRail.Current.readyToFire) - { - if (!rotRail.Current.readyMissile) - { - rotRail.Current.RotateToMissile(cm); - return; - } - - if (rotRail.Current.readyMissile.part.name != cm.part.name) - { - rotRail.Current.RotateToMissile(cm); - } - } - else - { - if (!rotRail.Current.nextMissile) - { - rotRail.Current.RotateToMissile(cm); - } - else if (rotRail.Current.nextMissile.part.name != cm.part.name) - { - rotRail.Current.RotateToMissile(cm); - } - } - } - } - - void SetMissileTurrets() - { - MissileLauncher cm = CurrentMissile as MissileLauncher; - using (List.Enumerator mt = vessel.FindPartModulesImplementing().GetEnumerator()) - while (mt.MoveNext()) - { - if (mt.Current == null) continue; - if (!mt.Current.isActiveAndEnabled) continue; - if (weaponIndex > 0 && cm && mt.Current.ContainsMissileOfType(cm) && (!mt.Current.activeMissileOnly || cm.missileTurret == mt.Current)) - { - mt.Current.EnableTurret(); - } - else - { - mt.Current.DisableTurret(); - } - } - } - - public void CycleWeapon(bool forward) - { - if (forward) weaponIndex++; - else weaponIndex--; - weaponIndex = (int)Mathf.Repeat(weaponIndex, weaponArray.Length); - - hasSingleFired = true; - triggerTimer = 0; - - UpdateList(); - - DisplaySelectedWeaponMessage(); - - if (vessel.isActiveVessel && !guardMode) - { - audioSource.PlayOneShot(clickSound); - } - } - - public void CycleWeapon(int index) - { - if (index >= weaponArray.Length) - { - index = 0; - } - weaponIndex = index; - - UpdateList(); - - if (vessel.isActiveVessel && !guardMode) - { - audioSource.PlayOneShot(clickSound); - - DisplaySelectedWeaponMessage(); - } - } - - public Part FindSym(Part p) - { - using (List.Enumerator pSym = p.symmetryCounterparts.GetEnumerator()) - while (pSym.MoveNext()) - { - if (pSym.Current == null) continue; - if (pSym.Current != p && pSym.Current.vessel == vessel) - { - return pSym.Current; - } - } - - return null; - } - - private MissileBase GetAsymMissile() - { - if (weaponIndex == 0) return null; - if (weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb || - weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Missile || - weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.SLW) - { - MissileBase firstMl = null; - using (List.Enumerator ml = vessel.FindPartModulesImplementing().GetEnumerator()) - while (ml.MoveNext()) - { - if (ml.Current == null) continue; - MissileLauncher launcher = ml.Current as MissileLauncher; - if (launcher != null) - { - if (weaponArray[weaponIndex].GetPart() == null || launcher.part.name != weaponArray[weaponIndex].GetPart().name) continue; - } - else - { - BDModularGuidance guidance = ml.Current as BDModularGuidance; - if (guidance != null) - { //We have set of parts not only a part - if (guidance.GetShortName() != weaponArray[weaponIndex]?.GetShortName()) continue; - } - } - if (firstMl == null) firstMl = ml.Current; - - if (!FindSym(ml.Current.part)) - { - return ml.Current; - } - } - return firstMl; - } - return null; - } - - private MissileBase GetRotaryReadyMissile() - { - if (weaponIndex == 0) return null; - if (weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb || - weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Missile || - weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.SLW) - { - //TODO BDModularGuidance: 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 (!missile.rotaryRail) - { - return missile; - } - if (missile.rotaryRail.readyToFire && missile.rotaryRail.readyMissile == CurrentMissile) - { - return missile; - } - } - using (List.Enumerator ml = vessel.FindPartModulesImplementing().GetEnumerator()) - while (ml.MoveNext()) - { - if (ml.Current == null) continue; - if (weaponArray[weaponIndex].GetPart() == null || ml.Current.part.name != weaponArray[weaponIndex].GetPart().name) continue; - - if (!ml.Current.rotaryRail) - { - return ml.Current; - } - if (ml.Current.rotaryRail.readyToFire && ml.Current.rotaryRail.readyMissile.part.name == weaponArray[weaponIndex].GetPart().name) - { - return ml.Current.rotaryRail.readyMissile; - } - } - return null; - } - return null; - } - - bool CheckBombClearance(MissileBase ml) - { - if (!BDArmorySettings.BOMB_CLEARANCE_CHECK) return true; - - if (ml.part.ShieldedFromAirstream) - { - return false; - } - - //TODO BDModularGuidance: Bombs and turrents - MissileLauncher launcher = ml as MissileLauncher; - if (launcher != null) - { - if (launcher.rotaryRail && launcher.rotaryRail.readyMissile != ml) - { - return false; - } - - if (launcher.missileTurret && !launcher.missileTurret.turretEnabled) - { - return false; - } - - if (ml.dropTime > 0.3f) - { - //debug lines - LineRenderer lr = null; - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) - { - lr = GetComponent(); - 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) + - ((FlightGlobals.getGeeForceAtPosition(transform.position) - vessel.acceleration) * - 0.5f * time * time); - Vector3 crossAxis = Vector3.Cross(direction, ml.MissileReferenceTransform.transform.right).normalized; - - float rayDistance; - if (launcher.thrust == 0 || launcher.cruiseThrust == 0) - { - rayDistance = 8; - } - else - { - //distance till engine starts based on grav accel and vessel accel - rayDistance = direction.magnitude; - } - - Ray[] rays = - { - new Ray(ml.MissileReferenceTransform.position - (radius*crossAxis), direction), - new Ray(ml.MissileReferenceTransform.position + (radius*crossAxis), direction), - new Ray(ml.MissileReferenceTransform.position, direction) - }; - - if (lr) - { - lr.useWorldSpace = false; - lr.positionCount = 4; - lr.SetPosition(0, transform.InverseTransformPoint(rays[0].origin)); - lr.SetPosition(1, transform.InverseTransformPoint(rays[0].GetPoint(rayDistance))); - lr.SetPosition(2, transform.InverseTransformPoint(rays[1].GetPoint(rayDistance))); - lr.SetPosition(3, transform.InverseTransformPoint(rays[1].origin)); - } - - using (IEnumerator rt = rays.AsEnumerable().GetEnumerator()) - while (rt.MoveNext()) - { - RaycastHit[] hits = Physics.RaycastAll(rt.Current, rayDistance, 557057); - using (IEnumerator t1 = hits.AsEnumerable().GetEnumerator()) - while (t1.MoveNext()) - { - Part p = t1.Current.collider.GetComponentInParent(); - - if ((p == null || p == ml.part) && p != null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: RAYCAST HIT, clearance is FALSE! part=" + p?.name + ", collider=" + p?.collider); - return false; - } - } - return true; - } - - //forward check for no-drop missiles - RaycastHit[] hitparts = Physics.RaycastAll(new Ray(ml.MissileReferenceTransform.position, ml.GetForwardTransform()), 50, 557057); - using (IEnumerator t = hitparts.AsEnumerable().GetEnumerator()) - while (t.MoveNext()) - { - Part p = t.Current.collider.GetComponentInParent(); - if ((p == null || p == ml.part) && p != null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: RAYCAST HIT, clearance is FALSE! part=" + p?.name + ", collider=" + p?.collider); - return false; - } - } - return true; - } - - void RefreshModules() - { - radars = vessel.FindPartModulesImplementing(); - // 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(); - */ - jammers = vessel.FindPartModulesImplementing(); - targetingPods = vessel.FindPartModulesImplementing(); - wmModules = vessel.FindPartModulesImplementing(); - } - - #endregion Weapon Info - /* - #region Weapon Choice - // Unnecessary. Bool Smart Pick now handles Antirad selection - bool TryPickAntiRad(TargetInfo target) - { - CycleWeapon(0); //go to start of array - while (true) - { - CycleWeapon(true); - if (selectedWeapon == null) return false; - if (selectedWeapon.GetWeaponClass() != WeaponClasses.Missile) continue; - List.Enumerator ml = selectedWeapon.GetPart().FindModulesImplementing().GetEnumerator(); - while (ml.MoveNext()) - { - if (ml.Current == null) continue; - if (ml.Current.TargetingMode == MissileBase.TargetingModes.AntiRad) - { - return true; - } - break; - } - ml.Dispose(); - //return; - } - } - - #endregion Weapon Choice - */ - #region Targeting - - #region Smart Targeting - - void SmartFindTarget() - { - var lastTarget = currentTarget; - List targetsTried = new List(); - string targetDebugText = ""; - - if (overrideTarget) //begin by checking the override target, since that takes priority - { - targetsTried.Add(overrideTarget); - SetTarget(overrideTarget); - if (SmartPickWeapon_EngagementEnvelope(overrideTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging an override target with " + selectedWeapon); - } - overrideTimer = 15f; - return; - } - else if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + 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 - - //if AIRBORNE, try to engage airborne target first - if (!vessel.LandedOrSplashed && !targetMissiles) - { - 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); - if (SmartPickWeapon_EngagementEnvelope(potentialAirTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + targetDebugText + selectedWeapon); - } - return; - } - else if (!BDArmorySettings.DISABLE_RAMMING) - { - if (!HasWeaponsAndAmmo() && pilotAI != null && pilotAI.allowRamming) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[MissileFire]: " + vessel.vesselName + targetDebugText + "ramming."); - } - return; - } - } - } - } - - TargetInfo potentialTarget = null; - //=========HIGH PRIORITY MISSILES============= - //first engage any missiles targeting this vessel - potentialTarget = BDATargetManager.GetMissileTarget(this, true); - if (potentialTarget) - { - targetsTried.Add(potentialTarget); - SetTarget(potentialTarget); - if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging incoming missile with " + selectedWeapon); - } - return; - } - } - - //then engage any missiles that are not engaged - potentialTarget = BDATargetManager.GetUnengagedMissileTarget(this); - if (potentialTarget) - { - targetsTried.Add(potentialTarget); - SetTarget(potentialTarget); - if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging unengaged missile with " + selectedWeapon); - } - return; - } - } - - //=========END HIGH PRIORITY MISSILES============= - - //============VESSEL THREATS============ - if (!targetMissiles) - { - // select target based on competition style - if (BDArmorySettings.DEFAULT_FFA_TARGETING) - { - potentialTarget = BDATargetManager.GetClosestTargetWithBiasAndHysteresis(this); - targetDebugText = " is engaging an FFA target with "; - } - else if (this.targetPriorityEnabled) - { - potentialTarget = BDATargetManager.GetHighestPriorityTarget(this); - targetDebugText = " is engaging highest priority target with "; - } - else - { - potentialTarget = BDATargetManager.GetLeastEngagedTarget(this); - targetDebugText = " is engaging the least engaged target with "; - } - - if (potentialTarget) - { - targetsTried.Add(potentialTarget); - SetTarget(potentialTarget); - /* - if (CrossCheckWithRWR(potentialTarget) && TryPickAntiRad(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging the least engaged radar target with " + - selectedWeapon.GetShortName()); - } - return; - } - */ - if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + targetDebugText + selectedWeapon.GetShortName()); - } - return; - } - } - - //then engage the closest enemy - potentialTarget = BDATargetManager.GetClosestTarget(this); - if (potentialTarget) - { - targetsTried.Add(potentialTarget); - SetTarget(potentialTarget); - /* - if (CrossCheckWithRWR(potentialTarget) && TryPickAntiRad(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging the closest radar target with " + - selectedWeapon.GetShortName()); - } - return; - } - */ - if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging the closest target with " + - selectedWeapon.GetShortName()); - } - return; - } - } - } - //============END VESSEL THREATS============ - - //============LOW PRIORITY MISSILES========= - //try to engage least engaged hostile missiles first - potentialTarget = BDATargetManager.GetMissileTarget(this); - if (potentialTarget) - { - targetsTried.Add(potentialTarget); - SetTarget(potentialTarget); - if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]:" + vessel.vesselName + " is engaging a missile with " + selectedWeapon.GetShortName()); - } - return; - } - } - - //then try to engage closest hostile missile - potentialTarget = BDATargetManager.GetClosestMissileTarget(this); - if (potentialTarget) - { - targetsTried.Add(potentialTarget); - SetTarget(potentialTarget); - if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]:" + vessel.vesselName + " is engaging a missile with " + selectedWeapon.GetShortName()); - } - return; - } - } - //==========END LOW PRIORITY MISSILES============= - - if (targetMissiles) //NO MISSILES BEYOND THIS POINT// - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]:" + vessel.vesselName + " is disengaging - no valid weapons"); - } - CycleWeapon(0); - SetTarget(null); - return; - } - - //if nothing works, get all remaining targets and try weapons against them - using (List.Enumerator finalTargets = BDATargetManager.GetAllTargetsExcluding(targetsTried, this).GetEnumerator()) - while (finalTargets.MoveNext()) - { - if (finalTargets.Current == null) continue; - SetTarget(finalTargets.Current); - if (!SmartPickWeapon_EngagementEnvelope(finalTargets.Current)) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + vessel.vesselName + " is engaging a final target with " + - selectedWeapon.GetShortName()); - } - return; - } - - //no valid targets found - if (potentialTarget == null || selectedWeapon == null) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + 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. - { - vesselRadarData.UnlockAllTargets(); - } - return; - } - - Debug.Log("[BDArmory]: Unhandled target case"); - } - - // Update target priority UI - public void UpdateTargetPriorityUI(TargetInfo target) - { - // Return if no target - if (target == null) - { - TargetScoreLabel = ""; - TargetLabel = ""; - return; - } - - // Get UI fields - var TargetBiasFields = Fields["targetBias"]; - var TargetRangeFields = Fields["targetWeightRange"]; - 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 TargetFriendliesEngagingFields = Fields["targetWeightFriendliesEngaging"]; - var TargetThreatFields = Fields["targetWeightThreat"]; - - // Calculate score values - float targetBiasValue = targetBias; - float targetRangeValue = target.TargetPriRange(this); - 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 targetFriendliesEngagingValue = target.TargetPriFriendliesEngaging(this); - float targetThreatValue = target.TargetPriThreat(target.weaponManager, this); - - // Calculate total target score - float targetScore = targetBiasValue * ( - targetWeightRange * targetRangeValue + - targetWeightATA * targetATAValue + - targetWeightAccel * targetAccelValue + - targetWeightClosureTime * targetClosureTimeValue + - targetWeightWeaponNumber * targetWeaponNumberValue + - targetWeightMass * targetMassValue + - targetWeightFriendliesEngaging * targetFriendliesEngagingValue + - targetWeightThreat * targetThreatValue + - targetWeightAoD * targetAoDValue); - - // 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"); - - TargetScoreLabel = targetScore.ToString("0.00"); - TargetLabel = target.Vessel.GetDisplayName(); - } - - // extension for feature_engagementenvelope: new smartpickweapon method - bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) - { - // Part 1: Guard conditions (when not to pick a weapon) - // ------ - if (!target) - return false; - - if (AI != null && AI.pilotEnabled && !AI.CanEngage()) - return false; - - // Part 2: check weapons against individual target types - // ------ - - float distance = Vector3.Distance(transform.position + vessel.Velocity(), target.position + target.velocity); - IBDWeapon targetWeapon = null; - float targetWeaponRPM = 0; - float targetWeaponTDPS = 0; - float targetWeaponImpact = 0; - float targetLaserDamage = 0; - float targetYield = 0; - float targetRocketPower = 0; - float targetRocketAccel = 0; - - if (target.isMissile) - { - // iterate over weaponTypesMissile and pick suitable one based on engagementRange (and dynamic launch zone for missiles) - // Prioritize by: - // 1. Lasers - // 2. Guns - // 3. AA missiles - using (List.Enumerator item = weaponTypesMissile.GetEnumerator()) - while (item.MoveNext()) - { - if (item.Current == null) continue; - // candidate, check engagement envelope - if (!CheckEngagementEnvelope(item.Current, distance)) continue; - // weapon usable, if missile continue looking for lasers/guns, else take it - WeaponClasses candidateClass = item.Current.GetWeaponClass(); - - if (candidateClass == WeaponClasses.DefenseLaser) - { - float canidateYTraverse = ((ModuleWeapon)item.Current).yawRange; - float canidatePTraverse = ((ModuleWeapon)item.Current).maxPitch; - bool electrolaser = ((ModuleWeapon)item.Current).electroLaser; - - if (electrolaser) continue; //electrolasers useless against missiles - - if (targetWeapon != null && (canidateYTraverse > 0 || canidatePTraverse > 0)) //prioritize turreted lasers - { - targetWeapon = item.Current; - break; - } - targetWeapon = item.Current; // then any laser - break; - } - - if (candidateClass == WeaponClasses.Gun) - { - // For point defense, favor turrets and RoF - float candidateRPM = ((ModuleWeapon)item.Current).roundsPerMinute; - float canidateYTraverse = ((ModuleWeapon)item.Current).yawRange; - float canidatePTraverse = ((ModuleWeapon)item.Current).maxPitch; - if (targetWeapon != null && (canidateYTraverse > 0 || canidatePTraverse > 0)) - { - candidateRPM *= 2.0f; // weight selection towards turrets - } - if ((targetWeapon != null) && (targetWeaponRPM > candidateRPM)) - continue; //dont replace better guns (but do replace missiles) - - targetWeapon = item.Current; - targetWeaponRPM = candidateRPM; - } - - if (candidateClass != WeaponClasses.Missile) continue; - // TODO: for AA, favour higher thrust+turnDPS - - MissileLauncher mlauncher = item.Current as MissileLauncher; - float candidateTDPS = 0f; - - if (mlauncher != null) - { - candidateTDPS = mlauncher.thrust + mlauncher.maxTurnRateDPS; - } - else - { //is modular missile - BDModularGuidance mm = item.Current as BDModularGuidance; - candidateTDPS = 5000; - } - if ((targetWeapon != null) && ((targetWeapon.GetWeaponClass() == WeaponClasses.Gun) || (targetWeaponTDPS > candidateTDPS))) - continue; //dont replace guns or better missiles - - targetWeapon = item.Current; - targetWeaponTDPS = candidateTDPS; - } - } - - //else if (!target.isLanded) - else if (target.isFlying) - { - // iterate over weaponTypesAir and pick suitable one based on engagementRange (and dynamic launch zone for missiles) - // Prioritize by: - // 1. AA missiles (if we're flying, otherwise use guns if we're within gun range) - // 1. Lasers - // 2. Guns - // - using (List.Enumerator item = weaponTypesAir.GetEnumerator()) - while (item.MoveNext()) - { - if (item.Current == null) continue; - // candidate, check engagement envelope - if (!CheckEngagementEnvelope(item.Current, distance)) continue; - // weapon usable, if missile continue looking for lasers/guns, else take it - WeaponClasses candidateClass = item.Current.GetWeaponClass(); - - if (candidateClass == WeaponClasses.DefenseLaser) - { - // For AA, favour higher power/turreted - float candidatePower = ((ModuleWeapon)item.Current).laserDamage; - bool canidateGimbal = ((ModuleWeapon)item.Current).turret; - float canidateTraverse = ((ModuleWeapon)item.Current).yawRange; - bool electrolaser = ((ModuleWeapon)item.Current).electroLaser; - - if (electrolaser = true && target.isDebilitated) continue; // don't select EMP weapons if craft already disabld - - if ((targetWeapon != null) && (canidateGimbal = true && canidateTraverse > 0)) - { - candidatePower *= 1.5f; // weight selection towards turreted lasers - } - if ((targetWeapon != null) && (targetLaserDamage > candidatePower)) - continue; //dont replace better guns (but do replace missiles) - - targetWeapon = item.Current; - targetLaserDamage = candidatePower; - if (distance <= gunRange) - break; - } - - if (candidateClass == WeaponClasses.Gun) - { - // For AAA, favour higher RPM and turrets - float candidateRPM = ((ModuleWeapon)item.Current).roundsPerMinute; - bool canidateGimbal = ((ModuleWeapon)item.Current).turret; - float canidateTraverse = ((ModuleWeapon)item.Current).yawRange; - bool canidatePFuzed = ((ModuleWeapon)item.Current).proximityDetonation; - bool canidateVTFuzed = ((ModuleWeapon)item.Current).airDetonation; - float Cannistershot = ((ModuleWeapon)item.Current).ProjectileCount; - if ((targetWeapon != null) && (canidateGimbal = true && canidateTraverse > 0)) - { - candidateRPM *= 1.5f; // weight selection towards turrets - } - if (targetWeapon != null && (canidatePFuzed || canidateVTFuzed)) - { - candidateRPM *= 1.5f; // weight selection towards flak ammo - } - if (targetWeapon != null && Cannistershot > 0) - { - candidateRPM *= (1 + (Cannistershot / 2)); // weight selection towards cluster ammo based on submunition count - } - if ((targetWeapon != null) && ((targetWeaponRPM > candidateRPM) || (targetWeapon.GetWeaponClass() == WeaponClasses.Missile))) - continue; //dont replace better guns or missiles - - targetWeapon = item.Current; - targetWeaponRPM = candidateRPM; - } - if (candidateClass == WeaponClasses.Rocket) - { - //for AA, favor higher accel and proxifuze - float canidateRocketAccel = (((ModuleWeapon)item.Current).thrust / ((ModuleWeapon)item.Current).rocketMass); - float candidateRPM = ((ModuleWeapon)item.Current).roundsPerMinute; - bool canidatePFuzed = ((ModuleWeapon)item.Current).proximityDetonation; - - if ((targetWeapon != null) && (targetRocketAccel > canidateRocketAccel)) - { - candidateRPM *= 1.5f; //weight towards faster rockets - } - if (targetWeapon != null && canidatePFuzed) - { - candidateRPM *= 1.5f; // weight selection towards flak ammo - } - if ((targetWeapon != null) && (targetWeapon.GetWeaponClass() == WeaponClasses.Gun)) continue;// don't replace guns - if ((targetWeapon != null) && (targetWeaponRPM > candidateRPM)) - continue; - - targetWeapon = item.Current; - targetWeaponRPM = candidateRPM; - targetRocketAccel = canidateRocketAccel; - } - if (candidateClass != WeaponClasses.Missile) continue; - MissileLauncher mlauncher = item.Current as MissileLauncher; - float candidateTDPS = 0f; - - if (mlauncher != null) - { - candidateTDPS = mlauncher.thrust + mlauncher.maxTurnRateDPS; - } - else - { //is modular missile - BDModularGuidance mm = item.Current as BDModularGuidance; - candidateTDPS = 5000; - } - - if (targetWeapon == null) - { - targetWeapon = item.Current; - targetWeaponTDPS = candidateTDPS; - } - else if ((!vessel.LandedOrSplashed) || ((distance > gunRange) && (vessel.LandedOrSplashed))) // If we're not airborne, we want to prioritize guns - { - if (targetWeaponTDPS > candidateTDPS) - continue; //dont better missiles - - targetWeapon = item.Current; - targetWeaponTDPS = candidateTDPS; - } - } - } - else if (target.isLandedOrSurfaceSplashed) - { - // iterate over weaponTypesGround and pick suitable one based on engagementRange (and dynamic launch zone for missiles) - // Prioritize by: - // 1. ground attack missiles (cruise, gps, unguided) if target not moving - // 2. ground attack missiles (guided) if target is moving - // 3. Bombs / Rockets - // 4. Guns - using (List.Enumerator item = weaponTypesGround.GetEnumerator()) - while (item.MoveNext()) - { - if (item.Current == null) continue; - // candidate, check engagement envelope - if (!CheckEngagementEnvelope(item.Current, distance)) continue; - // weapon usable, if missile continue looking for lasers/guns, else take it - WeaponClasses candidateClass = item.Current.GetWeaponClass(); - - if (candidateClass == WeaponClasses.Missile) - { - // Priority Sequence: - // - Antiradiation - // - guided missiles - // - by blast strength - float canidateYield = ((MissileBase)item.Current).GetBlastRadius(); - double srfSpeed = currentTarget.Vessel.horizontalSrfSpeed; - bool canidateAGM = false; - bool canidateAntiRad = false; - - if (srfSpeed < 1) // set higher than 0 in case of physics jitteriness - { - if (((MissileBase)item.Current).TargetingMode == MissileBase.TargetingModes.Gps || - (((MissileBase)item.Current).GuidanceMode == MissileBase.GuidanceModes.Cruise || - ((MissileBase)item.Current).GuidanceMode == MissileBase.GuidanceModes.AGMBallistic || - ((MissileBase)item.Current).GuidanceMode == MissileBase.GuidanceModes.None)) - { - if (targetWeapon != null && targetYield > canidateYield) continue; //prioritize biggest Boom - targetYield = canidateYield; - canidateAGM = true; - targetWeapon = item.Current; - if (distance > ((MissileBase)item.Current).engageRangeMin) - break; - } - } - if (((MissileBase)item.Current).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.pingsData[i].signalStrength == 0 || rwr.pingsData[i].signalStrength == 5) - { - if ((rwr.pingWorldPositions[i] - guardTarget.CoM).sqrMagnitude < 20 * 20) //is current target a hostile radar source? - { - canidateAntiRad = true; - } - } - } - if (canidateAntiRad) - { - if (targetWeapon != null && targetYield > canidateYield) continue; //prioritize biggest Boom - targetYield = canidateYield; - targetWeapon = item.Current; - canidateAGM = true; - } - } - else if (((MissileBase)item.Current).TargetingMode == MissileBase.TargetingModes.Laser) - { - if ((targetWeapon != null && targetYield > canidateYield) && !canidateAntiRad) continue; - canidateAGM = true; - targetYield = canidateYield; - targetWeapon = item.Current; - } - else - { - if (!canidateAGM) - { - if (targetWeapon != null && targetYield > canidateYield) continue; - targetYield = canidateYield; - targetWeapon = item.Current; - } - } - } - - // TargetInfo.isLanded includes splashed but not underwater, for whatever reasons. - // If target is splashed, and we have torpedoes, use torpedoes, because, obviously, - // torpedoes are the best kind of sausage for splashed targets, - // almost as good as STS missiles, which we don't have. - if (candidateClass == WeaponClasses.SLW && target.isSplashed) - { - float canidateYield = ((MissileBase)item.Current).GetBlastRadius(); - // not sure on the desired selection priority algorithm, so placeholder By Yield for now - float droptime = ((MissileBase)item.Current).dropTime; - - if (droptime > 0 || vessel.LandedOrSplashed) //make sure it's an airdropped torpedo if flying - { - if (targetYield > canidateYield) continue; - targetYield = canidateYield; - targetWeapon = item.Current; - if (distance > gunRange) - break; - } - } - - if (candidateClass == WeaponClasses.Bomb) - { - // only useful if we are flying - float canidateYield = ((MissileBase)item.Current).GetBlastRadius(); - if (!vessel.LandedOrSplashed) - { - // Priority Sequence: - // - guided (JDAM) - // - by blast strength - // - find way to implement cluster bomb selection priority? - if (((MissileBase)item.Current).GuidanceMode == MissileBase.GuidanceModes.AGMBallistic) - { - if (targetYield > canidateYield) continue; //prioritize biggest Boom - targetYield = canidateYield; - targetWeapon = item.Current; - if (targetWeapon != null && distance > canidateYield) // don't drop bombs when within blast radius - break; //Prioritize guided bombs - } - if (((MissileBase)item.Current).GuidanceMode == MissileBase.GuidanceModes.None) - { - if (targetYield > canidateYield) continue; - targetYield = canidateYield; - targetWeapon = item.Current; - if (targetWeapon != null && distance > canidateYield) - break; //then standard - } - } - } - - if (candidateClass == WeaponClasses.Rocket) - { - float canidateRocketPower = ((ModuleWeapon)item.Current).tntMass; - - if ((targetWeapon != null) && (targetWeapon.GetWeaponClass() == WeaponClasses.Bomb)) continue; - // dont replace bombs - - if ((targetWeapon != null) && (targetRocketPower > canidateRocketPower)) - continue; //don't replace higher yield rockets - targetWeapon = item.Current; - targetRocketPower = canidateRocketPower; - } - - if ((candidateClass != WeaponClasses.Gun)) continue; - // Flying: prefer bombs/rockets/missiles - if (!vessel.LandedOrSplashed) - if (targetWeapon != null) - // dont replace bombs/rockets - continue; - // else: - if ((distance > gunRange) && (targetWeapon != null)) - continue; - // For Ground Attack, favour higher blast strength - float candidateImpact = ((ModuleWeapon)item.Current).tntMass; - - if ((targetWeapon != null) && (targetWeaponImpact > candidateImpact)) - continue; //dont replace better guns - - targetWeapon = item.Current; - targetWeaponImpact = candidateImpact; - } - } - else if (target.isUnderwater) - { - // iterate over weaponTypesSLW (Ship Launched Weapons) and pick suitable one based on engagementRange - // Prioritize by: - // 1. Depth Charges - // 2. Torpedos - using (List.Enumerator item = weaponTypesSLW.GetEnumerator()) - while (item.MoveNext()) - { - if (item.Current == null) continue; - if (CheckEngagementEnvelope(item.Current, distance)) - { - if (item.Current.GetMissileType().ToLower() == "depthcharge") - { - targetWeapon = item.Current; - break; - } - if (item.Current.GetMissileType().ToLower() != "torpedo") continue; - targetWeapon = item.Current; - break; - } - } - } - - // return result of weapon selection - if (targetWeapon != null) - { - //update the legacy lists & arrays, especially selectedWeapon and weaponIndex - selectedWeapon = targetWeapon; - // find it in weaponArray - for (int i = 1; i < weaponArray.Length; i++) - { - weaponIndex = i; - if (selectedWeapon.GetShortName() == weaponArray[weaponIndex].GetShortName()) - { - break; - } - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory] : " + vessel.vesselName + " - Selected weapon " + selectedWeapon.GetShortName()); - } - - PrepareWeapons(); - DisplaySelectedWeaponMessage(); - return true; - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory] : " + 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) - // { - // var engageableWeapon = weapon as EngageableWeapon; - // Debug.Log("DEBUG flying target:" + target.Vessel + ", weapon:" + weapon + " can engage:" + CheckEngagementEnvelope(weapon, distance) + ", engageEnabled:" + engageableWeapon.engageEnabled + ", min/max:" + engageableWeapon.GetEngagementRangeMin() + "/" + engageableWeapon.GetEngagementRangeMax()); - // } - // if (target.isLandedOrSurfaceSplashed) - // foreach (var weapon in weaponTypesAir) - // { - // var engageableWeapon = weapon as EngageableWeapon; - // 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; - } - } - - // extension for feature_engagementenvelope: check engagement parameters of the weapon if it can be used against the current target - bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) - { - EngageableWeapon engageableWeapon = weaponCandidate as EngageableWeapon; - - if (engageableWeapon == null) return true; - if (!engageableWeapon.engageEnabled) return true; - if (distanceToTarget < engageableWeapon.GetEngagementRangeMin()) return false; - if (distanceToTarget > engageableWeapon.GetEngagementRangeMax()) return false; - - switch (weaponCandidate.GetWeaponClass()) - { - case WeaponClasses.DefenseLaser: - // TODO: is laser treated like a gun? - - case WeaponClasses.Gun: - { - ModuleWeapon gun = (ModuleWeapon)weaponCandidate; - - // check yaw range of turret - ModuleTurret turret = gun.turret; - float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; - if (turret != null) - if (!TargetInTurretRange(turret, gimbalTolerance)) - return false; - - // check overheat - if (gun.isOverheated) - return false; - - // check ammo - if (CheckAmmo(gun)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory] : " + vessel.vesselName + " - Firing possible with " + weaponCandidate.GetShortName()); - } - return true; - } - break; - } - - case WeaponClasses.Missile: - { - MissileBase ml = (MissileBase)weaponCandidate; - - // lock radar if needed - if (ml.TargetingMode == MissileBase.TargetingModes.Radar) - using (List.Enumerator rd = radars.GetEnumerator()) - 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 (vessel.srfSpeed > ml.minLaunchSpeed && distanceToTarget < dlz.maxLaunchRange && distanceToTarget > dlz.minLaunchRange) - { - return true; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory] : " + vessel.vesselName + " - Failed DLZ test: " + weaponCandidate.GetShortName() + ", distance: " + distanceToTarget + ", DLZ min/max: " + dlz.minLaunchRange + "/" + dlz.maxLaunchRange); - } - break; - } - - case WeaponClasses.Bomb: - if (!vessel.LandedOrSplashed) - return true; // TODO: bomb always allowed? - break; - - case WeaponClasses.Rocket: - { - ModuleWeapon rocket = (ModuleWeapon)weaponCandidate; - - // check yaw range of turret - ModuleTurret turret = rocket.turret; - float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; - if (turret != null) - if (!TargetInTurretRange(turret, gimbalTolerance)) - return false; - //check reloading and crewed - if (rocket.isReloading || !rocket.hasGunner) - return false; - - // check ammo - if (CheckAmmo(rocket)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory] : " + vessel.vesselName + " - Firing possible with " + weaponCandidate.GetShortName()); - } - return true; - } - break; - } - - case WeaponClasses.SLW: - { - // Enable sonar, or radar, if no sonar is found. - if (((MissileBase)weaponCandidate).TargetingMode == MissileBase.TargetingModes.Radar) - using (List.Enumerator rd = radars.GetEnumerator()) - while (rd.MoveNext()) - { - if (rd.Current != null || rd.Current.canLock) - rd.Current.EnableRadar(); - } - return true; - } - - default: - throw new ArgumentOutOfRangeException(); - } - - return false; - } - - void SetTarget(TargetInfo target) - { - if (target) - { - if (currentTarget) - { - currentTarget.Disengage(this); - } - target.Engage(this); - if (currentTarget != target && pilotAI && pilotAI.IsExtending) pilotAI.StopExtending(); - currentTarget = target; - guardTarget = target.Vessel; - } - else - { - if (currentTarget) - { - currentTarget.Disengage(this); - } - guardTarget = null; - currentTarget = null; - } - } - - #endregion Smart Targeting - - public bool CanSeeTarget(TargetInfo target) - { - // 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; - - // can we get a visual sight of the target? - if ((target.Vessel.transform.position - transform.position).sqrMagnitude < guardRange * guardRange) - { - if (RadarUtils.TerrainCheck(target.Vessel.transform.position, transform.position)) - { - return false; - } - - return true; - } - - return false; - } - - /// - /// Override for legacy targeting only! Remove when removing legcy mode! - /// - /// - /// - public bool CanSeeTarget(Vessel target) - { - // can we get a visual sight of the target? - if ((target.transform.position - transform.position).sqrMagnitude < guardRange * guardRange) - { - if (RadarUtils.TerrainCheck(target.transform.position, transform.position)) - { - return false; - } - - return true; - } - - return false; - } - - void SearchForRadarSource() - { - antiRadTargetAcquired = false; - - if (rwr && rwr.rwrEnabled) - { - float closestAngle = 360; - MissileBase missile = CurrentMissile; - - if (!missile) return; - - float maxOffBoresight = missile.maxOffBoresight; - - if (missile.TargetingMode != MissileBase.TargetingModes.AntiRad) return; - - for (int i = 0; i < rwr.pingsData.Length; i++) - { - if (rwr.pingsData[i].exists && (rwr.pingsData[i].signalStrength == 0 || rwr.pingsData[i].signalStrength == 5)) - { - float angle = Vector3.Angle(rwr.pingWorldPositions[i] - missile.transform.position, missile.GetForwardTransform()); - - if (angle < closestAngle && angle < maxOffBoresight) - { - closestAngle = angle; - antiRadiationTarget = rwr.pingWorldPositions[i]; - antiRadTargetAcquired = true; - } - } - } - } - } - - void SearchForLaserPoint() - { - MissileBase ml = CurrentMissile; - if (!ml || ml.TargetingMode != MissileBase.TargetingModes.Laser) - { - return; - } - - MissileLauncher launcher = ml as MissileLauncher; - if (launcher != null) - { - foundCam = BDATargetManager.GetLaserTarget(launcher, - launcher.GuidanceMode == MissileBase.GuidanceModes.BeamRiding); - } - else - { - foundCam = BDATargetManager.GetLaserTarget((BDModularGuidance)ml, false); - } - - if (foundCam) - { - laserPointDetected = true; - } - else - { - laserPointDetected = false; - } - } - - void SearchForHeatTarget() - { - if (CurrentMissile != null) - { - if (!CurrentMissile || CurrentMissile.TargetingMode != MissileBase.TargetingModes.Heat) - { - return; - } - - float scanRadius = CurrentMissile.lockedSensorFOV * 0.5f; - float maxOffBoresight = CurrentMissile.maxOffBoresight * 0.85f; - - if (vesselRadarData && vesselRadarData.locked) - { - 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), 0f, scanRadius, CurrentMissile.heatThreshold, CurrentMissile.allAspect, this); - } - } - - bool CrossCheckWithRWR(TargetInfo v) - { - bool matchFound = false; - if (rwr && rwr.rwrEnabled) - { - for (int i = 0; i < rwr.pingsData.Length; i++) - { - if (rwr.pingsData[i].exists && (rwr.pingWorldPositions[i] - v.position).sqrMagnitude < 20 * 20) - { - matchFound = true; - break; - } - } - } - - return matchFound; - } - - public void SendTargetDataToMissile(MissileBase ml) - { //TODO BDModularGuidance: implement all targetings on base - if (ml.TargetingMode == MissileBase.TargetingModes.Laser && laserPointDetected) - { - ml.lockedCamera = foundCam; - } - else if (ml.TargetingMode == MissileBase.TargetingModes.Gps) - { - if (designatedGPSCoords != Vector3d.zero) - { - ml.targetGPSCoords = designatedGPSCoords; - ml.TargetAcquired = true; - } - } - else if (ml.TargetingMode == MissileBase.TargetingModes.Heat && heatTarget.exists) - { - ml.heatTarget = heatTarget; - heatTarget = TargetSignatureData.noTarget; - } - else if (ml.TargetingMode == MissileBase.TargetingModes.Radar && vesselRadarData && vesselRadarData.locked)//&& radar && radar.lockedTarget.exists) - { - ml.radarTarget = vesselRadarData.lockedTargetData.targetData; - ml.vrd = vesselRadarData; - vesselRadarData.LastMissile = ml; - } - else if (ml.TargetingMode == MissileBase.TargetingModes.AntiRad && antiRadTargetAcquired) - { - ml.TargetAcquired = true; - ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(antiRadiationTarget, - vessel.mainBody); - } - } - - #endregion Targeting - - #region Guard - - public void ResetGuardInterval() - { - targetScanTimer = 0; - } - - void GuardMode() - { - if (!gameObject.activeInHierarchy) return; - if (BDArmorySettings.PEACE_MODE) return; - - UpdateGuardViewScan(); - - //setting turrets to guard mode - if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) - { - //make this not have to go every frame - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; //want to find all weapons in WeaponGroup, rather than all weapons of parttype - weapon.Current.EnableWeapon(); - weapon.Current.aiControlled = true; - if (weapon.Current.yawRange >= 5 && (weapon.Current.maxPitch - weapon.Current.minPitch) >= 5) - weapon.Current.maxAutoFireCosAngle = 1; //this is why turrets are sniper accurate, knock this down if turrets should be less aim-bot - else - //weapon.Current.maxAutoFireCosAngle = vessel.LandedOrSplashed ? 0.9993908f : 0.9975641f; //2 : 4 degrees - weapon.Current.maxAutoFireCosAngle = adjustedAutoFireCosAngle; //user-adjustable from 0-2deg - } - } - - if (!guardTarget && selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) - { - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; - weapon.Current.autoFire = false; - weapon.Current.visualTargetVessel = null; - } - } - - if (missilesAway < 0) - missilesAway = 0; - - if (missileIsIncoming) - { - if (!isLegacyCMing) - { - // StartCoroutine(LegacyCMRoutine()); // Depreciated - } - - targetScanTimer -= Time.fixedDeltaTime; //advance scan timing (increased urgency) - } - - // Update target priority UI - if ((targetPriorityEnabled) && (currentTarget)) - UpdateTargetPriorityUI(currentTarget); - - //scan and acquire new target - //if (Time.time - targetScanTimer > Mathf.Max(targetScanInterval,10f)) - if (Time.time - targetScanTimer > Mathf.Max(targetScanInterval, 0.5f)) // stupid hack to stop them retargetting too quickly - { - targetScanTimer = Time.time; - - if (!guardFiringMissile) - { - - SmartFindTarget(); - - if (guardTarget == null || selectedWeapon == null) - { - SetCargoBays(); - return; - } - - //firing - if (weaponIndex > 0) - { - if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW) - { - 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); - - 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 (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]:" + 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. - { - StartCoroutine(GuardMissileRoutine()); - } - } - else if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]:" + vessel.vesselName + " waiting for missile to be ready..."); - } - - // if (!launchAuthorized || !pilotAuthorized || missilesAway >= maxMissilesOnTarget) - // { - // targetScanTimer -= 0.5f * targetScanInterval; - // } - } - else if (selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) - { - if (!guardFiringMissile) - { - StartCoroutine(GuardBombRoutine()); - } - } - else if (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || - selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || - selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) - { - StartCoroutine(GuardTurretRoutine()); - } - } - } - SetCargoBays(); - } - - if (overrideTimer > 0) - { - overrideTimer -= TimeWarp.fixedDeltaTime; - } - else - { - overrideTimer = 0; - overrideTarget = null; - } - } - - void UpdateGuardViewScan() - { - ViewScanResults results = RadarUtils.GuardScanInDirection(this, transform, guardAngle, guardRange); - incomingThreatVessel = null; - incomingWeaponManager = null; - - if (results.foundMissile) - { - if (rwr && !rwr.rwrEnabled) rwr.EnableRWR(); - if (rwr && rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; - } - - if (results.foundHeatMissile) - { - StartCoroutine(UnderAttackRoutine()); - - FireFlares(); - - incomingThreatPosition = results.threatPosition; - incomingThreatVessel = results.threatVessel; - - if (results.threatVessel) - { - if (!incomingMissileVessel || - (incomingMissileVessel.transform.position - vessel.transform.position).sqrMagnitude > - (results.threatVessel.transform.position - vessel.transform.position).sqrMagnitude) - { - incomingMissileVessel = results.threatVessel; - } - } - } - - if (results.foundRadarMissile) - { - StartCoroutine(UnderAttackRoutine()); - - FireChaff(); - FireECM(); - - incomingThreatPosition = results.threatPosition; - incomingThreatVessel = results.threatVessel; - - if (results.threatVessel) - { - if (!incomingMissileVessel || - (incomingMissileVessel.transform.position - vessel.transform.position).sqrMagnitude > - (results.threatVessel.transform.position - vessel.transform.position).sqrMagnitude) - { - incomingMissileVessel = results.threatVessel; - } - } - } - - if (results.foundAGM) - { - StartCoroutine(UnderAttackRoutine()); - - //do smoke CM here. - if (targetMissiles && guardTarget == null) - { - //targetScanTimer = Mathf.Min(targetScanInterval, Time.time - targetScanInterval + 0.5f); - targetScanTimer -= targetScanInterval / 2; - } - } - - incomingMissileDistance = Mathf.Min(results.missileThreatDistance, incomingMissileDistance); - - if (results.firingAtMe) - { - StartCoroutine(UnderAttackRoutine()); - - incomingThreatPosition = results.threatPosition; - incomingThreatVessel = results.threatVessel; - if (ufRoutine != null) - { - StopCoroutine(ufRoutine); - underFire = false; - } - if (priorThreatVessel == incomingThreatVessel) - { - incomingMissTime += Time.fixedDeltaTime; - } - else - { - priorThreatVessel = incomingThreatVessel; - incomingMissTime = 0f; - } - if (results.threatWeaponManager != null) - { - incomingWeaponManager = results.threatWeaponManager; - incomingMissDistance = results.missDistance; - TargetInfo nearbyFriendly = BDATargetManager.GetClosestFriendly(this); - TargetInfo nearbyThreat = BDATargetManager.GetTargetFromWeaponManager(results.threatWeaponManager); - - if (nearbyThreat?.weaponManager != null && nearbyFriendly?.weaponManager != null) - if (Team.IsEnemy(nearbyThreat.weaponManager.Team) && - nearbyFriendly.weaponManager.Team == Team) - //turns out that there's no check for AI on the same team going after each other due to this. Who knew? - { - if (nearbyThreat == currentTarget && nearbyFriendly.weaponManager.currentTarget != null) - //if being attacked by the current target, switch to the target that the nearby friendly was engaging instead - { - SetOverrideTarget(nearbyFriendly.weaponManager.currentTarget); - nearbyFriendly.weaponManager.SetOverrideTarget(nearbyThreat); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: " + 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]: " + vessel.vesselName + " called for help from " + - nearbyFriendly.Vessel.vesselName); - } - } - } - ufRoutine = StartCoroutine(UnderFireRoutine()); - } - else - incomingMissTime = 0f; // Reset incoming fire time - } - - public void ForceScan() - { - targetScanTimer = -100; - } - - void StartGuardTurretFiring() - { - if (!guardTarget) return; - if (selectedWeapon == null) return; - - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; - weapon.Current.visualTargetVessel = guardTarget; - weapon.Current.autoFireTimer = Time.time; - //weapon.Current.autoFireLength = 3 * targetScanInterval / 4; - weapon.Current.autoFireLength = (fireBurstLength < 0.5) ? targetScanInterval / 2 : fireBurstLength; - } - } - - public void SetOverrideTarget(TargetInfo target) - { - overrideTarget = target; - targetScanTimer = -100; - } - - public void UpdateMaxGuardRange() - { - UI_FloatRange rangeEditor = (UI_FloatRange)Fields["guardRange"].uiControlEditor; - rangeEditor.maxValue = BDArmorySettings.MAX_GUARD_VISUAL_RANGE; - } - - 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("[BDThreat]: Threat from " + threat.GetDisplayName() + " is " + closureTime.ToString("0.0") + " seconds away!"); - } - return closureTime; - } - - // moved from pilot AI, as it does not really do anything AI related? - bool GetLaunchAuthorization(Vessel targetV, MissileFire mf) - { - bool launchAuthorized = false; - MissileBase missile = mf.CurrentMissile; - if (missile != null && targetV != null) - { - Vector3 target = targetV.transform.position; - if (!targetV.LandedOrSplashed) - { - target = MissileGuidance.GetAirToAirFireSolution(missile, targetV); - } - - float boresightFactor = targetV.LandedOrSplashed ? 0.75f : 0.35f; - - //if(missile.TargetingMode == MissileBase.TargetingModes.Gps) maxOffBoresight = 45; - - float fTime = 2f; - Vector3 futurePos = target + (targetV.Velocity() * fTime); - Vector3 myFuturePos = vessel.ReferenceTransform.position + (vessel.Velocity() * fTime); - bool fDot = Vector3.Dot(vessel.ReferenceTransform.up, futurePos - myFuturePos) > 0; //check target won't likely be behind me soon - - if (fDot && Vector3.Angle(missile.GetForwardTransform(), target - missile.transform.position) < missile.maxOffBoresight * boresightFactor) - { - launchAuthorized = true; - } - } - - return launchAuthorized; - } - - /// - /// Check if AI is online and can target the current guardTarget with direct fire weapons - /// - /// true if AI might fire - bool AIMightDirectFire() - { - return AI != null && AI.pilotEnabled && AI.CanEngage() && guardTarget && AI.IsValidFixedWeaponTarget(guardTarget); - } - - #endregion Guard - - #region Turret - - int CheckTurret(float distance) - { - if (weaponIndex == 0 || selectedWeapon == null || - !(selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || - selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser || - selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket)) - { - return 2; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Checking turrets"); - } - float finalDistance = distance; - //vessel.LandedOrSplashed ? distance : distance/2; //decrease distance requirement if airborne - - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; - float gimbalTolerance = vessel.LandedOrSplashed ? 0 : 15; - if (((AI != null && AI.pilotEnabled && AI.CanEngage()) || (TargetInTurretRange(weapon.Current.turret, gimbalTolerance))) && weapon.Current.maxEffectiveDistance >= finalDistance) - { - if (weapon.Current.isOverheated) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + selectedWeapon + " is overheated!"); - } - return -1; - } - if (weapon.Current.isReloading) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + selectedWeapon + " is reloading!"); - } - return -1; - } - if (!weapon.Current.hasGunner) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + selectedWeapon + " has no gunner!"); - } - return -1; - } - if (CheckAmmo(weapon.Current) || BDArmorySettings.INFINITE_AMMO) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + selectedWeapon + " is valid!"); - } - return 1; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + selectedWeapon + " has no ammo."); - } - return -1; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: " + selectedWeapon + " cannot reach target (" + distance + " vs " + weapon.Current.maxEffectiveDistance + ", yawRange: " + weapon.Current.yawRange + "). Continuing."); - } - //else return 0; - } - return 2; - } - - bool TargetInTurretRange(ModuleTurret turret, float tolerance) - { - if (!turret) - { - return false; - } - - if (!guardTarget) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Checking turret range but no guard target"); - } - return false; - } - - Transform turretTransform = turret.yawTransform.parent; - Vector3 direction = guardTarget.transform.position - turretTransform.position; - Vector3 directionYaw = Vector3.ProjectOnPlane(direction, turretTransform.up); - Vector3 directionPitch = Vector3.ProjectOnPlane(direction, turretTransform.right); - - float angleYaw = Vector3.Angle(turretTransform.forward, directionYaw); - float signedAnglePitch = 90 - Vector3.Angle(turretTransform.up, directionPitch); - bool withinPitchRange = (signedAnglePitch >= turret.minPitch - tolerance && signedAnglePitch <= turret.maxPitch + tolerance); - - if (angleYaw < (turret.yawRange / 2) + tolerance && withinPitchRange) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Checking turret range - target is INSIDE gimbal limits! signedAnglePitch: " + signedAnglePitch + ", minPitch: " + turret.minPitch + ", maxPitch: " + turret.maxPitch); - } - return true; - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: Checking turret range - target is OUTSIDE gimbal limits! signedAnglePitch: " + signedAnglePitch + ", minPitch: " + turret.minPitch + ", maxPitch: " + turret.maxPitch + ", angleYaw: " + angleYaw); - } - return false; - } - } - - public bool CheckAmmo(ModuleWeapon weapon) - { - string ammoName = weapon.ammoName; - if (ammoName == "ElectricCharge") return true; // Electric charge is almost always rechargable, so weapons that use it always have ammo. - using (List.Enumerator p = vessel.parts.GetEnumerator()) - while (p.MoveNext()) - { - if (p.Current == null) continue; - using (IEnumerator resource = p.Current.Resources.GetEnumerator()) - while (resource.MoveNext()) - { - if (resource.Current == null) continue; - if (resource.Current.resourceName != ammoName) continue; - if (resource.Current.amount > 0) - { - return true; - } - } - } - - return false; - } - - public bool outOfAmmo = false; // Indicator for being out of ammo. - public bool HasWeaponsAndAmmo(List weaponClasses = null) - { // 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. - bool hasWeaponsAndAmmo = false; - foreach (var weapon in vessel.FindPartModulesImplementing()) - { - 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) - { - 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 { 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. - return hasWeaponsAndAmmo; - } - - public int CountWeapons(List weaponClasses = null) - { // Count number of weapons with ammo - int countWeaponsAndAmmo = 0; - foreach (var weapon in vessel.FindPartModulesImplementing()) - { - 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 { countWeaponsAndAmmo++; } // Other weapon types don't have ammo, or use electric charge, which could recharge, so count them. - } - return countWeaponsAndAmmo; - } - - - void ToggleTurret() - { - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (selectedWeapon == null || weapon.Current.GetShortName() != selectedWeapon.GetShortName()) - { - weapon.Current.DisableWeapon(); - } - else - { - weapon.Current.EnableWeapon(); - } - } - } - - #endregion Turret - - #region Aimer - - void BombAimer() - { - if (selectedWeapon == null) - { - showBombAimer = false; - return; - } - if (!bombPart || selectedWeapon.GetPart() != bombPart) - { - if (selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) - { - bombPart = selectedWeapon.GetPart(); - } - else - { - showBombAimer = false; - return; - } - } - - showBombAimer = - ( - !MapView.MapIsEnabled && - vessel.isActiveVessel && - selectedWeapon != null && - selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb && - bombPart != null && - BDArmorySettings.DRAW_AIMERS && - vessel.verticalSpeed < 50 && - AltitudeTrigger() - ); - - if (!showBombAimer && (!guardMode || weaponIndex <= 0 || - selectedWeapon.GetWeaponClass() != WeaponClasses.Bomb)) return; - MissileBase ml = bombPart.GetComponent(); - - float simDeltaTime = 0.1f; - float simTime = 0; - Vector3 dragForce = Vector3.zero; - Vector3 prevPos = ml.MissileReferenceTransform.position; - Vector3 currPos = ml.MissileReferenceTransform.position; - //Vector3 simVelocity = vessel.rb_velocity; - Vector3 simVelocity = vessel.Velocity(); //Issue #92 - - MissileLauncher launcher = ml as MissileLauncher; - if (launcher != null) - { - simVelocity += launcher.decoupleSpeed * - (launcher.decoupleForward - ? launcher.MissileReferenceTransform.forward - : -launcher.MissileReferenceTransform.up); - } - else - { //TODO: BDModularGuidance review this value - simVelocity += 5 * -launcher.MissileReferenceTransform.up; - } - - List pointPositions = new List(); - pointPositions.Add(currPos); - - prevPos = ml.MissileReferenceTransform.position; - currPos = ml.MissileReferenceTransform.position; - - bombAimerPosition = Vector3.zero; - - bool simulating = true; - while (simulating) - { - prevPos = currPos; - currPos += simVelocity * simDeltaTime; - float atmDensity = - (float) - FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currPos), - FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody); - - simVelocity += FlightGlobals.getGeeForceAtPosition(currPos) * simDeltaTime; - float simSpeedSquared = simVelocity.sqrMagnitude; - - launcher = ml as MissileLauncher; - float drag = 0; - if (launcher != null) - { - drag = launcher.simpleDrag; - if (simTime > launcher.deployTime) - { - drag = launcher.deployedDrag; - } - } - else - { - //TODO:BDModularGuidance drag calculation - drag = ml.vessel.parts.Sum(x => x.dragScalar); - } - - dragForce = (0.008f * bombPart.mass) * drag * 0.5f * simSpeedSquared * atmDensity * simVelocity.normalized; - simVelocity -= (dragForce / bombPart.mass) * simDeltaTime; - - Ray ray = new Ray(prevPos, currPos - prevPos); - RaycastHit hitInfo; - if (Physics.Raycast(ray, out hitInfo, Vector3.Distance(prevPos, currPos), (1 << 15) | (1 << 17))) - { - bombAimerPosition = hitInfo.point; - simulating = false; - } - else if (FlightGlobals.getAltitudeAtPos(currPos) < 0) - { - bombAimerPosition = currPos - - (FlightGlobals.getAltitudeAtPos(currPos) * FlightGlobals.getUpAxis()); - simulating = false; - } - - simTime += simDeltaTime; - pointPositions.Add(currPos); - } - - //debug lines - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) - { - Vector3[] pointsArray = pointPositions.ToArray(); - LineRenderer lr = GetComponent(); - if (!lr) - { - lr = gameObject.AddComponent(); - } - lr.enabled = true; - lr.startWidth = .1f; - lr.endWidth = .1f; - lr.positionCount = pointsArray.Length; - for (int i = 0; i < pointsArray.Length; i++) - { - lr.SetPosition(i, pointsArray[i]); - } - } - else - { - if (gameObject.GetComponent()) - { - gameObject.GetComponent().enabled = false; - } - } - } - - bool AltitudeTrigger() - { - const float maxAlt = 10000; - double asl = vessel.mainBody.GetAltitude(vessel.CoM); - double radarAlt = asl - vessel.terrainAltitude; - - return radarAlt < maxAlt || asl < maxAlt; - } - - #endregion Aimer - } -} diff --git a/BDArmory/Modules/MissileLauncher.cs b/BDArmory/Modules/MissileLauncher.cs deleted file mode 100644 index dd0ef6ba6..000000000 --- a/BDArmory/Modules/MissileLauncher.cs +++ /dev/null @@ -1,2189 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.FX; -using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Parts; -using BDArmory.Radar; -using BDArmory.Targeting; -using BDArmory.UI; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class MissileLauncher : MissileBase - { - #region Variable Declarations - - [KSPField] - public string homingType = "AAM"; - - [KSPField] - public string targetingType = "none"; - - public MissileTurret missileTurret = null; - public BDRotaryRail rotaryRail = null; - - [KSPField] - public string exhaustPrefabPath; - - [KSPField] - public string boostExhaustPrefabPath; - - [KSPField] - public string boostExhaustTransformName; - - #region Aero - - [KSPField] - public bool aero = false; - - [KSPField] - public float liftArea = 0.015f; - - [KSPField] - public float steerMult = 0.5f; - - [KSPField] - public float torqueRampUp = 30f; - Vector3 aeroTorque = Vector3.zero; - float controlAuthority; - float finalMaxTorque; - - [KSPField] - public float aeroSteerDamping = 0; - - #endregion Aero - - [KSPField] - public float maxTorque = 90; - - [KSPField] - public float thrust = 30; - - [KSPField] - public float cruiseThrust = 3; - - [KSPField] - public float boostTime = 2.2f; - - [KSPField] - public float cruiseTime = 45; - - [KSPField] - public float cruiseDelay = 0; - - [KSPField] - public float maxAoA = 35; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_Direction"),//Direction: - UI_Toggle(disabledText = "#LOC_BDArmory_Direction_disabledText", enabledText = "#LOC_BDArmory_Direction_enabledText")]//Lateral--Forward - public bool decoupleForward = false; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_DecoupleSpeed"),//Decouple Speed - UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.Editor)] - public float decoupleSpeed = 0; - - [KSPField] - public float clearanceRadius = 0.14f; - - public override float ClearanceRadius => clearanceRadius; - - [KSPField] - public float clearanceLength = 0.14f; - - public override float ClearanceLength => clearanceLength; - - [KSPField] - public float optimumAirspeed = 220; - - [KSPField] - public float blastRadius = 150; - - [KSPField] - public float blastPower = 25; - - [KSPField] - public float blastHeat = -1; - - [KSPField] - public float maxTurnRateDPS = 20; - - [KSPField] - public bool proxyDetonate = true; - - [KSPField] - public string audioClipPath = string.Empty; - - AudioClip thrustAudio; - - [KSPField] - public string boostClipPath = string.Empty; - - AudioClip boostAudio; - - [KSPField] - public bool isSeismicCharge = false; - - [KSPField] - public float rndAngVel = 0; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxAltitude"),//Max Altitude - UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 10f, scene = UI_Scene.All)] - public float maxAltitude = 0f; - - [KSPField] - public string rotationTransformName = string.Empty; - Transform rotationTransform; - - [KSPField] - public bool terminalManeuvering = false; - - [KSPField] - public string terminalGuidanceType = ""; - - [KSPField] - public float terminalGuidanceDistance = 0.0f; - - private bool terminalGuidanceActive; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TerminalGuidance"), UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true")]//Terminal Guidance: false true - public bool terminalGuidanceShouldActivate = true; - - [KSPField] - public string explModelPath = "BDArmory/Models/explosion/explosion"; - - public string explSoundPath = "BDArmory/Sounds/explode1"; - - [KSPField] - public bool spoolEngine = false; - - [KSPField] - public bool hasRCS = false; - - [KSPField] - public float rcsThrust = 1; - float rcsRVelThreshold = 0.13f; - KSPParticleEmitter upRCS; - KSPParticleEmitter downRCS; - KSPParticleEmitter leftRCS; - KSPParticleEmitter rightRCS; - KSPParticleEmitter forwardRCS; - float rcsAudioMinInterval = 0.2f; - - private AudioSource audioSource; - public AudioSource sfAudioSource; - List pEmitters; - List gaplessEmitters; - - float cmTimer; - - //deploy animation - [KSPField] - public string deployAnimationName = ""; - - [KSPField] - public float deployedDrag = 0.02f; - - [KSPField] - public float deployTime = 0.2f; - - [KSPField] - public bool useSimpleDrag = false; - - [KSPField] - public float simpleDrag = 0.02f; - - [KSPField] - public float simpleStableTorque = 5; - - [KSPField] - public Vector3 simpleCoD = new Vector3(0, 0, -1); - - [KSPField] - public float agmDescentRatio = 1.45f; - - float currentThrust; - - public bool deployed; - //public float deployedTime; - - AnimationState[] deployStates; - - bool hasPlayedFlyby; - - float debugTurnRate; - - List boosters; - - [KSPField] - public bool decoupleBoosters = false; - - [KSPField] - public float boosterDecoupleSpeed = 5; - - [KSPField] - public float boosterMass = 0; - - Transform vesselReferenceTransform; - - [KSPField] - public string boostTransformName = string.Empty; - List boostEmitters; - List boostGaplessEmitters; - - [KSPField] - public bool torpedo = false; - - [KSPField] - public float waterImpactTolerance = 25; - - //ballistic options - [KSPField] - public bool indirect = false; - - [KSPField] - public bool vacuumSteerable = true; - - public GPSTargetInfo designatedGPSInfo; - - float[] rcsFiredTimes; - KSPParticleEmitter[] rcsTransforms; - - #endregion Variable Declarations - - [KSPAction("Fire Missile")] - public void AGFire(KSPActionParam param) - { - if (BDArmorySetup.Instance.ActiveWeaponManager != null && BDArmorySetup.Instance.ActiveWeaponManager.vessel == vessel) BDArmorySetup.Instance.ActiveWeaponManager.SendTargetDataToMissile(this); - if (missileTurret) - { - missileTurret.FireMissile(this); - } - else if (rotaryRail) - { - rotaryRail.FireMissile(this); - } - else - { - FireMissile(); - } - if (BDArmorySetup.Instance.ActiveWeaponManager != null) BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); - } - - [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_FireMissile", active = true)]//Fire Missile - public void GuiFire() - { - if (BDArmorySetup.Instance.ActiveWeaponManager != null && BDArmorySetup.Instance.ActiveWeaponManager.vessel == vessel) BDArmorySetup.Instance.ActiveWeaponManager.SendTargetDataToMissile(this); - if (missileTurret) - { - missileTurret.FireMissile(this); - } - else if (rotaryRail) - { - rotaryRail.FireMissile(this); - } - else - { - FireMissile(); - } - if (BDArmorySetup.Instance.ActiveWeaponManager != null) BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); - } - - [KSPEvent(guiActive = true, guiActiveEditor = false, active = true, guiName = "#LOC_BDArmory_Jettison")]//Jettison - public override void Jettison() - { - if (missileTurret) return; - - part.decouple(0); - if (BDArmorySetup.Instance.ActiveWeaponManager != null) BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); - } - - [KSPAction("Jettison")] - public void AGJettsion(KSPActionParam param) - { - Jettison(); - } - - void ParseWeaponClass() - { - missileType = missileType.ToLower(); - if (missileType == "bomb") - { - weaponClass = WeaponClasses.Bomb; - } - else if (missileType == "torpedo" || missileType == "depthcharge") - { - weaponClass = WeaponClasses.SLW; - } - else - { - weaponClass = WeaponClasses.Missile; - } - } - - public override void OnStart(StartState state) - { - //base.OnStart(state); - ParseWeaponClass(); - - if (shortName == string.Empty) - { - shortName = part.partInfo.title; - } - - gaplessEmitters = new List(); - pEmitters = new List(); - boostEmitters = new List(); - boostGaplessEmitters = new List(); - - Fields["maxOffBoresight"].guiActive = false; - Fields["maxOffBoresight"].guiActiveEditor = false; - Fields["maxStaticLaunchRange"].guiActive = false; - Fields["maxStaticLaunchRange"].guiActiveEditor = false; - Fields["minStaticLaunchRange"].guiActive = false; - Fields["minStaticLaunchRange"].guiActiveEditor = false; - - if (isTimed) - { - Fields["detonationTime"].guiActive = true; - Fields["detonationTime"].guiActiveEditor = true; - } - else - { - Fields["detonationTime"].guiActive = false; - Fields["detonationTime"].guiActiveEditor = false; - } - - ParseModes(); - // extension for feature_engagementenvelope - InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); - - List.Enumerator pEemitter = part.FindModelComponents().GetEnumerator(); - while (pEemitter.MoveNext()) - { - if (pEemitter.Current == null) continue; - EffectBehaviour.AddParticleEmitter(pEemitter.Current); - pEemitter.Current.emit = false; - } - pEemitter.Dispose(); - - if (HighLogic.LoadedSceneIsFlight) - { - //TODO: Backward compatibility wordaround - if (part.FindModuleImplementing() == null) - { - FromBlastPowerToTNTMass(); - } - else - { - //New Explosive module - DisablingExplosives(part); - } - - MissileReferenceTransform = part.FindModelTransform("missileTransform"); - if (!MissileReferenceTransform) - { - MissileReferenceTransform = part.partTransform; - } - - if (!string.IsNullOrEmpty(exhaustPrefabPath)) - { - using (var t = part.FindModelTransforms("exhaustTransform").AsEnumerable().GetEnumerator()) - while (t.MoveNext()) - { - if (t.Current == null) continue; - GameObject exhaustPrefab = (GameObject)Instantiate(GameDatabase.Instance.GetModel(exhaustPrefabPath)); - exhaustPrefab.SetActive(true); - using (var emitter = exhaustPrefab.GetComponentsInChildren().AsEnumerable().GetEnumerator()) - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - emitter.Current.emit = false; - } - exhaustPrefab.transform.parent = t.Current; - exhaustPrefab.transform.localPosition = Vector3.zero; - exhaustPrefab.transform.localRotation = Quaternion.identity; - } - } - - if (!string.IsNullOrEmpty(boostExhaustPrefabPath) && !string.IsNullOrEmpty(boostExhaustTransformName)) - { - using (var t = part.FindModelTransforms(boostExhaustTransformName).AsEnumerable().GetEnumerator()) - while (t.MoveNext()) - { - if (t.Current == null) continue; - GameObject exhaustPrefab = (GameObject)Instantiate(GameDatabase.Instance.GetModel(boostExhaustPrefabPath)); - exhaustPrefab.SetActive(true); - IEnumerator emitter = exhaustPrefab.GetComponentsInChildren().AsEnumerable().GetEnumerator(); - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - emitter.Current.emit = false; - } - emitter.Dispose(); - exhaustPrefab.transform.parent = t.Current; - exhaustPrefab.transform.localPosition = Vector3.zero; - exhaustPrefab.transform.localRotation = Quaternion.identity; - } - } - - boosters = new List(); - if (!string.IsNullOrEmpty(boostTransformName)) - { - IEnumerator t = part.FindModelTransforms(boostTransformName).AsEnumerable().GetEnumerator(); - while (t.MoveNext()) - { - if (t.Current == null) continue; - boosters.Add(t.Current.gameObject); - IEnumerator be = t.Current.GetComponentsInChildren().AsEnumerable().GetEnumerator(); - while (be.MoveNext()) - { - if (be.Current == null) continue; - if (be.Current.useWorldSpace) - { - if (be.Current.GetComponent()) continue; - BDAGaplessParticleEmitter ge = be.Current.gameObject.AddComponent(); - ge.part = part; - boostGaplessEmitters.Add(ge); - } - else - { - if (!boostEmitters.Contains(be.Current)) - { - boostEmitters.Add(be.Current); - } - EffectBehaviour.AddParticleEmitter(be.Current); - } - } - be.Dispose(); - } - t.Dispose(); - } - - IEnumerator pEmitter = part.partTransform.Find("model").GetComponentsInChildren().AsEnumerable().GetEnumerator(); - while (pEmitter.MoveNext()) - { - if (pEmitter.Current == null) continue; - if (pEmitter.Current.GetComponent() || boostEmitters.Contains(pEmitter.Current)) - { - continue; - } - - if (pEmitter.Current.useWorldSpace) - { - BDAGaplessParticleEmitter gaplessEmitter = pEmitter.Current.gameObject.AddComponent(); - gaplessEmitter.part = part; - gaplessEmitters.Add(gaplessEmitter); - } - else - { - if (pEmitter.Current.transform.name != boostTransformName) - { - pEmitters.Add(pEmitter.Current); - } - else - { - boostEmitters.Add(pEmitter.Current); - } - EffectBehaviour.AddParticleEmitter(pEmitter.Current); - } - } - pEmitter.Dispose(); - - cmTimer = Time.time; - - part.force_activate(); - - List.Enumerator pe = pEmitters.GetEnumerator(); - while (pe.MoveNext()) - { - if (pe.Current == null) continue; - if (hasRCS) - { - if (pe.Current.gameObject.name == "rcsUp") upRCS = pe.Current; - else if (pe.Current.gameObject.name == "rcsDown") downRCS = pe.Current; - else if (pe.Current.gameObject.name == "rcsLeft") leftRCS = pe.Current; - else if (pe.Current.gameObject.name == "rcsRight") rightRCS = pe.Current; - else if (pe.Current.gameObject.name == "rcsForward") forwardRCS = pe.Current; - } - - if (!pe.Current.gameObject.name.Contains("rcs") && !pe.Current.useWorldSpace) - { - pe.Current.sizeGrow = 99999; - } - } - pe.Dispose(); - - if (rotationTransformName != string.Empty) - { - rotationTransform = part.FindModelTransform(rotationTransformName); - } - - if (hasRCS) - { - SetupRCS(); - KillRCS(); - } - SetupAudio(); - } - - if (GuidanceMode != GuidanceModes.Cruise) - { - CruiseAltitudeRange(); - Fields["CruiseAltitude"].guiActive = false; - Fields["CruiseAltitude"].guiActiveEditor = false; - Fields["CruiseSpeed"].guiActive = false; - Fields["CruiseSpeed"].guiActiveEditor = false; - Events["CruiseAltitudeRange"].guiActive = false; - Events["CruiseAltitudeRange"].guiActiveEditor = false; - Fields["CruisePredictionTime"].guiActiveEditor = false; - } - - if (GuidanceMode != GuidanceModes.AGM) - { - Fields["maxAltitude"].guiActive = false; - Fields["maxAltitude"].guiActiveEditor = false; - } - if (GuidanceMode != GuidanceModes.AGMBallistic) - { - Fields["BallisticOverShootFactor"].guiActive = false; - Fields["BallisticOverShootFactor"].guiActiveEditor = false; - Fields["BallisticAngle"].guiActive = false; - Fields["BallisticAngle"].guiActiveEditor = false; - } - - if (part.partInfo.title.Contains("Bomb")) - { - Fields["dropTime"].guiActive = false; - Fields["dropTime"].guiActiveEditor = false; - } - - if (TargetingModeTerminal != TargetingModes.None) - { - Fields["terminalGuidanceShouldActivate"].guiName += terminalGuidanceType; - } - else - { - Fields["terminalGuidanceShouldActivate"].guiActive = false; - Fields["terminalGuidanceShouldActivate"].guiActiveEditor = false; - } - - if (deployAnimationName != "") - { - deployStates = Misc.Misc.SetUpAnimation(deployAnimationName, part); - } - else - { - deployedDrag = simpleDrag; - } - - SetInitialDetonationDistance(); - - // fill activeRadarLockTrackCurve with default values if not set by part config: - if ((TargetingMode == TargetingModes.Radar || TargetingModeTerminal == TargetingModes.Radar) && activeRadarRange > 0 && activeRadarLockTrackCurve.minTime == float.MaxValue) - { - activeRadarLockTrackCurve.Add(0f, 0f); - activeRadarLockTrackCurve.Add(activeRadarRange, RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); // TODO: tune & balance constants! - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: OnStart missile " + shortName + ": setting default locktrackcurve with maxrange/minrcs: " + activeRadarLockTrackCurve.maxTime + "/" + RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); - } - } - - /// - /// This method will convert the blastPower to a tnt mass equivalent - /// - private void FromBlastPowerToTNTMass() - { - blastPower = BlastPhysicsUtils.CalculateExplosiveMass(blastRadius); - } - - void OnCollisionEnter(Collision col) - { - base.CollisionEnter(col); - } - - void SetupAudio() - { - audioSource = gameObject.AddComponent(); - audioSource.minDistance = 1; - audioSource.maxDistance = 1000; - audioSource.loop = true; - audioSource.pitch = 1f; - audioSource.priority = 255; - audioSource.spatialBlend = 1; - - if (audioClipPath != string.Empty) - { - audioSource.clip = GameDatabase.Instance.GetAudioClip(audioClipPath); - } - - sfAudioSource = gameObject.AddComponent(); - sfAudioSource.minDistance = 1; - sfAudioSource.maxDistance = 2000; - sfAudioSource.dopplerLevel = 0; - sfAudioSource.priority = 230; - sfAudioSource.spatialBlend = 1; - - if (audioClipPath != string.Empty) - { - thrustAudio = GameDatabase.Instance.GetAudioClip(audioClipPath); - } - - if (boostClipPath != string.Empty) - { - boostAudio = GameDatabase.Instance.GetAudioClip(boostClipPath); - } - - UpdateVolume(); - BDArmorySetup.OnVolumeChange += UpdateVolume; - } - - void UpdateVolume() - { - if (audioSource) - { - audioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; - } - if (sfAudioSource) - { - sfAudioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; - } - } - - 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() - { - KillRCS(); - if (upRCS) EffectBehaviour.RemoveParticleEmitter(upRCS); - if (downRCS) EffectBehaviour.RemoveParticleEmitter(downRCS); - if (leftRCS) EffectBehaviour.RemoveParticleEmitter(leftRCS); - if (rightRCS) EffectBehaviour.RemoveParticleEmitter(rightRCS); - if (pEmitters != null) - foreach (var pe in pEmitters) - if (pe) EffectBehaviour.RemoveParticleEmitter(pe); - if (boostEmitters != null) - foreach (var pe in boostEmitters) - if (pe) EffectBehaviour.RemoveParticleEmitter(pe); - BDArmorySetup.OnVolumeChange -= UpdateVolume; - GameEvents.onPartDie.Remove(PartDie); - } - - public override float GetBlastRadius() - { - if (part.FindModuleImplementing() != null) - { - return part.FindModuleImplementing().GetBlastRadius(); - } - else - { - return blastRadius; - } - } - - public override void FireMissile() - { - if (HasFired) return; - - SetupExplosive(this.part); - HasFired = true; - - Debug.Log("[BDArmory]: Missile Fired! " + vessel.vesselName); - - GameEvents.onPartDie.Add(PartDie); - BDATargetManager.FiredMissiles.Add(this); - - if (GetComponentInChildren()) - { - BDArmorySetup.numberOfParticleEmitters++; - } - - List.Enumerator wpm = vessel.FindPartModulesImplementing().GetEnumerator(); - while (wpm.MoveNext()) - { - if (wpm.Current == null) continue; - Team = wpm.Current.Team; - break; - } - wpm.Dispose(); - - sfAudioSource.PlayOneShot(GameDatabase.Instance.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; - - SetLaserTargeting(); - SetAntiRadTargeting(); - - part.decouple(0); - part.force_activate(); - part.Unpack(); - vessel.situation = Vessel.Situations.FLYING; - part.rb.isKinematic = false; - part.bodyLiftMultiplier = 0; - part.dragModel = Part.DragModel.NONE; - - //add target info to vessel - AddTargetInfoToVessel(); - StartCoroutine(DecoupleRoutine()); - - vessel.vesselName = GetShortName(); - vessel.vesselType = VesselType.Probe; - - TimeFired = Time.time; - - //setting ref transform for navball - GameObject refObject = new GameObject(); - refObject.transform.rotation = Quaternion.LookRotation(-transform.up, transform.forward); - refObject.transform.parent = transform; - part.SetReferenceTransform(refObject.transform); - vessel.SetReferenceTransform(part); - vesselReferenceTransform = refObject.transform; - DetonationDistanceState = DetonationDistanceStates.NotSafe; - MissileState = MissileStates.Drop; - part.crashTolerance = 9999; //to combat stresses of launch, missle generate a lot of G Force - - StartCoroutine(MissileRoutine()); - } - - IEnumerator DecoupleRoutine() - { - yield return new WaitForFixedUpdate(); - - if (rndAngVel > 0) - { - part.rb.angularVelocity += UnityEngine.Random.insideUnitSphere.normalized * rndAngVel; - } - - if (decoupleForward) - { - part.rb.velocity += decoupleSpeed * part.transform.forward; - } - else - { - part.rb.velocity += decoupleSpeed * -part.transform.up; - } - } - - /// - /// Fires the missileBase on target vessel. Used by AI currently. - /// - /// V. - public void FireMissileOnTarget(Vessel v) - { - if (!HasFired) - { - legacyTargetVessel = v; - FireMissile(); - } - } - - void OnDisable() - { - if (TargetingMode == TargetingModes.AntiRad) - { - RadarWarningReceiver.OnRadarPing -= ReceiveRadarPing; - } - } - - public override void OnFixedUpdate() - { - debugString.Length = 0; - - if (HasFired && !HasExploded && part != null) - { - CheckDetonationState(); - CheckDetonationDistance(); - - part.rb.isKinematic = false; - AntiSpin(); - - //simpleDrag - if (useSimpleDrag) - { - SimpleDrag(); - } - - //flybyaudio - float mCamDistanceSqr = (FlightCamera.fetch.mainCamera.transform.position - transform.position).sqrMagnitude; - float mCamRelVSqr = (float)(FlightGlobals.ActiveVessel.Velocity() - vessel.Velocity()).sqrMagnitude; - if (!hasPlayedFlyby - && FlightGlobals.ActiveVessel != vessel - && FlightGlobals.ActiveVessel != SourceVessel - && mCamDistanceSqr < 400 * 400 && mCamRelVSqr > 300 * 300 - && mCamRelVSqr < 800 * 800 - && Vector3.Angle(vessel.Velocity(), FlightGlobals.ActiveVessel.transform.position - transform.position) < 60) - { - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/missileFlyby")); - hasPlayedFlyby = true; - } - - if (vessel.isActiveVessel) - { - audioSource.dopplerLevel = 0; - } - else - { - audioSource.dopplerLevel = 1f; - } - - if (TimeIndex > 0.5f) - { - if (torpedo) - { - if (vessel.altitude > 0) - { - part.crashTolerance = waterImpactTolerance; - } - else - { - part.crashTolerance = 1; - } - } - else - { - part.crashTolerance = 1; - } - } - - UpdateThrustForces(); - UpdateGuidance(); - //RaycastCollisions(); - - //Timed detonation - if (isTimed && TimeIndex > detonationTime) - { - Detonate(); - } - } - } - - private void CheckMiss() - { - float sqrDist = (float)((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (vessel.CoM + (vessel.Velocity() * Time.fixedDeltaTime))).sqrMagnitude; - if (sqrDist < 160000 || MissileState == MissileStates.PostThrust) - { - checkMiss = true; - } - if (maxAltitude != 0f) - { - if (vessel.altitude >= maxAltitude) checkMiss = true; - } - - //kill guidance if missileBase has missed - if (!HasMissed && checkMiss) - { - bool noProgress = MissileState == MissileStates.PostThrust && (Vector3.Dot(vessel.Velocity() - TargetVelocity, TargetPosition - vessel.transform.position) < 0); - if (Vector3.Dot(TargetPosition - transform.position, transform.forward) < 0 || noProgress) - { - Debug.Log("[BDArmory]: Missile has missed!"); - - if (vessel.altitude >= maxAltitude && maxAltitude != 0f) - Debug.Log("[BDArmory]: CheckMiss trigged by MaxAltitude"); - - HasMissed = true; - guidanceActive = false; - - TargetMf = null; - - MissileLauncher launcher = this as MissileLauncher; - if (launcher != null) - { - if (launcher.hasRCS) launcher.KillRCS(); - } - - if (sqrDist < Mathf.Pow(GetBlastRadius() * 0.5f, 2)) part.Destroy(); - - isTimed = true; - detonationTime = TimeIndex + 1.5f; - return; - } - } - } - - - 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) - { - UpdateLaserTarget(); - debugTarget = TargetPosition.ToString(); - } - else if (TargetingMode == TargetingModes.Gps) - { - UpdateGPSTarget(); - debugTarget = UpdateGPSTarget().ToString(); - } - else if (TargetingMode == TargetingModes.AntiRad) - { - UpdateAntiRadiationTarget(); - debugTarget = TargetPosition.ToString(); - } - - UpdateTerminalGuidance(); - } - - if (MissileState != MissileStates.Idle && MissileState != MissileStates.Drop) //guidance - { - //guidance and attitude stabilisation scales to atmospheric density. //use part.atmDensity - float atmosMultiplier = Mathf.Clamp01(2.5f * (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(transform.position), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody)); - - if (vessel.srfSpeed < optimumAirspeed) - { - float optimumSpeedFactor = (float)vessel.srfSpeed / (2 * optimumAirspeed); - controlAuthority = Mathf.Clamp01(atmosMultiplier * (-Mathf.Abs(2 * optimumSpeedFactor - 1) + 1)); - } - else - { - controlAuthority = Mathf.Clamp01(atmosMultiplier); - } - - if (vacuumSteerable) - { - controlAuthority = 1; - } - - debugString.Append($"controlAuthority: {controlAuthority}"); - debugString.Append(Environment.NewLine); - - if (guidanceActive)// && timeIndex - dropTime > 0.5f) - { - WarnTarget(); - - if (legacyTargetVessel && legacyTargetVessel.loaded) - { - Vector3 targetCoMPos = legacyTargetVessel.CoM; - TargetPosition = targetCoMPos + legacyTargetVessel.Velocity() * Time.fixedDeltaTime; - } - - //increaseTurnRate after launch - float turnRateDPS = Mathf.Clamp(((TimeIndex - dropTime) / boostTime) * maxTurnRateDPS * 25f, 0, maxTurnRateDPS); - if (!hasRCS) - { - turnRateDPS *= controlAuthority; - } - - //decrease turn rate after thrust cuts out - if (TimeIndex > dropTime + boostTime + cruiseTime) - { - var clampedTurnRate = Mathf.Clamp(maxTurnRateDPS - ((TimeIndex - dropTime - boostTime - cruiseTime) * 0.45f), - 1, maxTurnRateDPS); - turnRateDPS = clampedTurnRate; - - if (!vacuumSteerable) - { - turnRateDPS *= atmosMultiplier; - } - - if (hasRCS) - { - turnRateDPS = 0; - } - } - - if (hasRCS) - { - if (turnRateDPS > 0) - { - DoRCS(); - } - else - { - KillRCS(); - } - } - debugTurnRate = turnRateDPS; - - finalMaxTorque = Mathf.Clamp((TimeIndex - dropTime) * torqueRampUp, 0, maxTorque); //ramp up torque - - if (GuidanceMode == GuidanceModes.AAMLead) - { - AAMGuidance(); - } - else if (GuidanceMode == GuidanceModes.AGM) - { - AGMGuidance(); - } - else if (GuidanceMode == GuidanceModes.AGMBallistic) - { - AGMBallisticGuidance(); - } - else if (GuidanceMode == GuidanceModes.BeamRiding) - { - BeamRideGuidance(); - } - else if (GuidanceMode == GuidanceModes.RCS) - { - part.transform.rotation = Quaternion.RotateTowards(part.transform.rotation, Quaternion.LookRotation(TargetPosition - part.transform.position, part.transform.up), turnRateDPS * Time.fixedDeltaTime); - } - else if (GuidanceMode == GuidanceModes.Cruise) - { - CruiseGuidance(); - } - else if (GuidanceMode == GuidanceModes.SLW) - { - SLWGuidance(); - } - - } - else - { - CheckMiss(); - TargetMf = null; - if (aero) - { - aeroTorque = MissileGuidance.DoAeroForces(this, transform.position + (20 * vessel.Velocity()), liftArea, .25f, aeroTorque, maxTorque, maxAoA); - } - } - - if (aero && aeroSteerDamping > 0) - { - part.rb.AddRelativeTorque(-aeroSteerDamping * part.transform.InverseTransformVector(part.rb.angularVelocity)); - } - - if (hasRCS && !guidanceActive) - { - KillRCS(); - } - } - - debugString.Append("Missile target=" + debugTarget); - debugString.Append(Environment.NewLine); - } - - // feature_engagementenvelope: terminal guidance mode for cruise missiles - private void UpdateTerminalGuidance() - { - // check if guidance mode should be changed for terminal phase - float distanceSqr = (TargetPosition - transform.position).sqrMagnitude; - - if (terminalGuidanceShouldActivate && !terminalGuidanceActive && (TargetingModeTerminal != TargetingModes.None) && (distanceSqr < terminalGuidanceDistance * terminalGuidanceDistance)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory][Terminal Guidance]: missile " + this.name + " updating targeting mode: " + terminalGuidanceType); - - TargetingMode = TargetingModeTerminal; - terminalGuidanceActive = true; - TargetAcquired = false; - - switch (TargetingModeTerminal) - { - 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.signalStrength, terminalGuidanceDistance, heatThreshold, true, SourceVessel ? SourceVessel.FindPartModuleImplementing() : null, true); - if (heatTarget.exists) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory][Terminal Guidance]: Heat target acquired! Position: " + heatTarget.position + ", heatscore: " + heatTarget.signalStrength); - } - TargetAcquired = true; - TargetPosition = heatTarget.position + (heatTarget.velocity * Time.fixedDeltaTime); - TargetVelocity = heatTarget.velocity; - TargetAcceleration = heatTarget.acceleration; - lockFailTimer = 0; - targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); - - // 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.signalStrength *= futureFactor / currentFactor; - } - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory][Terminal Guidance]: Missile heatseeker could not acquire a target lock."); - } - } - break; - - case TargetingModes.Radar: - - // pretend we have an active radar seeker for ground targets: - TargetSignatureData[] scannedTargets = new TargetSignatureData[5]; - TargetSignatureData.ResetTSDArray(ref scannedTargets); - Ray ray = new Ray(transform.position, GetForwardTransform()); - - //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 smallestAngle = maxOffBoresight; - TargetSignatureData lockedTarget = TargetSignatureData.noTarget; - - for (int i = 0; i < scannedTargets.Length; i++) - { - if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - TargetPosition).sqrMagnitude < sqrThresh) - { - //re-check engagement envelope, only lock appropriate targets - if (CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo)) - { - lockedTarget = scannedTargets[i]; - ActiveRadar = true; - } - } - } - - if (lockedTarget.exists) - { - radarTarget = lockedTarget; - TargetAcquired = true; - TargetPosition = radarTarget.predictedPositionWithChaffFactor; - TargetVelocity = radarTarget.velocity; - TargetAcceleration = radarTarget.acceleration; - targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); - - if (weaponClass == WeaponClasses.SLW) - RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), 45, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); - else - RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), 45, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); - - Debug.Log("[BDArmory][Terminal Guidance]: Pitbull! Radar missileBase has gone active. Radar sig strength: " + radarTarget.signalStrength.ToString("0.0") + " - target: " + radarTarget.vessel.name); - } - else - { - TargetAcquired = true; - TargetPosition = VectorUtils.GetWorldSurfacePostion(UpdateGPSTarget(), vessel.mainBody); //putting back the GPS target if no radar target found - TargetVelocity = Vector3.zero; - TargetAcceleration = Vector3.zero; - targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); - Debug.Log("[BDArmory][Terminal Guidance]: Missile radar could not acquire a target lock - Defaulting to GPS Target"); - } - break; - - case TargetingModes.Laser: - // not very useful, currently unsupported! - break; - - case TargetingModes.Gps: - // from gps to gps -> no actions need to be done! - break; - - case TargetingModes.AntiRad: - 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][Terminal Guidance]: Antiradiation mode set! Waiting for radar signals..."); - break; - } - } - } - - void UpdateThrustForces() - { - if (MissileState == MissileStates.PostThrust) return; - 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); - - part.rb.AddRelativeForce(currentThrust * Throttle * Vector3.forward); - } - } - - IEnumerator MissileRoutine() - { - MissileState = MissileStates.Drop; - StartCoroutine(AnimRoutine()); - yield return new WaitForSeconds(dropTime); - yield return StartCoroutine(BoostRoutine()); - yield return new WaitForSeconds(cruiseDelay); - yield return StartCoroutine(CruiseRoutine()); - } - - IEnumerator AnimRoutine() - { - yield return new WaitForSeconds(deployTime); - - if (!string.IsNullOrEmpty(deployAnimationName)) - { - deployed = true; - IEnumerator anim = deployStates.AsEnumerable().GetEnumerator(); - while (anim.MoveNext()) - { - if (anim.Current == null) continue; - anim.Current.speed = 1; - } - anim.Dispose(); - } - } - - IEnumerator BoostRoutine() - { - StartBoost(); - float boostStartTime = Time.time; - while (Time.time - boostStartTime < boostTime) - { - //light, sound & particle fx - //sound - if (!BDArmorySetup.GameIsPaused) - { - if (!audioSource.isPlaying) - { - audioSource.Play(); - } - } - else if (audioSource.isPlaying) - { - audioSource.Stop(); - } - - //particleFx - List.Enumerator emitter = boostEmitters.GetEnumerator(); - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - if (!hasRCS) - { - emitter.Current.sizeGrow = Mathf.Lerp(emitter.Current.sizeGrow, 0, 20 * Time.deltaTime); - } - } - emitter.Dispose(); - - List.Enumerator gpe = boostGaplessEmitters.GetEnumerator(); - while (gpe.MoveNext()) - { - if (gpe.Current == null) continue; - if ((!vessel.InVacuum() && Throttle > 0) && weaponClass != WeaponClasses.SLW || (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(part.transform.position) < 0)) //#710 - { - gpe.Current.emit = true; - gpe.Current.pEmitter.worldVelocity = 2 * ParticleTurbulence.flareTurbulence; - } - else - { - gpe.Current.emit = false; - } - } - gpe.Dispose(); - - //thrust - if (spoolEngine) - { - currentThrust = Mathf.MoveTowards(currentThrust, thrust, thrust / 10); - } - - yield return null; - } - EndBoost(); - } - - void StartBoost() - { - MissileState = MissileStates.Boost; - - if (boostAudio) - { - audioSource.clip = boostAudio; - } - else if (thrustAudio) - { - audioSource.clip = thrustAudio; - } - - IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator(); - while (light.MoveNext()) - { - if (light.Current == null) continue; - light.Current.intensity = 1.5f; - } - light.Dispose(); - - if (!spoolEngine) - { - currentThrust = thrust; - } - - if (string.IsNullOrEmpty(boostTransformName)) - { - boostEmitters = pEmitters; - boostGaplessEmitters = gaplessEmitters; - } - - List.Enumerator emitter = boostEmitters.GetEnumerator(); - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - emitter.Current.emit = true; - } - emitter.Dispose(); - - if (hasRCS) - { - forwardRCS.emit = true; - } - - if (!(thrust > 0)) return; - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/launch")); - RadarWarningReceiver.WarnMissileLaunch(transform.position, transform.forward); - } - - void EndBoost() - { - List.Enumerator emitter = boostEmitters.GetEnumerator(); - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - emitter.Current.emit = false; - } - emitter.Dispose(); - - List.Enumerator gEmitter = boostGaplessEmitters.GetEnumerator(); - while (gEmitter.MoveNext()) - { - if (gEmitter.Current == null) continue; - gEmitter.Current.emit = false; - } - gEmitter.Dispose(); - - if (decoupleBoosters) - { - part.mass -= boosterMass; - List.Enumerator booster = boosters.GetEnumerator(); - while (booster.MoveNext()) - { - if (booster.Current == null) continue; - booster.Current.AddComponent().DecoupleBooster(part.rb.velocity, boosterDecoupleSpeed); - } - booster.Dispose(); - } - - if (cruiseDelay > 0) - { - currentThrust = 0; - } - } - - IEnumerator CruiseRoutine() - { - StartCruise(); - float cruiseStartTime = Time.time; - while (Time.time - cruiseStartTime < cruiseTime) - { - if (!BDArmorySetup.GameIsPaused) - { - if (!audioSource.isPlaying || audioSource.clip != thrustAudio) - { - audioSource.clip = thrustAudio; - audioSource.Play(); - } - } - else if (audioSource.isPlaying) - { - audioSource.Stop(); - } - audioSource.volume = Throttle; - - //particleFx - List.Enumerator emitter = pEmitters.GetEnumerator(); - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - if (!hasRCS) - { - emitter.Current.sizeGrow = Mathf.Lerp(emitter.Current.sizeGrow, 0, 20 * Time.deltaTime); - } - - emitter.Current.maxSize = Mathf.Clamp01(Throttle / Mathf.Clamp((float)vessel.atmDensity, 0.2f, 1f)); - if (weaponClass != WeaponClasses.SLW || (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(part.transform.position) < 0)) //#710 - { - emitter.Current.emit = true; - } - else - { - emitter.Current.emit = false; // #710, shut down thrust FX for torps out of water - } - } - emitter.Dispose(); - - List.Enumerator gpe = gaplessEmitters.GetEnumerator(); - while (gpe.MoveNext()) - { - if (gpe.Current == null) continue; - if (weaponClass != WeaponClasses.SLW || (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(part.transform.position) < 0)) //#710 - { - gpe.Current.pEmitter.maxSize = Mathf.Clamp01(Throttle / Mathf.Clamp((float)vessel.atmDensity, 0.2f, 1f)); - gpe.Current.emit = true; - gpe.Current.pEmitter.worldVelocity = 2 * ParticleTurbulence.flareTurbulence; - } - else - { - gpe.Current.emit = false; - } - } - gpe.Dispose(); - - if (spoolEngine) - { - currentThrust = Mathf.MoveTowards(currentThrust, cruiseThrust, cruiseThrust / 10); - } - yield return null; - } - EndCruise(); - } - - void StartCruise() - { - MissileState = MissileStates.Cruise; - - if (thrustAudio) - { - audioSource.clip = thrustAudio; - } - - currentThrust = spoolEngine ? 0 : cruiseThrust; - - List.Enumerator pEmitter = pEmitters.GetEnumerator(); - while (pEmitter.MoveNext()) - { - if (pEmitter.Current == null) continue; - EffectBehaviour.AddParticleEmitter(pEmitter.Current); - pEmitter.Current.emit = true; - } - pEmitter.Dispose(); - - List.Enumerator gEmitter = gaplessEmitters.GetEnumerator(); - while (gEmitter.MoveNext()) - { - if (gEmitter.Current == null) continue; - EffectBehaviour.AddParticleEmitter(gEmitter.Current.pEmitter); - gEmitter.Current.emit = true; - } - gEmitter.Dispose(); - - if (!hasRCS) return; - forwardRCS.emit = false; - audioSource.Stop(); - } - - 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(); - - StartCoroutine(FadeOutAudio()); - StartCoroutine(FadeOutEmitters()); - } - - IEnumerator FadeOutAudio() - { - if (thrustAudio && audioSource.isPlaying) - { - while (audioSource.volume > 0 || audioSource.pitch > 0) - { - audioSource.volume = Mathf.Lerp(audioSource.volume, 0, 5 * Time.deltaTime); - audioSource.pitch = Mathf.Lerp(audioSource.pitch, 0, 5 * Time.deltaTime); - yield return null; - } - } - } - - IEnumerator FadeOutEmitters() - { - float fadeoutStartTime = Time.time; - while (Time.time - fadeoutStartTime < 5) - { - List.Enumerator pe = pEmitters.GetEnumerator(); - while (pe.MoveNext()) - { - if (pe.Current == null) continue; - pe.Current.maxEmission = Mathf.FloorToInt(pe.Current.maxEmission * 0.8f); - pe.Current.minEmission = Mathf.FloorToInt(pe.Current.minEmission * 0.8f); - } - pe.Dispose(); - - List.Enumerator gpe = gaplessEmitters.GetEnumerator(); - while (gpe.MoveNext()) - { - if (gpe.Current == null) continue; - gpe.Current.pEmitter.maxSize = Mathf.MoveTowards(gpe.Current.pEmitter.maxSize, 0, 0.005f); - gpe.Current.pEmitter.minSize = Mathf.MoveTowards(gpe.Current.pEmitter.minSize, 0, 0.008f); - gpe.Current.pEmitter.worldVelocity = ParticleTurbulence.Turbulence; - } - gpe.Dispose(); - yield return new WaitForFixedUpdate(); - } - - List.Enumerator pe2 = pEmitters.GetEnumerator(); - while (pe2.MoveNext()) - { - if (pe2.Current == null) continue; - pe2.Current.emit = false; - } - pe2.Dispose(); - - List.Enumerator gpe2 = gaplessEmitters.GetEnumerator(); - while (gpe2.MoveNext()) - { - if (gpe2.Current == null) continue; - gpe2.Current.emit = false; - } - gpe2.Dispose(); - } - - [KSPField] - public float beamCorrectionFactor; - - [KSPField] - public float beamCorrectionDamping; - - Ray previousBeam; - - void BeamRideGuidance() - { - if (!targetingPod) - { - guidanceActive = false; - return; - } - - if (RadarUtils.TerrainCheck(targetingPod.cameraParentTransform.position, transform.position)) - { - guidanceActive = false; - return; - } - Ray laserBeam = new Ray(targetingPod.cameraParentTransform.position + (targetingPod.vessel.Velocity() * Time.fixedDeltaTime), targetingPod.targetPointPosition - targetingPod.cameraParentTransform.position); - Vector3 target = MissileGuidance.GetBeamRideTarget(laserBeam, part.transform.position, vessel.Velocity(), beamCorrectionFactor, beamCorrectionDamping, (TimeIndex > 0.25f ? previousBeam : laserBeam)); - previousBeam = laserBeam; - DrawDebugLine(part.transform.position, target); - DoAero(target); - } - - void CruiseGuidance() - { - if (this._guidance == null) - { - this._guidance = new CruiseGuidance(this); - } - - Vector3 cruiseTarget = Vector3.zero; - - cruiseTarget = this._guidance.GetDirection(this, TargetPosition); - - Vector3 upDirection = VectorUtils.GetUpDirection(transform.position); - - //axial rotation - if (rotationTransform) - { - Quaternion originalRotation = transform.rotation; - Quaternion originalRTrotation = rotationTransform.rotation; - transform.rotation = Quaternion.LookRotation(transform.forward, upDirection); - rotationTransform.rotation = originalRTrotation; - Vector3 lookUpDirection = Vector3.ProjectOnPlane(cruiseTarget - transform.position, transform.forward) * 100; - lookUpDirection = transform.InverseTransformPoint(lookUpDirection + transform.position); - - lookUpDirection = new Vector3(lookUpDirection.x, 0, 0); - lookUpDirection += 10 * Vector3.up; - - rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, Quaternion.LookRotation(Vector3.forward, lookUpDirection), 0.04f); - Quaternion finalRotation = rotationTransform.rotation; - transform.rotation = originalRotation; - rotationTransform.rotation = finalRotation; - - vesselReferenceTransform.rotation = Quaternion.LookRotation(-rotationTransform.up, rotationTransform.forward); - } - DoAero(cruiseTarget); - CheckMiss(); - } - - void AAMGuidance() - { - Vector3 aamTarget; - if (TargetAcquired) - { - DrawDebugLine(transform.position + (part.rb.velocity * Time.fixedDeltaTime), TargetPosition); - float timeToImpact; - aamTarget = MissileGuidance.GetAirToAirTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact, optimumAirspeed); - TimeToImpact = timeToImpact; - 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)) - { - part.Destroy(); - } - } - else - { - aamTarget = transform.position + (20 * vessel.Velocity().normalized); - } - - if (TimeIndex > dropTime + 0.25f) - { - DoAero(aamTarget); - CheckMiss(); - } - - } - - void AGMGuidance() - { - if (TargetingMode != TargetingModes.Gps) - { - if (TargetAcquired) - { - //lose lock if seeker reaches gimbal limit - float targetViewAngle = Vector3.Angle(transform.forward, TargetPosition - transform.position); - - if (targetViewAngle > maxOffBoresight) - { - Debug.Log("[BDArmory]: AGM Missile guidance failed - target out of view"); - guidanceActive = false; - } - CheckMiss(); - } - else - { - if (TargetingMode == TargetingModes.Laser) - { - //keep going straight until found laser point - TargetPosition = laserStartPosition + (20000 * startDirection); - } - } - } - - Vector3 agmTarget = MissileGuidance.GetAirToGroundTarget(TargetPosition, vessel, agmDescentRatio); - DoAero(agmTarget); - } - - void SLWGuidance() - { - Vector3 SLWTarget; - if (TargetAcquired) - { - DrawDebugLine(transform.position + (part.rb.velocity * Time.fixedDeltaTime), TargetPosition); - float timeToImpact; - SLWTarget = MissileGuidance.GetAirToAirTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact, optimumAirspeed); - TimeToImpact = timeToImpact; - if (Vector3.Angle(SLWTarget - transform.position, transform.forward) > maxOffBoresight * 0.75f) - { - SLWTarget = TargetPosition; - } - - //proxy detonation - if (proxyDetonate && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (transform.position)).sqrMagnitude < Mathf.Pow(GetBlastRadius() * 0.5f, 2)) - { - part.Destroy(); - } - } - else - { - SLWTarget = transform.position + (20 * vessel.Velocity().normalized); - } - - if (TimeIndex > dropTime + 0.25f) - { - DoAero(SLWTarget); - } - - if (SLWTarget.y > 0f) SLWTarget.y = getSWLWOffset; - - CheckMiss(); - - } - - void DoAero(Vector3 targetPosition) - { - aeroTorque = MissileGuidance.DoAeroForces(this, targetPosition, liftArea, controlAuthority * steerMult, aeroTorque, finalMaxTorque, maxAoA); - } - - void AGMBallisticGuidance() - { - DoAero(CalculateAGMBallisticGuidance(this, TargetPosition)); - } - - public override void Detonate() - { - if (HasExploded || !HasFired) return; - - Debug.Log("[BDArmory]: Detonate Triggered"); - - BDArmorySetup.numberOfParticleEmitters--; - HasExploded = true; - - if (legacyTargetVessel != null) - { - List.Enumerator wpm = legacyTargetVessel.FindPartModulesImplementing().GetEnumerator(); - while (wpm.MoveNext()) - { - if (wpm.Current == null) continue; - wpm.Current.missileIsIncoming = false; - } - wpm.Dispose(); - } - - if (SourceVessel == null) SourceVessel = vessel; - - if (part.FindModuleImplementing() != null) - { - part.FindModuleImplementing().DetonateIfPossible(); - } - else //TODO: Remove this backguard compatibility - { - Vector3 position = transform.position;//+rigidbody.velocity*Time.fixedDeltaTime; - - ExplosionFx.CreateExplosion(position, blastPower, explModelPath, explSoundPath, ExplosionSourceType.Missile, 0, part); - } - - 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()) - { - e.Current.GetComponent().enabled = false; - } - } - e.Dispose(); - - if (part != null) - { - part.Destroy(); - part.explode(); - } - } - - public override Vector3 GetForwardTransform() - { - return MissileReferenceTransform.forward; - } - - protected override void PartDie(Part p) - { - if (p == part) - { - Detonate(); - BDATargetManager.FiredMissiles.Remove(this); - GameEvents.onPartDie.Remove(PartDie); - } - } - - public static bool CheckIfMissile(Part p) - { - return p.GetComponent(); - } - - void WarnTarget() - { - if (legacyTargetVessel == null) return; - if (legacyTargetVessel == null) return; - List.Enumerator wpm = legacyTargetVessel.FindPartModulesImplementing().GetEnumerator(); - while (wpm.MoveNext()) - { - if (wpm.Current == null) continue; - wpm.Current.MissileWarning(Vector3.Distance(transform.position, legacyTargetVessel.transform.position), this); - break; - } - wpm.Dispose(); - } - - void SetupRCS() - { - rcsFiredTimes = new float[] { 0, 0, 0, 0 }; - rcsTransforms = new KSPParticleEmitter[] { upRCS, leftRCS, rightRCS, downRCS }; - } - - void DoRCS() - { - Vector3 relV = TargetVelocity - vessel.obt_velocity; - - for (int i = 0; i < 4; i++) - { - //float giveThrust = Mathf.Clamp(-localRelV.z, 0, rcsThrust); - float giveThrust = Mathf.Clamp(Vector3.Project(relV, rcsTransforms[i].transform.forward).magnitude * -Mathf.Sign(Vector3.Dot(rcsTransforms[i].transform.forward, relV)), 0, rcsThrust); - part.rb.AddForce(-giveThrust * rcsTransforms[i].transform.forward); - - if (giveThrust > rcsRVelThreshold) - { - rcsAudioMinInterval = UnityEngine.Random.Range(0.15f, 0.25f); - if (Time.time - rcsFiredTimes[i] > rcsAudioMinInterval) - { - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/popThrust")); - rcsTransforms[i].emit = true; - rcsFiredTimes[i] = Time.time; - } - } - else - { - rcsTransforms[i].emit = false; - } - - //turn off emit - if (Time.time - rcsFiredTimes[i] > rcsAudioMinInterval * 0.75f) - { - rcsTransforms[i].emit = false; - } - } - } - - public void KillRCS() - { - if (upRCS) upRCS.emit = false; - if (downRCS) downRCS.emit = false; - if (leftRCS) leftRCS.emit = false; - if (rightRCS) rightRCS.emit = false; - } - - void OnGUI() - { - if (HighLogic.LoadedSceneIsFlight) - { - try - { - drawLabels(); - } - catch (Exception) - { } - } - } - - void AntiSpin() - { - part.rb.angularDrag = 0; - part.angularDrag = 0; - Vector3 spin = Vector3.Project(part.rb.angularVelocity, part.rb.transform.forward);// * 8 * Time.fixedDeltaTime; - part.rb.angularVelocity -= spin; - //rigidbody.maxAngularVelocity = 7; - - if (guidanceActive) - { - part.rb.angularVelocity -= 0.6f * part.rb.angularVelocity; - } - else - { - part.rb.angularVelocity -= 0.02f * part.rb.angularVelocity; - } - } - - void SimpleDrag() - { - part.dragModel = Part.DragModel.NONE; - //float simSpeedSquared = (float)vessel.Velocity.sqrMagnitude; - float simSpeedSquared = (part.rb.GetPointVelocity(part.transform.TransformPoint(simpleCoD)) + (Vector3)Krakensbane.GetFrameVelocity()).sqrMagnitude; - Vector3 currPos = transform.position; - float drag = deployed ? deployedDrag : simpleDrag; - float dragMagnitude = (0.008f * part.rb.mass) * drag * 0.5f * simSpeedSquared * (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currPos), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody); - Vector3 dragForce = dragMagnitude * vessel.Velocity().normalized; - part.rb.AddForceAtPosition(-dragForce, transform.TransformPoint(simpleCoD)); - - Vector3 torqueAxis = -Vector3.Cross(vessel.Velocity(), part.transform.forward).normalized; - float AoA = Vector3.Angle(part.transform.forward, vessel.Velocity()); - AoA /= 20; - part.rb.AddTorque(AoA * simpleStableTorque * dragMagnitude * torqueAxis); - } - - void ParseModes() - { - homingType = homingType.ToLower(); - switch (homingType) - { - case "aam": - GuidanceMode = GuidanceModes.AAMLead; - break; - - case "aamlead": - GuidanceMode = GuidanceModes.AAMLead; - break; - - case "aampure": - GuidanceMode = GuidanceModes.AAMPure; - break; - - case "agm": - GuidanceMode = GuidanceModes.AGM; - break; - - case "agmballistic": - GuidanceMode = GuidanceModes.AGMBallistic; - break; - - case "cruise": - GuidanceMode = GuidanceModes.Cruise; - break; - - case "sts": - GuidanceMode = GuidanceModes.STS; - break; - - case "rcs": - GuidanceMode = GuidanceModes.RCS; - break; - - case "beamriding": - GuidanceMode = GuidanceModes.BeamRiding; - break; - - case "slw": - GuidanceMode = GuidanceModes.SLW; - break; - - default: - GuidanceMode = GuidanceModes.None; - break; - } - - targetingType = targetingType.ToLower(); - switch (targetingType) - { - case "radar": - TargetingMode = TargetingModes.Radar; - break; - - case "heat": - TargetingMode = TargetingModes.Heat; - break; - - case "laser": - TargetingMode = TargetingModes.Laser; - break; - - case "gps": - TargetingMode = TargetingModes.Gps; - maxOffBoresight = 360; - break; - - case "antirad": - TargetingMode = TargetingModes.AntiRad; - break; - - default: - TargetingMode = TargetingModes.None; - break; - } - - terminalGuidanceType = terminalGuidanceType.ToLower(); - switch (terminalGuidanceType) - { - case "radar": - TargetingModeTerminal = TargetingModes.Radar; - break; - - case "heat": - TargetingModeTerminal = TargetingModes.Heat; - break; - - case "laser": - TargetingModeTerminal = TargetingModes.Laser; - break; - - case "gps": - TargetingModeTerminal = TargetingModes.Gps; - maxOffBoresight = 360; - break; - - case "antirad": - TargetingModeTerminal = TargetingModes.AntiRad; - break; - - default: - TargetingModeTerminal = TargetingModes.None; - break; - } - } - - private string GetBrevityCode() - { - //torpedo: determine subtype - if (missileType.ToLower() == "torpedo") - { - if ((TargetingMode == TargetingModes.Radar) && (activeRadarRange > 0)) - return "Active Sonar"; - - if ((TargetingMode == TargetingModes.Radar) && (activeRadarRange <= 0)) - return "Passive Sonar"; - - if ((TargetingMode == TargetingModes.Laser) || (TargetingMode == TargetingModes.Gps)) - return "Optical/wireguided"; - - if ((TargetingMode == TargetingModes.Heat)) - return "Heat guided"; - - if ((TargetingMode == TargetingModes.None)) - return "Unguided"; - } - - if (missileType.ToLower() == "bomb") - { - if ((TargetingMode == TargetingModes.Laser) || (TargetingMode == TargetingModes.Gps)) - return "JDAM"; - - if ((TargetingMode == TargetingModes.None)) - return "Unguided"; - } - - //else: missiles: - - if (TargetingMode == TargetingModes.Radar) - { - //radar: determine subtype - if (activeRadarRange <= 0) - return "SARH"; - if (activeRadarRange > 0 && activeRadarRange < maxStaticLaunchRange) - return "Mixed SARH/F&F"; - if (activeRadarRange >= maxStaticLaunchRange) - return "Fire&Forget"; - } - - if (TargetingMode == TargetingModes.AntiRad) - return "Fire&Forget"; - - if (TargetingMode == TargetingModes.Heat) - return "Fire&Forget"; - - if (TargetingMode == TargetingModes.Laser) - return "SALH"; - - if (TargetingMode == TargetingModes.Gps) - { - return TargetingModeTerminal != TargetingModes.None ? "GPS/Terminal" : "GPS"; - } - - // default: - return "Unguided"; - } - - // RMB info in editor - public override string GetInfo() - { - ParseModes(); - - StringBuilder output = new StringBuilder(); - output.AppendLine($"{missileType.ToUpper()} - {GetBrevityCode()}"); - output.Append(Environment.NewLine); - output.AppendLine($"Targeting Type: {targetingType.ToLower()}"); - output.AppendLine($"Guidance Mode: {homingType.ToLower()}"); - if (missileRadarCrossSection != RadarUtils.RCS_MISSILES) - { - output.AppendLine($"Detectable cross section: {missileRadarCrossSection} m^2"); - } - output.AppendLine($"Min Range: {minStaticLaunchRange} m"); - output.AppendLine($"Max Range: {maxStaticLaunchRange} m"); - - if (TargetingMode == TargetingModes.Radar) - { - if (activeRadarRange > 0) - { - 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}"); - } - output.AppendLine($"Max Offborsight: {maxOffBoresight}"); - output.AppendLine($"Locked FOV: {lockedSensorFOV}"); - } - - if (TargetingMode == TargetingModes.Heat) - { - output.AppendLine($"All Aspect: {allAspect}"); - output.AppendLine($"Min Heat threshold: {heatThreshold}"); - output.AppendLine($"Max Offborsight: {maxOffBoresight}"); - output.AppendLine($"Locked FOV: {lockedSensorFOV}"); - } - - if (TargetingMode == TargetingModes.Gps) - { - 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}"); - output.AppendLine($"Max Offborsight: {maxOffBoresight}"); - output.AppendLine($"Locked FOV: {lockedSensorFOV}"); - } - - if (TargetingModeTerminal == TargetingModes.Heat) - { - output.AppendLine($"All Aspect: {allAspect}"); - output.AppendLine($"Min Heat threshold: {heatThreshold}"); - output.AppendLine($"Max Offborsight: {maxOffBoresight}"); - output.AppendLine($"Locked FOV: {lockedSensorFOV}"); - } - } - } - - IEnumerator partModules = part.Modules.GetEnumerator(); - output.AppendLine($"Warhead:"); - while (partModules.MoveNext()) - { - if (partModules.Current == null) continue; - if (partModules.Current.moduleName != "BDExplosivePart") continue; - float tntMass = ((BDExplosivePart)partModules.Current).tntMass; - output.AppendLine($"- Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(tntMass), 2)} m"); - output.AppendLine($"- tnt Mass: {tntMass} kg"); - break; - } - partModules.Dispose(); - - return output.ToString(); - } - } -} diff --git a/BDArmory/Modules/ModuleDrainEC.cs b/BDArmory/Modules/ModuleDrainEC.cs deleted file mode 100644 index b5a452af0..000000000 --- a/BDArmory/Modules/ModuleDrainEC.cs +++ /dev/null @@ -1,225 +0,0 @@ -using BDArmory.Control; -using BDArmory.UI; -using System.Collections; -using System.Linq; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class ModuleDrainEC : PartModule - { - public float incomingDamage = 0; //damage from EMP source - public float EMPDamage = 0; //total EMP buildup accrued - int EMPThreshold = 100; //craft get temporarily disabled - int BrickThreshold = 1000; //craft get permanently bricked - public bool softEMP = true; //can EMPdamage exceed EMPthreshold? - private bool disabled = false; //prevent further EMP buildup while rebooting - public bool bricked = false; //He's dead, jeb - - private void EnableVessel() - { - foreach (Part p in vessel.parts) - { - var engine = p.FindModuleImplementing(); - var engineFX = p.FindModuleImplementing(); - - if (engine != null) - { - engine.allowRestart = true; - } - if (engineFX != null) - { - engineFX.allowRestart = true; - } - var command = p.FindModuleImplementing(); - var weapon = p.FindModuleImplementing(); - if (weapon != null) - { - weapon.weaponState = ModuleWeapon.WeaponStates.Disabled; //allow weapons to be used again - } - 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) - { - AI.ActivatePilot(); //It's Alive! - } - var WM = p.FindModuleImplementing(); - if (WM != null) - { - WM.guardMode = true; - WM.debilitated = false; - } - } - vessel.ActionGroups.ToggleGroup(KSPActionGroup.Custom10); // restart engines - if (!vessel.FindPartModulesImplementing().Any(engine => engine.EngineIgnited)) // Find vessels that didn't activate their engines on AG10 and fire their next stage. - { - foreach (var engine in vessel.FindPartModulesImplementing()) - engine.Activate(); - } - disabled = false; - } - - void Update() - { - if (!HighLogic.LoadedSceneIsFlight) return; - if (BDArmorySetup.GameIsPaused) return; - - if (!bricked) - { - if (EMPDamage > 0 || incomingDamage > 0) - { - UpdateEMPLevel(); - } - } - - } - void UpdateEMPLevel() - { - if ((!disabled || (disabled && !softEMP)) && incomingDamage > 0) - { - EMPDamage += incomingDamage; //only accumulate EMP damage if it's hard EMP or craft isn't disabled - incomingDamage = 0; //reset incoming damage amount - } - if (disabled) - { - EMPDamage = Mathf.Clamp(EMPDamage - 5 * TimeWarp.fixedDeltaTime, 0, Mathf.Infinity); //speed EMP cooldown, if electrolaser'd takes about ~10 sec to reboot. may need to be reduced further - } //fatal if fast+low alt, but higher alt or good glide ratio is survivable - else - { - EMPDamage = Mathf.Clamp(EMPDamage - 1 * TimeWarp.fixedDeltaTime, 0, Mathf.Infinity); - } - if (EMPDamage > EMPThreshold && !bricked && !disabled) //does the damage exceed the soft cap, but not the hard cap? - { - disabled = true; //if so disable the craft - //Debug.Log("[EMP DEBUG]: vessel disabled"); // add a screenmassage the craft's been EMP'd? - DisableVessel(); - } - if (EMPDamage > BrickThreshold && !bricked) //does the damage exceed the hard cap? - { - bricked = true; //if so brick the craft - //Debug.Log("[EMP DEBUG]: vessel bricked"); - } - if (EMPDamage <= 0 && disabled && !bricked) //reset craft - { - EnableVessel(); - //Debug.Log("[EMP DEBUG]: vessel rebooted"); - } - } - private void DisableVessel() - { - foreach (Part p in vessel.parts) - { - var camera = p.FindModuleImplementing(); - var radar = p.FindModuleImplementing(); - var spaceRadar = p.FindModuleImplementing(); - if (radar != null) - { - if (radar.radarEnabled) - { - radar.DisableRadar(); - } - } - if (spaceRadar != null) - { - if (spaceRadar.radarEnabled) - { - spaceRadar.DisableRadar(); - } - } - if (camera != null) - { - if (camera.cameraEnabled) - { - camera.DisableCamera(); - } - } - var engine = p.FindModuleImplementing(); - var engineFX = p.FindModuleImplementing(); - if (engine != null) - { - if (engine.enabled) //kill engines - { - engine.Shutdown(); - engine.allowRestart = false; - } - } - if (engineFX != null) - { - if (engineFX.enabled) - { - engineFX.Shutdown(); - engineFX.allowRestart = false; - } - } - var command = p.FindModuleImplementing(); - var weapon = p.FindModuleImplementing(); - if (weapon != null) - { - weapon.weaponState = ModuleWeapon.WeaponStates.Locked; //prevent weapons from firing - } - if (command != null) - { - command.minimumCrew *= 10; //disable vessel control - } - - var AI = p.FindModuleImplementing(); - if (AI != null) - { - AI.DeactivatePilot(); //disable AI - } - var WM = p.FindModuleImplementing(); - if (WM != null) - { - WM.guardMode = false; //disable guardmode - WM.debilitated = true; //for weapon selection and targeting; - } - PartResource r = p.Resources.Where(pr => pr.resourceName == "ElectricCharge").FirstOrDefault(); - if (r != null) - { - if (r.amount >= 0) - { - p.RequestResource("ElectricCharge", r.amount); - } - } - } - - var empFX = Instantiate(GameDatabase.Instance.GetModel("BDArmory/FX/Electroshock"), - vessel.rootPart.transform.position, Quaternion.identity); - - empFX.SetActive(true); - empFX.transform.SetParent(vessel.rootPart.transform); - empFX.AddComponent(); - } - - } - - internal class EMPShock : MonoBehaviour - { - public void Start() - { - foreach (var pe in gameObject.GetComponentsInChildren()) - { - EffectBehaviour.AddParticleEmitter(pe); - pe.emit = true; - StartCoroutine(TimerRoutine()); - } - } - IEnumerator TimerRoutine() - { - yield return new WaitForSeconds(5); - Destroy(gameObject); - } - - private void OnDestroy() - { - foreach (var pe in gameObject.GetComponentsInChildren()) - { - EffectBehaviour.RemoveParticleEmitter(pe); - } - - } - } -} \ No newline at end of file diff --git a/BDArmory/Modules/ModuleEMP.cs b/BDArmory/Modules/ModuleEMP.cs deleted file mode 100644 index d0976bb8b..000000000 --- a/BDArmory/Modules/ModuleEMP.cs +++ /dev/null @@ -1,43 +0,0 @@ -using UnityEngine; - -namespace BDArmory.Modules -{ - public class ModuleEMP : PartModule - { - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_EMPBlastRadius"),//EMP Blast Radius - UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] - public float proximity = 5000; - - public override void OnStart(StartState state) - { - if (HighLogic.LoadedSceneIsFlight) - { - part.force_activate(); - part.OnJustAboutToBeDestroyed += DetonateEMPRoutine; - } - base.OnStart(state); - } - - public void DetonateEMPRoutine() - { - foreach (Vessel v in FlightGlobals.Vessels) - { - if (!v.HoldPhysics) - { - double targetDistance = Vector3d.Distance(this.vessel.GetWorldPos3D(), v.GetWorldPos3D()); - - if (targetDistance <= proximity) - { - var emp = v.rootPart.FindModuleImplementing(); - if (emp == null) - { - 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 - } - } - } - } - } -} diff --git a/BDArmory/Modules/ModuleMissileRearm.cs b/BDArmory/Modules/ModuleMissileRearm.cs deleted file mode 100644 index 8b77035a2..000000000 --- a/BDArmory/Modules/ModuleMissileRearm.cs +++ /dev/null @@ -1,545 +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("[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("[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("[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("[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("[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("[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("[ModuleMissileRearm]: Node on {0} is unexpected state '{1}'" + - dockingNode.part + dockingNode.fsm.currentStateName); - return false; - } - Debug.Log("[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("[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("[ModuleMissileRearm]: Part {0} is in state {1}" + newPart + newPart.State); - if (newPart.State == PartStates.DEAD) - { - Debug.Log("[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("[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("[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("[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 - { - Debug.Log("[ModuleMissileRearm]: WORKAROUND. Module {0} on part prefab is not awaken. Call Awake on it", module); - 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("[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 - { - badModules.Add(module); - } - } - // 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("[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("[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("[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("[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("[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("[ModuleMissileRearm]: Part {0} is in state {1}" + newPart + newPart.State); - if (newPart.State == PartStates.DEAD) - { - Debug.Log("[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/ModuleRadar.cs b/BDArmory/Modules/ModuleRadar.cs deleted file mode 100644 index 79dbe1231..000000000 --- a/BDArmory/Modules/ModuleRadar.cs +++ /dev/null @@ -1,1146 +0,0 @@ -using System; -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 -{ - public class ModuleRadar : PartModule - { - #region KSPFields (Part Configuration) - - #region General Configuration - - [KSPField] - public string radarName; - - [KSPField] - public int turretID = 0; - - [KSPField] - public string rotationTransformName = string.Empty; - Transform rotationTransform; - - #endregion General Configuration - - #region Radar Capabilities - - [KSPField] - public int rwrThreatType = 0; //IMPORTANT, configures which type of radar it will show up as on the RWR - public RadarWarningReceiver.RWRThreatTypes rwrType = RadarWarningReceiver.RWRThreatTypes.SAM; - - [KSPField] - public double resourceDrain = 0.825; //resource (EC/sec) usage of active radar - - [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 float lockRotationSpeed = 120; //in degrees per second, relevant for omni only - - [KSPField] - public float lockRotationAngle = 4; //??? - - [KSPField] - public bool showDirectionWhileScan = false; //radar can show direction indicator of contacts (false: can show contacts as blocks only) - - [KSPField] - public float multiLockFOV = 30; //?? - - [KSPField] - public float lockAttemptFOV = 2; //?? - - [KSPField] - public bool canScan = true; //radar has detection capabilities - - [KSPField] - public bool canLock = true; //radar has locking/tracking capabilities - - [KSPField] - public int maxLocks = 1; //how many targets can be locked/tracked simultaneously - - [KSPField] - 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? - - [KSPField] - public FloatCurve radarDetectionCurve = new FloatCurve(); //FloatCurve defining at what range which RCS size can be detected - - [KSPField] - public FloatCurve radarLockTrackCurve = new FloatCurve(); //FloatCurve defining at what range which RCS size can be locked/tracked - - [KSPField] - public float radarGroundClutterFactor = 0.25f; //Factor defining how effective the radar is for look-down, compensating for ground clutter (0=ineffective, 1=fully effective) - //default to 0.25, so all cross sections of landed/splashed/submerged vessels are reduced to 1/4th, as these vessel usually a quite large - - #endregion Radar Capabilities - - #region Persisted State in flight - - [KSPField(isPersistant = true)] - public string linkedVesselID; - - [KSPField(isPersistant = true)] - public bool radarEnabled; - - [KSPField(isPersistant = true)] - public int rangeIndex = 99; - - [KSPField(isPersistant = true)] - public float currentAngle; - - #endregion Persisted State in flight - - #region DEPRECATED! ->see Radar Capabilities section for new detectionCurve + trackingCurve - - [Obsolete] - [KSPField] - public float minSignalThreshold = 90; - - [Obsolete] - [KSPField] - public float minLockedSignalThreshold = 90; - - #endregion DEPRECATED! ->see Radar Capabilities section for new detectionCurve + trackingCurve - - #endregion KSPFields (Part Configuration) - - #region KSP Events & Actions - - [KSPAction("Toggle Radar")] - public void AGEnable(KSPActionParam param) - { - if (radarEnabled) - { - DisableRadar(); - } - else - { - EnableRadar(); - } - } - - [KSPEvent(active = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ToggleRadar")]//Toggle Radar - public void Toggle() - { - if (radarEnabled) - { - DisableRadar(); - } - else - { - EnableRadar(); - } - } - - [KSPAction("Target Next")] - public void TargetNext(KSPActionParam param) - { - vesselRadarData.TargetNext(); - } - - [KSPAction("Target Prev")] - public void TargetPrev(KSPActionParam param) - { - vesselRadarData.TargetPrev(); - } - - #endregion KSP Events & Actions - - #region Part members - - //locks - [KSPField(isPersistant = false, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_CurrentLocks")]//Current Locks - public int currLocks; - - public bool locked - { - get { return currLocks > 0; } - } - - public int currentLocks - { - get { return currLocks; } - } - - private TargetSignatureData[] attemptedLocks; - private List lockedTargets; - - public TargetSignatureData lockedTarget - { - get - { - if (currLocks == 0) return TargetSignatureData.noTarget; - else - { - return lockedTargets[lockedTargetIndex]; - } - } - } - - private int lockedTargetIndex; - - public int currentLockIndex - { - get { return lockedTargetIndex; } - } - - public float radarMinDistanceDetect - { - get { return radarDetectionCurve.minTime; } - } - - //[KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "Detection Range")] - public float radarMaxDistanceDetect - { - get { return radarDetectionCurve.maxTime; } - } - - public float radarMinDistanceLockTrack - { - get { return radarLockTrackCurve.minTime; } - } - - //[KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "Locking Range")] - public float radarMaxDistanceLockTrack - { - get { return radarLockTrackCurve.maxTime; } - } - - //linked vessels - private List linkedToVessels; - public List availableRadarLinks; - private bool unlinkOnDestroy = true; - - //GUI - private bool drawGUI; - public float signalPersistTime; - public float signalPersistTimeForRwr; - - //scanning - private float currentAngleLock; - public Transform referenceTransform; - private float radialScanDirection = 1; - private float lockScanDirection = 1; - - public bool boresightScan; - - //locking - public float lockScanAngle; - 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 = null; - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - wpmr = mf.Current; - } - mf.Dispose(); - return wpmr; - } - set { wpmr = value; } - } - - public VesselRadarData vesselRadarData; - private string myVesselID; - - // part state - private bool startupComplete; - public float leftLimit; - public float rightLimit; - private int snapshotTicker; - - #endregion Part members - - 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 - } - - 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 EnableRadar() - { - EnsureVesselRadarData(); - radarEnabled = true; - - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - weaponManager = mf.Current; - if (vesselRadarData) - { - vesselRadarData.weaponManager = mf.Current; - } - break; - } - mf.Dispose(); - - UpdateToggleGuiName(); - vesselRadarData.AddRadar(this); - } - - public void DisableRadar() - { - if (locked) - { - UnlockAllTargets(); - } - - radarEnabled = false; - UpdateToggleGuiName(); - - if (vesselRadarData) - { - vesselRadarData.RemoveRadar(this); - } - - List.Enumerator vrd = linkedToVessels.GetEnumerator(); - while (vrd.MoveNext()) - { - if (vrd.Current == null) continue; - vrd.Current.UnlinkDisabledRadar(this); - } - vrd.Dispose(); - } - - void OnDestroy() - { - if (HighLogic.LoadedSceneIsFlight) - { - if (vesselRadarData) - { - vesselRadarData.RemoveRadar(this); - vesselRadarData.RemoveDataFromRadar(this); - } - - if (linkedToVessels != null) - { - List.Enumerator vrd = linkedToVessels.GetEnumerator(); - while (vrd.MoveNext()) - { - if (vrd.Current == null) continue; - if (unlinkOnDestroy) - { - vrd.Current.UnlinkDisabledRadar(this); - } - else - { - vrd.Current.BeginWaitForUnloadedLinkedRadar(this, myVesselID); - } - } - vrd.Dispose(); - } - } - } - - public override void OnStart(StartState state) - { - base.OnStart(state); - - if (HighLogic.LoadedSceneIsFlight) - { - myVesselID = vessel.id.ToString(); - RadarUtils.SetupResources(); - - if (string.IsNullOrEmpty(radarName)) - { - radarName = part.partInfo.title; - } - - linkedToVessels = new List(); - - signalPersistTime = omnidirectional - ? 360 / (scanRotationSpeed + 5) - : directionalFieldOfView / (scanRotationSpeed + 5); - - rwrType = (RadarWarningReceiver.RWRThreatTypes)rwrThreatType; - if (rwrType == RadarWarningReceiver.RWRThreatTypes.Sonar) - signalPersistTimeForRwr = RadarUtils.ACTIVE_MISSILE_PING_PERISTS_TIME; - else - signalPersistTimeForRwr = signalPersistTime / 2; - - if (rotationTransformName != string.Empty) - { - rotationTransform = part.FindModelTransform(rotationTransformName); - } - - attemptedLocks = new TargetSignatureData[3]; - TargetSignatureData.ResetTSDArray(ref attemptedLocks); - lockedTargets = new List(); - - referenceTransform = (new GameObject()).transform; - referenceTransform.parent = transform; - referenceTransform.localPosition = Vector3.zero; - - 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; - } - } - - // check for not updated legacy part: - if ((canScan && (radarMinDistanceDetect == float.MaxValue)) || (canLock && (radarMinDistanceLockTrack == float.MaxValue))) - { - Debug.Log("[BDArmory]: WARNING: " + part.name + " has legacy definition, missing new radarDetectionCurve and radarLockTrackCurve definitions! Please update for the part to be usable!"); - } - } - - /* - void OnGoOnRails(Vessel v) - { - if (v != vessel) return; - unlinkOnDestroy = false; - //myVesselID = vessel.id.ToString(); - } - */ - - IEnumerator StartUpRoutine() - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: StartupRoutine: " + radarName + " enabled: " + radarEnabled); - while (!FlightGlobals.ready || vessel.packed) - { - yield return null; - } - - yield return new WaitForFixedUpdate(); - - // DISABLE RADAR - /* - if (radarEnabled) - { - EnableRadar(); - } - */ - - yield return null; - - if (!vesselRadarData.hasLoadedExternalVRDs) - { - RecoverLinkedVessels(); - vesselRadarData.hasLoadedExternalVRDs = true; - } - - UpdateToggleGuiName(); - startupComplete = true; - } - - 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); - } - - void FixedUpdate() - { - if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && startupComplete) - { - if (!vessel.IsControllable && radarEnabled) - { - DisableRadar(); - } - - if (radarEnabled) - { - DrainElectricity(); //physics behaviour, thus moved here from update - - if (locked) - { - for (int i = 0; i < lockedTargets.Count; i++) - { - UpdateLock(i); - } - - if (canTrackWhileScan) - { - Scan(); - } - } - else if (boresightScan) - { - BoresightScan(); - } - else if (canScan) - { - Scan(); - } - } - } - } - - void UpdateSlaveData() - { - if (slaveTurrets && weaponManager) - { - weaponManager.slavingTurrets = true; - if (locked) - { - weaponManager.slavedPosition = lockedTarget.predictedPosition; - weaponManager.slavedVelocity = lockedTarget.velocity; - weaponManager.slavedAcceleration = lockedTarget.acceleration; - weaponManager.slavedTarget = lockedTarget; - } - } - } - - void LateUpdate() - { - if (HighLogic.LoadedSceneIsFlight && (canScan || canLock)) - { - UpdateModel(); - } - } - - void UpdateModel() - { - //model rotation - if (radarEnabled) - { - if (rotationTransform && canScan) - { - Vector3 direction; - if (locked) - { - direction = - Quaternion.AngleAxis(canTrackWhileScan ? currentAngle : lockScanAngle, referenceTransform.up) * - referenceTransform.forward; - } - else - { - direction = Quaternion.AngleAxis(currentAngle, referenceTransform.up) * referenceTransform.forward; - } - - Vector3 localDirection = - Vector3.ProjectOnPlane(rotationTransform.parent.InverseTransformDirection(direction), Vector3.up); - if (localDirection != Vector3.zero) - { - rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, - Quaternion.LookRotation(localDirection, Vector3.up), 10 * TimeWarp.fixedDeltaTime); - } - } - - //lock turret - if (lockingTurret && canLock) - { - if (locked) - { - lockingTurret.AimToTarget(lockedTarget.predictedPosition, lockingPitch, lockingYaw); - } - else - { - lockingTurret.ReturnTurret(); - } - } - } - else - { - if (rotationTransform) - { - rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, - Quaternion.identity, 5 * TimeWarp.fixedDeltaTime); - } - - if (lockingTurret) - { - lockingTurret.ReturnTurret(); - } - } - } - - void Scan() - { - float angleDelta = scanRotationSpeed * Time.fixedDeltaTime; - RadarUtils.RadarUpdateScanLock(weaponManager, currentAngle, referenceTransform, angleDelta, referenceTransform.position, this, false, ref attemptedLocks); - - if (omnidirectional) - { - currentAngle = Mathf.Repeat(currentAngle + angleDelta, 360); - } - else - { - currentAngle += radialScanDirection * angleDelta; - - if (locked) - { - float targetAngle = VectorUtils.SignedAngle(referenceTransform.forward, - Vector3.ProjectOnPlane(lockedTarget.position - referenceTransform.position, - referenceTransform.up), referenceTransform.right); - leftLimit = Mathf.Clamp(targetAngle - (multiLockFOV / 2), -directionalFieldOfView / 2, - directionalFieldOfView / 2); - rightLimit = Mathf.Clamp(targetAngle + (multiLockFOV / 2), -directionalFieldOfView / 2, - directionalFieldOfView / 2); - - if (radialScanDirection < 0 && currentAngle < leftLimit) - { - currentAngle = leftLimit; - radialScanDirection = 1; - } - else if (radialScanDirection > 0 && currentAngle > rightLimit) - { - currentAngle = rightLimit; - radialScanDirection = -1; - } - } - else - { - if (Mathf.Abs(currentAngle) > directionalFieldOfView / 2) - { - currentAngle = Mathf.Sign(currentAngle) * directionalFieldOfView / 2; - radialScanDirection = -radialScanDirection; - } - } - } - } - - public bool TryLockTarget(Vector3 position) - { - if (!canLock) - { - return false; - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Trying to radar lock target with (" + radarName + ")"); - - if (currentLocks == maxLocks) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: - Failed, this radar already has the maximum allowed targets locked."); - return false; - } - - Vector3 targetPlanarDirection = Vector3.ProjectOnPlane(position - referenceTransform.position, - referenceTransform.up); - float angle = Vector3.Angle(targetPlanarDirection, referenceTransform.forward); - if (referenceTransform.InverseTransformPoint(position).x < 0) - { - angle = -angle; - } - //TargetSignatureData.ResetTSDArray(ref attemptedLocks); - RadarUtils.RadarUpdateScanLock(weaponManager, angle, referenceTransform, lockAttemptFOV, referenceTransform.position, this, true, ref attemptedLocks, signalPersistTime); - - for (int i = 0; i < attemptedLocks.Length; i++) - { - if (attemptedLocks[i].exists && (attemptedLocks[i].predictedPosition - position).sqrMagnitude < 40 * 40) - { - if (!locked && !omnidirectional) - { - float targetAngle = VectorUtils.SignedAngle(referenceTransform.forward, - Vector3.ProjectOnPlane(attemptedLocks[i].position - referenceTransform.position, - referenceTransform.up), referenceTransform.right); - currentAngle = targetAngle; - } - lockedTargets.Add(attemptedLocks[i]); - currLocks = lockedTargets.Count; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: - Acquired lock on target (" + attemptedLocks[i].vessel?.name + ")"); - - vesselRadarData.AddRadarContact(this, lockedTarget, true); - vesselRadarData.UpdateLockedTargets(); - return true; - } - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: - Failed to lock on target."); - - return false; - } - - void BoresightScan() - { - if (locked) - { - boresightScan = false; - return; - } - - currentAngle = Mathf.Lerp(currentAngle, 0, 0.08f); - RadarUtils.RadarUpdateScanBoresight(new Ray(transform.position, transform.up), boresightFOV, ref attemptedLocks, Time.fixedDeltaTime, this); - - for (int i = 0; i < attemptedLocks.Length; i++) - { - if (!attemptedLocks[i].exists || !(attemptedLocks[i].age < 0.1f)) continue; - TryLockTarget(attemptedLocks[i].predictedPosition); - boresightScan = false; - return; - } - } - - void UpdateLock(int index) - { - TargetSignatureData lockedTarget = lockedTargets[index]; - - Vector3 targetPlanarDirection = - Vector3.ProjectOnPlane(lockedTarget.predictedPosition - referenceTransform.position, - referenceTransform.up); - float lookAngle = Vector3.Angle(targetPlanarDirection, referenceTransform.forward); - if (referenceTransform.InverseTransformPoint(lockedTarget.predictedPosition).x < 0) - { - lookAngle = -lookAngle; - } - - if (omnidirectional) - { - if (lookAngle < 0) lookAngle += 360; - } - - lockScanAngle = lookAngle + currentAngleLock; - if (!canTrackWhileScan && index == lockedTargetIndex) - { - currentAngle = lockScanAngle; - } - float angleDelta = lockRotationSpeed * Time.fixedDeltaTime; - float lockedSignalPersist = lockRotationAngle / lockRotationSpeed; - //RadarUtils.ScanInDirection(lockScanAngle, referenceTransform, angleDelta, referenceTransform.position, minLockedSignalThreshold, ref attemptedLocks, lockedSignalPersist); - bool radarSnapshot = (snapshotTicker > 30); - if (radarSnapshot) - { - snapshotTicker = 0; - } - else - { - snapshotTicker++; - } - //RadarUtils.ScanInDirection (new Ray (referenceTransform.position, lockedTarget.predictedPosition - referenceTransform.position), lockRotationAngle * 2, minLockedSignalThreshold, ref attemptedLocks, lockedSignalPersist, true, rwrType, radarSnapshot); - - if ( - Vector3.Angle(lockedTarget.position - referenceTransform.position, - this.lockedTarget.position - referenceTransform.position) > multiLockFOV / 2) - { - UnlockTargetAt(index, true); - return; - } - - RadarUtils.RadarUpdateLockTrack( - new Ray(referenceTransform.position, lockedTarget.predictedPosition - referenceTransform.position), - lockedTarget.predictedPosition, lockRotationAngle * 2, this, lockedSignalPersist, true, index, lockedTarget.vessel); - - //if still failed or out of FOV, unlock. - if (!lockedTarget.exists || - (!omnidirectional && - Vector3.Angle(lockedTarget.position - referenceTransform.position, transform.up) > - directionalFieldOfView / 2)) - { - //UnlockAllTargets(); - UnlockTargetAt(index, true); - return; - } - - //unlock if over-jammed - // MOVED TO RADARUTILS! - - //cycle scan direction - if (index == lockedTargetIndex) - { - currentAngleLock += lockScanDirection * angleDelta; - if (Mathf.Abs(currentAngleLock) > lockRotationAngle / 2) - { - currentAngleLock = Mathf.Sign(currentAngleLock) * lockRotationAngle / 2; - lockScanDirection = -lockScanDirection; - } - } - } - - public void UnlockAllTargets() - { - if (!locked) return; - - lockedTargets.Clear(); - currLocks = 0; - lockedTargetIndex = 0; - - if (vesselRadarData) - { - vesselRadarData.UnlockAllTargetsOfRadar(this); - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Radar Targets were cleared (" + radarName + ")."); - } - - public void SetActiveLock(TargetSignatureData target) - { - for (int i = 0; i < lockedTargets.Count; i++) - { - if (target.vessel == lockedTargets[i].vessel) - { - lockedTargetIndex = i; - return; - } - } - } - - public void UnlockTargetAt(int index, bool tryRelock = false) - { - Vessel rVess = lockedTargets[index].vessel; - - if (tryRelock) - { - UnlockTargetAt(index, false); - if (rVess) - { - StartCoroutine(RetryLockRoutine(rVess)); - } - return; - } - - lockedTargets.RemoveAt(index); - currLocks = lockedTargets.Count; - if (lockedTargetIndex > index) - { - lockedTargetIndex--; - } - - lockedTargetIndex = Mathf.Clamp(lockedTargetIndex, 0, currLocks - 1); - lockedTargetIndex = Mathf.Max(lockedTargetIndex, 0); - - if (vesselRadarData) - { - //vesselRadarData.UnlockTargetAtPosition(position); - vesselRadarData.RemoveVesselFromTargets(rVess); - } - } - - IEnumerator RetryLockRoutine(Vessel v) - { - yield return null; - vesselRadarData.TryLockTarget(v); - } - - public void UnlockTargetVessel(Vessel v) - { - for (int i = 0; i < lockedTargets.Count; i++) - { - if (lockedTargets[i].vessel == v) - { - UnlockTargetAt(i); - return; - } - } - } - - void SlaveTurrets() - { - List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mtc.MoveNext()) - { - if (mtc.Current == null) continue; - mtc.Current.slaveTurrets = false; - } - mtc.Dispose(); - - List.Enumerator rad = vessel.FindPartModulesImplementing().GetEnumerator(); - while (rad.MoveNext()) - { - if (rad.Current == null) continue; - rad.Current.slaveTurrets = false; - } - rad.Dispose(); - - slaveTurrets = true; - } - - void UnslaveTurrets() - { - using (List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator()) - while (mtc.MoveNext()) - { - if (mtc.Current == null) continue; - mtc.Current.slaveTurrets = false; - } - - using (List.Enumerator rad = vessel.FindPartModulesImplementing().GetEnumerator()) - while (rad.MoveNext()) - { - if (rad.Current == null) continue; - rad.Current.slaveTurrets = false; - } - - if (weaponManager) - { - weaponManager.slavingTurrets = false; - } - - slaveTurrets = false; - } - - public void UpdateLockedTargetInfo(TargetSignatureData newData) - { - int index = -1; - for (int i = 0; i < lockedTargets.Count; i++) - { - if (lockedTargets[i].vessel != newData.vessel) continue; - index = i; - break; - } - - if (index >= 0) - { - lockedTargets[index] = newData; - } - } - - public void ReceiveContactData(TargetSignatureData contactData, bool _locked) - { - if (vesselRadarData) - { - vesselRadarData.AddRadarContact(this, contactData, _locked); - } - - List.Enumerator vrd = linkedToVessels.GetEnumerator(); - while (vrd.MoveNext()) - { - if (vrd.Current == null) continue; - if (vrd.Current.canReceiveRadarData && vrd.Current.vessel != contactData.vessel) - { - vrd.Current.AddRadarContact(this, contactData, _locked); - } - } - vrd.Dispose(); - } - - public void AddExternalVRD(VesselRadarData vrd) - { - if (!linkedToVessels.Contains(vrd)) - { - linkedToVessels.Add(vrd); - } - } - - public void RemoveExternalVRD(VesselRadarData vrd) - { - linkedToVessels.Remove(vrd); - } - - void OnGUI() - { - if (drawGUI) - { - if (boresightScan) - { - BDGUIUtils.DrawTextureOnWorldPos(transform.position + (3500 * transform.up), - BDArmorySetup.Instance.dottedLargeGreenCircle, new Vector2(156, 156), 0); - } - } - } - - public void RecoverLinkedVessels() - { - string[] vesselIDs = linkedVesselID.Split(new char[] { ',' }); - for (int i = 0; i < vesselIDs.Length; i++) - { - StartCoroutine(RecoverLinkedVesselRoutine(vesselIDs[i])); - } - } - - IEnumerator RecoverLinkedVesselRoutine(string vesselID) - { - while (true) - { - using (var v = BDATargetManager.LoadedVessels.GetEnumerator()) - while (v.MoveNext()) - { - if (v.Current == null || !v.Current.loaded || v.Current == vessel) continue; - if (v.Current.id.ToString() != vesselID) continue; - VesselRadarData vrd = v.Current.gameObject.GetComponent(); - if (!vrd) continue; - StartCoroutine(RelinkVRDWhenReadyRoutine(vrd)); - yield break; - } - - yield return new WaitForSeconds(0.5f); - } - } - - IEnumerator RelinkVRDWhenReadyRoutine(VesselRadarData vrd) - { - while (!vrd.radarsReady || vrd.vessel.packed) - { - yield return null; - } - yield return null; - vesselRadarData.LinkVRD(vrd); - Debug.Log("[BDArmory]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + - vrd.vessel.vesselName); - } - - public string getRWRType(int i) - { - switch (i) - { - case 0: - return Localizer.Format("#autoLOC_bda_1000002"); // #autoLOC_bda_1000002 = SAM - - case 1: - return Localizer.Format("#autoLOC_bda_1000003"); // #autoLOC_bda_1000003 = FIGHTER - - case 2: - return Localizer.Format("#autoLOC_bda_1000004"); // #autoLOC_bda_1000004 = AWACS - - case 3: - case 4: - return Localizer.Format("#autoLOC_bda_1000005"); // #autoLOC_bda_1000005 = MISSILE - - case 5: - return Localizer.Format("#autoLOC_bda_1000006"); // #autoLOC_bda_1000006 = DETECTION - - case 6: - return Localizer.Format("#autoLOC_bda_1000017"); // #autoLOC_bda_1000017 = SONAR - } - return Localizer.Format("#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); - - 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(Localizer.Format("#autoLOC_bda_1000021", resourceDrain)); - if (!isLinkOnly) - { - output.AppendLine(Localizer.Format("#autoLOC_bda_1000022", directionalFieldOfView)); - output.AppendLine(Localizer.Format("#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)); - if (canLock) - { - output.AppendLine(Localizer.Format("#autoLOC_bda_1000028", maxLocks)); - } - output.AppendLine(Localizer.Format("#autoLOC_bda_1000029", canRecieveRadarData)); - - output.Append(Environment.NewLine); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000030")); - - if (canScan) - output.AppendLine(Localizer.Format("#autoLOC_bda_1000031", radarDetectionCurve.Evaluate(radarMaxDistanceDetect), radarMaxDistanceDetect)); - else - output.AppendLine(Localizer.Format("#autoLOC_bda_1000032")); - if (canLock) - output.AppendLine(Localizer.Format("#autoLOC_bda_1000033", radarLockTrackCurve.Evaluate(radarMaxDistanceLockTrack), radarMaxDistanceLockTrack)); - else - output.AppendLine(Localizer.Format("#autoLOC_bda_1000034")); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000035", radarGroundClutterFactor)); - } - - 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(Localizer.Format("#autoLOC_bda_1000016"), 5.0f, ScreenMessageStyle.UPPER_CENTER); // #autoLOC_bda_1000016 = Radar Requires EC - DisableRadar(); - } - } - } -} diff --git a/BDArmory/Modules/ModuleReactiveArmor.cs b/BDArmory/Modules/ModuleReactiveArmor.cs deleted file mode 100644 index 8123818dc..000000000 --- a/BDArmory/Modules/ModuleReactiveArmor.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BDArmory.Core.Module; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class ModuleReactiveArmor : PartModule - { - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Damage Percentage Threshold"), - // UI_FloatRange(controlEnabled = true, scene = UI_Scene.All, minValue = 0f, maxValue = 100f, stepIncrement = 1f)] - public float DAMAGEMODIFIER1 = 75; - - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Armor Percentage Threshold"), - // UI_FloatRange(controlEnabled = true, scene = UI_Scene.All, minValue = 0f, maxValue = 100f, stepIncrement = 1f)] - public float ARMORMODIFIER1 = 75; - - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Damage Modifer [Stage 2]"), - // UI_FloatRange(controlEnabled = true, scene = UI_Scene.All, minValue = 0f, maxValue = 100f, stepIncrement = 1f)] - public float DAMAGEMODIFIER2 = 35; - - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Armor Modifer [Stage 2]"), - // UI_FloatRange(controlEnabled = true, scene = UI_Scene.All, minValue = 0f, maxValue = 100f, stepIncrement = 1f)] - public float ARMORMODIFIER2 = 35; - - public bool underAttack = false; - private float partHPmax = 0.0f; - private float partHPtotal = 0.0f; - private float partArmorMax = 0.0f; - private float partArmorTotal = 0.0f; - private double stage = 1; - - - private HitpointTracker hp; - - private HitpointTracker GetHP() - { - HitpointTracker hp = null; - - hp = part.FindModuleImplementing(); - - return hp; - } - - public override void OnStart(StartState state) - { - initializeData(); - - useTextureAll(false); - - if (HighLogic.LoadedSceneIsFlight) - { - Setup(); - } - base.OnStart(state); - } - - public void Update() - { - if (!HighLogic.LoadedSceneIsFlight) return; - if (stage == 1) - { - CheckPart(); - } - } - - private void ScreenMsg(string msg) - { - ScreenMessages.PostScreenMessage(new ScreenMessage(msg, 0.005f, ScreenMessageStyle.UPPER_RIGHT)); - } - - private void Setup() - { - hp = GetHP(); - - partHPmax = hp.maxHitPoints; - partHPtotal = hp.Hitpoints; - partArmorMax = hp.ArmorThickness; - partArmorTotal = hp.Armor; - } - - public void CheckPart() - { - hp = GetHP(); - partHPtotal = hp.Hitpoints; - partArmorTotal = hp.Armor; - - if (stage != 1 || (!(partHPtotal <= partHPmax * DAMAGEMODIFIER1 / 100) && - !(partArmorTotal <= partArmorMax * ARMORMODIFIER1 / 100))) return; - stage = 2; - hp.Armor = ARMORMODIFIER2 * partArmorMax / 100; - hp.ArmorThickness = hp.Armor; - hp.Hitpoints = DAMAGEMODIFIER2 * partHPmax / 100; - hp.maxHitPoints = hp.Hitpoints; - - partHPmax = hp.maxHitPoints; - partHPtotal = hp.Hitpoints; - partArmorMax = hp.ArmorThickness; - partArmorTotal = hp.Armor; - - nextTextureEvent(); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - [KSPField] - public string currentTextureName = string.Empty; - - [KSPField] - public string textureRootFolder = string.Empty; - - [KSPField] - public string objectNames = string.Empty; - - [KSPField] - public string textureNames = string.Empty; - - [KSPField] - public string mapNames = string.Empty; - - [KSPField] - public string textureDisplayNames = "Default"; - - [KSPField(isPersistant = true)] - public int selectedTexture = 0; - - [KSPField(isPersistant = true)] - public string selectedTextureURL = string.Empty; - - [KSPField(isPersistant = true)] - public string selectedMapURL = string.Empty; - - [KSPField] - public string additionalMapType = "_BumpMap"; - - [KSPField] - public bool mapIsNormal = true; - - private List targetObjectTransforms = new List(); - private List> targetMats = new List>(); - private List texList = new List(); - private List mapList = new List(); - private List objectList = new List(); - private List textureDisplayList = new List(); - - private bool initialized = false; - - List ListChildren(Transform a) - { - List childList = new List(); - foreach (Transform b in a) - { - childList.Add(b); - childList.AddRange(ListChildren(b)); - } - return childList; - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_NextTexture")]//Next Texture - public void nextTextureEvent() - { - selectedTexture++; - if (selectedTexture >= texList.Count && selectedTexture >= mapList.Count) - selectedTexture = 0; - useTextureAll(true); - } - - public void useTextureAll(bool calledByPlayer) - { - applyTexToPart(calledByPlayer); - } - - private void applyTexToPart(bool calledByPlayer) - { - initializeData(); - foreach (List matList in targetMats) - { - foreach (Material mat in matList) - { - useTextureOrMap(mat); - } - } - } - - public void useTextureOrMap(Material targetMat) - { - if (targetMat == null) return; - useTexture(targetMat); - - useMap(targetMat); - } - - private void useMap(Material targetMat) - { - if (mapList.Count <= selectedTexture) return; - if (GameDatabase.Instance.ExistsTexture(mapList[selectedTexture])) - { - targetMat.SetTexture(additionalMapType, GameDatabase.Instance.GetTexture(mapList[selectedTexture], mapIsNormal)); - selectedMapURL = mapList[selectedTexture]; - - if (selectedTexture < textureDisplayList.Count && texList.Count == 0) - { - currentTextureName = textureDisplayList[selectedTexture]; - } - } - } - - private void useTexture(Material targetMat) - { - if (texList.Count <= selectedTexture) return; - if (!GameDatabase.Instance.ExistsTexture(texList[selectedTexture])) return; - targetMat.mainTexture = GameDatabase.Instance.GetTexture(texList[selectedTexture], false); - selectedTextureURL = texList[selectedTexture]; - - currentTextureName = selectedTexture > textureDisplayList.Count - 1 ? - getTextureDisplayName(texList[selectedTexture]) : - textureDisplayList[selectedTexture]; - } - - private string getTextureDisplayName(string longName) - { - string[] splitString = longName.Split('/'); - return splitString[splitString.Length - 1]; - } - - private void initializeData() - { - if (initialized) return; - objectList = parseNames(objectNames, true); - texList = parseNames(textureNames, true, true, textureRootFolder); - mapList = parseNames(mapNames, true, true, textureRootFolder); - textureDisplayList = parseNames(textureDisplayNames); - - foreach (string targetObjectName in objectList) - { - Transform[] targetObjectTransformArray = part.FindModelTransforms(targetObjectName); - List matList = new List(); - foreach (Transform t in targetObjectTransformArray) - { - if (t == null || t.gameObject.GetComponent() == null) continue; - Material targetMat = t.gameObject.GetComponent().material; - if (targetMat == null) continue; - if (!matList.Contains(targetMat)) - { - matList.Add(targetMat); - } - } - targetMats.Add(matList); - } - initialized = true; - } - - ///////////////////////////////////////////////////////////////////// - - public static List parseNames(string names) - { - return parseNames(names, false, true, string.Empty); - } - - public static List parseNames(string names, bool replaceBackslashErrors) - { - return parseNames(names, replaceBackslashErrors, true, string.Empty); - } - - public static List parseNames(string names, bool replaceBackslashErrors, bool trimWhiteSpace, string prefix) - { - List source = names.Split(';').ToList(); - for (int i = source.Count - 1; i >= 0; i--) - { - if (source[i] == string.Empty) - { - source.RemoveAt(i); - } - } - if (trimWhiteSpace) - { - for (int i = 0; i < source.Count; i++) - { - source[i] = source[i].Trim(' '); - } - } - if (prefix != string.Empty) - { - for (int i = 0; i < source.Count; i++) - { - source[i] = prefix + source[i]; - } - } - if (replaceBackslashErrors) - { - for (int i = 0; i < source.Count; i++) - { - source[i] = source[i].Replace('\\', '/'); - } - } - return source.ToList(); - } - - public static List parseIntegers(string stringOfInts) - { - List newIntList = new List(); - string[] valueArray = stringOfInts.Split(';'); - for (int i = 0; i < valueArray.Length; i++) - { - int newValue = 0; - if (int.TryParse(valueArray[i], out newValue)) - { - newIntList.Add(newValue); - } - else - { - Debug.Log("invalid integer: " + valueArray[i]); - } - } - return newIntList; - } - } -} diff --git a/BDArmory/Modules/ModuleSelfSealingTank.cs b/BDArmory/Modules/ModuleSelfSealingTank.cs new file mode 100644 index 000000000..0fbcb0c0f --- /dev/null +++ b/BDArmory/Modules/ModuleSelfSealingTank.cs @@ -0,0 +1,613 @@ +using KSP.Localization; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System.Text; +using System; +using UnityEngine; + +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Modules +{ + class ModuleSelfSealingTank : PartModule, IPartMassModifier + { + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) + { + return FBmass + ArmorMass + FISmass; + } + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.FIXED; + + [KSPField(isPersistant = true)] + public bool SSTank = false; + + [KSPEvent(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_SSTank", active = true)]//Self-Sealing Tank + public void ToggleTankOption() + { + SSTank = !SSTank; + if (!SSTank) + { + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" + + using (IEnumerator resource = part.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount = Math.Floor(resource.Current.maxAmount * 1.11112); + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + else + { + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" + + using (IEnumerator resource = part.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount *= 0.9; + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + GUIUtils.RefreshAssociatedWindows(part); + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + + var tank = pSym.Current.FindModuleImplementing(); + if (tank == null) continue; + + tank.SSTank = SSTank; + + if (!SSTank) + { + tank.Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" + + using (IEnumerator resource = pSym.Current.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount = Math.Floor(resource.Current.maxAmount * 1.11112); + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + else + { + tank.Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" + + using (IEnumerator resource = pSym.Current.Resources.GetEnumerator()) + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + resource.Current.maxAmount *= 0.9; + resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); + } + } + GUIUtils.RefreshAssociatedWindows(pSym.Current); + } + } + + [KSPField(isPersistant = true)] + public bool InertTank = false; + + [KSPEvent(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FIS", active = true)]//Self-Sealing Tank + public void ToggleInertOption() + { + InertTank = !InertTank; + if (!InertTank) + { + 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 = StringUtils.Localize("#LOC_BDArmory_FIS_Off");//"Disable self-sealing tank" + FISmass = 0.15f; + FireBottles = 0; + FBSetup(null, null); + Fields["FireBottles"].guiActiveEditor = false; + Fields["FBRemaining"].guiActive = false; + } + partmass = (FISmass + ArmorMass + FBmass); + GUIUtils.RefreshAssociatedWindows(part); + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + + var tank = pSym.Current.FindModuleImplementing(); + if (tank == null) continue; + + tank.InertTank = InertTank; + + if (!InertTank) + { + 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 = 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); + GUIUtils.RefreshAssociatedWindows(pSym.Current); + } + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + + [KSPField(isPersistant = true)] + public bool armoredCockpit = false; + + [KSPEvent(advancedTweakable = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_Armorcockpit_On", active = true)]//"Add Armored Cockpit" + public void TogglecockpitArmor() + { + armoredCockpit = !armoredCockpit; + if (!armoredCockpit) + { + Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_On");//"Add Armored Cockpit" + ArmorMass = 0; + } + else + { + Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_Off");//"Remove Armored Cockpit" + ArmorMass = 0.2f * part.CrewCapacity; + } + partmass = (FISmass + ArmorMass + FBmass); + GUIUtils.RefreshAssociatedWindows(part); + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + + var tank = pSym.Current.FindModuleImplementing(); + if (tank == null) continue; + + tank.armoredCockpit = armoredCockpit; + + if (!armoredCockpit) + { + tank.Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_On");//"Enable self-sealing tank" + tank.ArmorMass = 0; + } + else + { + 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); + GUIUtils.RefreshAssociatedWindows(pSym.Current); + } + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + + + [KSPField(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_AddedMass")]//safety systems mass + public float partmass = 0f; + + public float FBmass { get; private set; } = 0f; + public float FISmass { get; private set; } = 0f; + private float ArmorMass = 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)] + public float FireBottles = 0; + + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_FB_Remaining", guiActiveEditor = false), UI_Label(scene = UI_Scene.Flight)] + public float FBRemaining; + + Coroutine firebottleRoutine; + + PartResource fuel; + PartResource monoprop; + PartResource solid; + public bool isOnFire = 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") || part.name.Contains("procedural")) //could add other proc parts here for similar support + { + procPart = true; + } + else + { + if (part.Modules.Contains("ModuleB9PartSwitch")) + { + var B9FuelSwitch = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleB9PartSwitch", "baseVolume"); + if (B9FuelSwitch != null) procPart = true; + } + } + if (HighLogic.LoadedSceneIsEditor) + { + UI_FloatRange FBEditor = (UI_FloatRange)Fields["FireBottles"].uiControlEditor; + FBEditor.onFieldChanged = FBSetup; + } + cockpit = part.FindModuleImplementing(); + if (cockpit != null) + { + if (cockpit.minimumCrew >= 1) + { + Events["TogglecockpitArmor"].guiActiveEditor = true; + Events["ToggleTankOption"].guiActiveEditor = false; + Events["ToggleInertOption"].guiActiveEditor = false; + Fields["FireBottles"].guiActiveEditor = false; + Fields["FBRemaining"].guiActive = false; + FireBottles = 0; + if (!armoredCockpit) + { + Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_On");//"Add Armored Cockpit" + } + else + { + Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_Off");//"Remove Armored Cockpit" + ArmorMass = 0.2f * part.CrewCapacity; + } + } + else part.RemoveModule(this); //don't assign to drone cores + } + 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(); + if (engine != null) + { + Events["ToggleTankOption"].guiActiveEditor = false; + Events["ToggleInertOption"].guiActiveEditor = false; + if (solid != null && engine.throttleLocked && !engine.allowShutdown) //SRB? + { + 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 + } + else + { + Events["ToggleTankOption"].guiActiveEditor = true; //tank with integrated seperatrons? + Events["ToggleInertOption"].guiActiveEditor = true; + InertTank = false; + } + } + } + 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; + Fields["FireBottles"].guiActiveEditor = false; + Fields["FBRemaining"].guiActive = false; + Fields["partmass"].guiActiveEditor = false; + FireBottles = 0; + } + } + if (!SSTank) + { + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" + } + else + { + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" + } + if (!InertTank) + { + Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_On");//"Enable self-sealing tank" + FISmass = 0; + } + else + { + Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_Off");//"Disable self-sealing tank" + FISmass = 0.15f; + Fields["FireBottles"].guiActiveEditor = false; + Fields["FBRemaining"].guiActive = false; + } + 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 && monoprop == null)) part.RemoveModule(this); //PWing with no tank + } + FBSetup(null, null); + //Debug.Log("[BDArmory.SelfSealingTank]: SST: " + SSTank + "; Inerting: " + InertTank + "; armored cockpit: " + armoredCockpit); + } + /* + public override void OnLoad(ConfigNode node) + { + base.OnLoad(node); + + if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) return; + + if (part.partInfo != null) + { + if (HighLogic.LoadedSceneIsEditor) + { + FBSetup(null, null); + } + else + { + if (part.vessel != null) + { + FBSetup(null, null); + var SSTString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleSelfSealingTank", "SSTank"); + if (!string.IsNullOrEmpty(SSTString)) + { + try + { + SSTank = bool.Parse(SSTString); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.ModuleSelfSealingTank]: Exception parsing SSTank: " + e.Message); + } + } + else + { + SSTank = false; + } + var InertString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleSelfSealingTank", "InertTank"); + if (!string.IsNullOrEmpty(InertString)) + { + try + { + InertTank = bool.Parse(InertString); + FISmass = InertTank ? 0.15f : 0; + //partmass += FISmass; + } + catch (Exception e) + { + Debug.LogError("[BDArmory.ModuleSelfSealingTank]: Exception parsing InertTank: " + e.Message); + } + } + else + { + InertTank = false; + FISmass = 0; + } + var cockpitString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleSelfSealingTank", "armoredCockpit"); + if (!string.IsNullOrEmpty(cockpitString)) + { + try + { + armoredCockpit = bool.Parse(InertString); + ArmorMass = armoredCockpit ? 0.2f : 0; + //partmass += ArmorMass; + } + catch (Exception e) + { + Debug.LogError("[BDArmory.ModuleSelfSealingTank]: Exception parsing armoredCockpit: " + e.Message); + } + } + else + { + armoredCockpit = false; + ArmorMass = 0; + } + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + else + { + enabled = false; + } + } + } + } + */ + void FBSetup(BaseField field, object obj) + { + if (externallyCalled) return; + + FBmass = (0.01f * FireBottles); + FBRemaining = FireBottles; + partmass = FBmass + FISmass + ArmorMass; + //part.transform.localScale = (Vector3.one * (origScale + (CASELevel/10))); + //Debug.Log("[BDArmory.ModuleCASE] part.mass = " + part.mass + "; CASElevel = " + CASELevel + "; CASEMass = " + CASEmass + "; Scale = " + part.transform.localScale); + + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + + var tank = pSym.Current.FindModuleImplementing(); + if (tank == null) continue; + tank.externallyCalled = true; + tank.FBmass = FBmass; + tank.FBRemaining = FBRemaining; + tank.partmass = partmass + FISmass + ArmorMass; + tank.externallyCalled = false; + GUIUtils.RefreshAssociatedWindows(pSym.Current); + } + GUIUtils.RefreshAssociatedWindows(part); + } + + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + output.Append(Environment.NewLine); + output.AppendLine($" Can outfit part with Fire Suppression Systems."); //localize this at some point, future me + var engine = part.FindModuleImplementing(); + if (engine == null) + { + output.AppendLine($" Can upgrade to Self-Sealing Tank."); + } + output.AppendLine(""); + + return output.ToString(); + } + public void Extinguishtank() + { + isOnFire = true; + if (FireBottles > 0 || InertTank) //shouldn't be catching fire in the first place if interted, but just in case + { + //if (engine != null && engine.EngineIgnited && engine.allowRestart) + //{ + // engine.Shutdown(); + // enginerestartTime = Time.time; + //} + if (firebottleRoutine == null) + { + if (InertTank) + { + firebottleRoutine = StartCoroutine(ExtinguishRoutine(0, false)); + } + else + { + firebottleRoutine = StartCoroutine(ExtinguishRoutine(4, true)); + } + //Debug.Log("[BDArmory.SelfSealingTank]: Fire detected; beginning ExtinguishRoutine. Firebottles remaining: " + FireBottles); + } + } + else + { + if (engine != null && engine.EngineIgnited && engine.allowRestart) + { + if (part.vessel.verticalSpeed < 30) //not diving/trying to climb. With the vessel registry, could also grab AI state to add a !evading check + { + engine.Shutdown(); + enginerestartTime = Time.time + 10; + if (firebottleRoutine == null) + { + firebottleRoutine = StartCoroutine(ExtinguishRoutine(10, false)); + //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? + } + } + } + IEnumerator ExtinguishRoutine(float time, bool useBottle) + { + //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 + } + if (useBottle) + { + FireBottles--; + FBRemaining = FireBottles; + GUIUtils.RefreshAssociatedWindows(part); + //Debug.Log("[BDArmory.SelfSealingTank]: Consuming firebottle. FB remaining: " + FireBottles); + isOnFire = false; + } + ResetCoroutine(); + } + private void ResetCoroutine() + { + if (firebottleRoutine != null) + { + StopCoroutine(firebottleRoutine); + firebottleRoutine = null; + } + } + private float updateTimer = 0; + void Update() + { + if (HighLogic.LoadedSceneIsEditor) + { + if (procPart) + { + updateTimer -= Time.deltaTime; + if (updateTimer < 0) + { + fuel = part.Resources.Where(pr => pr.resourceName == "LiquidFuel").FirstOrDefault(); + monoprop = part.Resources.Where(pr => pr.resourceName == "MonoPropellant").FirstOrDefault(); + if (fuel != null || monoprop != null) + { + Events["ToggleTankOption"].guiActiveEditor = true; + Events["ToggleInertOption"].guiActiveEditor = fuel != null; //I don't think inerting would work on something containing its own oxidizer... + if (InertTank && !Events["ToggleInertOption"].guiActiveEditor) ToggleInertOption(); // If inerting was somehow enabled previously, but is now not valid, disable it. + if (!InertTank) + { + Fields["FireBottles"].guiActiveEditor = true; + Fields["FBRemaining"].guiActive = true; + } + else + { + Fields["FireBottles"].guiActiveEditor = false; + Fields["FBRemaining"].guiActive = false; + } + Fields["partmass"].guiActiveEditor = true; + } + else + { + Events["ToggleTankOption"].guiActiveEditor = false; + Events["ToggleInertOption"].guiActiveEditor = false; + Fields["FireBottles"].guiActiveEditor = false; + Fields["FBRemaining"].guiActive = false; + 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 + } + } + } + } + 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) + { + if (InertTank) return; + if (!isOnFire) + { + 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(); + if (vesselFire != null) + { + fireStarter = vesselFire.SourceVessel; + } + else + { + fireStarter = part.vessel.GetName(); + } + BulletHitFX.AttachFire(transform.position, part, 50, fireStarter); + 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; + } + } + if (engine != null) + { + if (enginerestartTime > 0 && (Time.time > enginerestartTime)) + { + enginerestartTime = -1; + engine.Activate(); + } + } + } + } + } +} 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/ModuleWeapon.cs b/BDArmory/Modules/ModuleWeapon.cs deleted file mode 100644 index 9c24fc42c..000000000 --- a/BDArmory/Modules/ModuleWeapon.cs +++ /dev/null @@ -1,3552 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using BDArmory.Bullets; -using BDArmory.Competition; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.FX; -using BDArmory.Misc; -using BDArmory.Targeting; -using BDArmory.UI; -using KSP.UI.Screens; -using KSP.Localization; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class ModuleWeapon : EngageableWeapon, IBDWeapon - { - #region Declarations - - public static ObjectPool bulletPool; - - public static Dictionary rocketPool = new Dictionary(); //for ammo switching - public static ObjectPool shellPool; - - Coroutine startupRoutine; - Coroutine shutdownRoutine; - - bool finalFire; - - public int rippleIndex = 0; - public string OriginalShortName { get; private set; } - - // WeaponTypes.Cannon is deprecated. identical behavior is achieved with WeaponType.Ballistic and bulletInfo.explosive = true. - public enum WeaponTypes - { - Ballistic, - Rocket, //Cannon's depreciated, lets use this for rocketlaunchers - Laser - } - - public enum WeaponStates - { - Enabled, - Disabled, - PoweringUp, - PoweringDown, - Locked - } - - public enum BulletDragTypes - { - None, - AnalyticEstimate, - NumericalIntegration - } - - public WeaponStates weaponState = WeaponStates.Disabled; - - //animations - private float fireAnimSpeed = 1; - //is set when setting up animation so it plays a full animation for each shot (animation speed depends on rate of fire) - - public float bulletBallisticCoefficient; - - public WeaponTypes eWeaponType; - - public float heat; - public bool isOverheated; - - private bool wasFiring; - //used for knowing when to stop looped audio clip (when you're not shooting, but you were) - - AudioClip reloadCompleteAudioClip; - AudioClip fireSound; - AudioClip overheatSound; - AudioClip chargeSound; - AudioSource audioSource; - AudioSource audioSource2; - AudioLowPassFilter lowpassFilter; - - private BDStagingAreaGauge gauge; - private int AmmoID; - - //AI - public bool aiControlled = false; - public bool autoFire; - public float autoFireLength = 0; - public float autoFireTimer = 0; - - //used by AI to lead moving targets - private float targetDistance; - private Vector3 targetPosition; - private Vector3 targetVelocity; // local frame velocity - private Vector3 targetAcceleration; // local frame - private Vector3 targetVelocityPrevious; // for acceleration calculation - private Vector3 targetAccelerationPrevious; - private Vector3 relativeVelocity; - public Vector3 finalAimTarget; - Vector3 lastFinalAimTarget; - public Vessel visualTargetVessel; - bool targetAcquired; - - 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 - { - get { return Time.time - timeFired < 1; } - } - - //used to reduce volume of audio if multiple guns are being fired (needs to be improved/changed) - //private int numberOfGuns = 0; - - //AI will fire gun if target is within this Cos(angle) of barrel - public float maxAutoFireCosAngle = 0.9993908f; //corresponds to ~2 degrees - - //aimer textures - Vector3 pointingAtPosition; - Vector3 bulletPrediction; - Vector3 fixedLeadOffset = Vector3.zero; - - float predictedFlightTime = 1; //for rockets - Vector3 trajectoryOffset = Vector3.zero; - - //gapless particles - List gaplessEmitters = new List(); - - //muzzleflash emitters - List muzzleFlashEmitters; - - //module references - [KSPField] public int turretID = 0; - public ModuleTurret turret; - MissileFire mf; - - public MissileFire weaponManager - { - get - { - if (mf) return mf; - List.Enumerator wm = vessel.FindPartModulesImplementing().GetEnumerator(); - while (wm.MoveNext()) - { - if (wm.Current == null) continue; - mf = wm.Current; - break; - } - wm.Dispose(); - return mf; - } - } - - bool pointingAtSelf; //true if weapon is pointing at own vessel - bool userFiring; - Vector3 laserPoint; - public bool slaved; - - public Transform turretBaseTransform - { - get - { - if (turret) - { - return turret.yawTransform.parent; - } - else - { - return fireTransforms[0]; - } - } - } - - public float maxPitch - { - get { return turret ? turret.maxPitch : 0; } - } - - public float minPitch - { - get { return turret ? turret.minPitch : 0; } - } - - public float yawRange - { - get { return turret ? turret.yawRange : 0; } - } - - //weapon interface - public WeaponClasses GetWeaponClass() - { - if (eWeaponType == WeaponTypes.Ballistic) - { - return WeaponClasses.Gun; - } - else if (eWeaponType == WeaponTypes.Rocket) - { - return WeaponClasses.Rocket; - } - else - { - return WeaponClasses.DefenseLaser; - } - } - - public Part GetPart() - { - return part; - } - - public double ammoCount; - public string ammoLeft; //#191 - - public string GetSubLabel() //think BDArmorySetup only calls this for the first instance of a particular ShortName, so this probably won't result in a group of n guns having n GetSublabelCalls per frame - { - using (List.Enumerator craftPart = vessel.parts.GetEnumerator()) - { - ammoLeft = "Ammo Left: " + ammoCount.ToString("0"); - int lastAmmoID = this.AmmoID; - using (List.Enumerator weapon = vessel.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - if (weapon.Current.GetShortName() != this.GetShortName()) continue; - 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"); - lastAmmoID = weapon.Current.AmmoID; - } - } - } - return ammoLeft; - } - public string GetMissileType() - { - return string.Empty; - } - -#if DEBUG - Vector3 relVelAdj; - Vector3 accAdj; - Vector3 gravAdj; -#endif - - #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 WeaponName; - - [KSPField] - public string fireTransformName = "fireTransform"; - public Transform[] fireTransforms; - - [KSPField] - public string shellEjectTransformName = "shellEject"; - public Transform[] shellEjectTransforms; - - [KSPField] - public bool hasDeployAnim = false; - - [KSPField] - public string deployAnimName = "deployAnim"; - AnimationState deployState; - - [KSPField] - public bool hasFireAnimation = false; - - [KSPField] - public string fireAnimName = "fireAnim"; - private AnimationState fireState; - - [KSPField] - public bool spinDownAnimation = false; - private bool spinningDown; - - //weapon specifications - [KSPField] - public float maxTargetingRange = 2000; //max range for raycasting and sighting - - [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 - - [KSPField] - public float maxDeviation = 1; //inaccuracy two standard deviations in degrees (two because backwards compatibility :) - - [KSPField] - public float maxEffectiveDistance = 2500; //used by AI to select appropriate weapon - - [KSPField] - public float bulletMass = 0.3880f; //mass in KG - used for damage and recoil and drag - - [KSPField] - public float caliber = 30; //caliber in mm, used for penetration calcs - - [KSPField] - public float bulletDmgMult = 1; //Used for heat damage modifier for non-explosive bullets - - [KSPField] - public float bulletVelocity = 1030; //velocity in meters/second - - [KSPField] - public float ECPerShot = 0; //EC to use per shot for weapons like railguns - - public int ProjectileCount = 1; - - [KSPField] - public bool BeltFed = true; //draws from an ammo bin; default behavior - - [KSPField] - public int RoundsPerMag = 1; //For weapons fed from clips/mags. left at one as sanity check, incase this not set if !BeltFed - public int RoundsRemaining = 0; - public bool isReloading; - - [KSPField] - public bool crewserved = false; //does the weapon need a gunner? - public bool hasGunner = true; //if so, are they present? - private KerbalSeat gunnerSeat; - private bool gunnerSeatLookedFor = false; - - [KSPField] - public float ReloadTime = 10; - public float ReloadTimer = 0; - - [KSPField] - public bool BurstFire = false; // set to true for weapons that fire multiple times per triggerpull - - [KSPField] - public string bulletDragTypeName = "AnalyticEstimate"; - public BulletDragTypes bulletDragType; - - //drag area of the bullet in m^2; equal to Cd * A with A being the frontal area of the bullet; as a first approximation, take Cd to be 0.3 - //bullet mass / bullet drag area. Used in analytic estimate to speed up code - [KSPField] - public float bulletDragArea = 1.209675e-5f; - - private BulletInfo bulletInfo; - - [KSPField] - public string bulletType = "def"; - - public string currentType = "def"; - - [KSPField] - public string ammoName = "50CalAmmo"; //resource usage - - [KSPField] - public float requestResourceAmount = 1; //amount of resource/ammo to deplete per shot - - [KSPField] - public float shellScale = 0.66f; //scale of shell to eject - - [KSPField] - public bool hasRecoil = true; - - [KSPField] - public float recoilReduction = 1; //for reducing recoil on large guns with built in compensation - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FireLimits"),//Fire Limits - UI_Toggle(disabledText = "#LOC_BDArmory_FireLimits_disabledText", enabledText = "#LOC_BDArmory_FireLimits_enabledText")]//None--In range - public bool onlyFireInRange = true; - //prevent firing when gun's turret is trying to exceed gimbal limits - - [KSPField] - public bool bulletDrop = true; //projectiles are affected by gravity - - [KSPField] - public string weaponType = "ballistic"; - //ballistic, cannon or laser - - //laser info - [KSPField] - public float laserDamage = 10000; //base damage/second of lasers - [KSPField] public bool pulseLaser = false; //pulse vs beam - [KSPField] public bool HEpulses = false; //do the pulses have blast damage - [KSPField] public bool HeatRay = false; //conic AoE - [KSPField] public bool electroLaser = false; //Drains EC from target/induces EMP effects - float beamDuration = 0.1f; // duration of pulselaser beamFX - float beamScoreTime = 0.2f; //frequency of score accumulation for beam lasers, currently 5x/sec - float BeamTracker = 0; // timer for scoring shots fired for beams - float ScoreAccumulator = 0; //timer for scoring shots hit for beams - LineRenderer[] laserRenderers; - - public string rocketModelPath; - public float rocketMass = 1; - public float thrust = 1; - public float thrustTime = 1; - public float blastRadius = 1; - public bool descendingOrder = true; - public float thrustDeviation = 0.10f; - [KSPField] public bool rocketPod = true; //is the RL a rocketpod, or a gyrojet gun? - [KSPField] public bool externalAmmo = false; //used for rocketlaunchers that are Gyrojet guns drawing from ammoboxes instead of internals - Transform[] rockets; - double rocketsMax; - private RocketInfo rocketInfo; - - public float tntMass = 0; - - //deprectated - //[KSPField] public float cannonShellRadius = 30; //max radius of explosion forces/damage - //[KSPField] public float cannonShellPower = 8; //explosion's impulse force - //[KSPField] public float cannonShellHeat = -1; //if non-negative, heat damage - - //projectile graphics - [KSPField] - public string projectileColor = "255, 130, 0, 255"; //final color of projectile; left public for lasers - Color projectileColorC; - - [KSPField] - public bool fadeColor = false; - - [KSPField] - public string startColor = "255, 160, 0, 200"; - //if fade color is true, projectile starts at this color - - Color startColorC; - - [KSPField] - public float tracerStartWidth = 0.25f; //set from bulletdefs, left for lasers - - [KSPField] - public float tracerEndWidth = 0.2f; - - [KSPField] - public float tracerLength = 0; - //if set to zero, tracer will be the length of the distance covered by the projectile in one physics timestep - - [KSPField] - public float tracerDeltaFactor = 2.65f; - - [KSPField] - public float nonTracerWidth = 0.01f; - - [KSPField] - public int tracerInterval = 0; - - [KSPField] - public float tracerLuminance = 1.75f; - int tracerIntervalCounter; - - [KSPField] - public string bulletTexturePath = "BDArmory/Textures/bullet"; - - [KSPField] - public string laserTexturePath = "BDArmory/Textures/laser"; - - [KSPField] - public bool oneShotWorldParticles = false; - - //heat - [KSPField] - public float maxHeat = 3600; - - [KSPField] - public float heatPerShot = 75; - - [KSPField] - public float heatLoss = 250; - - //canon explosion effects - [KSPField] - public string explModelPath = "BDArmory/Models/explosion/explosion"; - - [KSPField] - public string explSoundPath = "BDArmory/Sounds/explode1"; - - //Used for scaling laser damage down based on distance. - [KSPField] - public float tanAngle = 0.0001f; - //Angle of divergeance/2. Theoretical minimum value calculated using θ = (1.22 L/RL)/2, - //where L is laser's wavelength and RL is the radius of the mirror (=gun). - - //audioclip paths - [KSPField] - public string fireSoundPath = "BDArmory/Parts/50CalTurret/sounds/shot"; - - [KSPField] - public string overheatSoundPath = "BDArmory/Parts/50CalTurret/sounds/turretOverheat"; - - [KSPField] - public string chargeSoundPath = "BDArmory/Parts/laserTest/sounds/charge"; - - //audio - [KSPField] - public bool oneShotSound = true; - //play audioclip on every shot, instead of playing looping audio while firing - - [KSPField] - public float soundRepeatTime = 1; - //looped audio will loop back to this time (used for not playing the opening bit, eg the ramp up in pitch of gatling guns) - - [KSPField] - public string reloadAudioPath = string.Empty; - AudioClip reloadAudioClip; - - [KSPField] - public string reloadCompletePath = string.Empty; - - [KSPField] - public bool showReloadMeter = false; //used for cannons or guns with extremely low rate of fire - - //Air Detonating Rounds - [KSPField] - public bool airDetonation = false; - - [KSPField] - public bool proximityDetonation = false; - - [KSPField] - public bool airDetonationTiming = true; - - [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_DefaultDetonationRange", guiActiveEditor = false)]//Fuzed Detonation Range - public float defaultDetonationRange = 3500; // maxairDetrange works for altitude fuzing, use this for VT fuzing - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ProximityFuzeRadius"), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Proximity Fuze Radius - public float detonationRange = -1f; // give ability to set proximity range - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxDetonationRange"),//Max Detonation Range - UI_FloatRange(minValue = 500, maxValue = 8000f, stepIncrement = 5f, scene = UI_Scene.All)] - public float maxAirDetonationRange = 3500; // could probably get rid of this entirely, max engagement range more or less already does this - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Ammo_Type"),//Ammunition Types - UI_FloatRange(minValue = 1, maxValue = 999, stepIncrement = 1, scene = UI_Scene.All)] - public float AmmoTypeNum = 1; - - [KSPField(isPersistant = true)] - public string SelectedAmmoType; //presumably Aubranium can use this to filter allowed/banned ammotypes - - public List ammoList; - - [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Ammo_LoadedAmmo")]//Status - public string guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Slug"); - - [KSPField(isPersistant = true)] - private bool canHotSwap = false; //for select weapons that it makes sense to be able to swap ammo types while in-flight, like the Abrams turret - - //auto proximity tracking - [KSPField] - public float autoProxyTrackRange = 0; - bool atprAcquired; - int aptrTicker; - - float timeFired; - public float initialFireDelay = 0; //used to ripple fire multiple weapons of this type - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Barrage")]//Barrage - public bool - useRippleFire = true; - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ToggleBarrage")]//Toggle Barrage - public void ToggleRipple() - { - List.Enumerator craftPart = EditorLogic.fetch.ship.parts.GetEnumerator(); - while (craftPart.MoveNext()) - { - if (craftPart.Current == null) continue; - if (craftPart.Current.name != part.name) continue; - List.Enumerator weapon = craftPart.Current.FindModulesImplementing().GetEnumerator(); - while (weapon.MoveNext()) - { - if (weapon.Current == null) continue; - weapon.Current.useRippleFire = !weapon.Current.useRippleFire; - } - weapon.Dispose(); - } - craftPart.Dispose(); - } - - IEnumerator IncrementRippleIndex(float delay) - { - if (delay > 0) - { - yield return new WaitForSeconds(delay); - } - weaponManager.gunRippleIndex = weaponManager.gunRippleIndex + 1; - - //Debug.Log("incrementing ripple index to: " + weaponManager.gunRippleIndex); - } - - #endregion KSPFields - - #region KSPActions - - [KSPAction("Toggle Weapon")] - public void AGToggle(KSPActionParam param) - { - Toggle(); - } - - [KSPField(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_Status")]//Status - public string guiStatusString = - "Disabled"; - - //PartWindow buttons - [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_Toggle")]//Toggle - public void Toggle() - { - if (weaponState == WeaponStates.Disabled || weaponState == WeaponStates.PoweringDown) - { - EnableWeapon(); - } - else - { - DisableWeapon(); - } - } - - bool agHoldFiring; - - [KSPAction("Fire (Toggle)")] - public void AGFireToggle(KSPActionParam param) - { - agHoldFiring = (param.type == KSPActionType.Activate); - } - - [KSPAction("Fire (Hold)")] - public void AGFireHold(KSPActionParam param) - { - StartCoroutine(FireHoldRoutine(param.group)); - } - - IEnumerator FireHoldRoutine(KSPActionGroup group) - { - KeyBinding key = Misc.Misc.AGEnumToKeybinding(group); - if (key == null) - { - yield break; - } - - while (key.GetKey()) - { - agHoldFiring = true; - yield return null; - } - - agHoldFiring = false; - yield break; - } - [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_Jettison", active = true, guiActiveEditor = false)]//Jettison - public void Jettison() // make rocketpods jettisonable - { - if ((turret || eWeaponType != WeaponTypes.Rocket) || (eWeaponType == WeaponTypes.Rocket && (!rocketPod || (rocketPod && externalAmmo)))) - { - return; - } - part.decouple(0); - if (BDArmorySetup.Instance.ActiveWeaponManager != null) - BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); - } - #endregion KSPActions - - #region KSP Events - - public override void OnAwake() - { - base.OnAwake(); - - part.stagingIconAlwaysShown = true; - this.part.stackIconGrouping = StackIconGrouping.SAME_TYPE; - } - - public void Start() - { - part.stagingIconAlwaysShown = true; - this.part.stackIconGrouping = StackIconGrouping.SAME_TYPE; - - Events["HideUI"].active = false; - Events["ShowUI"].active = true; - ParseWeaponType(); - - // extension for feature_engagementenvelope - InitializeEngagementRange(0, maxEffectiveDistance); - if (string.IsNullOrEmpty(GetShortName())) - { - shortName = part.partInfo.title; - } - OriginalShortName = shortName; - WeaponName = shortName; - IEnumerator emitter = part.FindModelComponents().AsEnumerable().GetEnumerator(); - while (emitter.MoveNext()) - { - if (emitter.Current == null) continue; - emitter.Current.emit = false; - EffectBehaviour.AddParticleEmitter(emitter.Current); - } - emitter.Dispose(); - - if (roundsPerMinute >= 1500 || (eWeaponType == WeaponTypes.Laser && !pulseLaser)) - { - Events["ToggleRipple"].guiActiveEditor = false; - Fields["useRippleFire"].guiActiveEditor = false; - } - - if (eWeaponType != WeaponTypes.Rocket)//disable rocket RoF slider for non rockets - { - Fields["roundsPerMinute"].guiActiveEditor = false; - } - - int typecount = 0; - ammoList = BDAcTools.ParseNames(bulletType); - for (int i = 0; i < ammoList.Count; i++) - { - typecount++; - } - if (ammoList.Count > 1) - { - if (!canHotSwap) - { - Fields["AmmoTypeNum"].guiActive = false; - } - UI_FloatRange ATrangeEditor = (UI_FloatRange)Fields["AmmoTypeNum"].uiControlEditor; - ATrangeEditor.maxValue = (float)typecount; - ATrangeEditor.onFieldChanged = SetupAmmo; - UI_FloatRange ATrangeFlight = (UI_FloatRange)Fields["AmmoTypeNum"].uiControlFlight; - ATrangeFlight.maxValue = (float)typecount; - ATrangeFlight.onFieldChanged = SetupAmmo; - } - else //disable ammo selector - { - Fields["AmmoTypeNum"].guiActive = false; - Fields["AmmoTypeNum"].guiActiveEditor = false; - } - - vessel.Velocity(); - if (BurstFire) - { - BeltFed = false; - } - if (eWeaponType == WeaponTypes.Ballistic) - { - if (airDetonation) - { - UI_FloatRange detRange = (UI_FloatRange)Fields["maxAirDetonationRange"].uiControlEditor; - detRange.maxValue = maxEffectiveDistance; //altitude fuzing clamped to max range - } - else //disable fuze GUI elements on un-fuzed munitions - { - Fields["maxAirDetonationRange"].guiActive = false; - Fields["maxAirDetonationRange"].guiActiveEditor = false; - Fields["defaultDetonationRange"].guiActive = false; - Fields["defaultDetonationRange"].guiActiveEditor = false; - Fields["detonationRange"].guiActive = false; - Fields["detonationRange"].guiActiveEditor = false; - } - } - if (eWeaponType == WeaponTypes.Rocket) - { - if (rocketPod && externalAmmo) - { - BeltFed = false; - } - if (!rocketPod) - { - externalAmmo = true; - } - } - if (eWeaponType == WeaponTypes.Laser) - { - if (!pulseLaser) - { - roundsPerMinute = 3000; //50 rounds/sec or 1 'round'/FixedUpdate - } - if (HEpulses) - { - pulseLaser = true; - HeatRay = false; - } - if (HeatRay) - { - HEpulses = false; - electroLaser = false; - } - //disable fuze GUI elements - Fields["maxAirDetonationRange"].guiActive = false; - Fields["maxAirDetonationRange"].guiActiveEditor = false; - Fields["defaultDetonationRange"].guiActive = false; - Fields["defaultDetonationRange"].guiActiveEditor = false; - Fields["detonationRange"].guiActive = false; - Fields["detonationRange"].guiActiveEditor = false; - Fields["guiAmmoTypeString"].guiActiveEditor = false; //ammoswap - Fields["guiAmmoTypeString"].guiActive = false; - - } - muzzleFlashEmitters = new List(); - IEnumerator mtf = part.FindModelTransforms("muzzleTransform").AsEnumerable().GetEnumerator(); - while (mtf.MoveNext()) - { - if (mtf.Current == null) continue; - KSPParticleEmitter kpe = mtf.Current.GetComponent(); - EffectBehaviour.AddParticleEmitter(kpe); - muzzleFlashEmitters.Add(kpe); - kpe.emit = false; - } - mtf.Dispose(); - - if (HighLogic.LoadedSceneIsFlight) - { - if (eWeaponType == WeaponTypes.Ballistic) - { - if (bulletPool == null) - { - SetupBulletPool(); - } - if (shellPool == null) - { - SetupShellPool(); - } - } - if (eWeaponType == WeaponTypes.Rocket) - { - if (rocketPod)// only call these for rocket pods - { - MakeRocketArray(); - UpdateRocketScales(); - } - else - { - if (shellPool == null) - { - SetupShellPool(); - } - } - } - - //setup transforms - fireTransforms = part.FindModelTransforms(fireTransformName); - shellEjectTransforms = part.FindModelTransforms(shellEjectTransformName); - - //setup emitters - IEnumerator pe = part.FindModelComponents().AsEnumerable().GetEnumerator(); - while (pe.MoveNext()) - { - if (pe.Current == null) continue; - pe.Current.maxSize *= part.rescaleFactor; - pe.Current.minSize *= part.rescaleFactor; - pe.Current.shape3D *= part.rescaleFactor; - pe.Current.shape2D *= part.rescaleFactor; - pe.Current.shape1D *= part.rescaleFactor; - - if (pe.Current.useWorldSpace && !oneShotWorldParticles) - { - BDAGaplessParticleEmitter gpe = pe.Current.gameObject.AddComponent(); - gpe.part = part; - gaplessEmitters.Add(gpe); - } - else - { - EffectBehaviour.AddParticleEmitter(pe.Current); - } - } - pe.Dispose(); - - //setup projectile colors - projectileColorC = Misc.Misc.ParseColor255(projectileColor); - startColorC = Misc.Misc.ParseColor255(startColor); - - //init and zero points - targetPosition = Vector3.zero; - pointingAtPosition = Vector3.zero; - bulletPrediction = Vector3.zero; - - //setup audio - SetupAudio(); - - // 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; - - //laser setup - if (eWeaponType == WeaponTypes.Laser) - { - SetupLaserSpecifics(); - if (maxTargetingRange < maxEffectiveDistance) - { - maxEffectiveDistance = maxTargetingRange; - } - } - if (crewserved) - { - CheckCrewed(); - } - - if (ammoList.Count > 1) - { - UI_FloatRange ATrangeFlight = (UI_FloatRange)Fields["AmmoTypeNum"].uiControlFlight; - ATrangeFlight.maxValue = (float)typecount; - if (!canHotSwap) - { - Fields["AmmoTypeNum"].guiActive = false; - } - } - } - else if (HighLogic.LoadedSceneIsEditor) - { - fireTransforms = part.FindModelTransforms(fireTransformName); - WeaponNameWindow.OnActionGroupEditorOpened.Add(OnActionGroupEditorOpened); - WeaponNameWindow.OnActionGroupEditorClosed.Add(OnActionGroupEditorClosed); - } - //turret setup - List.Enumerator turr = part.FindModulesImplementing().GetEnumerator(); - while (turr.MoveNext()) - { - if (turr.Current == null) continue; - if (turr.Current.turretID != turretID) continue; - turret = turr.Current; - turret.SetReferenceTransform(fireTransforms[0]); - break; - } - turr.Dispose(); - - if (!turret) - { - Fields["onlyFireInRange"].guiActive = false; - Fields["onlyFireInRange"].guiActiveEditor = false; - } - if (HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight) - { - if ((turret || eWeaponType != WeaponTypes.Rocket) || (eWeaponType == WeaponTypes.Rocket && (!rocketPod || (rocketPod && externalAmmo)))) - { - Events["Jettison"].guiActive = false; - } - } - //setup animations - if (hasDeployAnim) - { - deployState = Misc.Misc.SetUpSingleAnimation(deployAnimName, part); - deployState.normalizedTime = 0; - deployState.speed = 0; - deployState.enabled = true; - } - if (hasFireAnimation) - { - fireState = Misc.Misc.SetUpSingleAnimation(fireAnimName, part); - fireState.enabled = false; - } - - if (eWeaponType != WeaponTypes.Laser) - { - SetupAmmo(null, null); - - if (eWeaponType == WeaponTypes.Rocket) - { - if (rocketInfo == null) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Failed To load rocket : " + currentType); - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: AmmoType Loaded : " + currentType); - } - } - else - { - if (bulletInfo == null) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: Failed To load bullet : " + currentType); - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory]: BulletType Loaded : " + currentType); - } - } - } - - BDArmorySetup.OnVolumeChange += UpdateVolume; - } - - void OnDestroy() - { - if (muzzleFlashEmitters != null) - foreach (var pe in muzzleFlashEmitters) - if (pe) EffectBehaviour.RemoveParticleEmitter(pe); - foreach (var pe in part.FindModelComponents()) - if (pe) EffectBehaviour.RemoveParticleEmitter(pe); - BDArmorySetup.OnVolumeChange -= UpdateVolume; - WeaponNameWindow.OnActionGroupEditorOpened.Remove(OnActionGroupEditorOpened); - WeaponNameWindow.OnActionGroupEditorClosed.Remove(OnActionGroupEditorClosed); - } - public void PAWRefresh() - { - if (!proximityDetonation) - { - Fields["maxAirDetonationRange"].guiActive = false; - Fields["maxAirDetonationRange"].guiActiveEditor = false; - Fields["defaultDetonationRange"].guiActive = false; - Fields["defaultDetonationRange"].guiActiveEditor = false; - Fields["detonationRange"].guiActive = false; - Fields["detonationRange"].guiActiveEditor = false; - } - else - { - Fields["maxAirDetonationRange"].guiActive = true; - Fields["maxAirDetonationRange"].guiActiveEditor = true; - Fields["defaultDetonationRange"].guiActive = true; - Fields["defaultDetonationRange"].guiActiveEditor = true; - Fields["detonationRange"].guiActive = true; - Fields["detonationRange"].guiActiveEditor = true; - } - Misc.Misc.RefreshAssociatedWindows(part); - } - void Update() - { - if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && vessel.IsControllable) - { - if (lowpassFilter) - { - if (InternalCamera.Instance && InternalCamera.Instance.isActive) - { - lowpassFilter.enabled = true; - } - else - { - lowpassFilter.enabled = false; - } - } - - if (weaponState == WeaponStates.Enabled && - (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 || autoFire || agHoldFiring) && - (yawRange == 0 || (maxPitch - minPitch) == 0 || - turret.TargetInRange(finalAimTarget, 10, float.MaxValue))) - { - if (useRippleFire && ((pointingAtSelf || isOverheated || isReloading) || (aiControlled && engageRangeMax < targetDistance)))// is weapon within set max range? - { - StartCoroutine(IncrementRippleIndex(0)); - finalFire = false; - } - else if (eWeaponType == WeaponTypes.Ballistic || eWeaponType == WeaponTypes.Rocket) //WeaponTypes.Cannon is deprecated - { - finalFire = true; - } - } - else - { - if (spinDownAnimation) spinningDown = true; - if (!oneShotSound && wasFiring) - { - audioSource.Stop(); - wasFiring = false; - audioSource2.PlayOneShot(overheatSound); - } - } - } - else - { - audioSource.Stop(); - autoFire = false; - } - - if (spinningDown && spinDownAnimation && hasFireAnimation) - { - if (fireState.normalizedTime > 1) fireState.normalizedTime = 0; - fireState.speed = fireAnimSpeed; - fireAnimSpeed = Mathf.Lerp(fireAnimSpeed, 0, 0.04f); - } - - // Draw gauges - if (vessel.isActiveVessel) - { - vessel.GetConnectedResourceTotals(AmmoID, out double ammoCurrent, out double ammoMax); - gauge.UpdateAmmoMeter((float)(ammoCurrent / ammoMax)); - - ammoCount = ammoCurrent; - if (showReloadMeter) - { - if (isReloading) - { - gauge.UpdateReloadMeter(ReloadTimer); - } - else - { - gauge.UpdateReloadMeter((Time.time - timeFired) * roundsPerMinute / 60); - } - } - gauge.UpdateHeatMeter(heat / maxHeat); - } - } - } - - void FixedUpdate() - { - if (HighLogic.LoadedSceneIsFlight && !vessel.packed) - { - if (!vessel.IsControllable) - { - if (weaponState != WeaponStates.PoweringDown || weaponState != WeaponStates.Disabled) - { - DisableWeapon(); - } - return; - } - - UpdateHeat(); - if (weaponState == WeaponStates.Enabled && - (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) - { - //Aim(); - StartCoroutine(AimAndFireAtEndOfFrame()); - - if (eWeaponType == WeaponTypes.Laser) - { - if ((userFiring || autoFire || agHoldFiring) && - (!turret || turret.TargetInRange(targetPosition, 10, float.MaxValue))) - { - if (useRippleFire && (aiControlled && engageRangeMax < targetDistance))// is weapon within set max range? - { - StartCoroutine(IncrementRippleIndex(0)); - finalFire = false; - } - else - { - finalFire = 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; - } - } - if (!pulseLaser || !oneShotSound) - { - audioSource.Stop(); - } - } - } - } - else if (eWeaponType == WeaponTypes.Laser) - { - for (int i = 0; i < laserRenderers.Length; i++) - { - laserRenderers[i].enabled = false; - } - audioSource.Stop(); - } - - if (!BeltFed) - { - ReloadWeapon(); - } - if (crewserved) - { - CheckCrewed(); - } - } - lastFinalAimTarget = finalAimTarget; - } - - private void UpdateMenus(bool visible) - { - Events["HideUI"].active = visible; - Events["ShowUI"].active = !visible; - } - - private void OnActionGroupEditorOpened() - { - Events["HideUI"].active = false; - Events["ShowUI"].active = false; - } - - private void OnActionGroupEditorClosed() - { - Events["HideUI"].active = false; - Events["ShowUI"].active = true; - } - - [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_HideWeaponGroupUI", active = false)]//Hide Weapon Group UI - public void HideUI() - { - WeaponGroupWindow.HideGUI(); - UpdateMenus(false); - } - - [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_SetWeaponGroupUI", active = false)]//Set Weapon Group UI - public void ShowUI() - { - WeaponGroupWindow.ShowGUI(this); - UpdateMenus(true); - } - - void OnGUI() - { - if (HighLogic.LoadedSceneIsFlight && weaponState == WeaponStates.Enabled && vessel && !vessel.packed && vessel.isActiveVessel && - BDArmorySettings.DRAW_AIMERS && !aiControlled && !MapView.MapIsEnabled && !pointingAtSelf) - { - float size = 30; - - Vector3 reticlePosition; - if (BDArmorySettings.AIM_ASSIST) - { - if (targetAcquired && (slaved || yawRange < 1 || maxPitch - minPitch < 1)) - { - reticlePosition = pointingAtPosition + fixedLeadOffset; - - if (!slaved) - { - BDGUIUtils.DrawLineBetweenWorldPositions(pointingAtPosition, reticlePosition, 2, - new Color(0, 1, 0, 0.6f)); - } - - BDGUIUtils.DrawTextureOnWorldPos(pointingAtPosition, BDArmorySetup.Instance.greenDotTexture, - new Vector2(6, 6), 0); - - if (atprAcquired) - { - BDGUIUtils.DrawTextureOnWorldPos(targetPosition, BDArmorySetup.Instance.openGreenSquare, - new Vector2(20, 20), 0); - } - } - else - { - reticlePosition = bulletPrediction; - } - } - else - { - reticlePosition = pointingAtPosition; - } - - Texture2D texture; - if (Vector3.Angle(pointingAtPosition - transform.position, finalAimTarget - transform.position) < 1f) - { - texture = BDArmorySetup.Instance.greenSpikedPointCircleTexture; - } - else - { - texture = BDArmorySetup.Instance.greenPointCircleTexture; - } - BDGUIUtils.DrawTextureOnWorldPos(reticlePosition, texture, new Vector2(size, size), 0); - - if (BDArmorySettings.DRAW_DEBUG_LINES) - { - if (targetAcquired) - { - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, targetPosition, 2, - Color.blue); - } - } - } - - if (HighLogic.LoadedSceneIsEditor && BDArmorySetup.showWeaponAlignment) - { - DrawAlignmentIndicator(); - } - -#if DEBUG - if (BDArmorySettings.DRAW_DEBUG_LINES && weaponState == WeaponStates.Enabled && vessel && !vessel.packed && !MapView.MapIsEnabled) - { - BDGUIUtils.MarkPosition(targetPosition, transform, Color.cyan); - BDGUIUtils.DrawLineBetweenWorldPositions(targetPosition, targetPosition + relVelAdj, 2, Color.green); - BDGUIUtils.DrawLineBetweenWorldPositions(targetPosition + relVelAdj, targetPosition + relVelAdj + accAdj, 2, Color.magenta); - BDGUIUtils.DrawLineBetweenWorldPositions(targetPosition + relVelAdj + accAdj, targetPosition + relVelAdj + accAdj + gravAdj, 2, Color.yellow); - BDGUIUtils.MarkPosition(finalAimTarget, transform, Color.cyan, size: 4); - } -#endif - } - - #endregion KSP Events - //some code organization - //Ballistics - #region Guns - private void Fire() - { - if (BDArmorySetup.GameIsPaused) - { - if (audioSource.isPlaying) - { - audioSource.Stop(); - } - return; - } - - float timeGap = (60 / roundsPerMinute) * TimeWarp.CurrentRate; - if (Time.time - timeFired > timeGap - && !isOverheated - && !isReloading - && !pointingAtSelf - && (aiControlled || !Misc.Misc.CheckMouseIsOnGui()) - && WMgrAuthorized()) - { - bool effectsShot = false; - //Transform[] fireTransforms = part.FindModelTransforms("fireTransform"); - for (float iTime = Mathf.Min(Time.time - timeFired - timeGap, TimeWarp.fixedDeltaTime); iTime >= 0; iTime -= timeGap) - for (int i = 0; i < fireTransforms.Length; i++) - { - if (CanFire(requestResourceAmount)) - { - Transform fireTransform = fireTransforms[i]; - spinningDown = false; - - //recoil - if (hasRecoil) - { - part.rb.AddForceAtPosition((-fireTransform.forward) * (bulletVelocity * (bulletMass * ProjectileCount) / 1000 * BDArmorySettings.RECOIL_FACTOR * recoilReduction), - fireTransform.position, ForceMode.Impulse); - } - - if (!effectsShot) - { - WeaponFX(); - effectsShot = true; - } - - //firing bullet - for (int s = 0; s < ProjectileCount; s++) - { - GameObject firedBullet = bulletPool.GetPooledObject(); - PooledBullet pBullet = firedBullet.GetComponent(); - - - firedBullet.transform.position = fireTransform.position; - - pBullet.caliber = bulletInfo.caliber; - pBullet.bulletVelocity = bulletInfo.bulletVelocity; - pBullet.bulletMass = bulletInfo.bulletMass; - pBullet.explosive = bulletInfo.explosive; - pBullet.apBulletMod = bulletInfo.apBulletMod; - pBullet.bulletDmgMult = bulletDmgMult; - - //A = π x (Ø / 2)^2 - bulletDragArea = Mathf.PI * Mathf.Pow(caliber / 2f, 2f); - - //Bc = m/Cd * A - bulletBallisticCoefficient = bulletMass / ((bulletDragArea / 1000000f) * 0.295f); // mm^2 to m^2 - - //Bc = m/d^2 * i where i = 0.484 - //bulletBallisticCoefficient = bulletMass / Mathf.Pow(caliber / 1000, 2f) * 0.484f; - - pBullet.ballisticCoefficient = bulletBallisticCoefficient; - - pBullet.flightTimeElapsed = iTime; - // measure bullet lifetime in time rather than in distance, because distances get very relative in orbit - pBullet.timeToLiveUntil = Mathf.Max(maxTargetingRange, maxEffectiveDistance) / bulletVelocity * 1.1f + Time.time; - - timeFired = Time.time - iTime; - - Vector3 firedVelocity = - VectorUtils.GaussianDirectionDeviation(fireTransform.forward, (maxDeviation * (ProjectileCount / 2)) / 2) * bulletVelocity; //cannistershot is more inaccurate than slug - - pBullet.currentVelocity = (part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) + firedVelocity; // use the real velocity, w/o offloading - firedBullet.transform.position += (part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * Time.fixedDeltaTime - + pBullet.currentVelocity * iTime; - - pBullet.sourceVessel = vessel; - pBullet.bulletTexturePath = bulletTexturePath; - pBullet.projectileColor = projectileColorC; - pBullet.startColor = startColorC; - pBullet.fadeColor = fadeColor; - tracerIntervalCounter++; - if (tracerIntervalCounter > tracerInterval) - { - tracerIntervalCounter = 0; - pBullet.tracerStartWidth = tracerStartWidth; - pBullet.tracerEndWidth = tracerEndWidth; - pBullet.tracerLength = tracerLength; - } - else - { - pBullet.tracerStartWidth = nonTracerWidth; - pBullet.tracerEndWidth = nonTracerWidth; - pBullet.startColor.a *= 0.5f; - pBullet.projectileColor.a *= 0.5f; - pBullet.tracerLength = tracerLength * 0.4f; - } - pBullet.tracerDeltaFactor = tracerDeltaFactor; - pBullet.tracerLuminance = tracerLuminance; - pBullet.bulletDrop = bulletDrop; - - if (bulletInfo.explosive) - { - pBullet.bulletType = PooledBullet.PooledBulletTypes.Explosive; - pBullet.explModelPath = explModelPath; - pBullet.explSoundPath = explSoundPath; - pBullet.tntMass = bulletInfo.tntMass; - pBullet.airDetonation = airDetonation; - pBullet.detonationRange = detonationRange; - pBullet.maxAirDetonationRange = maxAirDetonationRange; - pBullet.defaultDetonationRange = defaultDetonationRange; - pBullet.proximityDetonation = proximityDetonation; - } - else - { - pBullet.bulletType = PooledBullet.PooledBulletTypes.Standard; - pBullet.airDetonation = false; - } - switch (bulletDragType) - { - case BulletDragTypes.None: - pBullet.dragType = PooledBullet.BulletDragTypes.None; - break; - - case BulletDragTypes.AnalyticEstimate: - pBullet.dragType = PooledBullet.BulletDragTypes.AnalyticEstimate; - break; - - case BulletDragTypes.NumericalIntegration: - pBullet.dragType = PooledBullet.BulletDragTypes.NumericalIntegration; - break; - } - - pBullet.bullet = BulletInfo.bullets[currentType]; - pBullet.gameObject.SetActive(true); - } - //heat - heat += heatPerShot; - //EC - DrainECPerShot(); - RoundsRemaining++; - } - else - { - spinningDown = true; - if (!oneShotSound && wasFiring) - { - audioSource.Stop(); - wasFiring = false; - audioSource2.PlayOneShot(overheatSound); - } - } - } - - if (useRippleFire) - { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - } - } - else - { - spinningDown = true; - } - } - #endregion Guns - //lasers - #region LaserFire - private bool FireLaser() - { - float chargeAmount; - if (pulseLaser) - { - chargeAmount = requestResourceAmount; - } - else - { - chargeAmount = requestResourceAmount * TimeWarp.fixedDeltaTime; - } - float timeGap = (60 / roundsPerMinute) * TimeWarp.CurrentRate; - beamDuration = 0.1f * TimeWarp.CurrentRate; - if ((!pulseLaser || ((Time.time - timeFired > timeGap) && pulseLaser)) - && !pointingAtSelf && !Misc.Misc.CheckMouseIsOnGui() && WMgrAuthorized() && !isOverheated) // && !isReloading) - { - if (CanFire(chargeAmount)) - { - if (oneShotSound && pulseLaser) - { - audioSource.Stop(); - audioSource.PlayOneShot(fireSound); - } - else - { - wasFiring = true; - if (!audioSource.isPlaying) - { - audioSource.clip = fireSound; - audioSource.loop = false; - audioSource.time = 0; - audioSource.Play(); - } - else - { - if (audioSource.time >= fireSound.length) - { - audioSource.time = soundRepeatTime; - } - } - } - var aName = vessel.GetName(); - if (pulseLaser) - { - for (float iTime = Mathf.Min(Time.time - timeFired - timeGap, TimeWarp.fixedDeltaTime); iTime >= 0; iTime -= timeGap) - { - timeFired = Time.time - iTime; - if (BDACompetitionMode.Instance && BDACompetitionMode.Instance.Scores.ContainsKey(aName)) - { - ++BDACompetitionMode.Instance.Scores[aName].shotsFired; - } - LaserBeam(aName); - if (hasFireAnimation) - { - PlayFireAnim(); - } - } - heat += heatPerShot; - if (useRippleFire) - { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - } - } - else - { - LaserBeam(aName); - heat += heatPerShot * TimeWarp.CurrentRate; - BeamTracker += 0.02f; - if (BeamTracker > beamScoreTime) - { - if (BDACompetitionMode.Instance && BDACompetitionMode.Instance.Scores.ContainsKey(aName)) - { - ++BDACompetitionMode.Instance.Scores[aName].shotsFired; - } - } - for (float iTime = TimeWarp.fixedDeltaTime; iTime >= 0; iTime -= timeGap) - timeFired = Time.time - iTime; - } - if (!BeltFed) - { - RoundsRemaining++; - } - return true; - } - else - { - return false; - } - } - else - { - return false; - } - } - private void LaserBeam(string vesselname) - { - for (int i = 0; i < fireTransforms.Length; i++) - { - float damage = laserDamage; - Transform tf = fireTransforms[i]; - LineRenderer lr = laserRenderers[i]; - Vector3 rayDirection = tf.forward; - - Vector3 targetDirection = Vector3.zero; //autoTrack enhancer - Vector3 targetDirectionLR = tf.forward; - if (pulseLaser) - { - rayDirection = VectorUtils.GaussianDirectionDeviation(tf.forward, maxDeviation / 4); - targetDirectionLR = rayDirection.normalized; - } - else if ((((visualTargetVessel != null && visualTargetVessel.loaded) || slaved) && (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) < 0.25f) //it turret and within .25 deg, snap to target - { - //targetDirection = targetPosition + (relativeVelocity * Time.fixedDeltaTime) * 2 - tf.position; - targetDirection = targetPosition - tf.position; - rayDirection = targetDirection; - targetDirectionLR = targetDirection.normalized; - } - Ray ray = new Ray(tf.position, rayDirection); - lr.useWorldSpace = false; - lr.SetPosition(0, Vector3.zero); - RaycastHit hit; - - if (Physics.Raycast(ray, out hit, maxTargetingRange, 9076737)) - { - lr.useWorldSpace = true; - laserPoint = hit.point + (targetVelocity * Time.fixedDeltaTime); - - lr.SetPosition(0, tf.position + (part.rb.velocity * Time.fixedDeltaTime)); - lr.SetPosition(1, laserPoint); - - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - - 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) - { - 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; - } - else - { - damage = (laserDamage / (1 + Mathf.PI * Mathf.Pow(tanAngle * distance, 2)) * TimeWarp.fixedDeltaTime * 0.425f); - p.AddDamage(damage); - } - if (HEpulses) - { - ExplosionFx.CreateExplosion(hit.point, - (laserDamage / 30000), - explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName); - } - if (HeatRay) - { - using (var hitsEnu = Physics.OverlapSphere(hit.point, (Mathf.Sin(maxDeviation) * (tf.position - laserPoint).magnitude), 557057).AsEnumerable().GetEnumerator()) - { - while (hitsEnu.MoveNext()) - { - KerbalEVA kerb = hitsEnu.Current.gameObject.GetComponentUpwards(); - Part hitP = kerb ? kerb.part : hitsEnu.Current.GetComponentInParent(); - if (hitP && hitP != p && hitP.vessel && hitP.vessel != vessel) - { - //p.AddDamage(damage); - p.AddSkinThermalFlux(damage); - } - } - } - } - if (BDArmorySettings.INSTAKILL) p.Destroy(); - - if (pulseLaser || (!pulseLaser && ScoreAccumulator > beamScoreTime)) - { - ScoreAccumulator = 0; - var aName = vesselname; - var tName = p.vessel.GetName(); - if (aName != tName && BDACompetitionMode.Instance.Scores.ContainsKey(aName) && BDACompetitionMode.Instance.Scores.ContainsKey(tName)) - { - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { - BDAScoreService.Instance.TrackHit(aName, tName, WeaponName, distance); - BDAScoreService.Instance.TrackDamage(aName, tName, damage); - } - var aData = BDACompetitionMode.Instance.Scores[aName]; - aData.Score += 1; - if (p.vessel.GetName() == "Pinata") - { - aData.PinataHits++; - } - var tData = BDACompetitionMode.Instance.Scores[tName]; - tData.lastPersonWhoHitMe = aName; - tData.lastHitTime = Planetarium.GetUniversalTime(); - tData.everyoneWhoHitMe.Add(aName); - if (tData.hitCounts.ContainsKey(aName)) - ++tData.hitCounts[aName]; - else - tData.hitCounts.Add(aName, 1); - if (tData.damageFromBullets.ContainsKey(aName)) - tData.damageFromBullets[aName] += damage; - else - tData.damageFromBullets.Add(aName, damage); - } - } - else - { - ScoreAccumulator += 0.02f; - } - } - - if (Time.time - timeFired > 6 / 120 && BDArmorySettings.BULLET_HITS) - { - BulletHitFX.CreateBulletHit(p, hit.point, hit, hit.normal, false, 0, 0); - } - } - else - { - laserPoint = lr.transform.InverseTransformPoint((targetDirectionLR * maxTargetingRange) + tf.position); - lr.SetPosition(1, laserPoint); - } - } - } - void SetupLaserSpecifics() - { - chargeSound = GameDatabase.Instance.GetAudioClip(chargeSoundPath); - if (HighLogic.LoadedSceneIsFlight) - { - audioSource.clip = fireSound; - } - - laserRenderers = new LineRenderer[fireTransforms.Length]; - - for (int i = 0; i < fireTransforms.Length; i++) - { - Transform tf = fireTransforms[i]; - laserRenderers[i] = tf.gameObject.AddComponent(); - Color laserColor = Misc.Misc.ParseColor255(projectileColor); - laserColor.a = laserColor.a / 2; - laserRenderers[i].material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); - laserRenderers[i].material.SetColor("_TintColor", laserColor); - laserRenderers[i].material.mainTexture = GameDatabase.Instance.GetTexture(laserTexturePath, false); - laserRenderers[i].material.SetTextureScale("_MainTex", new Vector2(0.01f, 1)); - laserRenderers[i].textureMode = LineTextureMode.Tile; - laserRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; //= false; - laserRenderers[i].receiveShadows = false; - laserRenderers[i].startWidth = tracerStartWidth; - laserRenderers[i].endWidth = tracerEndWidth; - laserRenderers[i].positionCount = 2; - laserRenderers[i].SetPosition(0, Vector3.zero); - laserRenderers[i].SetPosition(1, Vector3.zero); - laserRenderers[i].useWorldSpace = false; - laserRenderers[i].enabled = false; - } - } - #endregion - //Rockets - #region RocketFire - // this is the extent of RocketLauncher code that differs from ModuleWeapon - public void FireRocket() //#11, #673 - { - int rocketsLeft; - - float timeGap = (60 / roundsPerMinute) * TimeWarp.CurrentRate; - - if (Time.time - timeFired > timeGap && !isReloading || !pointingAtSelf && (aiControlled || !Misc.Misc.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) - { - - if (BDArmorySettings.INFINITE_AMMO) - { - rocketsLeft = 1; - } - else - { - if (!externalAmmo) - { - PartResource rocketResource = GetRocketResource(); - rocketsLeft = (int)rocketResource.amount; - } - else - { - vessel.GetConnectedResourceTotals(AmmoID, out double ammoCurrent, out double ammoMax); - rocketsLeft = Mathf.Clamp((int)(RoundsPerMag - RoundsRemaining), 0, Mathf.Clamp((int)ammoCurrent, 0, RoundsPerMag)); - } - } - if (rocketsLeft >= 1) - { - if (rocketPod) - { - for (int s = 0; s < ProjectileCount; s++) - { - Transform currentRocketTfm = rockets[rocketsLeft - 1]; - GameObject rocketObj = rocketPool[SelectedAmmoType].GetPooledObject(); - rocketObj.transform.position = currentRocketTfm.position; - rocketObj.transform.rotation = currentRocketTfm.rotation; - rocketObj.transform.localScale = part.rescaleFactor * Vector3.one; - PooledRocket rocket = rocketObj.GetComponent(); - rocket.explModelPath = explModelPath; - rocket.explSoundPath = explSoundPath; - rocket.spawnTransform = currentRocketTfm; - rocket.caliber = rocketInfo.caliber; - rocket.rocketMass = rocketMass; - rocket.blastRadius = blastRadius; - rocket.thrust = thrust; - rocket.thrustTime = thrustTime; - rocket.flak = proximityDetonation; - rocket.detonationRange = detonationRange; - rocket.maxAirDetonationRange = maxAirDetonationRange; - rocket.tntMass = rocketInfo.tntMass; - rocket.shaped = rocketInfo.shaped; - rocket.randomThrustDeviation = thrustDeviation; - rocket.bulletDmgMult = bulletDmgMult; - rocket.sourceVessel = vessel; - rocketObj.transform.SetParent(currentRocketTfm.parent); - rocket.rocketName = GetShortName() + " rocket"; - rocket.parentRB = part.rb; - rocket.rocket = RocketInfo.rockets[currentType]; - rocketObj.SetActive(true); - } - if (!BDArmorySettings.INFINITE_AMMO) - { - if (externalAmmo) - { - part.RequestResource(ammoName, 1d); - } - else - { - GetRocketResource().amount--; - } - } - if (!BeltFed) - { - RoundsRemaining++; - } - UpdateRocketScales(); - } - else - { - if (!isOverheated) - { - for (int i = 0; i < fireTransforms.Length; i++) - { - for (int s = 0; s < ProjectileCount; s++) - { - Transform currentRocketTfm = fireTransforms[i]; - GameObject rocketObj = rocketPool[SelectedAmmoType].GetPooledObject(); - rocketObj.transform.position = currentRocketTfm.position; - rocketObj.transform.rotation = currentRocketTfm.rotation; - rocketObj.transform.localScale = part.rescaleFactor * Vector3.one; - PooledRocket rocket = rocketObj.GetComponent(); - rocket.explModelPath = explModelPath; - rocket.explSoundPath = explSoundPath; - rocket.spawnTransform = currentRocketTfm; - rocket.caliber = rocketInfo.caliber; - rocket.rocketMass = rocketMass; - rocket.blastRadius = blastRadius; - rocket.thrust = thrust; - rocket.thrustTime = thrustTime; - rocket.flak = proximityDetonation; - rocket.detonationRange = detonationRange; - rocket.maxAirDetonationRange = maxAirDetonationRange; - rocket.tntMass = rocketInfo.tntMass; - rocket.shaped = rocketInfo.shaped; - rocket.randomThrustDeviation = thrustDeviation; - rocket.bulletDmgMult = bulletDmgMult; - rocket.sourceVessel = vessel; - rocketObj.transform.SetParent(currentRocketTfm); - rocket.parentRB = part.rb; - rocket.rocket = RocketInfo.rockets[currentType]; - rocket.rocketName = GetShortName() + " rocket"; - rocketObj.SetActive(true); - } - if (!BDArmorySettings.INFINITE_AMMO) - { - part.RequestResource(ammoName, 1d); - } - heat += heatPerShot; - if (!BeltFed) - { - RoundsRemaining++; - } - } - } - } - if (!effectsShot) - { - WeaponFX(); - effectsShot = true; - } - timeFired = Time.time - iTime; - } - } - } - if (useRippleFire) - { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - } - } - - void MakeRocketArray() - { - Transform rocketsTransform = part.FindModelTransform("rockets");// important to keep this seperate from the fireTransformName transform - int numOfRockets = rocketsTransform.childCount; // due to rockets.Rocket_n being inconsistantly aligned - rockets = new Transform[numOfRockets]; // (and subsequently messing up the aim() vestors) - if (rocketPod) // and this overwriting the previous fireTransFormName -> fireTransForms - { - RoundsPerMag = numOfRockets; - } - for (int i = 0; i < numOfRockets; i++) - { - string rocketName = rocketsTransform.GetChild(i).name; - int rocketIndex = int.Parse(rocketName.Substring(7)) - 1; - rockets[rocketIndex] = rocketsTransform.GetChild(i); - } - if (!descendingOrder) Array.Reverse(rockets); - } - - void UpdateRocketScales() - { - double rocketQty = 0; - - if (!externalAmmo) - { - PartResource rocketResource = GetRocketResource(); - rocketQty = rocketResource.amount; - rocketsMax = rocketResource.maxAmount; - } - else - { - rocketQty = (RoundsPerMag - RoundsRemaining); - rocketsMax = RoundsPerMag; - } - var rocketsLeft = Math.Floor(rocketQty); - - for (int i = 0; i < rocketsMax; i++) - { - if (i < rocketsLeft) rockets[i].localScale = Vector3.one; - else rockets[i].localScale = Vector3.zero; - } - } - - public PartResource GetRocketResource() - { - using (IEnumerator res = part.Resources.GetEnumerator()) - while (res.MoveNext()) - { - if (res.Current == null) continue; - if (res.Current.resourceName == ammoName) return res.Current; - } - return null; - } - #endregion RocketFire - //Shared FX and resource consumption code - #region WeaponUtilities - void DrainECPerShot() - { - if (ECPerShot == 0) return; - //double drainAmount = ECPerShot * TimeWarp.fixedDeltaTime; - double drainAmount = ECPerShot; - double chargeAvailable = part.RequestResource("ElectricCharge", drainAmount, ResourceFlowMode.ALL_VESSEL); - } - - bool CanFire(float AmmoPerShot) - { - if (ECPerShot != 0) - { - double chargeAvailable = part.RequestResource("ElectricCharge", ECPerShot, ResourceFlowMode.ALL_VESSEL); - if (chargeAvailable < ECPerShot * 0.95f && !CheatOptions.InfiniteElectricity) - { - ScreenMessages.PostScreenMessage("Weapon Requires EC", 5.0f, ScreenMessageStyle.UPPER_CENTER); - return false; - } - else return true; - } - if (!hasGunner) - { - ScreenMessages.PostScreenMessage("Weapon Requires Gunner", 5.0f, ScreenMessageStyle.UPPER_CENTER); - return false; - } - if ((BDArmorySettings.INFINITE_AMMO || part.RequestResource(ammoName.GetHashCode(), (double)AmmoPerShot) > 0)) - { - return true; - } - - return false; - } - - void PlayFireAnim() - { - float unclampedSpeed = (roundsPerMinute * fireState.length) / 60f; - float lowFramerateFix = 1; - if (roundsPerMinute > 500f) - { - lowFramerateFix = (0.02f / Time.deltaTime); - } - fireAnimSpeed = Mathf.Clamp(unclampedSpeed, 1f * lowFramerateFix, 20f * lowFramerateFix); - fireState.enabled = true; - if (unclampedSpeed == fireAnimSpeed || fireState.normalizedTime > 1) - { - fireState.normalizedTime = 0; - } - fireState.speed = fireAnimSpeed; - fireState.normalizedTime = Mathf.Repeat(fireState.normalizedTime, 1); - - //Debug.Log("fireAnim time: " + fireState.normalizedTime + ", speed; " + fireState.speed); - } - - void WeaponFX() - { - //sound - if (oneShotSound) - { - audioSource.Stop(); - audioSource.PlayOneShot(fireSound); - } - else - { - wasFiring = true; - if (!audioSource.isPlaying) - { - audioSource.clip = fireSound; - audioSource.loop = false; - audioSource.time = 0; - audioSource.Play(); - } - else - { - if (audioSource.time >= fireSound.length) - { - audioSource.time = soundRepeatTime; - } - } - } - //animation - if (hasFireAnimation) - { - PlayFireAnim(); - } - //muzzle flash - using (List.Enumerator pEmitter = muzzleFlashEmitters.GetEnumerator()) - while (pEmitter.MoveNext()) - { - if (pEmitter.Current == null) continue; - //KSPParticleEmitter pEmitter = mtf.gameObject.GetComponent(); - if (pEmitter.Current.useWorldSpace && !oneShotWorldParticles) continue; - if (pEmitter.Current.maxEnergy < 0.5f) - { - float twoFrameTime = Mathf.Clamp(Time.deltaTime * 2f, 0.02f, 0.499f); - pEmitter.Current.maxEnergy = twoFrameTime; - pEmitter.Current.minEnergy = twoFrameTime / 3f; - } - pEmitter.Current.Emit(); - } - - using (List.Enumerator gpe = gaplessEmitters.GetEnumerator()) - while (gpe.MoveNext()) - { - if (gpe.Current == null) continue; - gpe.Current.EmitParticles(); - } - - //shell ejection - if (BDArmorySettings.EJECT_SHELLS) - { - IEnumerator sTf = shellEjectTransforms.AsEnumerable().GetEnumerator(); - while (sTf.MoveNext()) - { - if (sTf.Current == null) continue; - GameObject ejectedShell = shellPool.GetPooledObject(); - ejectedShell.transform.position = sTf.Current.position; - //+(part.rb.velocity*TimeWarp.fixedDeltaTime); - ejectedShell.transform.rotation = sTf.Current.rotation; - ejectedShell.transform.localScale = Vector3.one * shellScale; - ShellCasing shellComponent = ejectedShell.GetComponent(); - shellComponent.initialV = part.rb.velocity; - ejectedShell.SetActive(true); - } - sTf.Dispose(); - } - } - #endregion WeaponUtilities - //misc. like check weaponmgr - #region WeaponSetup - bool WMgrAuthorized() - { - MissileFire manager = BDArmorySetup.Instance.ActiveWeaponManager; - if (manager != null && manager.vessel == vessel) - { - if (manager.hasSingleFired) return false; - else return true; - } - else - { - return true; - } - } - - void CheckWeaponSafety() - { - pointingAtSelf = false; - - // 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); - - 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, 9076737)) - { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - if (p && p.vessel && p.vessel == vessel) - { - pointingAtSelf = true; - break; - } - } - - pointingAtPosition = fireTransforms[i].position + (ray.direction * targetDistance); - } - } - - public void EnableWeapon() - { - if (weaponState == WeaponStates.Enabled || weaponState == WeaponStates.PoweringUp || weaponState == WeaponStates.Locked) - { - return; - } - - StopShutdownStartupRoutines(); - - startupRoutine = StartCoroutine(StartupRoutine()); - } - - public void DisableWeapon() - { - if (weaponState == WeaponStates.Disabled || weaponState == WeaponStates.PoweringDown) - { - return; - } - - StopShutdownStartupRoutines(); - - shutdownRoutine = StartCoroutine(ShutdownRoutine()); - } - - void ParseWeaponType() - { - weaponType = weaponType.ToLower(); - - switch (weaponType) - { - case "ballistic": - eWeaponType = WeaponTypes.Ballistic; - break; - case "rocket": - eWeaponType = WeaponTypes.Rocket; - break; - case "laser": - eWeaponType = WeaponTypes.Laser; - break; - case "cannon": - // Note: this type is deprecated. behavior is duplicated with Ballistic and bulletInfo.explosive = true - // Type remains for backward compatability for now. - eWeaponType = WeaponTypes.Ballistic; - break; - } - } - #endregion WeaponSetup - - #region Audio - - void UpdateVolume() - { - if (audioSource) - { - audioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; - } - if (audioSource2) - { - audioSource2.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; - } - if (lowpassFilter) - { - lowpassFilter.cutoffFrequency = BDArmorySettings.IVA_LOWPASS_FREQ; - } - } - - void SetupAudio() - { - fireSound = GameDatabase.Instance.GetAudioClip(fireSoundPath); - overheatSound = GameDatabase.Instance.GetAudioClip(overheatSoundPath); - if (!audioSource) - { - audioSource = gameObject.AddComponent(); - audioSource.bypassListenerEffects = true; - audioSource.minDistance = .3f; - audioSource.maxDistance = 1000; - audioSource.priority = 10; - audioSource.dopplerLevel = 0; - audioSource.spatialBlend = 1; - } - - if (!audioSource2) - { - audioSource2 = gameObject.AddComponent(); - audioSource2.bypassListenerEffects = true; - audioSource2.minDistance = .3f; - audioSource2.maxDistance = 1000; - audioSource2.dopplerLevel = 0; - audioSource2.priority = 10; - audioSource2.spatialBlend = 1; - } - - if (reloadAudioPath != string.Empty) - { - reloadAudioClip = (AudioClip)GameDatabase.Instance.GetAudioClip(reloadAudioPath); - } - if (reloadCompletePath != string.Empty) - { - reloadCompleteAudioClip = (AudioClip)GameDatabase.Instance.GetAudioClip(reloadCompletePath); - } - - if (!lowpassFilter && gameObject.GetComponents().Length == 0) - { - lowpassFilter = gameObject.AddComponent(); - lowpassFilter.cutoffFrequency = BDArmorySettings.IVA_LOWPASS_FREQ; - lowpassFilter.lowpassResonanceQ = 1f; - } - - UpdateVolume(); - } - - #endregion Audio - - #region Targeting - - void Aim() - { - //AI control - if (aiControlled && !slaved) - { - if (!targetAcquired) - { - autoFire = false; - return; - } - } - - if (!slaved && !aiControlled && (yawRange > 0 || maxPitch - minPitch > 0)) - { - //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, 9076737)) - { - targetPosition = hit.point; - - //aim through self vessel if occluding mouseray - - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - - if (p && p.vessel && p.vessel == vessel) - { - targetPosition = ray.direction * maxTargetingRange + - FlightCamera.fetch.mainCamera.transform.position; - } - } - else - { - targetPosition = (ray.direction * (maxTargetingRange + (FlightCamera.fetch.Distance * 0.75f))) + - FlightCamera.fetch.mainCamera.transform.position; - if (visualTargetVessel != null && visualTargetVessel.loaded) - { - targetPosition = ray.direction * - Vector3.Distance(visualTargetVessel.transform.position, - FlightCamera.fetch.mainCamera.transform.position) + - FlightCamera.fetch.mainCamera.transform.position; - } - } - } - - //aim assist - Vector3 finalTarget = targetPosition; - Vector3 originalTarget = targetPosition; - Vector3 pointingDirection = fireTransforms[0].forward; - targetDistance = Vector3.Distance(finalTarget, fireTransforms[0].position); - relativeVelocity = targetVelocity - part.rb.velocity; - - if (BDArmorySettings.AIM_ASSIST || aiControlled) - { - if (eWeaponType == WeaponTypes.Ballistic) //Gun targeting - { - float effectiveVelocity = bulletVelocity; - Quaternion.FromToRotation(targetAccelerationPrevious, targetAcceleration).ToAngleAxis(out float accelDAngle, out Vector3 accelDAxis); - Vector3 leadTarget = targetPosition; - - int iterations = 6; - while (--iterations >= 0) - { - finalTarget = targetPosition; - float time = (leadTarget - fireTransforms[0].position).magnitude / effectiveVelocity - (Time.fixedDeltaTime * 1.5f); - - if (targetAcquired) - { - finalTarget += relativeVelocity * time; -#if DEBUG - relVelAdj = relativeVelocity * time; - var vc = finalTarget; -#endif - var accelDExtAngle = accelDAngle * time / 3; - var extrapolatedAcceleration = - Quaternion.AngleAxis(accelDExtAngle, accelDAxis) - * targetAcceleration - * Mathf.Cos(accelDExtAngle * Mathf.Deg2Rad * 2.222f); - finalTarget += 0.5f * extrapolatedAcceleration * time * time; -#if DEBUG - accAdj = (finalTarget - vc); -#endif - } - else if (Misc.Misc.GetRadarAltitudeAtPos(targetPosition) < 2000) - { - //this vessel velocity compensation against stationary - finalTarget += (-(part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * time); - } - - leadTarget = finalTarget; - - if (bulletDrop) //rocket gravity ajdustment already done in TrajectorySim - { -#if DEBUG - var vc = finalTarget; -#endif - Vector3 up = (VectorUtils.GetUpDirection(finalTarget) + 2 * VectorUtils.GetUpDirection(fireTransforms[0].position)).normalized; - float gAccel = ((float)FlightGlobals.getGeeForceAtPosition(finalTarget).magnitude - + (float)FlightGlobals.getGeeForceAtPosition(fireTransforms[0].position).magnitude * 2) / 3; - Vector3 intermediateTarget = finalTarget + (0.5f * gAccel * time * time * up); - - var avGrav = (FlightGlobals.getGeeForceAtPosition(finalTarget) + 2 * FlightGlobals.getGeeForceAtPosition(fireTransforms[0].position)) / 3; - effectiveVelocity = bulletVelocity - * (float)Vector3d.Dot((intermediateTarget - fireTransforms[0].position).normalized, (finalTarget - fireTransforms[0].position).normalized) - + Vector3.Project(avGrav, finalTarget - fireTransforms[0].position).magnitude * time / 2 * (Vector3.Dot(avGrav, finalTarget - fireTransforms[0].position) < 0 ? -1 : 1); - finalTarget = intermediateTarget; -#if DEBUG - gravAdj = (finalTarget - vc); -#endif - } - } - } - //removed the detonationange += UnityEngine.random, that gets called every frame and just causes the prox fuze range to wander - if (eWeaponType == WeaponTypes.Rocket) //rocket aiming - { - finalTarget += trajectoryOffset; - finalTarget += targetVelocity * predictedFlightTime; - finalTarget += 0.5f * targetAcceleration * predictedFlightTime * predictedFlightTime; - } - targetDistance = Vector3.Distance(finalTarget, fireTransforms[0].position); - } - //airdetonation - if (airDetonation) - { - if (targetAcquired && airDetonationTiming) - { - //detonationRange = BlastPhysicsUtils.CalculateBlastRange(bulletInfo.tntMass); //this returns 0, use detonationRange GUI tweakable instead - defaultDetonationRange = targetDistance;// adds variable time fuze if/when proximity fuzes fail - - } - else - { - //detonationRange = defaultDetonationRange; - defaultDetonationRange = maxAirDetonationRange; //airburst at max range - } - } - fixedLeadOffset = originalTarget - finalTarget; //for aiming fixed guns to moving target - finalAimTarget = finalTarget; - - //final turret aiming - if (slaved && !targetAcquired) return; - if (turret) - { - bool origSmooth = turret.smoothRotation; - if (aiControlled || slaved) - { - turret.smoothRotation = false; - } - turret.AimToTarget(finalTarget); - turret.smoothRotation = origSmooth; - } - } - //moving RTS to get all the targeting code together for convenience once rockets get added - 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)))) - { - Transform fireTransform = fireTransforms[0]; - - if (eWeaponType == WeaponTypes.Rocket && rocketPod) - { - fireTransform = rockets[0].parent; // support for legacy RLs - } - - if (eWeaponType == WeaponTypes.Laser && - BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS) - { - Ray ray = new Ray(fireTransform.position, fireTransform.forward); - RaycastHit rayHit; - if (Physics.Raycast(ray, out rayHit, maxTargetingRange, 9076737)) - { - bulletPrediction = rayHit.point; - } - else - { - bulletPrediction = ray.GetPoint(maxTargetingRange); - } - pointingAtPosition = ray.GetPoint(maxTargetingRange); - } - else if (eWeaponType == WeaponTypes.Rocket || (eWeaponType == WeaponTypes.Ballistic && BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS)) - { - float simTime = 0; - Vector3 pointingDirection = fireTransform.forward; - float simDeltaTime; - if (eWeaponType == WeaponTypes.Rocket) - { - simDeltaTime = Time.fixedDeltaTime; - } - else - { - simDeltaTime = 0.155f; - } - Vector3 simVelocity = part.rb.velocity + Krakensbane.GetFrameVelocityV3f() + (bulletVelocity * fireTransform.forward); - Vector3 simCurrPos = fireTransform.position + ((part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * Time.fixedDeltaTime); - Vector3 simPrevPos = simCurrPos; - Vector3 simStartPos = simCurrPos; - if (eWeaponType == WeaponTypes.Rocket) - { - simVelocity = part.rb.velocity + Krakensbane.GetFrameVelocityV3f(); - simCurrPos = fireTransform.position + ((part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * Time.fixedDeltaTime); - simPrevPos = fireTransform.position + ((part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * Time.fixedDeltaTime); - simStartPos = fireTransform.position + ((part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * Time.fixedDeltaTime); - } - bool simulating = true; - - List pointPositions = new List(); - pointPositions.Add(simCurrPos); - - float atmosMultiplier = Mathf.Clamp01(2.5f * (float)FlightGlobals.getAtmDensity(vessel.staticPressurekPa, vessel.externalTemperature, vessel.mainBody)); - - while (simulating) - { - RaycastHit hit; - - if (eWeaponType == WeaponTypes.Rocket) - { - if (simTime > thrustTime) - { - simDeltaTime = 0.1f; - } - - if (simTime > 0.04f) - { - ///simDeltaTime = 0.02f; - simDeltaTime = Time.fixedDeltaTime; - if (simTime < thrustTime) - { - simVelocity += thrust / rocketMass * simDeltaTime * pointingDirection; - } - - //rotation (aero stabilize) - pointingDirection = Vector3.RotateTowards(pointingDirection, - simVelocity + Krakensbane.GetFrameVelocity(), - atmosMultiplier * (0.5f * (simTime)) * 50 * simDeltaTime * Mathf.Deg2Rad, 0); - } - } - if (bulletDrop || eWeaponType == WeaponTypes.Rocket) - { - simVelocity += FlightGlobals.getGeeForceAtPosition(simCurrPos) * simDeltaTime; - } - simCurrPos += simVelocity * simDeltaTime; - pointPositions.Add(simCurrPos); - if (!aiControlled && !slaved) - { - if (Physics.Raycast(simPrevPos, simCurrPos - simPrevPos, out hit, - Vector3.Distance(simPrevPos, simCurrPos), 9076737)) - { - Vessel hitVessel = null; - try - { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - hitVessel = (eva ? eva.part : hit.collider.gameObject.GetComponentInParent()).vessel; - } - catch (NullReferenceException) - { - } - - if (hitVessel == null || (hitVessel != null && hitVessel != vessel)) - { - bulletPrediction = hit.point; - simulating = false; - break; - } - } - else if (FlightGlobals.getAltitudeAtPos(simCurrPos) < 0) - { - bulletPrediction = simCurrPos; - simulating = false; - break; - } - } - simPrevPos = simCurrPos; - if (visualTargetVessel != null && visualTargetVessel.loaded && !visualTargetVessel.Landed && - (simStartPos - simCurrPos).sqrMagnitude > targetDistance * targetDistance) - { - bulletPrediction = simStartPos + (simCurrPos - simStartPos).normalized * targetDistance; - simulating = false; - } - - if ((simStartPos - simCurrPos).sqrMagnitude > maxTargetingRange * maxTargetingRange) - { - bulletPrediction = simStartPos + ((simCurrPos - simStartPos).normalized * maxTargetingRange); - simulating = false; - } - simTime += simDeltaTime; - } - Vector3 pointingPos = fireTransform.position + (fireTransform.forward * targetDistance); - trajectoryOffset = pointingPos - bulletPrediction; - predictedFlightTime = simTime; - - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) - { - Vector3[] pointsArray = pointPositions.ToArray(); - if (gameObject.GetComponent() == null) - { - LineRenderer lr = gameObject.AddComponent(); - lr.startWidth = .1f; - lr.endWidth = .1f; - lr.positionCount = pointsArray.Length; - for (int i = 0; i < pointsArray.Length; i++) - { - lr.SetPosition(i, pointsArray[i]); - } - } - else - { - LineRenderer lr = gameObject.GetComponent(); - lr.enabled = true; - lr.positionCount = pointsArray.Length; - for (int i = 0; i < pointsArray.Length; i++) - { - lr.SetPosition(i, pointsArray[i]); - } - } - } - } - } - } - //more organization, grouping like with like - public Vector3 GetLeadOffset() - { - return fixedLeadOffset; - } - void CheckAIAutofire() - { - //autofiring with AI - if (targetAcquired && aiControlled) - { - - Transform fireTransform = fireTransforms[0]; - - Vector3 targetRelPos = (finalAimTarget) - fireTransform.position; - Vector3 aimDirection = fireTransform.forward; - float targetCosAngle = Vector3.Dot(aimDirection, targetRelPos.normalized); - - 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; - - if (BDATargetManager.CheckSafeToFireGuns(weaponManager, aimDirection, 1000, 0.999962f) //~0.5 degree of unsafe angle, was 0.999848f (1deg) - && targetCosAngle >= maxAutoFireCosAngle) //check if directly on target - { - autoFire = true; - } - else - { - autoFire = false; - } - } - else // rockets - { - if (BDATargetManager.CheckSafeToFireGuns(weaponManager, aimDirection, 1000, 0.999962f)) - { - if (Vector3.Distance(finalAimTarget, fireTransform.position) > blastRadius) - autoFire = Vector3.Angle(targetRelPos, aimDirection) < 1f; //rockets already calculate where target will be - } - } - } - else - { - autoFire = false; - } - - //disable autofire after burst length - if (autoFire && Time.time - autoFireTimer > autoFireLength) - { - autoFire = false; - visualTargetVessel = null; - } - } - - IEnumerator AimAndFireAtEndOfFrame() - { - if (eWeaponType != WeaponTypes.Laser) yield return new WaitForEndOfFrame(); - if (this == null) yield break; - - UpdateTargetVessel(); - updateAcceleration(targetVelocity, targetPosition); - relativeVelocity = targetVelocity - vessel.rb_velocity; - - RunTrajectorySimulation(); - Aim(); - CheckWeaponSafety(); - CheckAIAutofire(); - - if (finalFire) - { - if (!BurstFire && useRippleFire && weaponManager.gunRippleIndex != rippleIndex) - { - finalFire = false; - } - else - { - finalFire = true; - } - if (eWeaponType == WeaponTypes.Laser) - { - if (finalFire) - { - 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; - } - } - if (!pulseLaser || !oneShotSound) - { - audioSource.Stop(); - } - } - } - } - else - { - if (eWeaponType == WeaponTypes.Ballistic) - { - if (finalFire) - Fire(); - } - if (eWeaponType == WeaponTypes.Rocket) - { - if (finalFire) - FireRocket(); - } - } - if (BurstFire && (RoundsRemaining < RoundsPerMag)) - { - finalFire = true; - } - else - { - finalFire = false; - } - } - - yield break; - } - - void DrawAlignmentIndicator() - { - if (fireTransforms == null || fireTransforms[0] == null) return; - - Part rootPart = EditorLogic.RootPart; - if (rootPart == null) return; - - Transform refTransform = rootPart.GetReferenceTransform(); - if (!refTransform) return; - - Vector3 fwdPos = fireTransforms[0].position + (5 * fireTransforms[0].forward); - BDGUIUtils.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); - - BDGUIUtils.DrawLineBetweenWorldPositions(fwdPos, refFwdPos, 2, XKCDColors.Orange); - - Vector2 guiPos; - if (BDGUIUtils.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)); - - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + pitchVector, fwdPos, 3, - Color.white); - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + yawVector, fwdPos, 3, Color.white); - - float pitch = Vector3.Angle(pitchVector, referenceDirection); - float yaw = Vector3.Angle(yawVector, referenceDirection); - - string convergeDistance; - - Vector3 projAxis = Vector3.Project(refTransform.position - fireTransforms[0].transform.position, - refRight); - float xDist = projAxis.magnitude; - 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"; - } - 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"); - - GUI.Label(angleRect, xAngle + "\n" + yAngle + "\n" + convergeDistance); - } - } - - #endregion Targeting - - #region Updates - void CheckCrewed() - { - if (!gunnerSeatLookedFor) // Only find the module once. - { - var kerbalSeats = part.Modules.OfType(); - if (kerbalSeats.Count() > 0) - gunnerSeat = kerbalSeats.First(); - else - gunnerSeat = null; - gunnerSeatLookedFor = true; - } - if ((gunnerSeat == null || gunnerSeat.Occupant == null) && part.protoModuleCrew.Count <= 0) //account for both lawn chairs and internal cabins - { - hasGunner = false; - } - else - { - hasGunner = true; - } - } - void UpdateHeat() - { - heat = Mathf.Clamp(heat - heatLoss * TimeWarp.fixedDeltaTime, 0, Mathf.Infinity); - if (heat > maxHeat && !isOverheated) - { - isOverheated = true; - autoFire = false; - audioSource.Stop(); - wasFiring = false; - audioSource2.PlayOneShot(overheatSound); - weaponManager.ResetGuardInterval(); - } - if (heat < maxHeat / 3 && isOverheated) //reset on cooldown - { - isOverheated = false; - } - } - void ReloadWeapon() - { - if (isReloading) - { - ReloadTimer = Mathf.Clamp((ReloadTimer + 1 * TimeWarp.fixedDeltaTime / ReloadTime), 0, 1); - } - if (RoundsRemaining >= RoundsPerMag && !isReloading) - { - isReloading = true; - autoFire = false; - audioSource.Stop(); - wasFiring = false; - weaponManager.ResetGuardInterval(); - showReloadMeter = true; - } - if (ReloadTimer >= 1 && isReloading) - { - RoundsRemaining = 0; - gauge.UpdateReloadMeter(1); - showReloadMeter = false; - isReloading = false; - ReloadTimer = 0; - } - } - void UpdateTargetVessel() - { - targetAcquired = false; - slaved = false; - bool atprWasAcquired = atprAcquired; - atprAcquired = false; - - if (weaponManager) - { - //legacy or visual range guard targeting - if (aiControlled && weaponManager && visualTargetVessel && - (visualTargetVessel.transform.position - transform.position).sqrMagnitude < weaponManager.guardRange * weaponManager.guardRange) - { - targetPosition = visualTargetVessel.CoM; - targetVelocity = visualTargetVessel.rb_velocity; - targetAcquired = true; - return; - } - - if (weaponManager.slavingTurrets && turret) - { - slaved = true; - targetPosition = weaponManager.slavedPosition; - targetVelocity = weaponManager.slavedTarget.vessel?.rb_velocity ?? (weaponManager.slavedVelocity - Krakensbane.GetFrameVelocityV3f()); - targetAcquired = true; - return; - } - - if (weaponManager.vesselRadarData && weaponManager.vesselRadarData.locked) - { - TargetSignatureData targetData = weaponManager.vesselRadarData.lockedTargetData.targetData; - targetVelocity = targetData.velocity - Krakensbane.GetFrameVelocityV3f(); - targetPosition = targetData.predictedPosition; - targetAcceleration = targetData.acceleration; - if (targetData.vessel) - { - targetVelocity = targetData.vessel?.rb_velocity ?? targetVelocity; - targetPosition = targetData.vessel.CoM; - } - targetAcquired = true; - return; - } - - //auto proxy tracking - if (vessel.isActiveVessel && autoProxyTrackRange > 0) - { - if (aptrTicker < 20) - { - aptrTicker++; - - if (atprWasAcquired) - { - targetAcquired = true; - atprAcquired = true; - } - } - else - { - aptrTicker = 0; - Vessel tgt = null; - float closestSqrDist = autoProxyTrackRange * autoProxyTrackRange; - using (var v = BDATargetManager.LoadedVessels.GetEnumerator()) - while (v.MoveNext()) - { - if (v.Current == null || !v.Current.loaded) continue; - if (!v.Current.IsControllable) continue; - if (v.Current == vessel) continue; - Vector3 targetVector = v.Current.transform.position - part.transform.position; - if (Vector3.Dot(targetVector, fireTransforms[0].forward) < 0) continue; - float sqrDist = (v.Current.transform.position - part.transform.position).sqrMagnitude; - if (sqrDist > closestSqrDist) continue; - if (Vector3.Angle(targetVector, fireTransforms[0].forward) > 20) continue; - tgt = v.Current; - closestSqrDist = sqrDist; - } - - if (tgt == null) return; - targetAcquired = true; - atprAcquired = true; - targetPosition = tgt.CoM; - targetVelocity = tgt.rb_velocity; - } - } - } - } - - /// - /// 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) - { - targetAccelerationPrevious = targetAcceleration; - targetAcceleration = (target_rb_velocity - Krakensbane.GetLastCorrection() - targetVelocityPrevious) / Time.fixedDeltaTime; - float altitude = (float)FlightGlobals.currentMainBody.GetAltitude(position); - if (altitude < 12 && altitude > -10) - targetAcceleration = Vector3.ProjectOnPlane(targetAcceleration, VectorUtils.GetUpDirection(position)); - targetVelocityPrevious = target_rb_velocity; - } - - void UpdateGUIWeaponState() - { - guiStatusString = weaponState.ToString(); - } - - IEnumerator StartupRoutine() - { - weaponState = WeaponStates.PoweringUp; - UpdateGUIWeaponState(); - - if (hasDeployAnim && deployState) - { - deployState.enabled = true; - deployState.speed = 1; - while (deployState.normalizedTime < 1) //wait for animation here - { - yield return null; - } - deployState.normalizedTime = 1; - deployState.speed = 0; - deployState.enabled = false; - } - - weaponState = WeaponStates.Enabled; - UpdateGUIWeaponState(); - BDArmorySetup.Instance.UpdateCursorState(); - } - - IEnumerator ShutdownRoutine() - { - weaponState = WeaponStates.PoweringDown; - UpdateGUIWeaponState(); - BDArmorySetup.Instance.UpdateCursorState(); - if (turret) - { - yield return new WaitForSeconds(0.2f); - - while (!turret.ReturnTurret()) //wait till turret has returned - { - yield return new WaitForFixedUpdate(); - } - } - - if (hasDeployAnim) - { - deployState.enabled = true; - deployState.speed = -1; - while (deployState.normalizedTime > 0) - { - yield return null; - } - deployState.normalizedTime = 0; - deployState.speed = 0; - deployState.enabled = false; - } - - weaponState = WeaponStates.Disabled; - UpdateGUIWeaponState(); - } - - void StopShutdownStartupRoutines() - { - if (shutdownRoutine != null) - { - StopCoroutine(shutdownRoutine); - shutdownRoutine = null; - } - - if (startupRoutine != null) - { - StopCoroutine(startupRoutine); - startupRoutine = null; - } - } - - #endregion Updates - - #region Bullets - - void ParseBulletDragType() - { - bulletDragTypeName = bulletDragTypeName.ToLower(); - - switch (bulletDragTypeName) - { - case "none": - bulletDragType = BulletDragTypes.None; - break; - - case "numericalintegration": - bulletDragType = BulletDragTypes.NumericalIntegration; - break; - - case "analyticestimate": - bulletDragType = BulletDragTypes.AnalyticEstimate; - break; - } - } - - void ParseBulletFuzeType(string type) - { - type = type.ToLower(); - if (type == "none") //no fuze present - { - proximityDetonation = false; - airDetonation = false; - airDetonationTiming = false; - } - if (type == "timed")//detonates after set distance - { - airDetonation = true; - airDetonationTiming = true; - proximityDetonation = false; - } - if (type == "proximity")//proximity fuzing - { - airDetonation = false; - airDetonationTiming = false; - proximityDetonation = true; - } - if (type == "flak") //detonates at set distance/proximity - { - proximityDetonation = true; - airDetonation = true; - airDetonationTiming = true; - } - } - - void SetupBulletPool() - { - GameObject templateBullet = new GameObject("Bullet"); - templateBullet.AddComponent(); - templateBullet.SetActive(false); - bulletPool = ObjectPool.CreateObjectPool(templateBullet, 100, true, true); - } - - void SetupShellPool() - { - GameObject templateShell = GameDatabase.Instance.GetModel("BDArmory/Models/shell/model"); - templateShell.SetActive(false); - templateShell.AddComponent(); - shellPool = ObjectPool.CreateObjectPool(templateShell, 50, true, true); - } - - void SetupRocketPool(string name, string modelpath) - { - var key = name; - if (!rocketPool.ContainsKey(key) || rocketPool[key] == null) - { - var RocketTemplate = GameDatabase.Instance.GetModel(modelpath); - if (RocketTemplate == null) - { - Debug.LogError("[ModuleWeapon]: model '" + modelpath + "' not found. Expect exceptions if trying to use this rocket."); - return; - } - RocketTemplate.SetActive(false); - RocketTemplate.AddComponent(); - rocketPool[key] = ObjectPool.CreateObjectPool(RocketTemplate, 10, true, true); - } - } - - void SetupAmmo(BaseField field, object obj) - { - ammoList = BDAcTools.ParseNames(bulletType); - currentType = ammoList[(int)AmmoTypeNum - 1].ToString(); - - if (eWeaponType == WeaponTypes.Ballistic) - { - bulletInfo = BulletInfo.bullets[currentType]; - guiAmmoTypeString = " "; //reset name - if (bulletInfo.subProjectileCount > 1) - { - guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Shot") + " "; - } - if (bulletInfo.apBulletMod > 1) - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_AP") + " "; - } - if (bulletInfo.tntMass > 0) - { - if (airDetonation || proximityDetonation) - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Flak") + " "; - } - else - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Explosive") + " "; - } - } - else - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Slug"); - } - - caliber = bulletInfo.caliber; - bulletVelocity = bulletInfo.bulletVelocity; - bulletMass = bulletInfo.bulletMass; - ProjectileCount = bulletInfo.subProjectileCount; - bulletDragTypeName = bulletInfo.bulletDragTypeName; - projectileColorC = Misc.Misc.ParseColor255(bulletInfo.projectileColor); - startColorC = Misc.Misc.ParseColor255(bulletInfo.startColor); - fadeColor = bulletInfo.fadeColor; - ParseBulletDragType(); - ParseBulletFuzeType(bulletInfo.fuzeType); - tntMass = bulletInfo.tntMass; - SetInitialDetonationDistance(); - tracerStartWidth = caliber / 300; - tracerEndWidth = caliber / 750; - nonTracerWidth = caliber / 500; - SelectedAmmoType = bulletInfo.name; //store selected ammo name as string for retrieval by web orc filter/later GUI implementation - } - if (eWeaponType == WeaponTypes.Rocket) - { - ammoList = BDAcTools.ParseNames(bulletType); - currentType = ammoList[(int)AmmoTypeNum - 1].ToString(); - rocketInfo = RocketInfo.rockets[currentType]; - guiAmmoTypeString = ""; - name = rocketInfo.name; - rocketMass = rocketInfo.rocketMass; - caliber = rocketInfo.caliber; - thrust = rocketInfo.thrust; - thrustTime = rocketInfo.thrustTime; - ProjectileCount = rocketInfo.subProjectileCount; - rocketModelPath = rocketInfo.rocketModelPath; - - tntMass = rocketInfo.tntMass; - guiAmmoTypeString = " "; //reset name - 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! - } - if (rocketInfo.explosive) - { - if (rocketInfo.flak) - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Flak"); - } - else if (rocketInfo.shaped) - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Shaped") + " "; - } - else - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_HE") + " "; - } - } - else - { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Kinetic"); - } - if (rocketInfo.flak) - { - proximityDetonation = true; - } - else - { - proximityDetonation = false; - } - PAWRefresh(); - SetInitialDetonationDistance(); - SelectedAmmoType = rocketInfo.name; //store selected ammo name as string for retrieval by web orc filter/later GUI implementation - SetupRocketPool(SelectedAmmoType, rocketModelPath); - } - } - protected void SetInitialDetonationDistance() - { - if (this.detonationRange == -1) - { - if (eWeaponType == WeaponTypes.Ballistic && (bulletInfo.tntMass != 0 && (proximityDetonation || airDetonation))) - { - blastRadius = BlastPhysicsUtils.CalculateBlastRange(bulletInfo.tntMass); //reproting as two so blastradius can be handed over to PooledRocket for detonation/safety stuff - detonationRange = blastRadius * 0.666f; - } - else if (eWeaponType == WeaponTypes.Rocket && rocketInfo.tntMass != 0) //don't fire rockets ar point blank - { - blastRadius = BlastPhysicsUtils.CalculateBlastRange(rocketInfo.tntMass); - detonationRange = blastRadius * 0.666f; - } - else - { - blastRadius = 0; - detonationRange = 0f; - proximityDetonation = false; - } - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory]: DetonationDistance = : " + detonationRange); - } - } - - #endregion Bullets - - #region RMB Info - - public override string GetInfo() - { - ammoList = BDAcTools.ParseNames(bulletType); - StringBuilder output = new StringBuilder(); - output.Append(Environment.NewLine); - output.AppendLine($"Weapon Type: {weaponType}"); - - if (weaponType == "laser") - { - if (!electroLaser) - { - output.AppendLine($"Electrolaser EMP damage: {Math.Round((ECPerShot / 20), 2)}/s"); - output.AppendLine($"Power Required: {ECPerShot}/s"); - } - else - { - output.AppendLine($"Laser damage: {laserDamage}"); - } - output.AppendLine($"Powered By: {ammoName}"); - if (ECPerShot > 0) - { - output.AppendLine($"Electric Charge required per shot: {ammoName}"); - } - if (pulseLaser) - { - output.AppendLine($"Rounds Per Minute: {roundsPerMinute * (fireTransforms?.Length ?? 1)}"); - } - if (HEpulses) - { - output.AppendLine($"Blast:"); - output.AppendLine($"- tnt mass: {Math.Round((laserDamage / 30000), 2)} kg"); - output.AppendLine($"- radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(laserDamage / 30000), 2)} m"); - } - } - else - { - output.AppendLine($"Rounds Per Minute: {roundsPerMinute * (fireTransforms?.Length ?? 1)}"); - output.AppendLine($"Ammunition: {ammoName}"); - if (ECPerShot > 0) - { - output.AppendLine($"Electric Charge required per shot: {ammoName}"); - } - output.AppendLine($"Max Range: {maxEffectiveDistance} m"); - if (weaponType == "ballistic") - { - for (int i = 0; i < ammoList.Count; i++) - { - BulletInfo binfo = BulletInfo.bullets[ammoList[i].ToString()]; - ParseBulletFuzeType(binfo.fuzeType); - output.AppendLine($"Bullet type: {ammoList[i]}"); - output.AppendLine($"Bullet mass: {Math.Round(binfo.bulletMass, 2)} kg"); - output.AppendLine($"Muzzle velocity: {Math.Round(binfo.bulletVelocity, 2)} m/s"); - output.AppendLine($"Explosive: {binfo.explosive}"); - if (binfo.subProjectileCount > 1) - { - output.AppendLine($"Cannister Round"); - output.AppendLine($" - Submunition count: {binfo.subProjectileCount}"); - } - if (binfo.explosive) - { - output.AppendLine($"Blast:"); - output.AppendLine($"- tnt mass: {Math.Round(binfo.tntMass, 3)} kg"); - output.AppendLine($"- radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(binfo.tntMass), 2)} m"); - output.AppendLine($"Air detonation: {airDetonation}"); - if (airDetonation) - { - output.AppendLine($"- auto timing: {airDetonationTiming}"); - output.AppendLine($"- max range: {maxAirDetonationRange} m"); - } - } - output.AppendLine(""); - } - } - if (weaponType == "rocket") - { - for (int i = 0; i < ammoList.Count; i++) - { - RocketInfo rinfo = RocketInfo.rockets[ammoList[i].ToString()]; - output.AppendLine($"Rocket type: {ammoList[i]}"); - output.AppendLine($"Rocket mass: {Math.Round(rinfo.rocketMass, 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) - { - 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}"); - } - output.AppendLine(""); - if (rinfo.subProjectileCount > 1) - { - output.AppendLine($"Cluster Rocket"); - output.AppendLine($" - Submunition count: {rinfo.subProjectileCount}"); - } - } - if (externalAmmo) - { - output.AppendLine($"Uses External Ammo"); - } - - } - } - output.AppendLine(""); - if (BurstFire) - { - output.AppendLine($"Burst Fire Weapon"); - output.AppendLine($" - Rounds Per Burst: {RoundsPerMag}"); - } - if (!BeltFed && !BurstFire) - { - output.AppendLine($" Reloadable"); - output.AppendLine($" - Shots before Reload: {RoundsPerMag}"); - output.AppendLine($" - Reload Time: {ReloadTime}"); - } - if (crewserved) - { - output.AppendLine($"Crew-served Weapon - Requires onboard Kerbal"); - } - return output.ToString(); - } - - #endregion RMB Info - } - - #region UI //borrowing code from ModularMissile GUI - - [KSPAddon(KSPAddon.Startup.EditorAny, false)] - public class WeaponGroupWindow : MonoBehaviour - { - internal static EventVoid OnActionGroupEditorOpened = new EventVoid("OnActionGroupEditorOpened"); - internal static EventVoid OnActionGroupEditorClosed = new EventVoid("OnActionGroupEditorClosed"); - - private static GUIStyle unchanged; - private static GUIStyle changed; - private static GUIStyle greyed; - private static GUIStyle overfull; - - private static WeaponGroupWindow instance; - private static Vector3 mousePos = Vector3.zero; - - private bool ActionGroupMode; - - private Rect guiWindowRect = new Rect(0, 0, 0, 0); - - private ModuleWeapon WPNmodule; - - [KSPField] public int offsetGUIPos = -1; - - private Vector2 scrollPos; - - [KSPField(isPersistant = false, guiActiveEditor = true, guiActive = false, guiName = "#LOC_BDArmory_ShowGroupEditor"), UI_Toggle(enabledText = "#LOC_BDArmory_ShowGroupEditor_enabledText", disabledText = "#LOC_BDArmory_ShowGroupEditor_disabledText")] [NonSerialized] public bool showRFGUI;//Show Group Editor--close Group GUI--open Group GUI - - private bool styleSetup; - - private string txtName = string.Empty; - - public static void HideGUI() - { - if (instance != null && instance.WPNmodule != null) - { - instance.WPNmodule.WeaponName = instance.WPNmodule.shortName; - instance.WPNmodule = null; - instance.UpdateGUIState(); - } - EditorLogic editor = EditorLogic.fetch; - if (editor != null) - editor.Unlock("BD_MN_GUILock"); - } - - public static void ShowGUI(ModuleWeapon WPNmodule) - { - if (instance != null) - { - instance.WPNmodule = WPNmodule; - instance.UpdateGUIState(); - } - } - - private void UpdateGUIState() - { - enabled = WPNmodule != null; - EditorLogic editor = EditorLogic.fetch; - if (!enabled && editor != null) - editor.Unlock("BD_MN_GUILock"); - } - - private IEnumerator CheckActionGroupEditor() - { - while (EditorLogic.fetch == null) - { - yield return null; - } - EditorLogic editor = EditorLogic.fetch; - while (EditorLogic.fetch != null) - { - if (editor.editorScreen == EditorScreen.Actions) - { - if (!ActionGroupMode) - { - HideGUI(); - OnActionGroupEditorOpened.Fire(); - } - EditorActionGroups age = EditorActionGroups.Instance; - if (WPNmodule && !age.GetSelectedParts().Contains(WPNmodule.part)) - { - HideGUI(); - } - ActionGroupMode = true; - } - else - { - if (ActionGroupMode) - { - HideGUI(); - OnActionGroupEditorClosed.Fire(); - } - ActionGroupMode = false; - } - yield return null; - } - } - - private void Awake() - { - enabled = false; - instance = this; - } - - private void OnDestroy() - { - instance = null; - } - - public void OnGUI() - { - if (!styleSetup) - { - styleSetup = true; - Styles.InitStyles(); - } - - EditorLogic editor = EditorLogic.fetch; - if (!HighLogic.LoadedSceneIsEditor || !editor) - { - return; - } - bool cursorInGUI = false; // nicked the locking code from Ferram - mousePos = Input.mousePosition; //Mouse location; based on Kerbal Engineer Redux code - mousePos.y = Screen.height - mousePos.y; - - int posMult = 0; - if (offsetGUIPos != -1) - { - posMult = offsetGUIPos; - } - if (ActionGroupMode) - { - if (guiWindowRect.width == 0) - { - guiWindowRect = new Rect(430 * posMult, 365, 438, 50); - } - new Rect(guiWindowRect.xMin + 440, mousePos.y - 5, 300, 20); - } - else - { - if (guiWindowRect.width == 0) - { - //guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, (Screen.height - 365)); - guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, 50); - } - new Rect(guiWindowRect.xMin - (230 - 8), mousePos.y - 5, 220, 20); - } - cursorInGUI = guiWindowRect.Contains(mousePos); - if (cursorInGUI) - { - editor.Lock(false, false, false, "BD_MN_GUILock"); - //if (EditorTooltip.Instance != null) - // EditorTooltip.Instance.HideToolTip(); - } - else - { - editor.Unlock("BD_MN_GUILock"); - } - guiWindowRect = GUILayout.Window(GetInstanceID(), guiWindowRect, GUIWindow, "Weapon Group GUI", Styles.styleEditorPanel); - } - - public void GUIWindow(int windowID) - { - InitializeStyles(); - - GUILayout.BeginVertical(); - GUILayout.Space(20); - - GUILayout.BeginHorizontal(); - - GUILayout.Label("Add to Weapon Group: "); - - txtName = GUILayout.TextField(txtName); - - if (GUILayout.Button("Save & Close")) - { - string newName = string.IsNullOrEmpty(txtName.Trim()) ? WPNmodule.OriginalShortName : txtName.Trim(); - - WPNmodule.WeaponName = newName; - WPNmodule.shortName = newName; - instance.WPNmodule.HideUI(); - } - - GUILayout.EndHorizontal(); - - scrollPos = GUILayout.BeginScrollView(scrollPos); - - GUILayout.EndScrollView(); - - GUILayout.EndVertical(); - - GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref guiWindowRect); - } - - private static void InitializeStyles() - { - if (unchanged == null) - { - if (GUI.skin == null) - { - unchanged = new GUIStyle(); - changed = new GUIStyle(); - greyed = new GUIStyle(); - overfull = new GUIStyle(); - } - else - { - unchanged = new GUIStyle(GUI.skin.textField); - changed = new GUIStyle(GUI.skin.textField); - greyed = new GUIStyle(GUI.skin.textField); - overfull = new GUIStyle(GUI.skin.label); - } - - unchanged.normal.textColor = Color.white; - unchanged.active.textColor = Color.white; - unchanged.focused.textColor = Color.white; - unchanged.hover.textColor = Color.white; - - changed.normal.textColor = Color.yellow; - changed.active.textColor = Color.yellow; - changed.focused.textColor = Color.yellow; - changed.hover.textColor = Color.yellow; - - greyed.normal.textColor = Color.gray; - - overfull.normal.textColor = Color.red; - } - } - } - - #endregion UI //borrowing code from ModularMissile GUI -} diff --git a/BDArmory/Modules/_description b/BDArmory/Modules/_description new file mode 100644 index 000000000..dc49f8717 --- /dev/null +++ b/BDArmory/Modules/_description @@ -0,0 +1,2 @@ +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/Properties/AssemblyInfo.cs b/BDArmory/Properties/AssemblyInfo.cs index ab858fb3d..2447f2e87 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.0.3")] -[assembly: AssemblyFileVersion("1.4.0.3")] +[assembly: AssemblyVersion("1.12.0.0")] +[assembly: AssemblyFileVersion("1.12.0.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..385e4df50 --- /dev/null +++ b/BDArmory/Radar/ModuleIRST.cs @@ -0,0 +1,529 @@ +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; + + [KSPField] + public string irstTransformName = string.Empty; + Transform irstTransform; + + public Vector3 irstForward + { + get { return irstTransform.up; } + } + + #endregion General Configuration + + #region Capabilities + + [KSPField] + public double resourceDrain = 0.825; //resource (EC/sec) usage of active irst + + [KSPField] + public string resourceName = "ElectricCharge"; + + private int resourceID; + + [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.IsPrimaryWM || wpmr.vessel != vessel) + wpmr = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return wpmr; + } + } + + 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 + } + void Start() + { + resourceID = PartResourceLibrary.Instance.GetDefinition(resourceName).id; + } + + public void EnsureVesselRadarData() + { + if (vessel == null) return; + //myVesselID = vessel.id.ToString(); + + if (vesselRadarData != null && vesselRadarData.vessel == vessel && vesselRadarData.weaponManager == WeaponManager) return; + + vesselRadarData = vessel.gameObject.GetComponent(); + if (vesselRadarData == null) + vesselRadarData = vessel.gameObject.AddComponent(); + + vesselRadarData.weaponManager = WeaponManager; + } + + public void EnableIRST() + { + EnsureVesselRadarData(); + irstEnabled = true; + + UpdateToggleGuiName(); + vesselRadarData.AddIRST(this); + var weaponManager = WeaponManager; + if (weaponManager != null) + { + weaponManager._irstsEnabled = true; + } + } + + public void DisableIRST() + { + irstEnabled = false; + UpdateToggleGuiName(); + + if (vesselRadarData) + { + vesselRadarData.RemoveIRST(this); + } + var weaponManager = WeaponManager; + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + BDATargetManager.ClearRadarReport(loadedvessels.Current, weaponManager); //reset radar contact status + } + if (weaponManager != null) + { + if (weaponManager.irsts.Count > 1) + { + using (List.Enumerator irst = weaponManager.irsts.GetEnumerator()) + while (irst.MoveNext()) + { + if (irst.Current == null) continue; + weaponManager._irstsEnabled = false; + if (irst.Current != this && irst.Current.irstEnabled) + { + weaponManager._irstsEnabled = true; + break; + } + } + } + else weaponManager._irstsEnabled = false; + } + } + + 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); + } + irstTransform = irstTransformName != string.Empty ? part.FindModelTransform(irstTransformName) : part.transform; + referenceTransform = (new GameObject()).transform; + referenceTransform.parent = irstTransform; + 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(irstTransform.position, vessel.mainBody), + VectorUtils.GetUpDirection(transform.position)); + } + else + { + referenceTransform.position = part.transform.position; + referenceTransform.rotation = Quaternion.LookRotation(irstTransform.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(resourceID, drainAmount, ResourceFlowMode.ALL_VESSEL); + if (chargeAvailable < drainAmount * 0.95f) + { + ScreenMessages.PostScreenMessage($"{part.partInfo.title} {StringUtils.Localize("#autoLOC_244332")} {PartResourceLibrary.Instance.GetDefinition(resourceName).displayName}", 5.0f, ScreenMessageStyle.UPPER_CENTER); // [part Title] Requires [localized resource name] + DisableIRST(); + } + } + } +} diff --git a/BDArmory/Radar/ModuleRadar.cs b/BDArmory/Radar/ModuleRadar.cs new file mode 100644 index 000000000..22649e752 --- /dev/null +++ b/BDArmory/Radar/ModuleRadar.cs @@ -0,0 +1,1575 @@ +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 ModuleRadar : PartModule + { + #region KSPFields (Part Configuration) + + #region General Configuration + + [KSPField] + public string radarName; + + [KSPField] + public int turretID = 0; + + [KSPField] + public string rotationTransformName = string.Empty; + Transform rotationTransform; + + [KSPField] + public string radarTransformName = string.Empty; + Transform radarTransform; + + #endregion General Configuration + + #region Radar Capabilities + + [KSPField] + public int rwrThreatType = 0; //IMPORTANT, configures which type of radar it will show up as on the RWR + public RadarWarningReceiver.RWRThreatTypes rwrType = RadarWarningReceiver.RWRThreatTypes.SAM; + + [KSPField] + public double resourceDrain = 0.825; //resource (EC/sec) usage of active radar + + [KSPField] + public string resourceName = "ElectricCharge"; + + private int resourceID; + + [KSPField] + public bool omnidirectional = true; //false=scan FoV limited to directionalFieldOfView + + // NOTE: The radar is assumed to have full roll stabilization capabilities! + [KSPField] + public string directionalFieldOfView = "90"; //relevant for NON-omnidirectional only + + [KSPField] + public string elevationFOV = "-1f"; //FoV of the radar in the vertical axis + + public float radarAzOffset = 0f; + public float radarAzFOV = 90f; + public float[] radarAzLimits = [-45f, 45f]; + public float radarElOffset = 0f; + public float radarElFOV = 90f; + public float[] radarElLimits = [-45f, 45f]; + + public float[] radarMinMaxAzLimits = [-45f, 45f]; + public float[] radarMinMaxElLimits = [-45f, 45f]; + + [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 float lockRotationSpeed = 120; //in degrees per second, relevant for omni only + + [KSPField] + public float lockRotationAngle = 4; //??? + + [KSPField] + public bool showDirectionWhileScan = false; //radar can show direction indicator of contacts (false: can show contacts as blocks only) + + [KSPField] + public float multiLockFOV = 30; //?? + + [KSPField] + public float lockAttemptFOV = 2; //?? + + [KSPField] + public bool canScan = true; //radar has detection capabilities + + [KSPField] + public bool canLock = true; //radar has locking/tracking capabilities + + [KSPField] + public int maxLocks = 1; //how many targets can be locked/tracked simultaneously + + [KSPField] + public bool canTrackWhileScan = false; //when tracking/locking, can we still detect/scan? + + [KSPField] + 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 + + [KSPField] + public FloatCurve radarLockTrackCurve = new FloatCurve(); //FloatCurve defining at what range which RCS size can be locked/tracked + + [KSPField] + public FloatCurve radarVelocityGate = new FloatCurve(); //FloatCurve defining the reduction in received RCS due to a doppler gate + + [KSPField] + public FloatCurve radarRangeGate = new FloatCurve(); //FloatCurve defining the reduction in received RCS due to a range gate + + [KSPField] + public float radarMinTrackSCR = 1f; + + [KSPField] + public bool radarCanNotch = true; + + [KSPField] + public float radarGroundClutterFactor = 0.25f; //Factor defining how effective the radar is for look-down, compensating for ground clutter (0=ineffective, 1=fully effective) + //default to 0.25, so all cross sections of landed/splashed/submerged vessels are reduced to 1/4th, as these vessel usually a quite large + [KSPField] + public float radarChaffClutterFactor = 1.0f; //Factor defining how effective the radar is at compensating for enemy chaff (0 = ineffective, 1 = no decrease in signal position/strength) + //default to 1, since that's legacy behavior. Relevant for guiding SARH ordnance. + [KSPField] + public int sonarType = 0; //0 = Radar; 1 == Active Sonar; 2 == Passive Sonar + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicRadar", advancedTweakable = true),//Disable Radar vs ARMs + UI_Toggle(enabledText = "#LOC_BDArmory_true", disabledText = "#LOC_BDArmory_false", scene = UI_Scene.All),]//Starboard (CW)--Port (CCW) + public bool DynamicRadar = false; + + public enum SonarModes + { + None = 0, + Active = 1, + passive = 2 + } + public SonarModes sonarMode = SonarModes.None; + + #endregion Radar Capabilities + + #region Persisted State in flight + + [KSPField(isPersistant = true)] + public string linkedVesselID; + + [KSPField(isPersistant = true)] + public bool radarEnabled; + + [KSPField(isPersistant = true)] + public int rangeIndex = 99; + + [KSPField(isPersistant = true)] + public float currentAngle; + + private float ReferenceUpdateTime = -1f; + public float TimeSinceReferenceUpdate => Time.fixedTime - ReferenceUpdateTime; + + // Variables to pre-calculate transform directions + public Vector3 currPosition; + public Vector3 currForward; + public Vector3 currUp; + public Vector3 currRight; + + private float DisplayUpdateTime = -1f; + public float TimeSinceDisplayUpdate => Time.fixedTime - DisplayUpdateTime; + + // Rotated forward vector according to azimuth and elevation + // offsets for display purposes + public Vector3 currDisplayForward; + + #endregion Persisted State in flight + + #region DEPRECATED! ->see Radar Capabilities section for new detectionCurve + trackingCurve + + [Obsolete] + [KSPField] + public float minSignalThreshold = 90; + + [Obsolete] + [KSPField] + public float minLockedSignalThreshold = 90; + + #endregion DEPRECATED! ->see Radar Capabilities section for new detectionCurve + trackingCurve + + #endregion KSPFields (Part Configuration) + + #region KSP Events & Actions + + [KSPAction("Toggle Radar")] + public void AGEnable(KSPActionParam param) + { + if (radarEnabled) + { + DisableRadar(); + } + else + { + EnableRadar(); + } + } + + [KSPEvent(active = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ToggleRadar")]//Toggle Radar + public void Toggle() + { + if (radarEnabled) + { + DisableRadar(); + } + else + { + EnableRadar(); + } + } + + [KSPAction("Target Next")] + public void TargetNext(KSPActionParam param) + { + vesselRadarData.TargetNext(); + } + + [KSPAction("Target Prev")] + public void TargetPrev(KSPActionParam param) + { + vesselRadarData.TargetPrev(); + } + + #endregion KSP Events & Actions + + #region Part members + + //locks + [KSPField(isPersistant = false, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_CurrentLocks")]//Current Locks + public int currLocks; + + public bool locked + { + get { return currLocks > 0; } + } + + public int currentLocks + { + get { return currLocks; } + } + + private TargetSignatureData[] attemptedLocks; + //private bool[] lockSuccesses; // Removed as it was deemed unecessary + private List lockedTargets; + + public TargetSignatureData lockedTarget + { + get + { + if (currLocks == 0) return TargetSignatureData.noTarget; + else + { + return lockedTargets[lockedTargetIndex]; + } + } + } + + private int lockedTargetIndex; + + public int currentLockIndex + { + get { return lockedTargetIndex; } + } + + public float radarMinDistanceDetect + { + get { return radarDetectionCurve.minTime; } + } + + //[KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "Detection Range")] + public float radarMaxDistanceDetect + { + get { return radarDetectionCurve.maxTime; } + } + + public float radarMinDistanceLockTrack + { + get { return radarLockTrackCurve.minTime; } + } + + //[KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "Locking Range")] + public float radarMaxDistanceLockTrack + { + get { return radarLockTrackCurve.maxTime; } + } + + public float radarMaxRangeGate + { + get { return radarRangeGate.maxTime; } + } + public float radarMinRangeGate + { + get { return radarRangeGate.minTime; } + } + + public float radarMaxVelocityGate + { + get { return radarVelocityGate.maxTime; } + } + + public float radarMinVelocityGate + { + get { return radarVelocityGate.minTime; } + } + + //linked vessels + private List linkedToVessels; + public int linkedVRDs + { + get { return linkedToVessels.Count; } + } + public List availableRadarLinks; + private bool unlinkOnDestroy = true; + + //GUI + private bool drawGUI; + public float signalPersistTime; + public float signalPersistTimeForRwr; + + //scanning + private float currentAngleLock; + public Transform referenceTransform; + private float radialScanDirection = 1; + private float lockScanDirection = 1; + + public bool boresightScan; + + //locking + public float lockScanAngle; + public bool slaveTurrets; + public ModuleTurret lockingTurret; + + // lockingPitch and lockingYaw are toggles for whether or not the radar should be able to control the lockingTurret's pitch/yaw + // this is associated with MissileTurret and ModuleWeapon turrets, if the radar is mounted on a turret. + public bool lockingPitch = true; + public bool lockingYaw = true; + + //vessel + private MissileFire wpmr; + + public MissileFire WeaponManager + { + get + { + if (wpmr == null || !wpmr.IsPrimaryWM || wpmr.vessel != vessel) + wpmr = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return wpmr; + } + } + + public VesselRadarData vesselRadarData; + private string myVesselID; + + // part state + private bool startupComplete; + public float leftLimit; + public float rightLimit; + private int snapshotTicker; + + #endregion Part members + + void UpdateToggleGuiName() + { + Events["Toggle"].guiName = radarEnabled ? StringUtils.Localize("#autoLOC_bda_1000000") : StringUtils.Localize("#autoLOC_bda_1000001"); // #autoLOC_bda_1000000 = Disable Radar // #autoLOC_bda_1000001 = Enable Radar + } + void Start() + { + resourceID = PartResourceLibrary.Instance.GetDefinition(resourceName).id; + } + + public void EnsureVesselRadarData(bool addRadar = false) + { + if (vessel == null) return; + //myVesselID = vessel.id.ToString(); + + bool swappedVessels = false; + if (vesselRadarData == null || (swappedVessels = (vesselRadarData.vessel != vessel)) || vesselRadarData.weaponManager != WeaponManager) + { + // Technically it would be better if we linked to the previous vessel here, but theoretically speaking, + // if guard mode is enabled post-decouple on the child craft it should automatically datalink with all + // available VRDs post swap taking care of this. If we do want to ensure this functions properly even + // without guard mode being enabled post decouple we would add a `QueueVRDLink(vrd)` function to + // vesselRadarData, save the previous VRD in this if statement, and then queue the link + if (swappedVessels) + vesselRadarData.RemoveRadar(this); + + vesselRadarData = vessel.gameObject.GetComponent(); + if (vesselRadarData == null) + vesselRadarData = vessel.gameObject.AddComponent(); + + vesselRadarData.weaponManager = WeaponManager; + + // Something wasn't right with the previous VRD so make sure we add the radar, primarily to take care of the multi-craft case + addRadar = true; + } + + if (addRadar && radarEnabled) + vesselRadarData.AddRadar(this); + } + + public void EnableRadar() + { + radarEnabled = true; + EnsureVesselRadarData(true); + + UpdateToggleGuiName(); + //vesselRadarData.AddRadar(this); // Moved this to EnsureVesselRadarData() to account for the multi-craft case + var wm = WeaponManager; + if (wm != null) + { + if (wm.guardMode) vesselRadarData.queueLinks = true; + if (sonarMode == SonarModes.None) + wm._radarsEnabled = true; + else if (sonarMode == SonarModes.Active) + wm._sonarsEnabled = true; + } + } + + public void DisableRadar() + { + if (locked) + { + UnlockAllTargets(); + } + + radarEnabled = false; + UpdateToggleGuiName(); + + if (vesselRadarData) + { + vesselRadarData.RemoveRadar(this); + } + + List.Enumerator vrd = linkedToVessels.GetEnumerator(); + while (vrd.MoveNext()) + { + if (vrd.Current == null) continue; + vrd.Current.UnlinkDisabledRadar(this); + } + vrd.Dispose(); + var weaponManager = WeaponManager; + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + BDATargetManager.ClearRadarReport(loadedvessels.Current, weaponManager); //reset radar contact status + } + if (weaponManager != null) + { + if (weaponManager.radars.Count > 1) + { + bool detectorsEnabled = false; + using (List.Enumerator rd = weaponManager.radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current == null || rd.Current.sonarMode != sonarMode) continue; + //mf._radarsEnabled = false; + detectorsEnabled = false; + if (rd.Current != this && rd.Current.radarEnabled) + { + //mf._radarsEnabled = true; + detectorsEnabled = true; + break; + } + } + + if (sonarMode == SonarModes.None) + weaponManager._radarsEnabled = detectorsEnabled; + else if (sonarMode == SonarModes.Active) + weaponManager._sonarsEnabled = detectorsEnabled; + } + else + { + if (sonarMode == SonarModes.None) + weaponManager._radarsEnabled = false; + else if (sonarMode == SonarModes.Active) + weaponManager._sonarsEnabled = false; + } + } + } + + void OnDestroy() + { + if (HighLogic.LoadedSceneIsFlight) + { + if (vesselRadarData) + { + vesselRadarData.RemoveRadar(this); + vesselRadarData.RemoveDataFromRadar(this); + } + + referenceTransform = null; + + if (linkedToVessels != null) + { + List.Enumerator vrd = linkedToVessels.GetEnumerator(); + while (vrd.MoveNext()) + { + if (vrd.Current == null) continue; + if (unlinkOnDestroy) + { + vrd.Current.UnlinkDisabledRadar(this); + } + else + { + vrd.Current.BeginWaitForUnloadedLinkedRadar(this, myVesselID); + } + } + vrd.Dispose(); + } + } + } + + public void ParseRadarLimits(in string radarLimitString, out float radarOffset, out float radarFOV, out float[] radarLimits, out float[] radarMinMaxLimits, bool elevationLimits = false) + { + // If we're parsing elevation limits + if (elevationLimits) + { + // Then consider if it's omnidirectional or not, by default, omni radars are allowed +/- 90° FoV + // Otherwise the default is a square radar scan area (based on radarAZLimits) + radarLimits = omnidirectional ? [-90f, 90f] : [radarAzLimits[0], radarAzLimits[1]]; + //radarMinMaxLimits = omnidirectional ? [90f, 90f] : [radarMinMaxAzLimits[0], radarMinMaxAzLimits[1]]; + // Even if the azimuth is offset, we should start with no offset for elevation + radarMinMaxLimits = omnidirectional ? [90f, 90f] : [0.5f * radarAzFOV, 0.5f * radarAzFOV]; + // Default omnidirectional FoV is 180° + radarFOV = omnidirectional ? 180f : radarAzFOV; + } + else + { + // If we're not parsing elevation limits, then default to a +/- 45° FoV + radarLimits = [-45f, 45f]; + radarMinMaxLimits = [45f, 45f]; + radarFOV = 90f; + } + + // For both az/el the dfault is 0 offset + radarOffset = 0f; + + string[] limitStrings = radarLimitString.Split([',']); + if (limitStrings.Length > 0) + { + // If we're setting a left/right limit + if (limitStrings.Length > 1) + { + float tempLim = -45f; + // Get first limit + if (float.TryParse(limitStrings[0], out float temp)) + tempLim = temp; + // Get second limit + if (float.TryParse(limitStrings[1], out temp)) + { + // Test which limit should be which + if (tempLim < temp) + { + radarLimits[0] = tempLim; + radarLimits[1] = temp; + } + else + { + radarLimits[0] = temp; + radarLimits[1] = tempLim; + } + } + + radarMinMaxLimits[0] = Mathf.Min(Mathf.Abs(radarLimits[0]), Mathf.Abs(radarLimits[1])); + radarMinMaxLimits[1] = Mathf.Max(Mathf.Abs(radarLimits[0]), Mathf.Abs(radarLimits[1])); + + // Set the offset + radarOffset = (radarLimits[1] + radarLimits[0]) * 0.5f; + // Set the total width + radarFOV = radarLimits[1] - radarLimits[0]; + } + else + { + // Set total width + if (float.TryParse(limitStrings[0], out float temp)) + { + if (temp < 0f) + return; + radarFOV = temp; + } + + // Set left/right limits + radarLimits[1] = 0.5f * radarFOV; + radarLimits[0] = -radarLimits[1]; + + radarMinMaxLimits[0] = radarLimits[1]; + radarMinMaxLimits[1] = radarLimits[1]; + } + } + } + + public override void OnStart(StartState state) + { + base.OnStart(state); + + if (HighLogic.LoadedSceneIsFlight) + { + myVesselID = vessel.id.ToString(); + RadarUtils.SetupResources(); + + if (string.IsNullOrEmpty(radarName)) + { + radarName = part.partInfo.title; + } + + linkedToVessels = new List(); + + ParseRadarLimits(directionalFieldOfView, out radarAzOffset, out radarAzFOV, out radarAzLimits, out radarMinMaxAzLimits); + // Retain old radar characteristics, if omnidirectional the radar should be able to see targets at +/- 90, otherwise + // the radar could previously see targets at +/- 90 but not lock them, so we'll just lock it to a square FoV + ParseRadarLimits(elevationFOV, out radarElOffset, out radarElFOV, out radarElLimits, out radarMinMaxElLimits, !omnidirectional); + if (BDArmorySettings.DEBUG_RADAR) + { + Debug.Log($"[BDArmory.ModuleRadar] radarAzOffset {radarAzOffset}, radarAzFOV: {radarAzFOV}, radarAzLimits: {radarAzLimits[0]},{radarAzLimits[1]}, radarMinMaxAzLimits: {radarMinMaxAzLimits[0]},{radarMinMaxAzLimits[1]}"); + Debug.Log($"[BDArmory.ModuleRadar] radarElOffset {radarElOffset}, radarElFOV: {radarElFOV}, radarElLimits: {radarElLimits[0]},{radarElLimits[1]}, radarMinMaxAzLimits: {radarMinMaxElLimits[0]},{radarMinMaxElLimits[1]}"); + } + + signalPersistTime = omnidirectional + ? 360 / (scanRotationSpeed + 5) + : radarAzFOV / (scanRotationSpeed + 5); + + rwrType = (RadarWarningReceiver.RWRThreatTypes)rwrThreatType; + sonarMode = (SonarModes)sonarType; + if (rwrType == RadarWarningReceiver.RWRThreatTypes.Sonar) + signalPersistTimeForRwr = RadarUtils.ACTIVE_MISSILE_PING_PERISTS_TIME; + else + { + signalPersistTimeForRwr = signalPersistTime / 2; + } + + if (rotationTransformName != string.Empty) + { + rotationTransform = part.FindModelTransform(rotationTransformName); + } + radarTransform = radarTransformName != string.Empty ? part.FindModelTransform(radarTransformName) : part.transform; + + attemptedLocks = new TargetSignatureData[Math.Max(maxLocks, 6)]; + //lockSuccesses = new bool[maxLocks]; + TargetSignatureData.ResetTSDArray(ref attemptedLocks); + lockedTargets = new List(); + + referenceTransform = (new GameObject()).transform; + referenceTransform.parent = radarTransform; + referenceTransform.localPosition = Vector3.zero; + + 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; + } + } + + // check for not updated legacy part: + if ((canScan && (radarMinDistanceDetect == float.MaxValue)) || (canLock && (radarMinDistanceLockTrack == float.MaxValue))) + { + 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; + } + } + + /* + void OnGoOnRails(Vessel v) + { + if (v != vessel) return; + unlinkOnDestroy = false; + //myVesselID = vessel.id.ToString(); + } + */ + + IEnumerator StartUpRoutine() + { + if (BDArmorySettings.DEBUG_RADAR) + Debug.Log("[BDArmory.ModuleRadar]: StartupRoutine: " + radarName + " enabled: " + radarEnabled); + yield return new WaitWhile(() => !FlightGlobals.ready || vessel.packed || !vessel.loaded); + yield return new WaitForFixedUpdate(); + + // DISABLE RADAR + /* + if (radarEnabled) + { + EnableRadar(); + } + */ + + if (!vesselRadarData.hasLoadedExternalVRDs) + { + RecoverLinkedVessels(); + vesselRadarData.hasLoadedExternalVRDs = true; + } + + UpdateToggleGuiName(); + startupComplete = true; + } + + void Update() + { + drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && radarEnabled && + vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); + } + + public void UpdateReferenceTransform() + { + if (TimeSinceReferenceUpdate < Time.fixedDeltaTime) + return; + + if (omnidirectional) + { + referenceTransform.position = part.transform.position; + currPosition = referenceTransform.position; + referenceTransform.rotation = + Quaternion.LookRotation(VectorUtils.GetNorthVector(currPosition, vessel.mainBody), + VectorUtils.GetUpDirection(currPosition)); + } + else + { + referenceTransform.position = part.transform.position; + currPosition = referenceTransform.position; + // THIS IMPLEMENTS FULL ROLL STABILIZATION + // We assume the radar can *always* roll such that the up direction is the projection of + // the up vector onto the radarTransform up plane. + referenceTransform.rotation = Quaternion.LookRotation(radarTransform.up, + VectorUtils.GetUpDirection(currPosition).ProjectOnPlanePreNormalized(radarTransform.up).normalized); + } + currForward = referenceTransform.forward; + currUp = referenceTransform.up; + currRight = referenceTransform.right; + + currDisplayForward = currForward; + + ReferenceUpdateTime = Time.fixedTime; + } + + public void UpdateDisplayTransform() + { + if (TimeSinceDisplayUpdate < Time.fixedDeltaTime) + return; + UpdateReferenceTransform(); + + if (radarElOffset != 0 || radarAzOffset != 0) + currDisplayForward = Quaternion.AngleAxis(radarElOffset, currRight) * Quaternion.AngleAxis(-radarAzOffset, currUp) * currForward; + } + + void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && startupComplete) + { + if (!vessel.IsControllable && radarEnabled) + { + DisableRadar(); + } + + if (radarEnabled) + { + UpdateReferenceTransform(); + + DrainElectricity(); //physics behaviour, thus moved here from update + + if (locked) + { + for (int i = lockedTargets.Count - 1; i >= 0; --i) // We need to iterate backwards as UnlockTargetAt (in UpdateLock) can remove items from the lockedTargets list. + { + UpdateLock(i); + } + + if (canTrackWhileScan) + { + Scan(); + } + } + else if (boresightScan) + { + BoresightScan(); + } + else if (canScan) + { + Scan(); + } + } + //if (!vessel.packed && radarEnabled) + //{ + // //UpdateInputs(); + //} + } + } + + void UpdateSlaveData() + { + var weaponManager = WeaponManager; + if (slaveTurrets && weaponManager) + { + weaponManager.slavingTurrets = true; + if (locked) + { + weaponManager.slavedPosition = lockedTarget.predictedPosition; + weaponManager.slavedVelocity = lockedTarget.velocity; + weaponManager.slavedAcceleration = lockedTarget.acceleration; + weaponManager.slavedTarget = lockedTarget; + } + } + } + + void LateUpdate() + { + if (HighLogic.LoadedSceneIsFlight && (canScan || canLock)) + { + UpdateModel(); + } + } + + void UpdateModel() + { + //model rotation + if (radarEnabled) + { + if (rotationTransform && canScan) + { + Vector3 direction; + if (locked) + { + direction = + Quaternion.AngleAxis(canTrackWhileScan ? currentAngle : lockScanAngle, currUp) * + currForward; + } + else + { + direction = Quaternion.AngleAxis(currentAngle, currUp) * currForward; + } + + 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); + } + } + + //lock turret + if (lockingTurret && canLock) + { + if (locked) + { + lockingTurret.AimToTarget(lockedTarget.predictedPosition, lockingPitch, lockingYaw); + } + else + { + lockingTurret.ReturnTurret(); + } + } + } + else + { + if (rotationTransform) + { + rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, + Quaternion.identity, 5 * TimeWarp.fixedDeltaTime); + } + + if (lockingTurret) + { + lockingTurret.ReturnTurret(); + } + } + } + + void Scan() + { + float angleDelta = scanRotationSpeed * Time.fixedDeltaTime; + RadarUtils.RadarUpdateScanLock(WeaponManager, currentAngle, radarElOffset, angleDelta, radarElFOV, this, false, ref attemptedLocks); + + if (omnidirectional) + { + currentAngle = Mathf.Repeat(currentAngle + angleDelta, 360f); + } + else + { + currentAngle += radialScanDirection * angleDelta; + + if (locked) + { + // If we're locked, then get the angle to the target + float targetAngle = VectorUtils.GetAngleOnPlane(lockedTarget.position - currPosition, currForward, currRight); + + // And then set the left/right limits based on multiLockFOV, limited by the radarAzLimits + leftLimit = Mathf.Clamp(targetAngle - (multiLockFOV * 0.5f), radarAzLimits[0], + radarAzLimits[1]); + rightLimit = Mathf.Clamp(targetAngle + (multiLockFOV * 0.5f), radarAzLimits[0], + radarAzLimits[1]); + + if (radialScanDirection < 0 && currentAngle < leftLimit) + { + // If we're past the left limit, set the angle to the left limit and reverse the direction of the scan + currentAngle = leftLimit; + radialScanDirection = 1; + } + else if (radialScanDirection > 0 && currentAngle > rightLimit) + { + // If we're past the right limit, set the angle to the right limit and reverse the direction of the scan + currentAngle = rightLimit; + radialScanDirection = -1; + } + } + else + { + // If we're beyond the radar limits + if (Mathf.Abs(currentAngle - radarAzOffset) > radarAzFOV * 0.5f) + { + // Set current angle to either the left/right limit + currentAngle = currentAngle < 0f ? radarAzLimits[0] : radarAzLimits[1]; + // Reverse the scan direction + radialScanDirection = -radialScanDirection; + } + } + } + } + + public bool TryLockTarget(Vector3 position, Vessel targetVessel = null) + { + //need a way to see what companion radars on the craft have already locked, so multiple radars aren't stacking locks on the same couple target craft? Or is updating attemptedLocks to missileFire.maxradarLocks enough? + if (!canLock) + { + return false; + } + + if (BDArmorySettings.DEBUG_RADAR) + { + if (targetVessel == null) + Debug.Log("[BDArmory.ModuleRadar]: Trying to radar lock target with (" + radarName + ")"); + else + Debug.Log("[BDArmory.ModuleRadar]: Trying to radar lock target " + targetVessel.vesselName + " with (" + radarName + ")"); + } + + var weaponManager = WeaponManager; + if (currentLocks == maxLocks) + { + if (!weaponManager.guardMode || !ClearUnneededLocks()) + { + if (BDArmorySettings.DEBUG_RADAR) + Debug.Log("[BDArmory.ModuleRadar]: - Failed, this radar already has the maximum allowed targets locked."); + return false; + } + } + + // -------------------- IMPORTANT NOTE: -------------------- + // Currently ALL instances of `TryLockTarget()` are gated behind functions + // which perform the `UpdateReferenceTransform()` check beforehand! + // ANY NEW USES OF `TryLockTarget()` MUST FOLLOW THIS CONVENTION! + // If this is, for some reason, impossible, then this check may be + // uncommented, performance impact is minimal as a check is done first + // to determine if an update is needed based on fixedTime elapsed since + // the last update. + //UpdateReferenceTransform(); + + //Vector3 targetPlanarDirection = (position - referenceTransform.position).ProjectOnPlanePreNormalized(referenceTransform.up); + //float angle = VectorUtils.Angle(targetPlanarDirection, referenceTransform.forward); + + //if (referenceTransform.InverseTransformPoint(position).x < 0) + //{ + // angle = -angle; + //} + + // Since now we're concerned with azimuth and elevation, may as well use this function + //VectorUtils.GetAzimuthElevation(position - currPosition, currForward, currUp, out float azimuthAngle, out float elevationAngle); + Vector3 relativePosition = position - currPosition; + // Note this would typically be the wrong way around, however because our radar code uses + // negative angles for the left and positive angles for the right, may as well take advantage + // of that fact. + float azimuthAngle = VectorUtils.GetAngleOnPlane(relativePosition, currForward, currRight); + float elevationAngle = VectorUtils.GetElevation(relativePosition, currUp); + + TargetSignatureData.ResetTSDArray(ref attemptedLocks); + // Scan in the target direction + RadarUtils.RadarUpdateScanLock(weaponManager, azimuthAngle, elevationAngle, lockAttemptFOV, lockAttemptFOV, this, true, ref attemptedLocks, signalPersistTime); + + // Check the locks to see if we've detected the target + for (int i = 0; i < attemptedLocks.Length; i++) + { + if (attemptedLocks[i].exists && (attemptedLocks[i].predictedPosition - position).sqrMagnitude < 40.0f * 40.0f) //(lockSuccesses[i] && attemptedLocks[i].exists && (attemptedLocks[i].predictedPosition - position).sqrMagnitude < 40 * 40) + { + // If locked onto a vessel that was not our target, return false + if ((attemptedLocks[i].vessel != null) && (targetVessel != null) && (attemptedLocks[i].vessel != targetVessel)) + return false; + + if (!locked && !omnidirectional) + { + // Note this would typically give the opposite of the desired sign, but because radar convention is reversed this is correct. + float targetAngle = VectorUtils.GetAngleOnPlane((attemptedLocks[i].position - currPosition), currForward, currRight); + currentAngle = targetAngle; + } + lockedTargets.Add(attemptedLocks[i]); + currLocks = lockedTargets.Count; + + 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); + //vesselRadarData.UpdateLockedTargets(); + if (linkedToVessels.Count > 0) + foreach (VesselRadarData vrd in linkedToVessels) + { + if (vrd) + { + vrd.AddRadarContact(this, lockedTarget, true, true); + //vrd.UpdateLockedTargets(); + } + } + return true; + } + } + + if (BDArmorySettings.DEBUG_RADAR) + Debug.Log("[BDArmory.ModuleRadar]: - Failed to lock on target."); + + return false; + } + + void BoresightScan() + { + if (locked) + { + boresightScan = false; + return; + } + + currentAngle = Mathf.Lerp(currentAngle, 0, 0.08f); + RadarUtils.RadarUpdateScanBoresight(new Ray(currPosition, currForward), boresightFOV, ref attemptedLocks, Time.fixedDeltaTime, this); + + for (int i = 0; i < attemptedLocks.Length; i++) + { + if (!attemptedLocks[i].exists || !(attemptedLocks[i].age < 0.1f)) continue; + TryLockTarget(attemptedLocks[i].predictedPosition); + boresightScan = false; + return; + } + } + + /// + /// Checks if targetPosition is within the radar's FoV limits + /// + /// World target position. + /// Boolean value, true if the target is within the radar's FoV limits. + public bool CheckFOV(Vector3 targetPosition) + { + if (omnidirectional) + { + // Check elevation only, determine angle from the vertical axis + return (Mathf.Abs(VectorUtils.GetElevation(targetPosition - currPosition, currUp) - radarElOffset) < 0.5f * radarElFOV); + } + else + { + // Target exists and omnidirectional, we must check if we're within radar FoV + //VectorUtils.GetAzimuthElevation(targetPosition - currPosition, currForward, currUp, out float az, out float el); + Vector3 relativePosition = targetPosition - currPosition; + + // Radar azimuth is reversed, for whatever reason + float az = VectorUtils.GetAngleOnPlane(relativePosition, currForward, currRight); + float el = VectorUtils.GetElevation(relativePosition, currUp); + + // Check if we're outside FoV + return (Mathf.Abs(az - radarAzOffset) < 0.5f * radarAzFOV && Mathf.Abs(el - radarElOffset) < 0.5f * radarElFOV); + } + } + + /// + /// Checks if the direction vector is within the radar's FoV limits + /// + /// Target direction relative to radar (unit vector). + /// Boolean value, true if the target is within the radar's FoV limits. + public bool CheckFOVDir(Vector3 dir) + { + if (omnidirectional) + { + // Check elevation only, determine angle from the vertical axis + return (Mathf.Abs(VectorUtils.GetElevation(dir, currUp, 1.0f, 1.0f) - radarElOffset) < 0.5f * radarElFOV); + } + else + { + // Target exists and omnidirectional, we must check if we're within radar FoV + // Radar azimuth is reversed, for whatever reason + float az = VectorUtils.GetAngleOnPlane(dir, currForward, currRight); + float el = VectorUtils.GetElevation(dir, currUp, 1.0f, 1.0f); + + // Check if we're outside FoV + return (Mathf.Abs(az - radarAzOffset) < 0.5f * radarAzFOV && Mathf.Abs(el - radarElOffset) < 0.5f * radarElFOV); + } + } + + void UpdateLock(int index) + { + TargetSignatureData lockedTarget = lockedTargets[index]; + + Vector3 targetPlanarDirection = (lockedTarget.predictedPosition - currPosition).ProjectOnPlanePreNormalized(currUp); + float lookAngle = VectorUtils.Angle(targetPlanarDirection, currForward); + if (referenceTransform.InverseTransformPoint(lockedTarget.predictedPosition).x < 0) + { + lookAngle = -lookAngle; + } + + if (omnidirectional) + { + if (lookAngle < 0) lookAngle += 360; + } + + lockScanAngle = lookAngle + currentAngleLock; + if (!canTrackWhileScan && index == lockedTargetIndex) + { + currentAngle = lockScanAngle; + } + float angleDelta = lockRotationSpeed * Time.fixedDeltaTime; + float lockedSignalPersist = lockRotationAngle / lockRotationSpeed; + //RadarUtils.ScanInDirection(lockScanAngle, referenceTransform, angleDelta, referenceTransform.position, minLockedSignalThreshold, ref attemptedLocks, lockedSignalPersist); + bool radarSnapshot = (snapshotTicker > 30); + if (radarSnapshot) + { + snapshotTicker = 0; + } + else + { + snapshotTicker++; + } + //RadarUtils.ScanInDirection (new Ray (referenceTransform.position, lockedTarget.predictedPosition - referenceTransform.position), lockRotationAngle * 2, minLockedSignalThreshold, ref attemptedLocks, lockedSignalPersist, true, rwrType, radarSnapshot); + + Vector3 vectorToTarget = lockedTarget.position - currPosition; + if (VectorUtils.Angle(vectorToTarget, this.lockedTarget.position - currPosition) > multiLockFOV * 0.5f) + { + UnlockTargetAt(index, true); + return; + } + + if (!RadarUtils.RadarUpdateLockTrack( + new Ray(currPosition, lockedTarget.predictedPosition - currPosition), + lockedTarget.predictedPosition, lockRotationAngle * 2, this, lockedSignalPersist, true, index, lockedTarget.vessel)) + { + UnlockTargetAt(index, true); + return; + } + + //if still failed or out of FOV, unlock. + // MOVED FOV CHECK TO RadarUpdateLockTrack! + if (!lockedTarget.exists) + { + //UnlockAllTargets(); + UnlockTargetAt(index, true); + return; + } + + //unlock if over-jammed + // MOVED TO RADARUTILS! + + //cycle scan direction + if (index == lockedTargetIndex) + { + currentAngleLock += lockScanDirection * angleDelta; + if (Mathf.Abs(currentAngleLock) > lockRotationAngle / 2) + { + currentAngleLock = Mathf.Sign(currentAngleLock) * lockRotationAngle / 2; + lockScanDirection = -lockScanDirection; + } + } + } + + public void UnlockAllTargets() + { + if (!locked) return; + + lockedTargets.Clear(); + currLocks = 0; + lockedTargetIndex = 0; + + if (vesselRadarData) + { + vesselRadarData.UnlockAllTargetsOfRadar(this); + } + + if (linkedToVessels.Count > 0) + foreach (VesselRadarData vrd in linkedToVessels) + { + if (vrd) + vrd.UnlockAllTargetsOfRadar(this); + } + + if (BDArmorySettings.DEBUG_RADAR) + Debug.Log("[BDArmory.ModuleRadar]: Radar Targets were cleared (" + radarName + ")."); + } + + public void SetActiveLock(TargetSignatureData target) + { + for (int i = 0; i < lockedTargets.Count; i++) + { + if (target.vessel == lockedTargets[i].vessel) + { + lockedTargetIndex = i; + return; + } + } + } + + 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 (rVess == null) + tryRelock = false; + + if (tryRelock) + { + UnlockTargetAt(index, false); + if (rVess) + { + StartCoroutine(RetryLockRoutine(rVess)); + } + return; + } + + lockedTargets.RemoveAt(index); + currLocks = lockedTargets.Count; + if (lockedTargetIndex > index) + { + lockedTargetIndex--; + } + + lockedTargetIndex = Mathf.Clamp(lockedTargetIndex, 0, currLocks - 1); + lockedTargetIndex = Mathf.Max(lockedTargetIndex, 0); + + if (vesselRadarData) + { + //vesselRadarData.UnlockTargetAtPosition(position); + vesselRadarData.RemoveVesselFromLockedTargets(rVess); + } + if (linkedToVessels.Count > 0) + foreach (VesselRadarData vrd in linkedToVessels) + { + if (vrd) + vrd.RemoveVesselFromLockedTargets(rVess); + } + } + + IEnumerator RetryLockRoutine(Vessel v) + { + yield return new WaitForFixedUpdate(); + if (vesselRadarData != null && vesselRadarData.isActiveAndEnabled) + vesselRadarData.TryLockTarget(v); + } + + public void UnlockTargetVessel(Vessel v) + { + for (int i = 0; i < lockedTargets.Count; i++) + { + if (lockedTargets[i].vessel == v) + { + UnlockTargetAt(i); + return; + } + } + } + + public bool ClearUnneededLocks(bool unlockAll = false) + { + if (!unlockAll && (currentLocks < maxLocks)) + return true; + + bool cleared = false; + var weaponManager = WeaponManager; + for (int i = 0; i < lockedTargets.Count; i++) + { + if (weaponManager.GetMissilesAway(lockedTargets[i].targetInfo)[1] == 0) + { + UnlockTargetAt(i); + i--; + cleared = true; + if (!unlockAll) break; + } + } + + return cleared; + } + + public void RefreshLockArray() + { + if (wpmr != null) + { + attemptedLocks = new TargetSignatureData[wpmr.MaxRadarLocks]; + TargetSignatureData.ResetTSDArray(ref attemptedLocks); + //lockSuccesses = new bool[wpmr.MaxRadarLocks]; + } + } + + void SlaveTurrets() + { + using (var mtc = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mtc.MoveNext()) + { + if (mtc.Current == null) continue; + mtc.Current.slaveTurrets = false; + } + + using (var rad = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (rad.MoveNext()) + { + if (rad.Current == null) continue; + rad.Current.slaveTurrets = false; + } + + slaveTurrets = true; + } + + void UnslaveTurrets() + { + using (var mtc = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mtc.MoveNext()) + { + if (mtc.Current == null) continue; + mtc.Current.slaveTurrets = false; + } + + using (var rad = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (rad.MoveNext()) + { + if (rad.Current == null) continue; + rad.Current.slaveTurrets = false; + } + + var weaponManager = WeaponManager; + if (weaponManager) + { + weaponManager.slavingTurrets = false; + } + + slaveTurrets = false; + } + + public void UpdateLockedTargetInfo(TargetSignatureData newData) + { + int index = -1; + for (int i = 0; i < lockedTargets.Count; i++) + { + if (lockedTargets[i].vessel != newData.vessel) continue; + index = i; + break; + } + + if (index >= 0) + { + lockedTargets[index] = newData; + } + } + + public void ReceiveContactData(TargetSignatureData contactData, bool _locked) + { + if (vesselRadarData) + { + vesselRadarData.AddRadarContact(this, contactData, _locked); + } + + List.Enumerator vrd = linkedToVessels.GetEnumerator(); + while (vrd.MoveNext()) + { + if (vrd.Current == null) continue; + if (vrd.Current.canReceiveRadarData && vrd.Current.vessel != contactData.vessel) + { + vrd.Current.AddRadarContact(this, contactData, _locked, true); + } + } + vrd.Dispose(); + } + + public void AddExternalVRD(VesselRadarData vrd) + { + if (!linkedToVessels.Contains(vrd)) + { + linkedToVessels.Add(vrd); + } + } + + public void RemoveExternalVRD(VesselRadarData vrd) + { + linkedToVessels.Remove(vrd); + } + + void OnGUI() + { + if (drawGUI) + { + if (boresightScan) + { + GUIUtils.DrawTextureOnWorldPos(transform.position + (3500 * transform.up), + BDArmorySetup.Instance.dottedLargeGreenCircle, new Vector2(156, 156), 0); + } + } + } + + public void RecoverLinkedVessels() + { + string[] vesselIDs = linkedVesselID.Split(new char[] { ',' }); + for (int i = 0; i < vesselIDs.Length; i++) + { + StartCoroutine(RecoverLinkedVesselRoutine(vesselIDs[i])); + } + } + + IEnumerator RecoverLinkedVesselRoutine(string vesselID) + { + while (true) + { + using (var v = BDATargetManager.LoadedVessels.GetEnumerator()) + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded || v.Current == vessel || VesselModuleRegistry.IgnoredVesselTypes.Contains(v.Current.vesselType)) continue; + if (v.Current.id.ToString() != vesselID) continue; + VesselRadarData vrd = v.Current.gameObject.GetComponent(); + if (!vrd) continue; + StartCoroutine(RelinkVRDWhenReadyRoutine(vrd)); + yield break; + } + + yield return new WaitForSecondsFixed(0.5f); + } + } + + IEnumerator RelinkVRDWhenReadyRoutine(VesselRadarData vrd) + { + 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); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + vrd.vessel.vesselName); + } + + public string getRWRType(int i) + { + switch (i) + { + case 0: + return StringUtils.Localize("#autoLOC_bda_1000002"); // #autoLOC_bda_1000002 = SAM + + case 1: + return StringUtils.Localize("#autoLOC_bda_1000003"); // #autoLOC_bda_1000003 = FIGHTER + + case 2: + return StringUtils.Localize("#autoLOC_bda_1000004"); // #autoLOC_bda_1000004 = AWACS + + case 3: + case 4: + return StringUtils.Localize("#autoLOC_bda_1000005"); // #autoLOC_bda_1000005 = MISSILE + + case 5: + return StringUtils.Localize("#autoLOC_bda_1000006"); // #autoLOC_bda_1000006 = DETECTION + + case 6: + return StringUtils.Localize("#autoLOC_bda_1000017"); // #autoLOC_bda_1000017 = SONAR + } + 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 = (canReceiveRadarData && !canScan && !canLock); + + StringBuilder output = new StringBuilder(); + output.Append(Environment.NewLine); + 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(StringUtils.Localize("#autoLOC_bda_1000021", resourceDrain)); + if (!isLinkOnly) + { + if (!omnidirectional) + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000022", radarAzLimits[0], radarAzLimits[1])); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000041", radarElLimits[0], radarElLimits[1])); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000023", getRWRType(rwrThreatType))); + + output.Append(Environment.NewLine); + 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(StringUtils.Localize("#autoLOC_bda_1000028", maxLocks)); + } + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000029", canReceiveRadarData)); + + output.Append(Environment.NewLine); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000030")); + + if (canScan) + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000031", radarDetectionCurve.Evaluate(radarMaxDistanceDetect), radarMaxDistanceDetect)); + else + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000032")); + if (canLock) + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000033", radarLockTrackCurve.Evaluate(radarMaxDistanceLockTrack), radarMaxDistanceLockTrack)); + else + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000034")); + + if (sonarType == 1) + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000039")); + if (sonarType == 2) + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000040")); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000035", radarGroundClutterFactor)); + } + + return output.ToString(); + } + + 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) + { + ScreenMessages.PostScreenMessage($"{part.partInfo.title} {StringUtils.Localize("#autoLOC_244332")} {PartResourceLibrary.Instance.GetDefinition(resourceName).displayName}", 5.0f, ScreenMessageStyle.UPPER_CENTER); // [part Title] Requires [localized resource name] + DisableRadar(); + } + } + } + +} diff --git a/BDArmory/Radar/ModuleSpaceRadar.cs b/BDArmory/Radar/ModuleSpaceRadar.cs new file mode 100644 index 000000000..fdeed4a31 --- /dev/null +++ b/BDArmory/Radar/ModuleSpaceRadar.cs @@ -0,0 +1,23 @@ +using BDArmory.Extensions; + +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.InVacuum()) // 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..2c4593304 100644 --- a/BDArmory/Radar/RadarDisplayData.cs +++ b/BDArmory/Radar/RadarDisplayData.cs @@ -12,5 +12,16 @@ public struct RadarDisplayData public ModuleRadar detectedByRadar; public TargetSignatureData targetData; public float signalPersistTime; + public float velAngle; + public int jammedIndex; + } + 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 114e7e0be..74a421ec8 100644 --- a/BDArmory/Radar/RadarUtils.cs +++ b/BDArmory/Radar/RadarUtils.cs @@ -1,14 +1,19 @@ +using System; using System.Collections.Generic; +using UnityEngine; + using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; using BDArmory.CounterMeasure; -using BDArmory.Misc; -using BDArmory.Modules; +using BDArmory.Extensions; +using BDArmory.ModIntegration; +using BDArmory.Settings; using BDArmory.Shaders; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using BDArmory.Damage; namespace BDArmory.Radar { @@ -17,6 +22,8 @@ public static class RadarUtils private static bool rcsSetupCompleted = false; private static int radarResolution = 128; + private static bool hangarHiddenExternally = false; + private static RenderTexture rcsRenderingVariable; private static RenderTexture rcsRendering1; private static RenderTexture rcsRendering2; @@ -44,14 +51,14 @@ public static class RadarUtils private static Texture2D drawTextureVentral; public static Texture2D GetTextureVentral { get { return drawTextureVentral; } } - // additional anti-exploit 45� offset renderings + // additional anti-exploit 45° offset renderings private static Texture2D drawTextureFrontal45; public static Texture2D GetTextureFrontal45 { get { return drawTextureFrontal45; } } private static Texture2D drawTextureLateral45; public static Texture2D GetTextureLateral45 { get { return drawTextureLateral45; } } private static Texture2D drawTextureVentral45; public static Texture2D GetTextureVentral45 { get { return drawTextureVentral45; } } - + internal static float rcsFrontal; // internal so that editor analysis window has access to the details internal static float rcsLateral; // dito internal static float rcsVentral; // dito @@ -62,139 +69,357 @@ public static class RadarUtils internal static float rcsTotal; // dito - internal const float RCS_NORMALIZATION_FACTOR = 4.0f; //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; internal const float ACTIVE_MISSILE_PING_PERISTS_TIME = 0.2f; internal const float MISSILE_DEFAULT_LOCKABLE_RCS = 5f; + internal const float MISSILE_DEFAULT_GATE_RCS = 0.05f; // RCS Aspects - private static float[,] rcsAspects = new float[95, 2] { - { 2f, 23f}, - { 11f, 23f}, - { 19f, 23f}, - { 29f, 23f}, - { 41f, 23f}, - { 47f, 23f}, - { 59f, 23f}, - { 71f, 23f}, - { 79f, 23f}, - { 89f, 23f}, - { 101f, 23f}, - { 109f, 23f}, - { 127f, 23f}, - { 131f, 23f}, - { 139f, 23f}, - { 149f, 23f}, - { 163f, 23f}, - { 173f, 23f}, - { 179f, 23f}, - { 2f, 11f}, - { 11f, 11f}, - { 19f, 11f}, - { 29f, 11f}, - { 41f, 11f}, - { 47f, 11f}, - { 59f, 11f}, - { 71f, 11f}, - { 79f, 11f}, - { 89f, 11f}, - { 101f, 11f}, - { 109f, 11f}, - { 127f, 11f}, - { 131f, 11f}, - { 139f, 11f}, - { 149f, 11f}, - { 163f, 11f}, - { 173f, 11f}, - { 179f, 11f}, - { 2f, 0f}, - { 11f, 0f}, - { 19f, 0f}, - { 29f, 0f}, - { 41f, 0f}, - { 47f, 0f}, - { 59f, 0f}, - { 71f, 0f}, - { 79f, 0f}, - { 89f, 0f}, - { 101f, 0f}, - { 109f, 0f}, - { 127f, 0f}, - { 131f, 0f}, - { 139f, 0f}, - { 149f, 0f}, - { 163f, 0f}, - { 173f, 0f}, - { 179f, 0f}, - { 2f, -11f}, - { 11f, -11f}, - { 19f, -11f}, - { 29f, -11f}, - { 41f, -11f}, - { 47f, -11f}, - { 59f, -11f}, - { 71f, -11f}, - { 79f, -11f}, - { 89f, -11f}, - { 101f, -11f}, - { 109f, -11f}, - { 127f, -11f}, - { 131f, -11f}, - { 139f, -11f}, - { 149f, -11f}, - { 163f, -11f}, - { 173f, -11f}, - { 179f, -11f}, - { 2f, -23f}, - { 11f, -23f}, - { 19f, -23f}, - { 29f, -23f}, - { 41f, -23f}, - { 47f, -23f}, - { 59f, -23f}, - { 71f, -23f}, - { 79f, -23f}, - { 89f, -23f}, - { 101f, -23f}, - { 109f, -23f}, - { 127f, -23f}, - { 131f, -23f}, - { 139f, -23f}, - { 149f, -23f}, - { 163f, -23f}, - { 173f, -23f}, - { 179f, -23f} - }; - private static int numAspects = rcsAspects.GetLength(0); // Number of aspects - public static float[,] worstRCSAspects = new float[3, 3]; // Worst three aspects + private static float[,] rcsAspectsConstant = new float[45, 2] { + { 2.000f, -6.133f}, + { 6.000f, 6.133f}, + { 10.000f, -14.311f}, + { 14.000f, 13.289f}, + { 18.000f, 0.000f}, + { 22.000f, 19.422f}, + { 26.000f, -9.200f}, + { 30.000f, 9.200f}, + { 34.000f, -21.467f}, + { 38.000f, -4.089f}, + { 42.000f, 15.333f}, + { 46.000f, -16.356f}, + { 50.000f, 4.089f}, + { 54.000f, -11.244f}, + { 58.000f, 21.467f}, + { 62.000f, -2.044f}, + { 66.000f, 11.244f}, + { 70.000f, -19.422f}, + { 74.000f, 2.044f}, + { 78.000f, 17.378f}, + { 82.000f, -7.156f}, + { 86.000f, -22.489f}, + { 90.000f, 7.156f}, + { 94.000f, -13.289f}, + { 98.000f, 14.311f}, + { 102.000f, -1.022f}, + { 106.000f, 22.489f}, + { 110.000f, -10.222f}, + { 114.000f, 10.222f}, + { 118.000f, -18.400f}, + { 122.000f, 18.400f}, + { 126.000f, -5.111f}, + { 130.000f, 5.111f}, + { 134.000f, -15.333f}, + { 138.000f, 12.267f}, + { 142.000f, -8.178f}, + { 146.000f, 20.444f}, + { 150.000f, 1.022f}, + { 154.000f, -20.444f}, + { 158.000f, 8.178f}, + { 162.000f, -12.267f}, + { 166.000f, 16.356f}, + { 170.000f, -3.067f}, + { 174.000f, -17.378f}, + { 178.000f, 3.067f} + }; + private static float[,] rcsAspectsRealTime = new float[107, 2] { + { 0f, 0f}, + { 90f, 0f}, + { 180f, 0f}, + { 11.25f, 0f}, + { 22.5f, 0f}, + { 33.75f, 0f}, + { 45f, 0f}, + { 56.25f, 0f}, + { 67.5f, 0f}, + { 78.75f, 0f}, + { 101.25f, 0f}, + { 112.5f, 0f}, + { 123.75f, 0f}, + { 135f, 0f}, + { 146.25f, 0f}, + { 157.5f, 0f}, + { 168.75f, 0f}, + { 5.625f, 8.42105f}, + { 5.625f, -8.42105f}, + { 16.875f, 8.42105f}, + { 16.875f, -8.42105f}, + { 28.125f, 8.42105f}, + { 28.125f, -8.42105f}, + { 39.375f, 8.42105f}, + { 39.375f, -8.42105f}, + { 50.625f, 8.42105f}, + { 50.625f, -8.42105f}, + { 61.875f, 8.42105f}, + { 61.875f, -8.42105f}, + { 73.125f, 8.42105f}, + { 73.125f, -8.42105f}, + { 84.375f, 8.42105f}, + { 84.375f, -8.42105f}, + { 95.625f, 8.42105f}, + { 95.625f, -8.42105f}, + { 106.875f, 8.42105f}, + { 106.875f, -8.42105f}, + { 118.125f, 8.42105f}, + { 118.125f, -8.42105f}, + { 129.375f, 8.42105f}, + { 129.375f, -8.42105f}, + { 140.625f, 8.42105f}, + { 140.625f, -8.42105f}, + { 151.875f, 8.42105f}, + { 151.875f, -8.42105f}, + { 163.125f, 8.42105f}, + { 163.125f, -8.42105f}, + { 174.375f, 8.42105f}, + { 174.375f, -8.42105f}, + { 0f, 18.94737f}, + { 0f, -18.94737f}, + { 22.5f, 18.94737f}, + { 22.5f, -18.94737f}, + { 45f, 18.94737f}, + { 45f, -18.94737f}, + { 67.5f, 18.94737f}, + { 67.5f, -18.94737f}, + { 90f, 18.94737f}, + { 90f, -18.94737f}, + { 112.5f, 18.94737f}, + { 112.5f, -18.94737f}, + { 135f, 18.94737f}, + { 135f, -18.94737f}, + { 157.5f, 18.94737f}, + { 157.5f, -18.94737f}, + { 180f, 18.94737f}, + { 180f, -18.94737f}, + { 11.25f, 31.57895f}, + { 11.25f, -31.57895f}, + { 33.75f, 31.57895f}, + { 33.75f, -31.57895f}, + { 56.25f, 31.57895f}, + { 56.25f, -31.57895f}, + { 78.75f, 31.57895f}, + { 78.75f, -31.57895f}, + { 101.25f, 31.57895f}, + { 101.25f, -31.57895f}, + { 123.75f, 31.57895f}, + { 123.75f, -31.57895f}, + { 146.25f, 31.57895f}, + { 146.25f, -31.57895f}, + { 168.75f, 31.57895f}, + { 168.75f, -31.57895f}, + { 0f, 47.36842f}, + { 0f, -47.36842f}, + { 45f, 47.36842f}, + { 45f, -47.36842f}, + { 90f, 47.36842f}, + { 90f, -47.36842f}, + { 135f, 47.36842f}, + { 135f, -47.36842f}, + { 180f, 47.36842f}, + { 180f, -47.36842f}, + { 22.5f, 66.31579f}, + { 22.5f, -66.31579f}, + { 67.5f, 66.31579f}, + { 67.5f, -66.31579f}, + { 112.5f, 66.31579f}, + { 112.5f, -66.31579f}, + { 157.5f, 66.31579f}, + { 157.5f, -66.31579f}, + { 0f, 90f}, + { 0f, -90f}, + { 90f, 90f}, + { 90f, -90f}, + { 180f, 90f}, + { 180f, -90f}, + }; + + public static float minRCSHeatmap = float.MaxValue; + public static float maxRCSHeatmap = 0f; + + private static int numAspectsForOverallRTEval = 83; // Use the first N rows of rcsAspectsRealTime for evaluating overall craft RCS + public static float[,] editorRCSAspects = new float[3, 3]; // Worst three aspects + static Shader RCSshader; + static double[] rcsValues; + static Color32[] pixels; + + /// + /// Force radar signature update + /// Optionally, pass in a list of the vessels to update, otherwise all vessels in BDATargetManager.LoadedVessels get updated. + /// + /// This appears to cause a rather large amount of memory to be consumed (not actually a leak though). + /// + public static void ForceUpdateRadarCrossSections(List vessels = null) + { + foreach (var vessel in (vessels == null ? BDATargetManager.LoadedVessels : vessels)) + { + if (vessel == null) continue; + GetVesselRadarCrossSection(vessel, true); + } + } /// /// Get a vessel radar siganture, including all modifiers (ECM, stealth, ...) /// - public static TargetInfo GetVesselRadarSignature(Vessel v) + public static TargetInfo GetVesselRadarSignature(Vessel v, bool updateJammers = true) { //1. baseSig = GetVesselRadarCrossSection - TargetInfo ti = GetVesselRadarCrossSection(v); - + TargetInfo ti = GetVesselRadarCrossSection(v, updateJammers: updateJammers); //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; } + public static float GetVesselRadarSignatureAtAspect(TargetInfo ti, Vector3 radarPosition, float distance) + { + if (ti.radarSignatureMatrix is null) + return ti.radarBaseSignature; + + try + { + Vector3 directionOfRadar = radarPosition - ti.Vessel.ReferenceTransform.position; + /*Vector3 azComponent = Vector3.ProjectOnPlane(directionOfRadar, ti.Vessel.ReferenceTransform.forward); + Vector3 elComponent = Vector3.ProjectOnPlane(directionOfRadar, ti.Vessel.ReferenceTransform.right); + + float azAngle = Mathf.Abs(Vector3.SignedAngle(ti.Vessel.ReferenceTransform.up, azComponent, ti.Vessel.ReferenceTransform.forward)); + float elAngle = Vector3.SignedAngle(ti.Vessel.ReferenceTransform.up, elComponent, -ti.Vessel.ReferenceTransform.right);*/ + + // NOTE! vessel.ReferenceTransform.up actually faces forwards! And vessel.ReferenceTransform.forward actually faces down! Who would've thunk? + // This is why we flip these in the function. It's also why `elAngle` has to be multiplied by a negative 1! + //VectorUtils.GetAzimuthElevation(directionOfRadar, ti.Vessel.ReferenceTransform.up, ti.Vessel.ReferenceTransform.forward, out float azAngle, out float elAngle); + float azAngle = VectorUtils.GetAngleOnPlane(directionOfRadar, ti.Vessel.ReferenceTransform.up, ti.Vessel.ReferenceTransform.right); + float elAngle = VectorUtils.GetElevation(directionOfRadar, ti.Vessel.ReferenceTransform.forward, distance, 1.0f); + + // Note that we would've also had to negate azAngle (due to the flipped z axis) but since we assume craft are left/right symmetric + // we just use an Abs here. + azAngle = Mathf.Abs(azAngle); + elAngle *= -1f; + + float signatureAtAspect = RCSMatrixEval(ti.radarSignatureMatrix, ti.radarBaseSignature, azAngle, elAngle); + + // Incorporate any signature modification + signatureAtAspect *= ti.radarModifiedSignature / ti.radarBaseSignature; + + if (BDArmorySettings.DEBUG_RADAR) + Debug.Log($"[BDArmory.RadarUtils]: {ti.Vessel.vesselName} signature of {signatureAtAspect.ToString("0.00")} m² at az/el {azAngle.ToString("0.0")}/{elAngle.ToString("0.0")} deg."); + + return signatureAtAspect; + } + catch (Exception e) + { + Debug.LogWarning($"[BDArmory.RadarUtils]: Failed to evaluate aspected RCS of {ti.Vessel.vesselName}, using radarModifiedSignature {ti.radarModifiedSignature} instead: {e.Message}"); + return ti.radarModifiedSignature; + } + } + + private static float RCSMatrixEval(float[,] rcsMatrix, float overallRCS, float azAngle, float elAngle) + { + float rcs; + + if (elAngle > 90f) + elAngle = 180f - elAngle; + else if (elAngle < -90f) + elAngle = -180f - elAngle; + + // Find the three closest evaluated az/el RCS pairs and convert to barycentric coordinates for triangular interpolation + // Using triangular interpolation so we don't need a perfect grid of az/el RCS pairs, which would drive up the time for RCS snapshots + // The below code could probably be re-written in a cleaner way + // -------------------------------------------------------------------------------- + float x1 = 0f; + float y1 = 0f; + float d1 = float.MaxValue; + float w1 = 0f; + float x2 = 0f; + float y2 = 0f; + float d2 = float.MaxValue; + float w2 = 0f; + float x3 = 0f; + float y3 = 0f; + float d3 = float.MaxValue; + float w3 = 0f; + float rcs1 = 0f; + float rcs2 = 0f; + float rcs3 = 0f; + + for (int i = 0; i < rcsMatrix.GetLength(0); i++) + { + float sqrDist = (rcsMatrix[i, 0] - azAngle) * (rcsMatrix[i, 0] - azAngle) + (rcsMatrix[i, 1] - elAngle) * (rcsMatrix[i, 1] - elAngle); + + if (sqrDist < d3) + { + if (sqrDist < d2) + { + if (sqrDist < d1) + { + d3 = d2; + x3 = x2; + y3 = y2; + rcs3 = rcs2; + + d2 = d1; + x2 = x1; + y2 = y1; + rcs2 = rcs1; + + d1 = sqrDist; + x1 = rcsMatrix[i, 0]; + y1 = rcsMatrix[i, 1]; + rcs1 = rcsMatrix[i, 2]; + } + else + { + d3 = d2; + x3 = x2; + y3 = y2; + rcs3 = rcs2; + + d2 = sqrDist; + x2 = rcsMatrix[i, 0]; + y2 = rcsMatrix[i, 1]; + rcs2 = rcsMatrix[i, 2]; + } + } + else + { + d3 = sqrDist; + x3 = rcsMatrix[i, 0]; + y3 = rcsMatrix[i, 1]; + rcs3 = rcsMatrix[i, 2]; + } + } + } + // -------------------------------------------------------------------------------- + + // Compute interpolation weights using barycentric coordinates + w1 = ((y2 - y3) * (azAngle - x3) + (x3 - x2) * (elAngle - y3)) / ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3)); + w2 = ((y3 - y1) * (azAngle - x3) + (x1 - x3) * (elAngle - y3)) / ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3)); + w3 = 1 - w1 - w2; + + if ((w1 > 0) && (w2 > 0) && (w3 > 0)) // If point is inside triangle, weights will all be positive, if not inside triangle use nearest neighbor + rcs = w1 * rcs1 + w2 * rcs2 + w3 * rcs3; + else + rcs = rcs1; + + // Compute weighted average for aspect + rcs = (1 - BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT) * rcs + BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT * overallRCS; + + return rcs; + } + /// /// Internal method: get a vessel base radar signature /// - private static TargetInfo GetVesselRadarCrossSection(Vessel v) + private static TargetInfo GetVesselRadarCrossSection(Vessel v, bool force = false, bool updateJammers = true) { //read vesseltargetinfo, or render against radar cameras TargetInfo ti = v.gameObject.GetComponent(); if (ti == null) { - // add targetinfo to vessel ti = v.gameObject.AddComponent(); } @@ -205,66 +430,81 @@ private static TargetInfo GetVesselRadarCrossSection(Vessel v) MissileBase missile = ti.MissileBaseModule; if (missile != null) { - if (missile.ActiveRadar) + if (!missile.updateRadarCS) + return ti; + + if (missile.ActiveRadar || missile.radarLOALSearching) + { ti.radarBaseSignature = RCS_MISSILES; + } else ti.radarBaseSignature = missile.missileRadarCrossSection; ti.radarBaseSignatureNeedsUpdate = false; + ti.radarSignatureMatrixNeedsUpdate = false; + missile.updateRadarCS = false; + + if (updateJammers) + { + // Update ECM impact on RCS if base RCS is modified + VesselECMJInfo jammer = v.gameObject.GetComponent(); + if (jammer != null) + jammer.UpdateJammerStrength(ti); + } + else + // NOTE: This might be called on startup depending on who initializes first, if VesselECMJInfo calls + // UpdateJammerStrength before tInfo gets constructed, then this will get triggered. + Debug.LogWarning($"[BDArmory.RadarUtils] DETECTED INFINITE LOOP! Missile: {missile.shortName} on vessel: {(v ? v.vesselName : "null")} caused infinite loop for some reason!"); + + return ti; } + + return ti; } - if (ti.radarBaseSignature == -1 || ti.radarBaseSignatureNeedsUpdate) + // Run intensive RCS rendering if 1. It has not been done yet, 2. If the competition just started (capture vessel changes such as gear-raise or robotics) + if (force || ti.radarBaseSignature == -1 || ti.radarBaseSignatureNeedsUpdate || (BDArmorySettings.ASPECTED_RCS && ti.radarSignatureMatrixNeedsUpdate)) { + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils] Performing RCS Rendering! Vessel: {(v ? v.vesselName : "null")}, with mass: {ti.radarMassAtUpdate}, force: {force}, ti.radarBaseSignature: {ti.radarBaseSignature}, radarBaseSignatureNeedsUpdate: {ti.radarBaseSignatureNeedsUpdate} and radarSignatureMatrixNeedsUpdate: {ti.radarSignatureMatrixNeedsUpdate}."); + // is it just some debris? then dont bother doing a real rcs rendering and just fake it with the parts mass - if (v.vesselType == VesselType.Debris && !v.IsControllable) + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(v.vesselType) || !v.IsControllable) { ti.radarBaseSignature = v.GetTotalMass(); } else { // perform radar rendering to obtain base cross section - ti.radarBaseSignature = RenderVesselRadarSnapshot(v, v.transform); + try + { + ti = RenderVesselRadarSnapshot(v, v.transform, ti); + } + catch (Exception e) // Unity physics sometimes breaks (MMGs sometimes cause this). + { + Debug.LogWarning($"[BDArmory.RadarUtils]: Failed to get a radar snapshot of {v.GetName()}, using mass instead: {e.Message}"); + ti.radarBaseSignature = v.GetTotalMass(); + } } + ti.radarSignatureMatrixNeedsUpdate = BDArmorySettings.ASPECTED_RCS ? false : ti.radarSignatureMatrixNeedsUpdate; ti.radarBaseSignatureNeedsUpdate = false; ti.alreadyScheduledRCSUpdate = false; - } - - 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); + ti.radarMassAtUpdate = v.GetTotalMass(); - //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.Clamp01(ti.radarBaseSignature / ti.radarModifiedSignature) * (1 - (vesseljammer.lockBreakStrength / ti.radarBaseSignature / 100)); + if (updateJammers) + { + // Update ECM impact on RCS if base RCS is modified + VesselECMJInfo jammer = v.gameObject.GetComponent(); + if (jammer != null) + jammer.UpdateJammerStrength(ti); + } + else + // NOTE: This might be called on startup depending on who initializes first, if VesselECMJInfo calls + // UpdateJammerStrength before tInfo gets constructed, then this will get triggered. + Debug.LogWarning($"[BDArmory.RadarUtils] DETECTED INFINITE LOOP! Vessel: {(v ? v.vesselName : "null")}, with mass: {ti.radarMassAtUpdate}, ti.radarBaseSignature: {ti.radarBaseSignature}, radarBaseSignatureNeedsUpdate: {ti.radarBaseSignatureNeedsUpdate} and radarSignatureMatrixNeedsUpdate: {ti.radarSignatureMatrixNeedsUpdate} caused infinite loop for some reason!"); } - return ti.radarModifiedSignature; + return ti; } /// @@ -280,13 +520,21 @@ public static float GetVesselChaffFactor(Vessel v) if (vci) { // lockbreaking strength relative to jammer's lockbreak strength in relation to vessel rcs signature: - // lockbreak_factor = baseSig/modifiedSig x (1 � lopckBreakStrength/baseSig/100) + // lockbreak_factor = baseSig/modifiedSig x (1 - lockBreakStrength/baseSig/100) chaffFactor = vci.GetChaffMultiplier(); } return chaffFactor; } + /// + /// Get the degree vessel's sonar return degraded by bubble screens between sonar and target, similar to lockBreakFactor + /// + public static float GetVesselBubbleFactor(Vector3 sensorPos, Vessel v) + { + float Factor = CMBubble.RaycastBubblescreen(new Ray(sensorPos, v.CoM - sensorPos)); + return Factor; + } /// /// Get a vessel ecm jamming area (in m) where radar display garbling occurs /// @@ -297,8 +545,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; } @@ -309,11 +558,17 @@ public static float GetVesselECMJammingDistance(Vessel v) /// and there we dont have a VESSEL, only a SHIPCONSTRUCT, so the EditorRcSWindow passes the transform separately. /// /// when true, we try to make the rendered vessel fill the rendertexture completely, for a better detailed view. This does skew the computed cross section, so it is only for a good visual in editor! - public static float RenderVesselRadarSnapshot(Vessel v, Transform t, bool inEditorZoom = false) + public static TargetInfo RenderVesselRadarSnapshot(Vessel v, Transform t, TargetInfo ti = null, bool inEditorZoom = false) { + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(v.vesselType)) Debug.LogError($"[BDArmory.RadarUtils]: Rendering radar snapshot of {v.vesselName}, which should be being ignored!"); + int numAspects = (BDArmorySettings.ASPECTED_RCS) ? rcsAspectsRealTime.GetLength(0) : rcsAspectsConstant.GetLength(0); // Number of aspects + float[,] rcsAspects = new float[numAspects, 2]; + rcsAspects = (BDArmorySettings.ASPECTED_RCS) ? rcsAspectsRealTime : rcsAspectsConstant; + float[,] rcsMatrix = new float[numAspects, 3]; const float radarDistance = 1000f; const float radarFOV = 2.0f; Vector3 presentationPosition = -t.forward * radarDistance; + rcsTotal = 0; SetupResources(); @@ -325,175 +580,401 @@ public static float RenderVesselRadarSnapshot(Vessel v, Transform t, bool inEdit { priorRotation = t.rotation; v.SetPosition(v.transform.position + presentationPosition); - //v.SetRotation(Quaternion.Euler(Mathf.PI/2, 0, 0)); v.SetRotation(new Quaternion(-0.7f, 0f, 0f, -0.7f)); + t = v.transform; - } + //move AB thrust transforms (fix for AirplanePlus .dds engine afterburner FX not using DXT5 standard and showing up in RCS render) + using (var engines = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (engines.MoveNext()) + { + if (engines.Current == null) continue; + using (var engineTransforms = engines.Current.thrustTransforms.GetEnumerator()) + while (engineTransforms.MoveNext()) + { + engineTransforms.Current.transform.position = engineTransforms.Current.transform.position + presentationPosition; + } + } + } Bounds vesselbounds = CalcVesselBounds(v, t); - - if (BDArmorySettings.DRAW_DEBUG_LABELS) + + if (BDArmorySettings.DEBUG_RADAR) { if (HighLogic.LoadedSceneIsFlight) - Debug.Log($"[BDArmory]: Rendering radar snapshot of vessel {v.name}, type {v.vesselType}"); + Debug.Log($"[BDArmory.RadarUtils]: Rendering radar snapshot of vessel {v.name}, type {v.vesselType}"); else - Debug.Log("[BDArmory]: Rendering radar snapshot of vessel"); - Debug.Log("[BDArmory]: - bounds: " + vesselbounds.ToString()); - Debug.Log("[BDArmory]: - rotation: " + t.rotation.ToString()); - //Debug.Log("[BDArmory]: - size: " + vesselbounds.size + ", magnitude: " + vesselbounds.size.magnitude); + Debug.Log("[BDArmory.RadarUtils]: Rendering radar snapshot of vessel"); + 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]: - rcs is zero."); + Debug.Log("[BDArmory.RadarUtils]: - rcs is zero."); } // revert presentation (only if outside editor and thus vessel is a real vessel) if (HighLogic.LoadedSceneIsFlight) v.SetPosition(v.transform.position - presentationPosition); - return 0f; + if (ti is not null) + { + ti.radarBaseSignature = 0f; + ti.radarSignatureMatrix = null; + } + + return ti; } + // Disable rendering of conformal decals, which messes with the parent part's RCS render. + if (ConformalDecals.hasConformalDecals) + SetConformalDecalRendering(v, false); + + // If in editor, turn off rendering hangar + if (!HighLogic.LoadedSceneIsFlight) + SetHangarRender(false); + float rcsVariable = 0f; - worstRCSAspects = new float[3, 3]; - double[] rcsValues = new double[numAspects]; - rcsTotal = 0; + if (editorRCSAspects is null) editorRCSAspects = new float[3, 3]; + Array.Clear(editorRCSAspects, 0, 9); + if (rcsValues is null) + rcsValues = new double[numAspects]; + else + Array.Resize(ref rcsValues, numAspects); + Array.Clear(rcsValues, 0, numAspects); Vector3 aspect; - // Loop through all aspects for (int i = 0; i < numAspects; i++) { // Determine camera vector for aspect aspect = Vector3.RotateTowards(t.up, -t.up, rcsAspects[i, 0] / 180f * Mathf.PI, 0); - aspect = Vector3.RotateTowards(aspect, Vector3.Cross(t.right, t.up), rcsAspects[i, 1] / 180f * Mathf.PI, 0); - + aspect = Vector3.RotateTowards(aspect, Vector3.Cross(t.right, t.up), -rcsAspects[i, 1] / 180f * Mathf.PI, 0); + // Render aspect - RenderSinglePass(t, false, aspect, vesselbounds, radarDistance, radarFOV, rcsRenderingVariable, drawTextureVariable); + RenderSinglePass(v, t, false, aspect, vesselbounds, radarDistance, radarFOV, rcsRenderingVariable, drawTextureVariable); // Count pixel colors to determine radar returns rcsVariable = 0; - var pixels = drawTextureVariable.GetPixels(); - for (int pixel = 0; pixel < pixels.Length; ++pixel) - rcsVariable += pixels[pixel].maxColorComponent; + pixels = drawTextureVariable.GetPixels32(); // GetPixels causes a memory leak, so we need to go via GetPixels32! + for (int pixelIndex = 0; pixelIndex < pixels.Length; ++pixelIndex) + { + var pixel = pixels[pixelIndex]; + var maxColorComponent = Mathf.Max(pixel.r, Mathf.Max(pixel.g, pixel.b)); + rcsVariable += (float)maxColorComponent / 255f; + } // normalize rcs value, so that a sphere with cross section of 1 m^2 gives a return of 1 m^2: rcsVariable /= RCS_NORMALIZATION_FACTOR; - rcsValues[i] = (double)rcsVariable; + rcsValues[i] = rcsVariable; + minRCSHeatmap = Mathf.Min(minRCSHeatmap, (float)rcsValues[i]); + + // Add values to RCS Matrix for real-time evaluation + rcsMatrix[i, 0] = rcsAspects[i, 0]; + rcsMatrix[i, 1] = rcsAspects[i, 1]; + rcsMatrix[i, 2] = rcsVariable; // Remember worst three RCS aspects to display in editor if (inEditorZoom) { - if (rcsVariable > worstRCSAspects[0, 2]) + if (!BDArmorySettings.ASPECTED_RCS) // Use worst aspects by default { - worstRCSAspects[2, 0] = worstRCSAspects[1, 0]; - worstRCSAspects[2, 1] = worstRCSAspects[1, 1]; - worstRCSAspects[2, 2] = worstRCSAspects[1, 2]; + if (rcsVariable > editorRCSAspects[0, 2]) + { + editorRCSAspects[2, 0] = editorRCSAspects[1, 0]; + editorRCSAspects[2, 1] = editorRCSAspects[1, 1]; + editorRCSAspects[2, 2] = editorRCSAspects[1, 2]; - worstRCSAspects[1, 0] = worstRCSAspects[0, 0]; - worstRCSAspects[1, 1] = worstRCSAspects[0, 1]; - worstRCSAspects[1, 2] = worstRCSAspects[0, 2]; + editorRCSAspects[1, 0] = editorRCSAspects[0, 0]; + editorRCSAspects[1, 1] = editorRCSAspects[0, 1]; + editorRCSAspects[1, 2] = editorRCSAspects[0, 2]; - worstRCSAspects[0, 0] = rcsAspects[i, 0]; - worstRCSAspects[0, 1] = rcsAspects[i, 1]; - worstRCSAspects[0, 2] = rcsVariable; - } - else if (rcsVariable > worstRCSAspects[1, 2]) - { - worstRCSAspects[2, 0] = worstRCSAspects[1, 0]; - worstRCSAspects[2, 1] = worstRCSAspects[1, 1]; - worstRCSAspects[2, 2] = worstRCSAspects[1, 2]; + editorRCSAspects[0, 0] = rcsAspects[i, 0]; + editorRCSAspects[0, 1] = rcsAspects[i, 1]; + editorRCSAspects[0, 2] = rcsVariable; + } + else if (rcsVariable > editorRCSAspects[1, 2]) + { + editorRCSAspects[2, 0] = editorRCSAspects[1, 0]; + editorRCSAspects[2, 1] = editorRCSAspects[1, 1]; + editorRCSAspects[2, 2] = editorRCSAspects[1, 2]; - worstRCSAspects[1, 0] = rcsAspects[i, 0]; - worstRCSAspects[1, 1] = rcsAspects[i, 1]; - worstRCSAspects[1, 2] = rcsVariable; + editorRCSAspects[1, 0] = rcsAspects[i, 0]; + editorRCSAspects[1, 1] = rcsAspects[i, 1]; + editorRCSAspects[1, 2] = rcsVariable; + } + else if (rcsVariable > editorRCSAspects[2, 2]) + { + editorRCSAspects[2, 0] = rcsAspects[i, 0]; + editorRCSAspects[2, 1] = rcsAspects[i, 1]; + editorRCSAspects[2, 2] = rcsVariable; + } } - else if (rcsVariable > worstRCSAspects[2, 2]) + else if (BDArmorySettings.ASPECTED_RCS && i <= 2) // For aspected RCS use first three evaluated aspects { - worstRCSAspects[2, 0] = rcsAspects[i, 0]; - worstRCSAspects[2, 1] = rcsAspects[i, 1]; - worstRCSAspects[2, 2] = rcsVariable; + editorRCSAspects[i, 0] = rcsAspects[i, 0]; + editorRCSAspects[i, 1] = rcsAspects[i, 1]; + editorRCSAspects[i, 2] = rcsVariable; } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { - Debug.Log($"[BDArmory]: RCS Aspect Vector for (az/el) {rcsAspects[i, 0]}/{rcsAspects[i, 1]} is: " + aspect.ToString()); - Debug.Log($"[BDArmory]: - Vessel rcs for (az/el) is: {rcsAspects[i, 0]}/{rcsAspects[i, 1]} = rcsVariable: {rcsVariable}"); + Debug.Log($"[BDArmory.RadarUtils]: - Vessel rcs for (az/el) is: {rcsAspects[i, 0]}/{rcsAspects[i, 1]} = rcsVariable: {rcsVariable}"); } } + // Re-size array for overall RCS calc when aspected RCS is enabled + if (BDArmorySettings.ASPECTED_RCS) + Array.Resize(ref rcsValues, numAspectsForOverallRTEval); + // Use third quartile for the total RCS (gives better results than average) - rcsTotal = (float)Quartile(rcsValues, 3); + rcsTotal = (float)Percentile(rcsValues, 75d); // If we are in the editor, render the three highest RCS aspects if (inEditorZoom) { // Determine camera vectors for aspects - Vector3 aspect1 = Vector3.RotateTowards(t.up, -t.up, worstRCSAspects[0, 0] / 180f * Mathf.PI, 0); - aspect1 = Vector3.RotateTowards(aspect1, Vector3.Cross(t.right, t.up), worstRCSAspects[0, 1] / 180f * Mathf.PI, 0); - Vector3 aspect2 = Vector3.RotateTowards(t.up, -t.up, worstRCSAspects[1, 0] / 180f * Mathf.PI, 0); - aspect2 = Vector3.RotateTowards(aspect2, Vector3.Cross(t.right, t.up), worstRCSAspects[1, 1] / 180f * Mathf.PI, 0); - Vector3 aspect3 = Vector3.RotateTowards(t.up, -t.up, worstRCSAspects[2, 0] / 180f * Mathf.PI, 0); - aspect3 = Vector3.RotateTowards(aspect3, Vector3.Cross(t.right, t.up), worstRCSAspects[2, 1] / 180f * Mathf.PI, 0); + Vector3 aspect1 = Vector3.RotateTowards(t.up, -t.up, editorRCSAspects[0, 0] / 180f * Mathf.PI, 0); + aspect1 = Vector3.RotateTowards(aspect1, Vector3.Cross(t.right, t.up), -editorRCSAspects[0, 1] / 180f * Mathf.PI, 0); + Vector3 aspect2 = Vector3.RotateTowards(t.up, -t.up, editorRCSAspects[1, 0] / 180f * Mathf.PI, 0); + aspect2 = Vector3.RotateTowards(aspect2, Vector3.Cross(t.right, t.up), -editorRCSAspects[1, 1] / 180f * Mathf.PI, 0); + Vector3 aspect3 = Vector3.RotateTowards(t.up, -t.up, editorRCSAspects[2, 0] / 180f * Mathf.PI, 0); + aspect3 = Vector3.RotateTowards(aspect3, Vector3.Cross(t.right, t.up), -editorRCSAspects[2, 1] / 180f * Mathf.PI, 0); // Render three highest aspects - RenderSinglePass(t, inEditorZoom, aspect1, vesselbounds, radarDistance, radarFOV, rcsRendering1, drawTexture1); - RenderSinglePass(t, inEditorZoom, aspect2, vesselbounds, radarDistance, radarFOV, rcsRendering2, drawTexture2); - RenderSinglePass(t, inEditorZoom, aspect3, vesselbounds, radarDistance, radarFOV, rcsRendering3, drawTexture3); + RenderSinglePass(v, t, inEditorZoom, aspect1, vesselbounds, radarDistance, radarFOV, rcsRendering1, drawTexture1); + RenderSinglePass(v, t, inEditorZoom, aspect2, vesselbounds, radarDistance, radarFOV, rcsRendering2, drawTexture2); + RenderSinglePass(v, t, inEditorZoom, aspect3, vesselbounds, radarDistance, radarFOV, rcsRendering3, drawTexture3); + } else { // revert presentation (only if outside editor and thus vessel is a real vessel) if (HighLogic.LoadedSceneIsFlight) { + //move AB thrust transforms (fix for AirplanePlus .dds engine afterburner FX not using DXT5 standard and showing up in RCS render) + using (var engines = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (engines.MoveNext()) + { + if (engines.Current == null) continue; + using (var engineTransforms = engines.Current.thrustTransforms.GetEnumerator()) + while (engineTransforms.MoveNext()) + { + engineTransforms.Current.transform.position = engineTransforms.Current.transform.position - presentationPosition; + } + } + v.SetRotation(priorRotation); v.SetPosition(v.transform.position - presentationPosition); } } + //if (!BDArmorySettings.DEBUG_RADAR) + //{ + using (List.Enumerator parts = (HighLogic.LoadedSceneIsEditor ? EditorLogic.fetch.ship.Parts.GetEnumerator() : v.parts.GetEnumerator())) + while (parts.MoveNext()) + { + HitpointTracker a = parts.Current.GetComponent(); + FlagDecal flag = parts.Current.GetComponent(); + if (parts.Current.GetComponent()) continue; + if (flag != null) + { + if (!flag.flagDisplayed) + { + flag.ToggleFlag(); + } + } + var r = parts.Current.GetComponentsInChildren(); + for (int i = 0; i < r.Length; i++) + { + try + { + if (r[i].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + int key = r[i].material.GetInstanceID(); + if (!a.defaultShader.ContainsKey(key)) + { + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: {r[i].material.name} ({key}) not found in defaultShader for part {parts.Current.partInfo.name} on {(HighLogic.LoadedSceneIsFlight ? v.vesselName : EditorLogic.fetch.ship.shipName)}"); // Enable this to see what materials aren't getting RCS shaders applied to them. + continue; + } + if (r[i].material.shader != a.defaultShader[key]) + { + if (a.defaultShader[key] != null) + { + r[i].material.shader = a.defaultShader[key]; + } + if (a.defaultColor.ContainsKey(key)) + { + if (a.defaultColor[key] != null) + { + if (parts.Current.name.Contains("B9.Aero.Wing.Procedural")) + r[i].material.SetColor("_MainTex", a.defaultColor[key]); + else + r[i].material.SetColor("_Color", a.defaultColor[key]); + } + else + { + if (parts.Current.name.Contains("B9.Aero.Wing.Procedural")) + r[i].material.SetColor("_MainTex", Color.white); + else + r[i].material.SetColor("_Color", Color.white); + } + } + } + } + catch (Exception e) + { + Debug.Log($"[RadarUtils]: material on {parts.Current.name} could not find default shader/color: {e.Message}\n{e.StackTrace}"); + } + } + } + //} + // Re-enable rendering of conformal decals. + if (ConformalDecals.hasConformalDecals) + SetConformalDecalRendering(v, true); + + // If in editor, turn back on rendering of hangar + if (!HighLogic.LoadedSceneIsFlight) + SetHangarRender(true); + + if (BDArmorySettings.DEBUG_RADAR) + { + Debug.Log($"[BDArmory.RadarUtils]: - Vessel all-aspect rcs is: rcsTotal: {rcsTotal}"); + } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (ti is not null) { - Debug.Log($"[BDArmory]: - Vessel all-aspect rcs is: rcsTotal: {rcsTotal}"); + ti.radarBaseSignature = rcsTotal; + ti.radarSignatureMatrix = rcsMatrix; } - return rcsTotal; + return ti; } - // Used to calculate third quartile for RCS dataset - internal static double Quartile(double[] array, int nth_quartile) + public static void SetConformalDecalRendering(Vessel v, bool renderEnabled) { - System.Array.Sort(array); - double dblPercentage = 0; + if (!ConformalDecals.hasConformalDecals) return; + if (HighLogic.LoadedSceneIsFlight && v == null) return; // Invalid vessel to render. + using List.Enumerator parts = HighLogic.LoadedSceneIsEditor ? EditorLogic.fetch.ship.Parts.GetEnumerator() : v.Parts.GetEnumerator(); + while (parts.MoveNext()) + { + foreach (var module in parts.Current.Modules) + { + if ((module.moduleName == "ModuleConformalDecal") || (module.moduleName == "ModuleConformalFlag") || (module.moduleName == "ModuleConformalText")) + { + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: Found {module.moduleName} for {parts.Current.name}."); + foreach (var r in parts.Current.GetComponentsInChildren()) + { + if (r.GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + r.enabled = renderEnabled; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: Set rendering for {r.name} on {parts.Current.name} to {renderEnabled}."); + } + var cdComponent = ConformalDecals.Instance.GetMCDComponent(parts.Current); + if (cdComponent != null) ConformalDecals.Instance.SetMCDIsAttached(cdComponent, renderEnabled); + } + } + } + } + + // Code to hide/show SPH/VAB during RCS render to prevent the hangar itself from affecting RCS calculation, code modified from HangarExtender + private static void SetHangarRender(bool renderEnabled) + { + if (!renderEnabled) + hangarHiddenExternally = false; + else if (renderEnabled && hangarHiddenExternally) + return; + + string[] rcsNames = { "vabscenery", "sphscenery", "vablvl1", "vablvl2", "vablvl3", "vabmodern", "sphlvl1", "sphlvl2", "sphlvl3", "sphmodern", "vabcrew", "sphcrew" }; + List rootNodes = new List(); + + foreach (Transform t in UnityEngine.Object.FindObjectsOfType()) + { + Transform newTransform = t.root; + while (newTransform.parent != null) + { + newTransform = newTransform.parent; + } + if (!rootNodes.Contains(newTransform)) + { + rootNodes.Add(newTransform); + } + } - switch (nth_quartile) + // Check for hidden hangars if we are setting rendering to false + if (!renderEnabled) { - case 0: - dblPercentage = 0; //Smallest value in the data set - break; - case 1: - dblPercentage = 25; //First quartile (25th percentile) - break; - case 2: - dblPercentage = 50; //Second quartile (50th percentile) - break; - - case 3: - dblPercentage = 75; //Third quartile (75th percentile) - break; - - case 4: - dblPercentage = 100; //Largest value in the data set - break; - default: - dblPercentage = 0; - break; + foreach (Transform t in rootNodes) + { + foreach (string s in rcsNames) + { + if (string.Equals(t.name.ToLower(), s)) + { + List skinRenderers = new List(); + t.transform.GetComponentsInChildren(skinRenderers); + foreach (SkinnedMeshRenderer r in skinRenderers) + { + if (!renderEnabled) // If turning rendering off, check if it is already off + hangarHiddenExternally = hangarHiddenExternally || !r.enabled; + + if (hangarHiddenExternally) return; + } + List renderers = new List(); + t.transform.GetComponentsInChildren(renderers); + foreach (MeshRenderer r in renderers) + { + if (!renderEnabled) // If turning rendering off, check if it is already off + hangarHiddenExternally = hangarHiddenExternally || !r.enabled; + + if (hangarHiddenExternally) return; + } + } + } + } + } + + // Set rendering + foreach (Transform t in rootNodes) + { + foreach (string s in rcsNames) + { + if (string.Equals(t.name.ToLower(), s)) + { + List skinRenderers = new List(); + t.transform.GetComponentsInChildren(skinRenderers); + foreach (SkinnedMeshRenderer r in skinRenderers) + { + if (!renderEnabled) // If turning rendering off, check if it is already off + hangarHiddenExternally = hangarHiddenExternally || !r.enabled; + + if (hangarHiddenExternally) + return; + else + r.enabled = renderEnabled; + } + List renderers = new List(); + t.transform.GetComponentsInChildren(renderers); + foreach (MeshRenderer r in renderers) + { + if (!renderEnabled) + hangarHiddenExternally = hangarHiddenExternally || !r.enabled; + + if (hangarHiddenExternally) + return; + else + r.enabled = renderEnabled; + } + } + } } + } + // Used to calculate percentiles for RCS dataset + internal static double Percentile(double[] array, double dblPercentage = 0) + { + System.Array.Sort(array); if (dblPercentage >= 100.0d) return array[array.Length - 1]; @@ -522,6 +1003,33 @@ internal static double Quartile(double[] array, int nth_quartile) } } + // Currently unused + public static void RCSHeatMap(float[,] rcsMatrix, Texture2D rcsMap) + { + float az; + float el; + float rcs; + Color rcsColor; + + //Detect edges on slice and write to output + for (int x = 0; x < radarResolution; x++) + { + az = (float)x / (radarResolution - 1) * 180f; + for (int y = 0; y < radarResolution; y++) + { + el = (float)y / (radarResolution - 1) * 180f - 90f; + rcs = RCSMatrixEval(rcsMatrix, rcsTotal, az, el); + rcs = Mathf.Clamp(rcs, minRCSHeatmap, maxRCSHeatmap); + if (rcs <= rcsTotal) + rcsColor = Color.HSVToRGB((0.5f * (rcsTotal - rcs) / (rcsTotal - minRCSHeatmap) + 0.5f) / 3f, 1, 1); + else + rcsColor = Color.HSVToRGB((0.5f - (0.5f / (maxRCSHeatmap - rcsTotal) * (rcs - rcsTotal))) / 3, 1, 1); + rcsMap.SetPixel(x, y, rcsColor); + } + } + rcsMap.Apply(); + } + /// /// Internal method: do the actual radar snapshot rendering from 3 sides and store it in a vesseltargetinfo attached to the vessel /// @@ -542,22 +1050,22 @@ 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]: Rendering radar snapshot of vessel {v.name}, type {v.vesselType}"); + Debug.Log($"[BDArmory.RadarUtils]: Rendering radar snapshot of vessel {v.name}, type {v.vesselType}"); else - Debug.Log("[BDArmory]: Rendering radar snapshot of vessel"); - Debug.Log("[BDArmory]: - bounds: " + vesselbounds.ToString()); - //Debug.Log("[BDArmory]: - size: " + vesselbounds.size + ", magnitude: " + vesselbounds.size.magnitude); + Debug.Log("[BDArmory.RadarUtils]: Rendering radar snapshot of vessel"); + Debug.Log("[BDArmory.RadarUtils]: - bounds: " + vesselbounds.ToString()); + //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]: - rcs is zero."); + Debug.Log("[BDArmory.RadarUtils]: - rcs is zero."); } // revert presentation (only if outside editor and thus vessel is a real vessel) @@ -568,16 +1076,16 @@ public static float RenderVesselRadarSnapshotLegacy(Vessel v, Transform t, bool } // pass1: frontal - RenderSinglePass(t, inEditorZoom, t.up, vesselbounds, radarDistance, radarFOV, rcsRenderingFrontal, drawTextureFrontal); + RenderSinglePass(v, t, inEditorZoom, t.up, vesselbounds, radarDistance, radarFOV, rcsRenderingFrontal, drawTextureFrontal); // pass2: lateral - RenderSinglePass(t, inEditorZoom, t.right, vesselbounds, radarDistance, radarFOV, rcsRenderingLateral, drawTextureLateral); + RenderSinglePass(v, t, inEditorZoom, t.right, vesselbounds, radarDistance, radarFOV, rcsRenderingLateral, drawTextureLateral); // pass3: Ventral - RenderSinglePass(t, inEditorZoom, t.forward, vesselbounds, radarDistance, radarFOV, rcsRenderingVentral, drawTextureVentral); + RenderSinglePass(v, t, inEditorZoom, t.forward, vesselbounds, radarDistance, radarFOV, rcsRenderingVentral, drawTextureVentral); - //additional 45� offset renderings: - RenderSinglePass(t, inEditorZoom, (t.up + t.right), vesselbounds, radarDistance, radarFOV, rcsRenderingFrontal, drawTextureFrontal45); - RenderSinglePass(t, inEditorZoom, (t.right + t.forward), vesselbounds, radarDistance, radarFOV, rcsRenderingLateral, drawTextureLateral45); - RenderSinglePass(t, inEditorZoom, (t.forward - t.up), vesselbounds, radarDistance, radarFOV, rcsRenderingVentral, drawTextureVentral45); + //additional 45° offset renderings: + RenderSinglePass(v, t, inEditorZoom, (t.up + t.right), vesselbounds, radarDistance, radarFOV, rcsRenderingFrontal, drawTextureFrontal45); + RenderSinglePass(v, t, inEditorZoom, (t.right + t.forward), vesselbounds, radarDistance, radarFOV, rcsRenderingLateral, drawTextureLateral45); + RenderSinglePass(v, t, inEditorZoom, (t.forward - t.up), vesselbounds, radarDistance, radarFOV, rcsRenderingVentral, drawTextureVentral45); // revert presentation (only if outside editor and thus vessel is a real vessel) if (HighLogic.LoadedSceneIsFlight) @@ -617,9 +1125,9 @@ 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]: - Vessel rcs is (frontal/lateral/ventral), (frontal45/lateral45/ventral45): {rcsFrontal}/{rcsLateral}/{rcsVentral}, {rcsFrontal45}/{rcsLateral45}/{rcsVentral45} = rcsTotal: {rcsTotal}"); + Debug.Log($"[BDArmory.RadarUtils]: - Vessel rcs is (frontal/lateral/ventral), (frontal45/lateral45/ventral45): {rcsFrontal}/{rcsLateral}/{rcsVentral}, {rcsFrontal45}/{rcsLateral45}/{rcsVentral45} = rcsTotal: {rcsTotal}"); } } @@ -627,12 +1135,13 @@ public static float RenderVesselRadarSnapshotLegacy(Vessel v, Transform t, bool } /// - /// Internal helpder method + /// Internal helper method /// - private static void RenderSinglePass(Transform t, bool inEditorZoom, Vector3 cameraDirection, Bounds vesselbounds, float radarDistance, float radarFOV, RenderTexture rcsRendering, Texture2D rcsTexture) + private static void RenderSinglePass(Vessel v, Transform t, bool inEditorZoom, Vector3 cameraDirection, Bounds vesselbounds, float radarDistance, float radarFOV, RenderTexture rcsRendering, Texture2D rcsTexture) { // 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); @@ -642,11 +1151,72 @@ private static void RenderSinglePass(Transform t, bool inEditorZoom, Vector3 cam radarCam.fieldOfView = Mathf.Atan(vesselbounds.size.magnitude / distanceToShip) * 180 / Mathf.PI; else radarCam.fieldOfView = radarFOV; - // setup rendertexture + // setup rendertexture + + ///////////////// + Color StealthAdjust; + RCSshader = BDAShaderLoader.RCSShader; + using (List.Enumerator parts = (HighLogic.LoadedSceneIsEditor ? EditorLogic.fetch.ship.Parts.GetEnumerator() : v.parts.GetEnumerator())) + while (parts.MoveNext()) + { + HitpointTracker a = parts.Current.GetComponent(); + FlagDecal flag = parts.Current.GetComponent(); + if (flag != null) + { + if (flag.flagDisplayed) + { + flag.ToggleFlag(); + } + } + if (parts.Current.GetComponent()) continue; //ignore kerbals + var r = parts.Current.GetComponentsInChildren(); + try + { + if (!a.RegisterProcWingShader && parts.Current.name.Contains("B9.Aero.Wing.Procedural")) + { + for (int s = 0; s < r.Length; s++) + { + if (r[s].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + int key = r[s].material.GetInstanceID(); + a.defaultShader.Add(key, r[s].material.shader); + if (r[s].material.HasProperty("_Color")) + { + a.defaultColor.Add(key, r[s].material.color); + } + } + a.RegisterProcWingShader = true; + } + for (int i = 0; i < r.Length; i++) + { + if (!a.defaultShader.ContainsKey(r[i].material.GetInstanceID())) continue; // Don't modify shaders that we don't have defaults for as we can't then replace them. + if (r[i].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + if (r[i].material.shader.name.Contains("Alpha")) continue; + if (r[i].material.shader.name.Contains("Waterfall")) continue; + if (r[i].material.shader.name.Contains("KSP/Particles")) continue; + r[i].material.shader = RCSshader; + r[i].material.SetVector("_LIGHTDIR", -cameraDirection); + r[i].material.SetColor("_RCSCOLOR", Color.white); + if (a != null) + { + StealthAdjust.r = a.radarReflectivity; + StealthAdjust.g = a.radarReflectivity; + StealthAdjust.b = a.radarReflectivity; + StealthAdjust.a = 1; + r[i].material.SetColor("_RCSCOLOR", StealthAdjust); + } + } + } + catch + { + Debug.Log("[RadarUtils]: material on " + parts.Current.name + "could not find set RCS shader/color"); + } + } + ///////////////// + radarCam.targetTexture = rcsRendering; RenderTexture.active = rcsRendering; - Shader.SetGlobalVector("_LIGHTDIR", -cameraDirection); - Shader.SetGlobalColor("_RCSCOLOR", Color.white); + //Shader.SetGlobalVector("_LIGHTDIR", -cameraDirection); + //Shader.SetGlobalColor("_RCSCOLOR", Color.white); radarCam.RenderWithShader(BDAShaderLoader.RCSShader, string.Empty); rcsTexture.ReadPixels(new Rect(0, 0, radarResolution, radarResolution), 0, 0); rcsTexture.Apply(); @@ -690,10 +1260,10 @@ public static void SetupResources() if (!rcsSetupCompleted) { //set up rendertargets and textures - rcsRenderingVariable = new RenderTexture(radarResolution, radarResolution, 16); - rcsRendering1 = new RenderTexture(radarResolution, radarResolution, 16); - rcsRendering2 = new RenderTexture(radarResolution, radarResolution, 16); - rcsRendering3 = new RenderTexture(radarResolution, radarResolution, 16); + rcsRenderingVariable = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); + rcsRendering1 = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); + rcsRendering2 = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); + rcsRendering3 = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); drawTextureVariable = new Texture2D(radarResolution, radarResolution, TextureFormat.RGB24, false); drawTexture1 = new Texture2D(radarResolution, radarResolution, TextureFormat.RGB24, false); @@ -722,9 +1292,9 @@ public static void SetupResourcesLegacy() if (!rcsSetupCompleted) { //set up rendertargets and textures - rcsRenderingFrontal = new RenderTexture(radarResolution, radarResolution, 16); - rcsRenderingLateral = new RenderTexture(radarResolution, radarResolution, 16); - rcsRenderingVentral = new RenderTexture(radarResolution, radarResolution, 16); + rcsRenderingFrontal = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); + rcsRenderingLateral = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); + rcsRenderingVentral = new RenderTexture(radarResolution, radarResolution, (int)RenderTextureFormat.R8); drawTextureFrontal = new Texture2D(radarResolution, radarResolution, TextureFormat.RGB24, false); drawTextureLateral = new Texture2D(radarResolution, radarResolution, TextureFormat.RGB24, false); drawTextureVentral = new Texture2D(radarResolution, radarResolution, TextureFormat.RGB24, false); @@ -788,100 +1358,292 @@ public static void CleanupResourcesLegacy() } /// - /// Determine for a vesselposition relative to the radar position how much effect the ground clutter factor will have. + /// Determine for a targetDirection 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, Vector3 position, Vector3 targetDirection, TargetInfo ti) { - Vector3 upVector = referenceTransform.up; + //Vector3 upVector = referenceTransform.up; + Vector3 upVector = VectorUtils.GetUpDirection(position); //ground clutter factor when looking down: - Vector3 targetDirection = (vesselposition - position); - float angleFromUp = Vector3.Angle(targetDirection, upVector); + //Vector3 targetDirection = (vesselposition - position); + float angleFromUp = VectorUtils.AnglePreNormalized(upVector, targetDirection); 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)); + lookDownAngle = Mathf.Clamp(lookDownAngle, 0, 90); // result range: 0 .. +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; } /// - /// 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. - /// Uses detectionCurve for rcs evaluation. + /// Determine how much of an effect enemies that are jamming have on the target /// - //was: public static void UpdateRadarLock(Ray ray, float fov, float minSignature, ref TargetSignatureData[] dataArray, float dataPersistTime, bool pingRWR, RadarWarningReceiver.RWRThreatTypes rwrType, bool radarSnapshot) - public static bool RadarUpdateScanBoresight(Ray ray, float fov, ref TargetSignatureData[] dataArray, float dataPersistTime, ModuleRadar radar) + public static float GetStandoffJammingModifier(Vessel v, Competition.BDTeam team, Vector3 position, Vessel targetV, float signature) { - int dataIndex = 0; - bool hasLocked = false; + if (targetV.ActiveController().WM == null) 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 - // guard clauses - if (!radar) - return false; + float standOffJammingMod = 0f; + string debugSOJ = "Standoff Jammer Lockbreak Strengths: \n"; using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) while (loadedvessels.MoveNext()) { - // ignore null, unloaded - if (loadedvessels.Current == null) continue; - if (!loadedvessels.Current.loaded) continue; - // ignore self, ignore behind ray - Vector3 vectorToTarget = (loadedvessels.Current.transform.position - ray.origin); - if (((vectorToTarget).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) || - (Vector3.Dot(vectorToTarget, ray.direction) < 0)) - continue; + // 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 = loadedvessels.Current.ActiveController().WM; - if (Vector3.Angle(loadedvessels.Current.CoM - ray.origin, ray.direction) < fov / 2f) + if (!wm) continue; + if (team.IsFriendly(wm.Team)) continue; + + VesselECMJInfo standOffJammer = loadedvessels.Current.gameObject.GetComponent(); + + if (standOffJammer && (standOffJammer.lockBreakStrength > 0)) { - // ignore when blocked by terrain - if (TerrainCheck(ray.origin, loadedvessels.Current.transform.position)) - continue; + 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 = relPositionTarget.DotNormalized(relPositionJammer); + 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.GetName() + "\n"; + } + } + + float modifiedSignature = Mathf.Max(signature - standOffJammingMod / 100f, 0f); + + if ((BDArmorySettings.DEBUG_RADAR) && (modifiedSignature != signature)) Debug.Log("[BDArmory.RadarUtils]: Standoff Jamming: " + targetV.GetName() + " signature relative to " + v.GetName() + " modified from " + signature + " to " + modifiedSignature + "\n" + debugSOJ); + + return modifiedSignature / signature; + } + + private static float CalculateRadarNotchingModifier(Vector3 position, Vector3 vesselposition, Vector3 vesselsrfvel, FloatCurve radarRangeGate, FloatCurve radarVelocityGate, + float radarMaxVelocityGate, float radarMaxRangeGate, float radarMinVelocityGate, float radarMinRangeGate, + float terrainRange, float targetRange, float targetAlt, out float notchMod) + { + terrainRange -= targetRange; + + terrainRange = BDAMath.Sqrt(0.25f * terrainRange * terrainRange + 0.75f * targetAlt * targetAlt); + + //terrainRange *= 0.001f; // m to km + + notchMod = 0f; + + if (radarRangeGate.minTime == float.MaxValue || radarVelocityGate.minTime == float.MaxValue) + return 1f; + + Vector3 targetDirection = (vesselposition - position) / targetRange; + + float inLineSpeed = Mathf.Abs(Vector3.Dot(vesselsrfvel, targetDirection)); + + //if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: Current inLineSpeed: {inLineSpeed}."); + + if (radarMaxVelocityGate < inLineSpeed) + return 1f; + + inLineSpeed = Mathf.Max(inLineSpeed, radarMinVelocityGate); + + notchMod = (1f - Mathf.Clamp01(radarVelocityGate.Evaluate(inLineSpeed))) * BDArmorySettings.RADAR_NOTCHING_FACTOR; + + if (radarMaxRangeGate < terrainRange) + { + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: Current notch multiplier: 1. Current notchMod: {notchMod}."); + return 1f; + } + + terrainRange = Mathf.Max(terrainRange, radarMinRangeGate); + + float multiplier = notchMod * Mathf.Clamp01(radarRangeGate.Evaluate(terrainRange)); + notchMod += multiplier; + + multiplier = 1f - multiplier; + + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: Current notch multiplier: {multiplier}. Current notchMod: {notchMod}."); + + return multiplier; + } + + public static float GetRadarNotchingSCR(float signature, float fov, float targetRange, float terrainRange, float terrainAngle) + { + float groundAngleFac = 1f / Mathf.Cos(terrainAngle * Mathf.Deg2Rad); + float equivArea = terrainRange / (targetRange * 1000f); // Get equivalent radius of target RCS, targetRange is in km + equivArea *= equivArea * signature * groundAngleFac; // Similar cones, equivalent area of the target projected onto the ground + + float groundArea = terrainRange * fov * Mathf.Deg2Rad; // Approximation of radius given the current FoV + groundArea = groundArea * groundArea * groundAngleFac * Mathf.PI; + + float SCR = equivArea / (groundArea * BDArmorySettings.RADAR_NOTCHING_SCR_FACTOR); // Default to -20 dBm^2 + + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils]: Current notch SCR: {SCR}. Current Terrain Range: {terrainRange}"); + + return SCR; + } + + private static bool RadarTerrainNotchingCheck(bool isNotSonar, Vector3 position, FloatCurve radarRangeGate, FloatCurve radarVelocityGate, + float radarMaxVelocityGate, float radarMaxRangeGate, float radarMinVelocityGate, float radarMinRangeGate, + Vessel radarVessel, Vessel targetVessel, Vector3 targetPosition, float distance, out float terrainR, out float terrainAngle, + out float notchMultiplier, out float notchMod, bool isMissile = false) + { + // NOTE: Distance here HAS to be given in km for radars and m for missiles, why? because radar FloatCurves are in km and missile FloatCurves are in m + notchMod = 0f; + notchMultiplier = 1f; + terrainR = 0f; + terrainAngle = 90f; + + if (isNotSonar) + { + bool surfaceTarget = (targetVessel.Landed || targetVessel.Splashed); + // If radar, then check against water + if (BDArmorySettings.RADAR_NOTCHING && !surfaceTarget && radarMinRangeGate != float.MaxValue && radarMinVelocityGate != float.MaxValue) + { + // Because radar curves are in km, we have to convert distance to km if it's not a missile + if (TerrainCheck(position, targetPosition, FlightGlobals.currentMainBody, distance + radarMaxRangeGate, out terrainR, out terrainAngle, true)) + return false; + notchMultiplier = CalculateRadarNotchingModifier(position, targetVessel.CoM, targetVessel.srf_velocity, + radarRangeGate, radarVelocityGate, radarMaxVelocityGate, radarMaxRangeGate, radarMinVelocityGate, radarMinRangeGate, + terrainR, distance, (float)targetVessel.radarAltitude, out notchMod); + } + else + { + if (targetVessel.Splashed) + { + if (TerrainCheck(position, targetPosition + targetVessel.upAxis * (targetVessel.altitude < 0f ? -targetVessel.altitude + 2f : 0f), FlightGlobals.currentMainBody, !isMissile && BDArmorySettings.RADAR_ALLOW_SURFACE_WARFARE && surfaceTarget && (radarVessel.Landed || radarVessel.Splashed))) + return false; + } + else + { + if (TerrainCheck(position, targetPosition, FlightGlobals.currentMainBody, !isMissile && BDArmorySettings.RADAR_ALLOW_SURFACE_WARFARE && surfaceTarget && (radarVessel.Landed || radarVessel.Splashed))) + return false; + } + } + } + else + { + if (TerrainCheck(position, targetPosition)) + return false; + } + + return true; + } + + /// + /// 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. + /// Uses detectionCurve for rcs evaluation. + /// + //was: public static void UpdateRadarLock(Ray ray, float fov, float minSignature, ref TargetSignatureData[] dataArray, float dataPersistTime, bool pingRWR, RadarWarningReceiver.RWRThreatTypes rwrType, bool radarSnapshot) + public static bool RadarUpdateScanBoresight(Ray ray, float fov, ref TargetSignatureData[] dataArray, float dataPersistTime, ModuleRadar radar) + { + int dataIndex = 0; + bool hasLocked = false; + + // fov is cone width, so we use half of it + fov *= 0.5f; + + // guard clauses + if (!radar) + return false; + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + // ignore null and unloaded + if (loadedvessels.Current == null || !loadedvessels.Current.loaded || !loadedvessels.Current.isActiveAndEnabled) continue; + if (loadedvessels.Current.IsUnderwater() && radar.sonarMode == ModuleRadar.SonarModes.None) //don't detect underwater targets with radar + continue; + if (!loadedvessels.Current.Splashed && radar.sonarMode != ModuleRadar.SonarModes.None) //don't detect flying targets with sonar + continue; + // ignore self, ignore behind ray + Vector3 vectorToTarget = (loadedvessels.Current.CoM - ray.origin); + //float distance = vectorToTarget.sqrMagnitude; + (float distance, Vector3 directionToTarget) = vectorToTarget.MagNorm(); + float angle = VectorUtils.AnglePreNormalized(ray.direction, directionToTarget); + if ((distance * distance < RADAR_IGNORE_DISTANCE_SQR) || + (angle > 90f)) + continue; + + if (angle < fov) + { + float terrainR = 0f, terrainAngle = 0f; + float notchMultiplier = 1f; + float notchMod = 0f; + + // evaluate range + + if (!RadarTerrainNotchingCheck(radar.sonarMode == ModuleRadar.SonarModes.None, ray.origin, radar.radarRangeGate, radar.radarVelocityGate, + radar.radarMaxVelocityGate, radar.radarMaxRangeGate, radar.radarMinVelocityGate, radar.radarMinRangeGate, radar.vessel, + loadedvessels.Current, loadedvessels.Current.CoM, distance, out terrainR, out terrainAngle, out notchMultiplier, out notchMod)) + continue; // 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); + float signature = 0; + // The only scenario in which this should occur is if the vessel is about to be destroyed, + // in which case the previous TargetInfo was destroyed, a new one was created, however + // as the vessel is de-activated, the new TargetInfo does not have a vessel as Awake() is + // not even called. I would've thought !vessel.loaded would've caught this but perhaps not. + if (ti.Vessel == null) + continue; + if (radar.sonarMode != ModuleRadar.SonarModes.passive) + { + signature = (BDArmorySettings.ASPECTED_RCS) ? GetVesselRadarSignatureAtAspect(ti, ray.origin, distance) : ti.radarModifiedSignature; + signature *= GetRadarGroundClutterModifier(radar.radarGroundClutterFactor, ray.origin, directionToTarget, ti); + signature *= GetStandoffJammingModifier(radar.vessel, radar.WeaponManager.Team, ray.origin, loadedvessels.Current, signature); + if (radar.sonarMode == ModuleRadar.SonarModes.Active && radar.vessel.Splashed && loadedvessels.Current.Splashed) signature *= GetVesselBubbleFactor(ray.origin, loadedvessels.Current); + if (radar.radarCanNotch) + signature *= notchMultiplier; + } + else + { + float selfNoise = BDATargetManager.GetVesselAcousticSignature(radar.vessel, ray.origin).Item1 / 3; + signature = BDATargetManager.GetVesselAcousticSignature(loadedvessels.Current, ray.origin).Item1 - selfNoise; + } // 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) + // Must convert from m to km due to all radar FloatCurves being specified in km + if (RadarCanDetect(radar, signature, distance * 0.001f)) { - //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, _range: distance); + dataArray[dataIndex].lockedByRadar = radar; + dataIndex++; + hasLocked = true; } } // our radar ping can be received at a higher range than we can detect, according to RWR range ping factor: - if (distance < radar.radarMaxDistanceDetect * RWR_PING_RANGE_FACTOR) - RadarWarningReceiver.PingRWR(loadedvessels.Current, ray.origin, radar.rwrType, radar.signalPersistTimeForRwr); + if (radar.sonarMode != ModuleRadar.SonarModes.passive) + { + if (distance < radar.radarMaxDistanceDetect * RWR_PING_RANGE_FACTOR) + RadarWarningReceiver.PingRWR(loadedvessels.Current, ray.origin, radar.rwrType, radar.signalPersistTimeForRwr); + } } } @@ -903,50 +1665,89 @@ public static bool RadarUpdateMissileLock(Ray ray, float fov, ref TargetSignatur if (!missile) return false; + // fov gives cone width, so halve it + fov *= 0.5f; + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) while (loadedvessels.MoveNext()) { - // ignore null, unloaded - if (loadedvessels.Current == null) continue; - if (!loadedvessels.Current.loaded) continue; + // ignore null, unloaded and ignored types + if (loadedvessels.Current == null || loadedvessels.Current.packed || !loadedvessels.Current.loaded || !loadedvessels.Current.isActiveAndEnabled || loadedvessels.Current == missile.vessel) continue; + if (!loadedvessels.Current.Splashed && missile.GetWeaponClass() == WeaponClasses.SLW) continue; //don't detect non-water targets if a torpedo + if (loadedvessels.Current.IsUnderwater() && missile.GetWeaponClass() != WeaponClasses.SLW) continue; //don't detect underwater targets with radar // IFF code check to prevent friendly lock-on (neutral vessel without a weaponmanager WILL be lockable!) - MissileFire wm = loadedvessels.Current.FindPartModuleImplementing(); + MissileFire wm = loadedvessels.Current.ActiveController().WM; if (wm != null) { - if (missile.Team.IsFriendly(wm.Team)) + if (missile.hasIFF && missile.Team.IsFriendly(wm.Team)) continue; } // ignore self, ignore behind ray - Vector3 vectorToTarget = (loadedvessels.Current.transform.position - ray.origin); - if (((vectorToTarget).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) || - (Vector3.Dot(vectorToTarget, ray.direction) < 0)) + Vector3 vectorToTarget = (loadedvessels.Current.CoM - ray.origin); + (float distance, Vector3 directionToTarget) = vectorToTarget.MagNorm(); + float angle = VectorUtils.AnglePreNormalized(ray.direction, directionToTarget); + //if (((vectorToTarget).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) || + // (Vector3.Dot(vectorToTarget, ray.direction) < 0)) + + // No targets behind the seeker's view! Note maybe this should change, + // as unlike radars, this is called with `maxOffBoresight` in some cases + // rather than `lockedSensorFoV`, and should be treated as an overall + // scan rather than just a sensor-look scan. + if (angle > 90f) continue; - if (Vector3.Angle(loadedvessels.Current.CoM - ray.origin, ray.direction) < fov / 2f) + if (angle < fov) { - // ignore when blocked by terrain - if (TerrainCheck(ray.origin, loadedvessels.Current.transform.position)) + // evaluate range + // range already evaluated above, NOTE: no conversion from m to km is needed here due to + // all missile radar FloatCurves being in m, not km. Unfortunately as this convention + // began in legacy code, not much we can do about it! + + float terrainR = float.MaxValue; + float terrainAngle = 90f; + float notchMultiplier = 1f; + float notchMod = 0f; + + if (!RadarTerrainNotchingCheck(missile.GetWeaponClass() != WeaponClasses.SLW, ray.origin, missile.activeRadarRangeGate, missile.activeRadarVelocityGate, + missile.activeRadarVelocityFilter, missile.activeRadarRangeFilter, missile.activeRadarVelocityGate.minTime, missile.activeRadarRangeGate.minTime, missile.vessel, + loadedvessels.Current, loadedvessels.Current.CoM, distance, out terrainR, out terrainAngle, out notchMultiplier, out notchMod, true)) continue; // 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; + // See comment in RadarUpdateScanBoresight for more info about this. + if (ti.Vessel == null) + continue; + if (ti != null) + { + signature = (BDArmorySettings.ASPECTED_RCS) ? GetVesselRadarSignatureAtAspect(ti, ray.origin, distance) : ti.radarModifiedSignature; + // no ground clutter modifier for missiles + signature *= ti.radarLockbreakFactor; //multiply lockbreak factor from active ecm - // evaluate range - float distance = (loadedvessels.Current.CoM - ray.origin).magnitude; - //TODO: Performance! better if we could switch to sqrMagnitude... + } //do not multiply chaff factor here + signature *= GetStandoffJammingModifier(missile.vessel, missile.Team, ray.origin, loadedvessels.Current, signature); + if (missile.GetWeaponClass() == WeaponClasses.SLW) signature *= GetVesselBubbleFactor(missile.transform.position, loadedvessels.Current); + + float baseSignature = signature; + // Does notching affect the notch mult? + if (missile.activeRadarCanNotch) + signature *= notchMultiplier; + + // check SCR if we're checking notching, are not a torpedo, the target isn't splashed and the radar is active + // technically the notchMultiplier < 1f condition should account for the rest + // Note, since SCR behavior is only for locked radars ActiveRadar has to be true, hence why it's evaluated + // before notchMultiplier, otherwise we don't account for SCR + bool SCRcheck = BDArmorySettings.RADAR_NOTCHING && missile.activeRadarCanNotch && (notchMultiplier < 1f) && missile.GetWeaponClass() != WeaponClasses.SLW && !loadedvessels.Current.Splashed; if (distance < missile.activeRadarRange) { //evaluate if we can detect such a signature at that range - float minDetectSig = missile.activeRadarLockTrackCurve.Evaluate(distance / 1000f); + float minDetectSig = missile.activeRadarLockTrackCurve.Evaluate(distance); - if (signature > minDetectSig) + if (signature > minDetectSig || (SCRcheck && baseSignature > minDetectSig && GetRadarNotchingSCR(baseSignature, fov, distance * 0.001f, terrainR, terrainAngle) > missile.activeRadarMinTrackSCR)) { // detected by radar // fill attempted locks array for locking later: @@ -961,7 +1762,7 @@ public static bool RadarUpdateMissileLock(Ray ray, float fov, ref TargetSignatur if (dataIndex < dataArray.Length) { - dataArray[dataIndex] = new TargetSignatureData(loadedvessels.Current, signature); + dataArray[dataIndex] = new TargetSignatureData(loadedvessels.Current, signature, _notchMod: notchMod, _range: distance); dataIndex++; hasLocked = true; } @@ -991,46 +1792,106 @@ public static bool RadarUpdateMissileLock(Ray ray, float fov, ref TargetSignatur /// relevant only for modeTryLock=true /// optional, relevant only for modeTryLock=true /// - public static bool RadarUpdateScanLock(MissileFire myWpnManager, float directionAngle, Transform referenceTransform, float fov, Vector3 position, ModuleRadar radar, bool modeTryLock, ref TargetSignatureData[] dataArray, float dataPersistTime = 0f) + public static bool RadarUpdateScanLock(MissileFire myWpnManager, float directionAngle, float elevationAngle, float azFov, float elFov, ModuleRadar radar, bool modeTryLock, ref TargetSignatureData[] dataArray, float dataPersistTime = 0f) { - Vector3 forwardVector = referenceTransform.forward; - Vector3 upVector = referenceTransform.up; - Vector3 lookDirection = Quaternion.AngleAxis(directionAngle, upVector) * forwardVector; + Vector3 position = radar.currPosition; + Vector3 forwardVector = radar.currForward; + Vector3 upVector = radar.currUp; + Vector3 rightVector = radar.currRight; + //Vector3 lookDirection = Quaternion.AngleAxis(directionAngle, upVector) * forwardVector; int dataIndex = 0; bool hasLocked = false; + float selfNoise = 0; + + // fov is cone width, so we halve it + azFov *= 0.5f; + elFov *= 0.5f; + + //for (int i = 0; i < lockArray.Length; i++) + //{ + // lockArray[i] = false; + //} // guard clauses if (!myWpnManager || !myWpnManager.vessel || !radar) return false; - + if (radar.sonarMode == ModuleRadar.SonarModes.passive) + { + selfNoise = BDATargetManager.GetVesselAcousticSignature(radar.vessel, position).Item1 / 3; + } using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) while (loadedvessels.MoveNext()) { // ignore null, unloaded and self - if (loadedvessels.Current == null) continue; - if (!loadedvessels.Current.loaded) continue; + if (loadedvessels.Current == null || loadedvessels.Current.packed || !loadedvessels.Current.loaded || !loadedvessels.Current.isActiveAndEnabled) continue; if (loadedvessels.Current == myWpnManager.vessel) continue; + Vector3 vectorToTarget = loadedvessels.Current.CoM - position; + float distance = vectorToTarget.sqrMagnitude; + // ignore too close ones - if ((loadedvessels.Current.transform.position - position).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) + if (distance < RADAR_IGNORE_DISTANCE_SQR) continue; + if (loadedvessels.Current.IsUnderwater() && radar.sonarMode == ModuleRadar.SonarModes.None) //don't detect underwater targets with radar + continue; + if (!loadedvessels.Current.Splashed && radar.sonarMode != ModuleRadar.SonarModes.None) //don't detect sonar targets when out of water + continue; + + // evaluate range + //TODO: Performance! better if we could switch to sqrMagnitude... + distance = BDAMath.Sqrt(distance); + + // Get azimuth and elevation relative to the target + //VectorUtils.GetAzimuthElevation(vectorToTarget, forwardVector, upVector, out float targetAz, out float targetEl); + float targetAz = VectorUtils.GetAngleOnPlane(vectorToTarget, forwardVector, rightVector); + float targetEl = VectorUtils.GetElevation(vectorToTarget, upVector, distance, 1.0f); + + // Correct for omnidirectional radars + if (directionAngle > 180f) + directionAngle -= 360f; + + // Since azimuth can go all the way around, if we get a + // reflex angle, get the conjugate. + float azDiff = Mathf.Abs(targetAz - directionAngle); + if (azDiff > 180f) + azDiff = 360f - azDiff; - Vector3 vesselDirection = Vector3.ProjectOnPlane(loadedvessels.Current.CoM - position, upVector); - if (Vector3.Angle(vesselDirection, lookDirection) < fov / 2f) + if (azDiff < azFov && Mathf.Abs(targetEl - elevationAngle) < elFov) { - // ignore when blocked by terrain - if (TerrainCheck(referenceTransform.position, loadedvessels.Current.transform.position)) + float terrainR = 0f, terrainAngle = 0f; + float notchMultiplier = 1f; + float notchMod = 0f; + + Vector3 directionToTarget = vectorToTarget / distance; + + if (!RadarTerrainNotchingCheck(radar.sonarMode == ModuleRadar.SonarModes.None, position, radar.radarRangeGate, radar.radarVelocityGate, + radar.radarMaxVelocityGate, radar.radarMaxRangeGate, radar.radarMinVelocityGate, radar.radarMinRangeGate, radar.vessel, + loadedvessels.Current, loadedvessels.Current.CoM, distance, out terrainR, out terrainAngle, out notchMultiplier, out notchMod)) continue; + // get vessel's radar signature TargetInfo ti = GetVesselRadarSignature(loadedvessels.Current); - float signature = ti.radarModifiedSignature; + float signature = 1; + // See comment in RadarUpdateScanBoresight for more info about this + if (ti.Vessel == null) + continue; + if (radar.sonarMode != ModuleRadar.SonarModes.passive) //radar or active soanr + { + signature = BDArmorySettings.ASPECTED_RCS ? GetVesselRadarSignatureAtAspect(ti, position, distance) : ti.radarModifiedSignature; + signature *= GetRadarGroundClutterModifier(radar.radarGroundClutterFactor, position, directionToTarget, ti); + if (radar.sonarMode == ModuleRadar.SonarModes.Active && radar.vessel.Splashed && loadedvessels.Current.Splashed) signature *= GetVesselBubbleFactor(position, loadedvessels.Current); + + if (radar.radarCanNotch) + signature *= notchMultiplier; + } + else //passive sonar + signature = BDATargetManager.GetVesselAcousticSignature(loadedvessels.Current, position).Item1 - selfNoise; //do not multiply chaff factor here - signature *= GetRadarGroundClutterModifier(radar, 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... + distance *= 0.001f; // Need to convert from m to km because of radar FloatCurves... + BDATargetManager.ClearRadarReport(loadedvessels.Current, myWpnManager); if (modeTryLock) // LOCK/TRACK TARGET: { //evaluate if we can lock/track such a signature at that range @@ -1038,15 +1899,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: @@ -1059,45 +1922,44 @@ public static bool RadarUpdateScanLock(MissileFire myWpnManager, float direction dataIndex++; } - if (dataIndex < dataArray.Length) + if (!(dataIndex < dataArray.Length)) { - dataArray[dataIndex] = new TargetSignatureData(loadedvessels.Current, signature); - dataIndex++; - hasLocked = true; + Array.Resize(ref dataArray, BDATargetManager.LoadedVessels.Count); } + + dataArray[dataIndex] = new TargetSignatureData(loadedvessels.Current, signature, _range: 1000f * distance); + dataArray[dataIndex].lockedByRadar = radar; + dataIndex++; + hasLocked = true; } } - - // our radar ping can be received at a higher range than we can lock/track, according to RWR range ping factor: - if (distance < radar.radarMaxDistanceLockTrack * RWR_PING_RANGE_FACTOR) - RadarWarningReceiver.PingRWR(loadedvessels.Current, position, radar.rwrType, radar.signalPersistTimeForRwr); + if (radar.sonarMode != ModuleRadar.SonarModes.passive) + { + // our radar ping can be received at a higher range than we can lock/track, according to RWR range ping factor: + if (distance < radar.radarMaxDistanceLockTrack * RWR_PING_RANGE_FACTOR) + RadarWarningReceiver.PingRWR(loadedvessels.Current, position, radar.rwrType, radar.signalPersistTimeForRwr); + } } 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); } - } - // our radar ping can be received at a higher range than we can detect, according to RWR range ping factor: - if (distance < radar.radarMaxDistanceDetect * RWR_PING_RANGE_FACTOR) - RadarWarningReceiver.PingRWR(loadedvessels.Current, position, radar.rwrType, radar.signalPersistTimeForRwr); + // report scanned targets only + radar.ReceiveContactData(new TargetSignatureData(loadedvessels.Current, signature, _range: 1000f * distance), false); + } + if (radar.sonarMode != ModuleRadar.SonarModes.passive) + { + // our radar ping can be received at a higher range than we can detect, according to RWR range ping factor: + if (distance < radar.radarMaxDistanceDetect * RWR_PING_RANGE_FACTOR) + RadarWarningReceiver.PingRWR(loadedvessels.Current, position, radar.rwrType, radar.signalPersistTimeForRwr); + } } } } @@ -1118,6 +1980,12 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov if (!radar) return false; + // fov is cone width, so we use half of it + fov *= 0.5f; + + Vector3 directionToTarget = Vector3.zero; + float distance = -1f; + // first: re-acquire lock if temporarily lost if (!lockedVessel) { @@ -1125,16 +1993,24 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov while (loadedvessels.MoveNext()) { // ignore null, unloaded - if (loadedvessels.Current == null) continue; - if (!loadedvessels.Current.loaded) continue; + if (loadedvessels.Current == null || !loadedvessels.Current.loaded || !loadedvessels.Current.isActiveAndEnabled) continue; + + // Seems like we only use it once so I've left it as is, but I've written this down as a reminder + // do consider replacing all .transform.position calls with .CoM + //targetPosition = loadedvessels.Current.transform.position; // ignore self, ignore behind ray - Vector3 vectorToTarget = (loadedvessels.Current.transform.position - ray.origin); - if (((vectorToTarget).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) || - (Vector3.Dot(vectorToTarget, ray.direction) < 0)) + Vector3 vectorToTarget = (loadedvessels.Current.CoM - ray.origin); + (float tempDistance, Vector3 tempDirectionToTarget) = vectorToTarget.MagNorm(); + float angle = VectorUtils.AnglePreNormalized(ray.direction, tempDirectionToTarget); + if ((tempDistance * tempDistance < RADAR_IGNORE_DISTANCE_SQR) || + (angle > 90f)) continue; - if (Vector3.Angle(loadedvessels.Current.CoM - ray.origin, ray.direction) < fov / 2) + // See RadarUpdateScanBoresight for discussion of the efficiency + // of performing VectorUtils.Angle() here, repeating the above Dot + // product. + if (angle < fov) { float sqrDist = Vector3.SqrMagnitude(loadedvessels.Current.CoM - predictedPos); if (sqrDist < closestSqrDist) @@ -1142,45 +2018,80 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov // best candidate so far, take it closestSqrDist = sqrDist; lockedVessel = loadedvessels.Current; + distance = tempDistance; + directionToTarget = tempDirectionToTarget; } } } } + else + { + (distance, directionToTarget) = (lockedVessel.CoM - ray.origin).MagNorm(); + } // second: track that lock if (lockedVessel) { - // blocked by terrain? - if (TerrainCheck(ray.origin, lockedVessel.transform.position)) - { - radar.UnlockTargetAt(lockIndex, true); + // Check within FoV + if (!radar.CheckFOVDir(directionToTarget)) + return false; + + // evaluate range + //TODO: Performance! better if we could switch to sqrMagnitude... + + float notchMultiplier = 1f; + float notchMod = 0f; + + float terrainR = float.MaxValue; + float terrainAngle = 90f; + + if (!RadarTerrainNotchingCheck(radar.sonarMode == ModuleRadar.SonarModes.None, ray.origin, radar.radarRangeGate, radar.radarVelocityGate, + radar.radarMaxVelocityGate, radar.radarMaxRangeGate, radar.radarMinVelocityGate, radar.radarMinRangeGate, radar.vessel, + lockedVessel, lockedVessel.CoM, distance, out terrainR, out terrainAngle, out notchMultiplier, out notchMod)) return false; - } // get vessel's radar signature TargetInfo ti = GetVesselRadarSignature(lockedVessel); - float signature = ti.radarModifiedSignature; - signature *= GetRadarGroundClutterModifier(radar, radar.referenceTransform, ray.origin, lockedVessel.CoM, ti); + // See comment in RadarUpdateScanBoresight for more about this + if (ti.Vessel == null) + return false; + float signature = (BDArmorySettings.ASPECTED_RCS) ? GetVesselRadarSignatureAtAspect(ti, ray.origin, distance) : ti.radarModifiedSignature; + signature *= GetRadarGroundClutterModifier(radar.radarGroundClutterFactor, ray.origin, directionToTarget, 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); + if (radar.sonarMode == ModuleRadar.SonarModes.Active && radar.vessel.Splashed && lockedVessel.Splashed) signature *= GetVesselBubbleFactor(radar.transform.position, lockedVessel); //do not multiply chaff factor here - // evaluate range - float distance = (lockedVessel.CoM - ray.origin).magnitude / 1000f; //TODO: Performance! better if we could switch to sqrMagnitude... + float baseSignature = signature; // Kept for notching + + if (radar.radarCanNotch) + signature *= notchMultiplier; + + distance *= 0.001f; // Convert from m to km due to radar FloatCurves... + if (distance > radar.radarMinDistanceLockTrack && distance < radar.radarMaxDistanceLockTrack) { //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); + radar.ReceiveContactData(new TargetSignatureData(lockedVessel, signature, _notchMod: notchMod, _range: 1000f * distance), locked); } else { - // cannot track, so unlock it - radar.UnlockTargetAt(lockIndex, true); - return false; + // cannot track, so unlock it, unless above SCR, note we only check SCR if we're checking for notching + // and the notchMultiplier < 1f, the rest of the conditions are a failsafe + if (BDArmorySettings.RADAR_NOTCHING && radar.radarCanNotch && (notchMultiplier < 1f) && radar.sonarMode == ModuleRadar.SonarModes.None && !(BDArmorySettings.RADAR_ALLOW_SURFACE_WARFARE && (lockedVessel.Landed || lockedVessel.Splashed) && (radar.vessel.Landed || radar.vessel.Splashed)) && radar.radarMinRangeGate != float.MaxValue && radar.radarMinVelocityGate != float.MaxValue) + { + if (baseSignature < minTrackSig || !RadarCanDetect(radar, baseSignature, distance) || (GetRadarNotchingSCR(baseSignature, fov, distance, terrainR, terrainAngle) < radar.radarMinTrackSCR)) + return false; + + radar.ReceiveContactData(new TargetSignatureData(lockedVessel, signature, _notchMod: notchMod, _range: 1000f * distance), locked); + } + else + return false; } } @@ -1193,17 +2104,141 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov else { // nothing tracked/locked at this index - radar.UnlockTargetAt(lockIndex, true); + 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; + + // fov is cone width, so we use half of it + fov *= 0.5f; + + 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 + Vector3 vectorToTarget = loadedvessels.Current.CoM - position; + float distance = vectorToTarget.sqrMagnitude; + if (distance < RADAR_IGNORE_DISTANCE_SQR) + continue; + + Vector3 vesselDirection = vectorToTarget.ProjectOnPlanePreNormalized(upVector); + float angle = VectorUtils.Angle(vesselDirection, lookDirection); + if (angle < fov) + { + // ignore when blocked by terrain + /* + if (TerrainCheck(referenceTransform.position, loadedvessels.Current.CoM)) + continue; + */ + if (loadedvessels.Current.Splashed) + { + if (TerrainCheck(position, loadedvessels.Current.CoM + loadedvessels.Current.upAxis * (loadedvessels.Current.altitude < 0f ? -loadedvessels.Current.altitude + 2f : 0f), FlightGlobals.currentMainBody)) + return false; + } + else + { + if (TerrainCheck(position, loadedvessels.Current.CoM, FlightGlobals.currentMainBody)) + return false; + } + + // get vessel's heat signature + TargetInfo tInfo = loadedvessels.Current.gameObject.GetComponent(); + if (tInfo == null) + { + tInfo = loadedvessels.Current.gameObject.AddComponent(); + } + + IRSig = BDATargetManager.GetVesselHeatSignature(loadedvessels.Current, 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 + + // evaluate range + distance = BDAMath.Sqrt(distance); + + signature *= Mathf.Clamp(VectorUtils.Angle(vectorToTarget, -irst.vessel.upAxis) / 90, 0.5f, 1.5f); + //ground will mask thermal sig + signature *= (GetRadarGroundClutterModifier(irst.GroundClutterFactor, position, vectorToTarget / distance, tInfo) * (tInfo.isSplashed ? 12 : 1)); + //cold ocean on the other hand... + + distance *= 0.001f; //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(position), FlightGlobals.getExternalTemperature(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 + if (radar.vessel.altitude < -10 && radar.sonarMode == ModuleRadar.SonarModes.None) return detected; // Normal Radar Should not detect stuff underwater + if (!radar.vessel.Splashed && radar.sonarMode != ModuleRadar.SonarModes.None) return detected; // Sonar should only work when in the water + + //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. /// (Visual Target acquisition) /// - public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Transform referenceTransform, float fov, float maxDistance) + public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Transform referenceTransform, float fov, float maxViewDistance, RadarWarningReceiver RWR = null) { fov *= 1.1f; var results = new ViewScanResults @@ -1211,12 +2246,15 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra foundMissile = false, foundHeatMissile = false, foundRadarMissile = false, + foundAntiRadiationMissile = false, + foundGPSMissile = false, foundAGM = false, firingAtMe = false, missDistance = float.MaxValue, - missileThreatDistance = float.MaxValue, + missDeviation = float.MaxValue, threatVessel = null, - threatWeaponManager = null + threatWeaponManager = null, + incomingMissiles = new List() }; if (!myWpnManager || !referenceTransform) @@ -1224,112 +2262,232 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra return results; } + // fov is cone width, so use the half angle + fov *= 0.5f; + Vector3 position = referenceTransform.position; - //Vector3d geoPos = VectorUtils.WorldPositionToGeoCoords(position, FlightGlobals.currentMainBody); Vector3 forwardVector = referenceTransform.forward; Vector3 upVector = referenceTransform.up; Vector3 lookDirection = -forwardVector; + var AI = myWpnManager.vessel.ActiveController().AI; + var ignoreMyTargetTargetingMe = AI != null && AI.pilotEnabled && AI.aiType switch + { + AIType.PilotAI => (AI as BDModulePilotAI).evasionIgnoreMyTargetTargetingMe, + AIType.OrbitalAI => (AI as BDModuleOrbitalAI).evasionIgnoreMyTargetTargetingMe, + _ => false + }; + float maxRWRDistance = RWR != null ? RWR.rwrDisplayRange : maxViewDistance; + float maxScanDistance = maxRWRDistance; using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) while (loadedvessels.MoveNext()) { - if (loadedvessels.Current == null) continue; - - if (loadedvessels.Current.loaded) + if (loadedvessels.Current == null || !loadedvessels.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(loadedvessels.Current.vesselType)) continue; + if (loadedvessels.Current == myWpnManager.vessel) continue; //ignore self + var tgtMF = loadedvessels.Current.ActiveController().WM; + if (tgtMF && tgtMF.vesselRadarData) maxRWRDistance = tgtMF.vesselRadarData.MaxRadarRange() * 2; + maxScanDistance = Mathf.Max(maxViewDistance, maxRWRDistance); + Vector3 vesselDirection = loadedvessels.Current.CoM - position; + Vector3 vesselProjectedDirection = (vesselDirection).ProjectOnPlanePreNormalized(upVector); + float vesselDistanceSqr = (vesselDirection).sqrMagnitude; + //BDATargetManager.ClearRadarReport(loadedvessels.Current, myWpnManager); //reset radar contact status + if (vesselDistanceSqr < maxScanDistance * maxScanDistance) // && VectorUtils.Angle(vesselProjectedDirection, lookDirection) < fov / 2f) // && VectorUtils.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 (loadedvessels.Current == myWpnManager.vessel) continue; //ignore self - - Vector3 vesselProjectedDirection = Vector3.ProjectOnPlane(loadedvessels.Current.transform.position - position, 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 / 2 && Vector3.Angle(loadedvessels.Current.transform.position - position, -myWpnManager.transform.forward) < myWpnManager.guardAngle / 2) + TargetInfo tInfo; + if ((tInfo = loadedvessels.Current.gameObject.GetComponent())) { - //Debug.Log("Found vessel: " + vessel.vesselName); - if (TerrainCheck(referenceTransform.position, loadedvessels.Current.transform.position)) + if (TerrainCheck(position, loadedvessels.Current.CoM, loadedvessels.Current.mainBody)) + { continue; //blocked by terrain - - BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); - - vesselDistance = Mathf.Sqrt(vesselDistance); - Vector3 predictedRelativeDirection = loadedvessels.Current.transform.position - myWpnManager.vessel.PredictPosition(vesselDistance / (950 + Vector3.Dot(myWpnManager.vessel.Velocity(), vesselDirection.normalized))); - - TargetInfo tInfo; - if ((tInfo = loadedvessels.Current.gameObject.GetComponent())) + } + if (tInfo.isMissile) { - if (tInfo.isMissile) + //if (TerrainCheck(position, loadedvessels.Current.CoM, FlightGlobals.currentMainBody)) + //{ + // continue; //blocked by terrain + //} + MissileBase missileBase = tInfo.MissileBaseModule; + if (missileBase != null) { - MissileBase missileBase; - if (missileBase = tInfo.MissileBaseModule) + if (missileBase.SourceVessel == myWpnManager.vessel) continue; // ignore missiles we've fired + float sightDistance = 0; + float angle = VectorUtils.Angle(vesselProjectedDirection, lookDirection); + if (angle < fov) + sightDistance = maxViewDistance; + //bool seenByRadar = myWpnManager.vesselRadarData && myWpnManager.vesselRadarData.detectedRadarTarget(loadedvessels.Current, myWpnManager).exists; + if (BDArmorySettings.VARIABLE_MISSILE_VISIBILITY) //missiles tracked visually { - if (missileBase.SourceVessel == myWpnManager.vessel) continue; // ignore missiles we've fired - - results.foundMissile = true; - results.threatVessel = missileBase.vessel; - Vector3 vectorFromMissile = myWpnManager.vessel.CoM - missileBase.part.transform.position; - Vector3 relV = missileBase.vessel.Velocity() - myWpnManager.vessel.Velocity(); - bool approaching = Vector3.Dot(relV, vectorFromMissile) > 0; - - if (missileBase.HasFired && missileBase.TimeIndex > 1 && approaching && (missileBase.TargetPosition - (myWpnManager.vessel.CoM + (myWpnManager.vessel.Velocity() * Time.fixedDeltaTime))).sqrMagnitude < 3600) + //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? + sightDistance = maxViewDistance * (missileBase.MissileState == MissileBase.MissileStates.Boost ? 1 : (missileBase.MissileState == MissileBase.MissileStates.Cruise ? 0.75f : 0.33f)); + } + if (RWR != null && RWR.enabled) + { + if (RWR.omniDetection || (missileBase.TargetingMode == MissileBase.TargetingModes.Radar && missileBase.ActiveRadar)) //omniRWR or active radar missile { - if (missileBase.TargetingMode == MissileBase.TargetingModes.Heat) - { + if (angle < RWR.fieldOfView * 0.5f) + sightDistance = maxRWRDistance; //missile tracked by RWR + } + } + //if (!seenByRadar && + 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.TimeToCPA(missileBase.vessel, myWpnManager.vessel, myWpnManager.evadeThreshold * 1.2f), + position = missileBase.transform.position, + vessel = missileBase.vessel, + weaponManager = missileBase.SourceVessel == null ? null : missileBase.SourceVessel.ActiveController().WM, + }); + switch (missileBase.TargetingMode) + { + case MissileBase.TargetingModes.Heat: results.foundHeatMissile = true; - results.missileThreatDistance = Mathf.Min(results.missileThreatDistance, Vector3.Distance(missileBase.part.transform.position, myWpnManager.part.transform.position)); - results.threatPosition = missileBase.transform.position; break; - } - else if (missileBase.TargetingMode == MissileBase.TargetingModes.Radar) - { + case MissileBase.TargetingModes.Radar: results.foundRadarMissile = true; - results.missileThreatDistance = Mathf.Min(results.missileThreatDistance, Vector3.Distance(missileBase.part.transform.position, myWpnManager.part.transform.position)); - results.threatPosition = missileBase.transform.position; - } - else if (missileBase.TargetingMode == MissileBase.TargetingModes.Laser) - { + break; + case MissileBase.TargetingModes.Laser: results.foundAGM = true; - results.missileThreatDistance = Mathf.Min(results.missileThreatDistance, Vector3.Distance(missileBase.part.transform.position, myWpnManager.part.transform.position)); break; - } + 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; + case MissileBase.TargetingModes.Gps: + case MissileBase.TargetingModes.Inertial: + results.foundGPSMissile = true; + break; } + if (missileBase.GetWeaponClass() == WeaponClasses.SLW) results.foundTorpedo = true; } + BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); //report all missiles in RWR range so default RWR Missile Approach Warning behavior can correctly detect missile } else { - using (List.Enumerator weapon = loadedvessels.Current.FindPartModulesImplementing().GetEnumerator()) - while (weapon.MoveNext()) + Debug.LogWarning("[BDArmory.RadarUtils]: Supposed missile (" + loadedvessels.Current.vesselName + ") has no MissileBase!"); + tInfo.isMissile = false; // The target vessel has lost it's missile base component and should no longer count as a missile. This can happen for modular missiles that are getting destroyed. + } + } + else if (myWpnManager.guardMode) // Only check being under fire when in guard mode (for non-guardmode CMs) and when within view range/FOV. + { + if (vesselDistanceSqr < maxViewDistance * maxViewDistance && + VectorUtils.Angle(vesselProjectedDirection, lookDirection) < fov && + myWpnManager.CanSeeTarget(tInfo, false, false)) + { + BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); //we have visual on the target, report it. + using var weapon = VesselModuleRegistry.GetModules(loadedvessels.Current).GetEnumerator(); + while (weapon.MoveNext()) + { + var threatWeaponManager = weapon.Current.WeaponManager; + if (weapon.Current == null || threatWeaponManager == null) continue; + if (ignoreMyTargetTargetingMe && myWpnManager.currentTarget != null && threatWeaponManager.vessel == myWpnManager.currentTarget.Vessel) continue; + // If we're being targeted, calculate a miss distance + if (threatWeaponManager.currentTarget != null && threatWeaponManager.currentTarget.Vessel == myWpnManager.vessel) { - if (weapon.Current == null || weapon.Current.weaponManager == null) continue; - // If we're being targeted, calculate a miss distance - if (weapon.Current.weaponManager.currentTarget != null && weapon.Current.weaponManager.currentTarget.Vessel == myWpnManager.vessel - && MissDistance(weapon.Current, myWpnManager.vessel) < results.missDistance) + var missDistance = MissDistance(weapon.Current, myWpnManager.vessel); + if (missDistance < results.missDistance) { results.firingAtMe = true; results.threatPosition = weapon.Current.fireTransforms[0].position; // Position of weapon that's attacking. results.threatVessel = weapon.Current.vessel; - results.threatWeaponManager = weapon.Current.weaponManager; - results.missDistance = MissDistance(weapon.Current, myWpnManager.vessel); + results.threatWeaponManager = threatWeaponManager; + results.missDistance = missDistance; + results.missDeviation = (weapon.Current.fireTransforms[0].position - myWpnManager.vessel.CoM).magnitude * weapon.Current.maxDeviation / 2f * Mathf.Deg2Rad; // y = x*tan(θ), expansion of tan(θ) is θ + O(θ^3). } } + } } } } + else BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager, false, true); //initial adding of TargetInfo to this vessel } } + // Sort incoming missiles by time + if (results.incomingMissiles.Count > 0) + { + results.foundMissile = true; + results.incomingMissiles.Sort(delegate (IncomingMissile m1, IncomingMissile m2) { return m1.time.CompareTo(m2.time); }); + + // If the missile is further away than 16s (max time calculated), then sort by distance + if (results.incomingMissiles[0].time >= 16f) + { + results.foundMissile = true; + results.incomingMissiles.Sort(delegate (IncomingMissile m1, IncomingMissile m2) { return m1.distance.CompareTo(m2.distance); }); + } + } return results; } - private static float MissDistance(ModuleWeapon threatWeapon, Vessel self) // Returns how far away bullets from enemy are from craft in meters + public static bool MissileIsThreat(MissileBase missile, MissileFire mf, bool threatToMeOnly = true) { + if (missile == null || missile.part == null) return false; + Vector3 vectorFromMissile = mf.vessel.CoM - missile.vessel.CoM; + //if ((vectorFromMissile.sqrMagnitude > (mf.rwr && mf.rwr.omniDetection ? mf.rwr.rwrDisplayRange * mf.rwr.rwrDisplayRange : mf.guardRange * mf.guardRange)) && (missile.TargetingMode != MissileBase.TargetingModes.Radar)) return false; + bool maneuverCapability = missile.vessel.InVacuum() ? true : missile.vessel.srfSpeed > missile.GetKinematicSpeed(); // Missiles with no ability to hit target are not a threat + if (threatToMeOnly) + { + Vector3 relV = missile.vessel.Velocity() - mf.vessel.Velocity(); + bool approaching = Vector3.Dot(relV, vectorFromMissile) > 0; + bool teammate = false; // Missile isn't coming from teammate + if (missile.SourceVessel != null && missile.SourceVessel.ActiveController().WM != null) + teammate = (missile.SourceVessel.ActiveController().WM.team == mf.team) && (missile.targetVessel != null ? missile.targetVessel != mf.vessel : true); // Missile is fired from teammate and not locked onto us + bool withinRadarFOV = (missile.TargetingMode == MissileBase.TargetingModes.Radar && !teammate) ? + (VectorUtils.Angle(missile.GetForwardTransform(), vectorFromMissile) <= Mathf.Clamp(missile.lockedSensorFOV, 40f, 90f) * 0.5f) : false; + var missileBlastRadiusSqr = teammate ? mf.vessel.GetRadius() : 3f * Mathf.Max(missile.GetBlastRadius(), mf.vessel.GetRadius()); // Blast radius or self radius, whichever is larger (use self radius if missile is from teammate) + missileBlastRadiusSqr *= missileBlastRadiusSqr; + + return (missile.HasFired && missile.MissileState > MissileBase.MissileStates.Drop && approaching && maneuverCapability && + ( + (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, Mathf.Max(mf.cmThreshold, mf.evadeThreshold)) < missileBlastRadiusSqr || // Closest approach is within blast radius of missile. + withinRadarFOV // We are within radar FOV of missile boresight. + )); + } + else + { + using (var friendly = FlightGlobals.Vessels.GetEnumerator()) + while (friendly.MoveNext()) + { + if (friendly.Current == null) + continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(friendly.Current.vesselType)) continue; + var wm = friendly.Current.ActiveController().WM; + if (wm == null || wm.Team != mf.Team) + continue; - // If we have a firing solution, use that, otherwise use relative vessel positions + Vector3 relV = missile.vessel.Velocity() - wm.vessel.Velocity(); + bool approaching = Vector3.Dot(relV, vectorFromMissile) > 0; + bool withinRadarFOV = (missile.TargetingMode == MissileBase.TargetingModes.Radar) ? + (VectorUtils.Angle(missile.GetForwardTransform(), vectorFromMissile) <= Mathf.Clamp(missile.lockedSensorFOV, 40f, 90f) * 0.5f) : false; + var missileBlastRadiusSqr = 3f * missile.GetBlastRadius(); + missileBlastRadiusSqr *= missileBlastRadiusSqr; + + return (missile.HasFired && missile.TimeIndex > 1f && approaching && maneuverCapability && + ( + (missile.TargetPosition - (wm.vessel.CoM + (wm.vessel.Velocity() * Time.fixedDeltaTime))).sqrMagnitude < missileBlastRadiusSqr || // Target position is within blast radius of missile. + wm.vessel.PredictClosestApproachSqrSeparation(missile.vessel, Mathf.Max(wm.evadeThreshold, wm.cmThreshold)) < missileBlastRadiusSqr || // Closest approach is within blast radius of missile. + withinRadarFOV // We are within radar FOV of missile boresight. + )); + } + } + return false; + } + + 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. + if (threatWeapon.maxEffectiveDistance * threatWeapon.maxEffectiveDistance < (fireTransform.position - self.CoM).sqrMagnitude) return float.MaxValue; + // If we have a firing solution, use that, otherwise use relative vessel positions Vector3 aimDirection = fireTransform.forward; 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); @@ -1341,44 +2499,214 @@ 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) //Thisversion of TerrainCheck is only used by weapon LOS check, and should never be disabled. + //{ + return Physics.Linecast(start, end, (int)LayerMasks.Scenery); + //} + //return false; + } + + /// + /// Helper method: check if line intersects terrain OR water + /// + public static bool TerrainCheck(Vector3 start, Vector3 end, CelestialBody body, bool forceIgnoreWater = false) + { + if (!BDArmorySettings.CHECK_WATER_TERRAIN || forceIgnoreWater) + return Physics.Linecast(start, end, (int)LayerMasks.Scenery); + + if (!Physics.Linecast(start, end, (int)LayerMasks.Scenery)) + { + float dummyR, dummyA; + bool result = checkWater(start, end, body, -1f, out dummyR, out dummyA); + return result; + } + + return true; + } + + /// + /// Helper method: check if line intersects terrain and gives range and angle of intersection. Note this check goes to up to sqrRange, though the boolean behavior is still restricted to between start and end + /// + public static bool TerrainCheck(Vector3 start, Vector3 end, CelestialBody body, float range, out float R, out float angle, bool forceWaterCheck = false) { + angle = 0f; if (!BDArmorySettings.IGNORE_TERRAIN_CHECK) { - return Physics.Linecast(start, end, 1 << 15); + Vector3 offset = end - start; + //if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils.TerrainCheck]: Current calcSqrDist: {sqrDist * 0.000001f} km^2."); + + RaycastHit hitInfo; + + if (Physics.Raycast(start, end - start, out hitInfo, range, (int)LayerMasks.Scenery, QueryTriggerInteraction.UseGlobal) && (((hitInfo.point - body.position).sqrMagnitude > body.Radius * body.Radius) || !body.ocean)) + { + // If we hit terrain and we're above sea level + R = hitInfo.distance; + angle = 90f - VectorUtils.Angle(hitInfo.normal, start - end); + + //if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils.TerrainCheck]: Hit terrain at sqrDist {R * R * 0.000001f} km^2. Terrain blocking?: {(R * R) < offset.sqrMagnitude}"); + + return (R * R) < offset.sqrMagnitude; + } + else + { + if (!(BDArmorySettings.CHECK_WATER_TERRAIN || forceWaterCheck)) + { + R = float.MaxValue; + return false; + } + + if (checkWater(start, end, body, range, out R, out angle, true)) + { + R = BDAMath.Sqrt(R); + + //if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.RadarUtils.TerrainCheck]: Hit water at sqrDist {R * R * 0.000001f} km^2. Water blocking?: {(R * R) < offset.sqrMagnitude}"); + + return (R * R) < offset.sqrMagnitude; + } + return false; + } } + R = float.MaxValue; + return false; + } + + public static bool checkWater(Vector3 start, Vector3 end, CelestialBody body, float range, out float sqrRange, out float angle, bool calcAngle = false) + { + angle = 0f; + if (body.ocean || !body.hasSolidSurface) + { + double R = body.Radius; + double x, y, z; + double xB, yB, zB; + double a, b, c, det; + + x = end.x - start.x; + y = end.y - start.y; + z = end.z - start.z; + xB = body.position.x; + yB = body.position.y; + zB = body.position.z; + + a = x * x + y * y + z * z; + b = 2.0 * (x * (start.x - xB) + y * (start.y - yB) + z * (start.z - zB)); + c = xB * xB + yB * yB + zB * zB + start.x * start.x + start.y * start.y + start.z * start.z - 2.0 * (xB * start.x + yB * start.y + zB * start.z) - R * R; + det = b * b - 4.0 * a * c; + if (a < 0.001 || det < 0 || b > 0) + { + sqrRange = float.MaxValue; + return false; + } + + double u; + + if (det < 0.0001f) + { + // Quadratic Eq assuming det = 0: u = - b / (2 * a) + u = (-0.5 * b / a); + } + else + { + // Quadratic Eq: u = (-b - sqrt(det)) / (2 * a) + u = 0.5f * (-b - Math.Sqrt(det)) / a; + } + + sqrRange = (float)(a * u * u); + + // If the point of intersection is further than the range we're checking then just ignore this + if (range < 0) + { + if (u > 1.0) + return false; + } + else if (sqrRange > range * range) + return false; + if (calcAngle) + { + Vector3 intcptVec; + intcptVec.x = (float)(u * x + start.x - xB); + intcptVec.y = (float)(u * y + start.y - yB); + intcptVec.z = (float)(u * z + start.z - zB); + + angle = VectorUtils.Angle(new Vector3(-(float)x, -(float)y, -(float)z), intcptVec); + angle = 90f - angle; + } + + return true; + } + + sqrRange = float.MaxValue; return false; } + // Previously log scale depended on window size, this has now been made + // independent of window size. Based on the default window size of + // 256 pixels for RWRs (256 looks to provide better curve than 360) + // 1f / Mathf.Log(128f + 1f) + const float logRangeDenominator = 0.20576925955053367050163980444128f; + // 1f / Mathf.Log(256f + 1f) + const float logRangeRadialDenominator = 0.18021017998330121274552856323254f; + /// /// Helper method: map a position onto the radar display /// public static Vector2 WorldToRadar(Vector3 worldPosition, Transform referenceTransform, Rect radarRect, float maxDistance) { - 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) + { + (float dist, Vector3 dir) = localPosition.MagNorm(); + float scale = Mathf.Log(dist * 128f / maxDistance + 1f) * logRangeDenominator; + localPosition = dir * scale; + return new Vector2((radarRect.width * 0.5f) * (1f + localPosition.x), (radarRect.height * 0.5f) * (1f - localPosition.z)); + } + else + { + float scale = (radarRect.height * 0.5f) / maxDistance; + return new Vector2((radarRect.width * 0.5f) + (localPosition.x * scale), ((radarRect.height * 0.5f) - (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) + public static Vector2 WorldToRadarRadial(Vector3 worldPosition, Transform referenceTransform, Rect radarRect, float maxDistance, float maxAngle, bool noLog = false) { if (referenceTransform == null) return new Vector2(); - float scale = maxDistance / (radarRect.height); Vector3 localPosition = referenceTransform.InverseTransformPoint(worldPosition); localPosition.y = 0; - 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 angle = VectorUtils.GetAngleOnPlane(localPosition, Vector3.forward, Vector3.right); + //if (localPosition.x < 0) angle = -angle; + float xPos = (radarRect.width * 0.5f) + ((angle / maxAngle) * radarRect.width * 0.5f); + float yPos = radarRect.height; + + if (BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY && !noLog) + { + float scale = Mathf.Log(localPosition.magnitude * 128f / maxDistance + 1f) * logRangeDenominator; + yPos -= radarRect.height * scale * scale; // Log^2 scales better here for some reason. + } + else + { + float scale = radarRect.height / maxDistance; + yPos -= localPosition.magnitude * scale; + } Vector2 radarPos = new Vector2(xPos, yPos); return radarPos; } + + /// + /// Returns string for use in RCS analysis window, if RCS is non-zero and 0.01 m^2 or lower, it will return result in dBsm instead of m^2 + /// + public static string RCSString(float rcs) + { + if (rcs >= 0.01f || rcs == 0f) + return rcs.ToString("0.00") + " m²"; + else + return (10f * Mathf.Log10(rcs)).ToString("0.0") + " dBsm"; + } } -} +} \ No newline at end of file diff --git a/BDArmory/Modules/RadarWarningReceiver.cs b/BDArmory/Radar/RadarWarningReceiver.cs similarity index 61% rename from BDArmory/Modules/RadarWarningReceiver.cs rename to BDArmory/Radar/RadarWarningReceiver.cs index 36d407cc0..1d70a1a15 100644 --- a/BDArmory/Modules/RadarWarningReceiver.cs +++ b/BDArmory/Radar/RadarWarningReceiver.cs @@ -1,13 +1,16 @@ 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; +using BDArmory.Extensions; -namespace BDArmory.Modules +namespace BDArmory.Radar { public class RadarWarningReceiver : PartModule { @@ -15,12 +18,13 @@ 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; public enum RWRThreatTypes { + None = -1, SAM = 0, Fighter = 1, AWACS = 2, @@ -29,16 +33,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" }; - - public MissileFire weaponManager; + string[] iconLabels = new string[] { "S", "F", "A", "M", "M", "D", "So", "T", "T", "J" }; // 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; + [KSPField] public float fieldOfView = 360; //for if making separate RWR and WM for mod competitions, etc. // 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; internal static bool resizingWindow = false; @@ -68,7 +74,7 @@ public enum RWRThreatTypes const float minPingInterval = 0.12f; const float pingPersistTime = 1; - const int dataCount = 10; + const int dataCount = 12; internal float rwrDisplayRange = BDArmorySettings.MAX_ACTIVE_RADAR_RANGE; internal static float RwrSize = 256; @@ -76,9 +82,12 @@ public enum RWRThreatTypes internal static float HeaderSize = 15; public TargetSignatureData[] pingsData; - public Vector3[] pingWorldPositions; + //public Vector3[] pingWorldPositions; List launchWarnings; + private float ReferenceUpdateTime = -1f; + public float TimeSinceReferenceUpdate => Time.fixedTime - ReferenceUpdateTime; + Transform rt; Transform referenceTransform @@ -104,11 +113,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) @@ -116,7 +125,7 @@ public override void OnStart(StartState state) if (HighLogic.LoadedSceneIsFlight) { pingsData = new TargetSignatureData[dataCount]; - pingWorldPositions = new Vector3[dataCount]; + //pingWorldPositions = new Vector3[dataCount]; TargetSignatureData.ResetTSDArray(ref pingsData); launchWarnings = new List(); @@ -139,24 +148,19 @@ public override void OnStart(StartState state) UpdateVolume(); BDArmorySetup.OnVolumeChange += UpdateVolume; - //float size = RwrDisplayRect.height + 20; if (!WindowRectRWRInitialized) { - BDArmorySetup.WindowRectRwr = new Rect(40, Screen.height - RwrDisplayRect.height, RwrDisplayRect.height + BorderSize, RwrDisplayRect.height + BorderSize + HeaderSize); + BDArmorySetup.WindowRectRwr = new Rect(BDArmorySetup.WindowRectRwr.x, BDArmorySetup.WindowRectRwr.y, RwrDisplayRect.height + BorderSize, RwrDisplayRect.height + BorderSize + HeaderSize); + // BDArmorySetup.WindowRectRwr = new Rect(40, Screen.height - RwrDisplayRect.height, RwrDisplayRect.height + BorderSize, RwrDisplayRect.height + BorderSize + HeaderSize); WindowRectRWRInitialized = true; } - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - mf.Current.rwr = this; - if (!weaponManager) + using (var mf = VesselModuleRegistry.GetMissileFires(vessel).GetEnumerator()) + while (mf.MoveNext()) { - weaponManager = mf.Current; + if (mf.Current == null) continue; + mf.Current.rwr = this; // Set the rwr on all weapon managers to this. } - } - mf.Dispose(); //if (rwrEnabled) EnableRWR(); EnableRWR(); } @@ -170,6 +174,18 @@ void UpdateVolume() } } + void UpdateReferenceTransform() + { + if (TimeSinceReferenceUpdate < Time.fixedDeltaTime) + return; + + Vector3 upVec = VectorUtils.GetUpDirection(transform.position); + + referenceTransform.rotation = Quaternion.LookRotation(vessel.ReferenceTransform.up.ProjectOnPlanePreNormalized(upVec), upVec); + + ReferenceUpdateTime = Time.fixedTime; + } + public void EnableRWR() { OnRadarPing += ReceivePing; @@ -193,103 +209,111 @@ 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) return; + if (part == null || !part.isActiveAndEnabled) return; + var weaponManager = vessel.ActiveController().WM; if (weaponManager == null) return; + if (!omniDetection && !radar) return; + + UpdateReferenceTransform(); - float sqrDist = (part.transform.position - source).sqrMagnitude; - if (sqrDist < Mathf.Pow(BDArmorySettings.MAX_ENGAGEMENT_RANGE, 2) && sqrDist > Mathf.Pow(100, 2) && - Vector3.Angle(direction, part.transform.position - source) < 15) + Vector3 currPos = part.transform.position; + float sqrDist = (currPos - source).sqrMagnitude; + //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 && VectorUtils.Angle(direction, currPos - source) < 15f) { StartCoroutine( - LaunchWarningRoutine(new TargetSignatureData(Vector3.zero, - RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange), Vector3.zero, - true, (float)RWRThreatTypes.MissileLaunch))); + LaunchWarningRoutine(new TargetSignatureData(source, + RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange), + true, RWRThreatTypes.MissileLaunch))); PlayWarningSound(RWRThreatTypes.MissileLaunch); - if (weaponManager && weaponManager.guardMode) + if (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 || v != vessel) return; if (referenceTransform == null) return; + var weaponManager = vessel.ActiveController().WM; if (weaponManager == null) return; + if (!rwrEnabled) return; - if (rwrEnabled && vessel && v == vessel) + //if we are airborne or on land, no Sonar or SLW type weapons on the RWR! + if ((type == RWRThreatTypes.Torpedo || type == RWRThreatTypes.TorpedoLock || type == RWRThreatTypes.Sonar) && (vessel.situation != Vessel.Situations.SPLASHED)) { - //if we are airborne or on land, no Sonar or SLW type weapons on the RWR! - if ((type == RWRThreatTypes.Torpedo || type == RWRThreatTypes.TorpedoLock || type == RWRThreatTypes.Sonar) && (vessel.situation != Vessel.Situations.SPLASHED)) - { - // rwr stays silent... - return; - } + // rwr stays silent... + return; + } + + UpdateReferenceTransform(); - if (type == RWRThreatTypes.MissileLaunch || type == RWRThreatTypes.Torpedo) + if (type == RWRThreatTypes.MissileLaunch || type == RWRThreatTypes.Torpedo) + { + StartCoroutine( + LaunchWarningRoutine(new TargetSignatureData(source, + RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange), + true, type))); + PlayWarningSound(type, (source - vessel.CoM).sqrMagnitude); + return; + } + else if (type == RWRThreatTypes.MissileLock) + { + if (weaponManager.guardMode) { - StartCoroutine( - LaunchWarningRoutine(new TargetSignatureData(Vector3.zero, - RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange), - Vector3.zero, true, (float)type))); - PlayWarningSound(type, (source - vessel.transform.position).sqrMagnitude); - return; - } - else if (type == RWRThreatTypes.MissileLock) - { - if (weaponManager && weaponManager.guardMode) - { - weaponManager.FireChaff(); - // TODO: if torpedo inbound, also fire accoustic decoys (not yet implemented...) - } + weaponManager.FireChaff(); + weaponManager.missileIsIncoming = true; + // TODO: if torpedo inbound, also fire accoustic decoys (not yet implemented...) } + } - int openIndex = -1; - for (int i = 0; i < dataCount; i++) - { - if (pingsData[i].exists && - ((Vector2)pingsData[i].position - - RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange)).sqrMagnitude < 900f) //prevent ping spam - { - break; - } + int openIndex = -1; + Vector2 currPos = RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange); + for (int i = 0; i < dataCount; i++) + { + TargetSignatureData tempPing = pingsData[i]; + if (tempPing.exists && + (tempPing.pingPosition - currPos).sqrMagnitude < (BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY ? 100f : 900f)) //prevent ping spam + break; - if (!pingsData[i].exists && openIndex == -1) - { - openIndex = i; - } + if (!tempPing.exists && openIndex == -1) + { + // as soon as we have an open index, break + openIndex = i; + break; } + } - if (openIndex >= 0) + if (openIndex >= 0) + { + pingsData[openIndex] = new TargetSignatureData(source, currPos, true, type); + //pingWorldPositions[openIndex] = source; //FIXME source is improperly defined + if (weaponManager.hasAntiRadiationOrdnance) { - referenceTransform.rotation = Quaternion.LookRotation(vessel.ReferenceTransform.up, - VectorUtils.GetUpDirection(transform.position)); - - pingsData[openIndex] = new TargetSignatureData(Vector3.zero, - RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange), Vector3.zero, - true, (float)type); // HACK! Evil misuse of signalstrength for the threat type! - pingWorldPositions[openIndex] = source; - StartCoroutine(PingLifeRoutine(openIndex, persistTime)); + 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); - } + PlayWarningSound(type, (source - vessel.CoM).sqrMagnitude); } } @@ -332,7 +356,8 @@ void PlayWarningSound(RWRThreatTypes type, float sqrDistance = 0f) audioSource.Play(); audioSourceRepeatDelay = audioSourceRepeatDelayTime; //set a min repeat delay to prevent too much audi pinging break; - + case RWRThreatTypes.None: + break; default: if (!audioSource.isPlaying) { @@ -352,14 +377,11 @@ void OnGUI() if (audioSourceRepeatDelay > 0) audioSourceRepeatDelay -= Time.fixedDeltaTime; - if (Event.current.type == EventType.MouseUp && resizingWindow) - { - resizingWindow = false; - } + if (resizingWindow && Event.current.type == EventType.MouseUp) { resizingWindow = false; } - BDArmorySetup.WindowRectRwr = GUI.Window(94353, BDArmorySetup.WindowRectRwr, WindowRwr, - "Radar Warning Receiver", GUI.skin.window); - BDGUIUtils.UseMouseEventInRect(RwrDisplayRect); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectRwr.position); + BDArmorySetup.WindowRectRwr = GUI.Window(94353, BDArmorySetup.WindowRectRwr, WindowRwr, "Radar Warning Receiver", GUI.skin.window); + GUIUtils.UseMouseEventInRect(RwrDisplayRect); } internal void WindowRwr(int windowID) @@ -368,6 +390,7 @@ internal void WindowRwr(int windowID) if (GUI.Button(new Rect(BDArmorySetup.WindowRectRwr.width - 18, 2, 16, 16), "X", GUI.skin.button)) { displayRWR = false; + BDArmorySetup.SaveConfig(); } GUI.BeginGroup(new Rect(BorderSize / 2, HeaderSize + (BorderSize / 2), RwrDisplayRect.width, RwrDisplayRect.height)); //GUI.DragWindow(RwrDisplayRect); @@ -377,27 +400,28 @@ internal void WindowRwr(int windowID) for (int i = 0; i < dataCount; i++) { - Vector2 pingPosition = (Vector2)pingsData[i].position; + TargetSignatureData currPing = pingsData[i]; + Vector2 pingPosition = currPing.pingPosition; //pingPosition = Vector2.MoveTowards(displayRect.center, pingPosition, displayRect.center.x - (pingSize/2)); Rect pingRect = new Rect(pingPosition.x - (pingSize / 2), pingPosition.y - (pingSize / 2), pingSize, pingSize); - if (!pingsData[i].exists) continue; - if (pingsData[i].signalStrength == (float)RWRThreatTypes.MissileLock) //Hack! Evil misuse of field signalstrength... + if (!currPing.exists) continue; + if (currPing.signalType == RWRThreatTypes.MissileLock) { GUI.DrawTexture(pingRect, rwrMissileTexture, ScaleMode.StretchToFill, true); } else { GUI.DrawTexture(pingRect, rwrDiamondTexture, ScaleMode.StretchToFill, true); - GUI.Label(pingRect, iconLabels[Mathf.RoundToInt(pingsData[i].signalStrength)], rwrIconLabelStyle); //Hack! Evil misuse of field signalstrength... + GUI.Label(pingRect, iconLabels[(int)currPing.signalType], rwrIconLabelStyle); } } List.Enumerator lw = launchWarnings.GetEnumerator(); while (lw.MoveNext()) { - Vector2 pingPosition = (Vector2)lw.Current.position; + Vector2 pingPosition = lw.Current.pingPosition; //pingPosition = Vector2.MoveTowards(displayRect.center, pingPosition, displayRect.center.x - (pingSize/2)); Rect pingRect = new Rect(pingPosition.x - (pingSize / 2), pingPosition.y - (pingSize / 2), pingSize, @@ -410,7 +434,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, Misc.Misc.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; @@ -420,28 +444,14 @@ internal void WindowRwr(int windowID) { if (Mouse.delta.x != 0 || Mouse.delta.y != 0) { - float diff = Mouse.delta.x + Mouse.delta.y; - UpdateRWRScale(diff); + float diff = (Mathf.Abs(Mouse.delta.x) > Mathf.Abs(Mouse.delta.y) ? Mouse.delta.x : Mouse.delta.y) / BDArmorySettings.UI_SCALE_ACTUAL; + BDArmorySettings.RWR_WINDOW_SCALE = Mathf.Clamp(BDArmorySettings.RWR_WINDOW_SCALE + diff / RwrSize, BDArmorySettings.RWR_WINDOW_SCALE_MIN, BDArmorySettings.RWR_WINDOW_SCALE_MAX); BDArmorySetup.ResizeRwrWindow(BDArmorySettings.RWR_WINDOW_SCALE); } } // End Resizing code. - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRwr); - } - - internal static void UpdateRWRScale(float diff) - { - float scaleDiff = ((diff / (BDArmorySetup.WindowRectRwr.width + BDArmorySetup.WindowRectRwr.height)) * 100 * .01f); - BDArmorySettings.RWR_WINDOW_SCALE += Mathf.Abs(scaleDiff) > .01f ? scaleDiff : scaleDiff > 0 ? .01f : -.01f; - BDArmorySettings.RWR_WINDOW_SCALE = - BDArmorySettings.RWR_WINDOW_SCALE > BDArmorySettings.RWR_WINDOW_SCALE_MAX - ? BDArmorySettings.RWR_WINDOW_SCALE_MAX - : BDArmorySettings.RWR_WINDOW_SCALE; - BDArmorySettings.RWR_WINDOW_SCALE = - BDArmorySettings.RWR_WINDOW_SCALE_MIN > BDArmorySettings.RWR_WINDOW_SCALE - ? BDArmorySettings.RWR_WINDOW_SCALE_MIN - : BDArmorySettings.RWR_WINDOW_SCALE; + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRwr); } public static void PingRWR(Vessel v, Vector3 source, RWRThreatTypes type, float persistTime) @@ -458,17 +468,18 @@ public static void PingRWR(Ray ray, float fov, RWRThreatTypes type, float persis while (vessel.MoveNext()) { if (vessel.Current == null || !vessel.Current.loaded) continue; - Vector3 dirToVessel = vessel.Current.transform.position - ray.origin; - if (Vector3.Angle(ray.direction, dirToVessel) < fov / 2) + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(vessel.Current.vesselType)) continue; + Vector3 dirToVessel = vessel.Current.CoM - ray.origin; + if (VectorUtils.Angle(ray.direction, dirToVessel) < fov * 0.5f) { PingRWR(vessel.Current, ray.origin, type, persistTime); } } } - 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 6436d0ed9..38759109b 100644 --- a/BDArmory/Radar/VesselRadarData.cs +++ b/BDArmory/Radar/VesselRadarData.cs @@ -1,27 +1,33 @@ 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; internal bool resizingWindow = false; public Rect RADARresizeRect = new Rect( - BDArmorySetup.WindowRectRadar.width - (17 * BDArmorySettings.RADAR_WINDOW_SCALE), - BDArmorySetup.WindowRectRadar.height - (17 * BDArmorySettings.RADAR_WINDOW_SCALE), - (16 * BDArmorySettings.RADAR_WINDOW_SCALE), - (16 * BDArmorySettings.RADAR_WINDOW_SCALE)); + BDArmorySetup.WindowRectRadar.width - 17 * BDArmorySettings.RADAR_WINDOW_SCALE, + BDArmorySetup.WindowRectRadar.height - 17 * BDArmorySettings.RADAR_WINDOW_SCALE, + 16 * BDArmorySettings.RADAR_WINDOW_SCALE, + 16 * BDArmorySettings.RADAR_WINDOW_SCALE); private int rCount; @@ -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; } @@ -37,7 +50,7 @@ public bool guiEnabled private bool drawGUI; - public MissileFire weaponManager; + public MissileFire weaponManager; // This is set and updated externally by ModuleIRST and ModuleRadar, but otherwise does not update dynamically. public bool canReceiveRadarData; //GUI @@ -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; @@ -105,7 +126,12 @@ public bool guiEnabled private List availableExternalVRDs; private Transform referenceTransform; - private Transform vesselReferenceTransform; + + // referenceTransform's position etc. + Vector3 currPosition; + Vector3 currForward; + Vector3 currUp; + Vector3 currRight; public MissileBase LastMissile; @@ -113,20 +139,37 @@ public bool guiEnabled //TargetSignatureData[] contacts = new TargetSignatureData[30]; private List displayedTargets; + private List displayedIRTargets; public bool locked; private int activeLockedTargetIndex; private List lockedTargetIndexes; + public int numLockedTargets + { + get { return lockedTargetIndexes.Count; } + } + public bool hasLoadedExternalVRDs = false; + private float lockedTargetsUpdateTime = -1f; + private float TimeSinceLockedTargetsUpdate => Time.fixedTime - lockedTargetsUpdateTime; + + private List lockedTargetList; + public List GetLockedTargets() { - List lockedTargets = new List(); - for (int i = 0; i < lockedTargetIndexes.Count; i++) + if (TimeSinceLockedTargetsUpdate > Time.fixedDeltaTime) { - lockedTargets.Add(displayedTargets[lockedTargetIndexes[i]].targetData); + lockedTargetList.Clear(); + for (int i = 0; i < lockedTargetIndexes.Count; i++) + { + lockedTargetList.Add(displayedTargets[lockedTargetIndexes[i]].targetData); + } + + lockedTargetsUpdateTime = Time.fixedTime; } - return lockedTargets; + + return lockedTargetList; } public RadarDisplayData lockedTargetData @@ -134,6 +177,130 @@ public RadarDisplayData lockedTargetData get { return displayedTargets[lockedTargetIndexes[activeLockedTargetIndex]]; } } + public TargetSignatureData activeIRTarget(Vessel desiredTarget, MissileFire mf) + { + TargetSignatureData data; + float targetMagnitude = 0; + int brightestTarget = 0; + for (int i = 0; i < displayedIRTargets.Count; i++) + { + if (desiredTarget != null) + { + if (displayedIRTargets[i].vessel == desiredTarget) + { + data = displayedIRTargets[i].targetData; + return data; + } + } + else + { + if (displayedIRTargets[i].targetData.Team == mf.Team) continue; + if (displayedIRTargets[i].magnitude > targetMagnitude) + { + targetMagnitude = displayedIRTargets[i].magnitude; + brightestTarget = i; + } + + } + } + if (targetMagnitude > 0) + { + data = displayedIRTargets[brightestTarget].targetData; + return data; + } + else + { + data = TargetSignatureData.noTarget; + return data; + } + } + + // Technically not the *most* efficient way of doing this, having each of the functions + // that use this perform the search and return the targetData directly would be reduce + // the amount of copies (these are structs) but this is more readable and maintainable + public int detectedRadarTargetIndex(Vessel desiredTarget, MissileFire mf) //passive sonar torpedoes, but could also be useful for LOAL missiles fired at detected but not locked targets, etc. + { + float targetMagnitude = 0; + int brightestTarget = 0; + for (int i = 0; i < displayedTargets.Count; i++) + { + if (desiredTarget != null) + { + if (displayedTargets[i].vessel == desiredTarget) + return i; + } + else + { + TargetSignatureData tData = displayedTargets[i].targetData; + if (tData.Team == mf.Team) continue; + if (tData.signalStrength > targetMagnitude) + { + targetMagnitude = tData.signalStrength; + brightestTarget = i; + } + + } + } + if (targetMagnitude > 0) + return brightestTarget; + else + return -1; + } + + public TargetSignatureData detectedRadarTarget(Vessel desiredTarget, MissileFire mf) //passive sonar torpedoes, but could also be useful for LOAL missiles fired at detected but not locked targets, etc. + { + int temp = detectedRadarTargetIndex(desiredTarget, mf); + if (temp >= 0) + return displayedTargets[temp].targetData; + else + return TargetSignatureData.noTarget; + } + + public (TargetSignatureData, bool) detectedRadarTargetLock(Vessel desiredTarget, MissileFire mf) //passive sonar torpedoes, but could also be useful for LOAL missiles fired at detected but not locked targets, etc. + { + int temp = detectedRadarTargetIndex(desiredTarget, mf); + if (temp >= 0) + { + // This reduces the copies produced of this struct + RadarDisplayData t = displayedTargets[temp]; + return (t.targetData, t.locked); + } + else + return (TargetSignatureData.noTarget, false); + } + + // The function previously did this, no clue why, but I've split off this behavior + // into its own function + public TargetSignatureData detectedRadarTargetGetRadar(Vessel desiredTarget, MissileFire mf) //passive sonar torpedoes, but could also be useful for LOAL missiles fired at detected but not locked targets, etc. + { + int temp = detectedRadarTargetIndex(desiredTarget, mf); + if (temp >= 0) + { + RadarDisplayData t = displayedTargets[temp]; + TargetSignatureData tempData = t.targetData; + tempData.lockedByRadar = t.detectedByRadar; + return tempData; + } + else + return TargetSignatureData.noTarget; + } + + public TargetSignatureData detectedRadarTarget() //passive sonar torpedoes, but could also be useful for LOAL missiles fired at detected but not locked targets ,etc. + { + TargetSignatureData data; + for (int i = 0; i < displayedTargets.Count; i++) + { + RadarDisplayData t = displayedTargets[i]; + if (t.vessel == weaponManager.currentTarget) + { + data = t.targetData; + return data; + } + } + data = TargetSignatureData.noTarget; + return data; + } + //turret slaving public bool slaveTurrets; @@ -160,24 +327,49 @@ public void AddRadar(ModuleRadar mr) public void RemoveRadar(ModuleRadar mr) { - availableRadars.Remove(mr); - rCount = availableRadars.Count; - RemoveDataFromRadar(mr); - //UpdateDataLinkCapability(); - linkCapabilityDirty = true; + if (availableRadars.Remove(mr)) + { + rCount = availableRadars.Count; + RemoveDataFromRadar(mr); + //UpdateDataLinkCapability(); + linkCapabilityDirty = true; + 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; + public bool queueLinks = false; private void Awake() { availableRadars = new List(); + availableIRSTs = new List(); externalRadars = new List(); myVessel = GetComponent(); lockedTargetIndexes = new List(); + lockedTargetList = new List(); availableExternalVRDs = new List(); distanceStyle = new GUIStyle @@ -200,12 +392,8 @@ private void Awake() fontSize = 12 }; - vesselReferenceTransform = (new GameObject()).transform; - //vesselReferenceTransform.parent = vessel.transform; - //vesselReferenceTransform.localPosition = Vector3.zero; - vesselReferenceTransform.localScale = Vector3.one; - displayedTargets = new List(); + displayedIRTargets = new List(); externalVRDs = new List(); waitingForVessels = new List(); @@ -217,15 +405,22 @@ private void Awake() { float width = RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE + BorderSize + ControlsWidth + Gap * 3; float height = RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE + BorderSize + HeaderSize; - BDArmorySetup.WindowRectRadar = new Rect(Screen.width - width, Screen.height - height, width, height); + BDArmorySetup.WindowRectRadar = new Rect(BDArmorySetup.WindowRectRadar.x, BDArmorySetup.WindowRectRadar.y, width, height); radarRectInitialized = true; } } private void Start() { + referenceTransform = (new GameObject()).transform; + rangeIndex = rIncrements.Length - 6; + // Default to 2 slots for jammers, this will dynamically resize itself + // as needed. + jammedPositions = new Vector2[2 * 4]; + jammedPositionsSize = 2; + //determine configured physics ranges and add a radar range level for the highest range if (vessel.vesselRanges.flying.load > rIncrements[rIncrements.Length - 1]) { @@ -234,13 +429,12 @@ private void Start() } UpdateLockedTargets(); - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - mf.Current.vesselRadarData = this; - } - mf.Dispose(); + using (var mf = VesselModuleRegistry.GetMissileFires(vessel).GetEnumerator()) + while (mf.MoveNext()) + { + if (mf.Current == null) continue; + mf.Current.vesselRadarData = this; + } GameEvents.onVesselDestroy.Add(OnVesselDestroyed); GameEvents.onVesselCreate.Add(OnVesselDestroyed); MissileFire.OnChangeTeam += OnChangeTeam; @@ -249,27 +443,14 @@ private void Start() if (!weaponManager) { - List.Enumerator mfa = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mfa.MoveNext()) - { - if (mfa.Current == null) continue; - weaponManager = mfa.Current; - break; - } - mfa.Dispose(); + weaponManager = vessel.ActiveController().WM; } - StartCoroutine(StartupRoutine()); } 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; } @@ -318,11 +499,15 @@ private void OnDestroy() GameEvents.onGameStateSave.Remove(OnGameStateSave); GameEvents.onPartDestroyed.Remove(PartDestroyed); + referenceTransform = null; + if (weaponManager) { if (slaveTurrets) { weaponManager.slavingTurrets = false; + weaponManager.slavedPosition = Vector3.zero; + weaponManager.slavedTarget = TargetSignatureData.noTarget; } } } @@ -349,15 +534,14 @@ 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; + _maxRadarRange = Mathf.Max(_maxRadarRange, MaxRadarRange()); + } + else if (availableIRSTs.Count > 0) + { + _maxRadarRange = Mathf.Max(_maxRadarRange, MaxIRSTRange()); } - rad.Dispose(); // Now rebuild range display array List newArray = new List(); for (int x = 0; x < baseIncrements.Length; x++) @@ -368,7 +552,41 @@ private void UpdateRangeCapability() break; } } - if (newArray.Count > 0) rIncrements = newArray.ToArray(); + if (newArray.Count > 0) + { + rIncrements = newArray.ToArray(); + rangeIndex = Mathf.Clamp(rangeIndex, 0, rIncrements.Length - 1); + } + } + + public float MaxRadarRange() + { + float overallMaxRange = 0f; + 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 > overallMaxRange) overallMaxRange = maxRange; + } + rad.Dispose(); + return overallMaxRange; + } + + public float MaxIRSTRange() + { + float overallMaxRange = 0f; + 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 > overallMaxRange) overallMaxRange = maxRange; + } + irst.Dispose(); + return overallMaxRange; } private void UpdateDataLinkCapability() @@ -379,7 +597,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; } @@ -410,14 +628,29 @@ private void UpdateDataLinkCapability() private void UpdateReferenceTransform() { - if (radarCount == 1 && !availableRadars[0].omnidirectional && !vessel.Landed) + if (!referenceTransform) return; + // Previously the following line had !vessel.Landed which is a bit weird, the b-scope display + // has no specific provisions for landed vessels, and thus the display would be completely + // wrong for landed vessels + if (radarCount == 1 && !availableRadars[0].omnidirectional)// && !vessel.Landed) { - referenceTransform = availableRadars[0].referenceTransform; + referenceTransform.position = vessel.CoM; + // Rotate reference transform such that we're pointed forwards along the radar's forward + // direction but rotated to negate any pitch, to create a stabilized perspective for radar displays + referenceTransform.rotation = + Quaternion.LookRotation(availableRadars[0].currForward.ProjectOnPlanePreNormalized(vessel.upAxis).normalized, vessel.upAxis); } else { - referenceTransform = vesselReferenceTransform; + referenceTransform.position = vessel.CoM; + referenceTransform.rotation = Quaternion.LookRotation(vessel.LandedOrSplashed ? + VectorUtils.GetNorthVector(vessel.CoM, vessel.mainBody) : + vessel.transform.up.ProjectOnPlanePreNormalized(vessel.upAxis), vessel.upAxis); } + currPosition = vessel.CoM; + currForward = referenceTransform.forward; + currUp = referenceTransform.up; + currRight = referenceTransform.right; } private void PartDestroyed(Part p) @@ -446,7 +679,7 @@ private void RemoveDisconnectedRadars() { radarsToRemove.Add(radar.Current); } - else if (!radar.Current.weaponManager || (weaponManager && radar.Current.weaponManager.Team != weaponManager.Team)) + else if (!radar.Current.WeaponManager || (weaponManager && radar.Current.WeaponManager.Team != weaponManager.Team)) { radarsToRemove.Add(radar.Current); } @@ -457,12 +690,41 @@ private void RemoveDisconnectedRadars() while (rrad.MoveNext()) { if (rrad.Current == null) continue; + rrad.Current.EnsureVesselRadarData(); RemoveRadar(rrad.Current); } 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 + { + var irstWM = irst.Current.WeaponManager; + if (!irstWM || (weaponManager && irstWM.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() @@ -473,29 +735,61 @@ public void UpdateLockedTargets() for (int i = 0; i < displayedTargets.Count; i++) { - if (!displayedTargets[i].vessel || !displayedTargets[i].locked) continue; + RadarDisplayData t = displayedTargets[i]; + if (!t.vessel || !t.locked) continue; locked = true; lockedTargetIndexes.Add(i); } + // Redo lockedTargetList + lockedTargetsUpdateTime = -1f; + activeLockedTargetIndex = locked ? Mathf.Clamp(activeLockedTargetIndex, 0, lockedTargetIndexes.Count - 1) : 0; } - private void UpdateSlaveData() + private bool UpdateSlaveData() { - if (!slaveTurrets || !weaponManager) return; + if (!weaponManager) //don't turn on auto-turret slaving when in manual control, let players use the button for that + { + return false; + } + if (!slaveTurrets || !locked) + { + weaponManager.slavedTarget = TargetSignatureData.noTarget; + weaponManager.slavingTurrets = false; + return false; + } weaponManager.slavingTurrets = true; - if (!locked) return; TargetSignatureData lockedTarget = lockedTargetData.targetData; - weaponManager.slavedPosition = lockedTarget.predictedPosition; + weaponManager.slavedPosition = lockedTarget.predictedPositionWithChaffFactor(lockedTargetData.detectedByRadar.radarChaffClutterFactor); weaponManager.slavedVelocity = lockedTarget.velocity; weaponManager.slavedAcceleration = lockedTarget.acceleration; weaponManager.slavedTarget = lockedTarget; + return true; + //This is only slaving turrets if there's a radar lock on the WM's guardTarget + //no radar-guided gunnery for scan radars? + //what about multiple turret multitarget tracking? } private void Update() + { + if (radarCount + irstCount > 0) + { + UpdateInputs(); + } + } + + private void LateUpdate() + { + drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && rCount + iCount > 0 && + vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); + if (drawGUI) + UpdateGUIData(); + } + + void FixedUpdate() { if (!vessel) { @@ -503,23 +797,17 @@ private void Update() return; } - UpdateReferenceTransform(); - - if (radarCount > 0) + if (radarCount + irstCount > 0) { //vesselReferenceTransform.parent = linkedRadars[0].transform; - vesselReferenceTransform.localScale = Vector3.one; - vesselReferenceTransform.position = vessel.CoM; - - vesselReferenceTransform.rotation = Quaternion.LookRotation(vessel.LandedOrSplashed ? - VectorUtils.GetNorthVector(vessel.transform.position, vessel.mainBody) : - Vector3.ProjectOnPlane(vessel.transform.up, vessel.upAxis), vessel.upAxis); + UpdateReferenceTransform(); CleanDisplayedContacts(); - UpdateInputs(); - - UpdateSlaveData(); + if (!UpdateSlaveData() && slaveTurrets) + { + UnslaveTurrets(); + } } else { @@ -541,10 +829,13 @@ private void Update() rangeCapabilityDirty = false; } - drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && rCount > 0 && - vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); + if (queueLinks && canReceiveRadarData) + LinkAllRadars(); + + if (externalLockCapabilityDirty) + CountExternalRadarMaxLocks(); - if (!vessel.loaded && radarCount == 0) + if (!vessel.loaded && (radarCount + irstCount == 0)) { Destroy(this); } @@ -563,7 +854,14 @@ public void CycleActiveLock() lockedTargetData.detectedByRadar.SetActiveLock(lockedTargetData.targetData); - UpdateLockedTargets(); + //UpdateLockedTargets(); + } + + public void SetMaxRange() + { + rangeIndex = rIncrements.Length - 1; + pingPositionsDirty = true; + UpdateRWRRange(); } private void IncreaseRange() @@ -589,71 +887,77 @@ private void DecreaseRange() /// private void UpdateRWRRange() { - List.Enumerator rwr = vessel.FindPartModulesImplementing().GetEnumerator(); - while (rwr.MoveNext()) - { - if (rwr.Current == null) continue; - rwr.Current.rwrDisplayRange = rIncrements[rangeIndex]; - } - rwr.Dispose(); + using (var rwr = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (rwr.MoveNext()) + { + if (rwr.Current == null) continue; + rwr.Current.rwrDisplayRange = rIncrements[rangeIndex]; + } } - private bool TryLockTarget(RadarDisplayData radarTarget) + private bool TryLockTarget(RadarDisplayData radarTarget, bool priorityLock = false) { if (radarTarget.locked) return false; ModuleRadar lockingRadar = null; //first try using the last radar to detect that target - if (CheckRadarForLock(radarTarget.detectedByRadar, radarTarget)) + bool acquiredLock = false; + if (radarTarget.detectedByRadar) { - lockingRadar = radarTarget.detectedByRadar; - } - else - { - List.Enumerator radar = availableRadars.GetEnumerator(); - while (radar.MoveNext()) + if (CheckRadarForLock(radarTarget.detectedByRadar, radarTarget, priorityLock)) { - if (radar.Current == null) continue; - if (!CheckRadarForLock(radar.Current, radarTarget)) continue; - lockingRadar = radar.Current; - break; + lockingRadar = radarTarget.detectedByRadar; + acquiredLock = lockingRadar.TryLockTarget(radarTarget.targetData.predictedPosition, radarTarget.vessel); } - radar.Dispose(); } - + if (!acquiredLock) //locks exceeded/target off scope, test if remaining radars have available locks & coveravge + { + using (List.Enumerator radar = availableRadars.GetEnumerator()) + while (radar.MoveNext()) + { + if (radar.Current == null) continue; + // If the radar is external + if (!CheckRadarForLock(radar.Current, radarTarget, priorityLock)) continue; + lockingRadar = radar.Current; + if (lockingRadar.TryLockTarget(radarTarget.targetData.predictedPosition, radarTarget.vessel)) + { + acquiredLock = true; + break; + } + } + } if (lockingRadar != null) { - return lockingRadar.TryLockTarget(radarTarget.targetData.predictedPosition); + return acquiredLock; } - - UpdateLockedTargets(); + //UpdateLockedTargets(); StartCoroutine(UpdateLocksAfterFrame()); return false; } private IEnumerator UpdateLocksAfterFrame() { - yield return null; + yield return new WaitForFixedUpdate(); UpdateLockedTargets(); } - public void TryLockTarget(Vector3 worldPosition) + public void TryLockTarget(Vector3 worldPosition, bool priorityLock = false) { List.Enumerator displayData = displayedTargets.GetEnumerator(); while (displayData.MoveNext()) { if (!(Vector3.SqrMagnitude(worldPosition - displayData.Current.targetData.predictedPosition) < 40 * 40)) continue; - TryLockTarget(displayData.Current); + TryLockTarget(displayData.Current, priorityLock); return; } displayData.Dispose(); return; } - public bool TryLockTarget(Vessel v) + public bool TryLockTarget(Vessel v, bool priorityLock = false) { - if (!v) return false; + if (v == null || v.packed) return false; using (List.Enumerator displayData = displayedTargets.GetEnumerator()) while (displayData.MoveNext()) @@ -676,19 +980,35 @@ public bool TryLockTarget(Vessel v) //return false; } - private bool CheckRadarForLock(ModuleRadar radar, RadarDisplayData radarTarget) + private bool CheckRadarForLock(ModuleRadar radar, RadarDisplayData radarTarget, bool priorityLock) { - if (!radar) return false; + // Technically all instances of this are now gated by a null check so this is no longer necessary + //if (!radar) return false; + + if (!radar.canLock) return false; + + bool guardModeActive = weaponManager && weaponManager.guardMode; + + if (!guardModeActive && (radar.locked && (radar.currentLocks == radar.maxLocks))) return false; + + // Ensure the radar's referenceTransform and related vectors are all updated... + radar.UpdateReferenceTransform(); + + TargetSignatureData tData = radarTarget.targetData; + Vector3 relativePos = tData.predictedPosition - radar.currPosition; + // Convert from m to km for the radar FloatCurves + float dist = relativePos.magnitude * 0.001f; return ( - radar.canLock - && (!radar.locked || radar.currentLocks < radar.maxLocks) - && 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) < - radar.directionalFieldOfView / 2) + RadarUtils.RadarCanDetect(radar, tData.signalStrength, dist) + && tData.signalStrength >= radar.radarLockTrackCurve.Evaluate(dist) + && (radar.CheckFOV(tData.predictedPosition) + && (!guardModeActive || // If not in Guard Mode + !radar.locked || // Or the radar isn't locked + (priorityLock && !radar.lockedTarget.targetInfo.isMissile) || // Or we're a priority lock + weaponManager.GetMissilesAway(radar.lockedTarget.targetInfo)[1] == 0 || // Or we're not guiding a missile + VectorUtils.Angle(relativePos, radar.lockedTarget.position - radar.currPosition) < radar.multiLockFOV * 0.5f)) // Or we're within the multiLockFOV ); } @@ -697,36 +1017,83 @@ private void DisableAllRadars() //rCount = 0; UnlinkAllExternalRadars(); - List.Enumerator radar = vessel.FindPartModulesImplementing().GetEnumerator(); - while (radar.MoveNext()) + var radars = VesselModuleRegistry.GetModules(vessel); + if (radars != null) { - if (radar.Current == null) continue; - radar.Current.DisableRadar(); + using (var radar = radars.GetEnumerator()) + while (radar.MoveNext()) + { + if (radar.Current == null) continue; + 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(); + } } - radar.Dispose(); } + public float GetCrankFOV() + { + // Get max FOV of radars onboard vessel, or the minimum FOV radars with target locks + + float fov = 0f; + var radars = VesselModuleRegistry.GetModules(vessel); + if (radars != null) + { + using (var radar = radars.GetEnumerator()) + while (radar.MoveNext()) + { + if (radar.Current == null) continue; + if (radar.Current.omnidirectional) return 360f; + // TODO: Account for radar orientation, as it is right now we just take the minimum azimuth limit! + fov = Mathf.Max(fov, radar.Current.radarMinMaxAzLimits[0]); + } + } + for (int i = 0; i < lockedTargetIndexes.Count; i++) + { + // TODO: Account for radar orientation, as it is right now we just take the minimum azimuth limit! + fov = Mathf.Min(fov, displayedTargets[lockedTargetIndexes[i]].detectedByRadar.radarMinMaxAzLimits[0]); + } + + return fov; + } + /// + /// Slew any Targeting Cameras to the radarlocked position + /// public void SlaveTurrets() { - List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mtc.MoveNext()) + var targetingCameras = VesselModuleRegistry.GetModules(vessel); + if (targetingCameras != null) { - if (mtc.Current == null) continue; - mtc.Current.slaveTurrets = false; + using (var mtc = targetingCameras.GetEnumerator()) + while (mtc.MoveNext()) + { + if (mtc.Current == null) continue; + mtc.Current.slaveTurrets = false; + } + slaveTurrets = true; } - mtc.Dispose(); - slaveTurrets = true; } public void UnslaveTurrets() { - List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mtc.MoveNext()) + var targetingCameras = VesselModuleRegistry.GetModules(vessel); + if (targetingCameras != null) { - if (mtc.Current == null) continue; - mtc.Current.slaveTurrets = false; + using (var mtc = targetingCameras.GetEnumerator()) + while (mtc.MoveNext()) + { + if (mtc.Current == null) continue; + mtc.Current.slaveTurrets = false; + } } - mtc.Dispose(); slaveTurrets = false; @@ -734,6 +1101,8 @@ public void UnslaveTurrets() { weaponManager.slavingTurrets = false; } + weaponManager.slavedPosition = Vector3.zero; + weaponManager.slavedTarget = TargetSignatureData.noTarget; //reset and null these so hitting the slave target button on a weapon later doesn't lock it to a legacy position/target } private void OnGUI() @@ -742,7 +1111,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) @@ -757,7 +1126,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; @@ -765,29 +1134,27 @@ 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); } } - if (Event.current.type == EventType.MouseUp && resizingWindow) - { - resizingWindow = false; - } + if (resizingWindow && Event.current.type == EventType.MouseUp) { resizingWindow = false; } const string windowTitle = "Radar"; + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectRadar.position); BDArmorySetup.WindowRectRadar = GUI.Window(524141, BDArmorySetup.WindowRectRadar, WindowRadar, windowTitle, GUI.skin.window); - BDGUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectRadar); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectRadar); if (linkWindowOpen && canReceiveRadarData) { @@ -795,7 +1162,7 @@ private void OnGUI() 16 + (numberOfAvailableLinks * linkRectEntryHeight)); LinkRadarWindow(); - BDGUIUtils.UseMouseEventInRect(linkWindowRect); + GUIUtils.UseMouseEventInRect(linkWindowRect); } } @@ -804,22 +1171,39 @@ 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(); return; } if (!referenceTransform) return; + if (availableRadars.Count + availableIRSTs.Count == 0) return; //============================== GUI.BeginGroup(RadarDisplayRect); - if (availableRadars.Count == 0) return; + var guiMatrix = GUI.matrix; //bool omnidirectionalDisplay = (radarCount == 1 && linkedRadars[0].omnidirectional); - float directionalFieldOfView = omniDisplay ? 0 : availableRadars[0].directionalFieldOfView; //bool linked = (radarCount > 1); Rect scanRect = new Rect(0, 0, RadarDisplayRect.width, RadarDisplayRect.height); - if (omniDisplay) + + //Vector3 refForward = referenceTransform.forward; + //Vector3 refUp = referenceTransform.up; + //Vector3 refRight = referenceTransform.right; + //Vector3 refPos = referenceTransform.position; + + // TODO: Overall many of these things could be cached, primarily the FoV lines, especially if we're gonna accurately represent + // them in 3D space. A lot of these calculations do not need to be repeated unless the radars are moving around on the vessel + // (which is entirely plausible, given some people like sticking them on gimbals, so maybe we check if the radar's relative + // orientation has changed?). At the very least the FoV vectors (we'd only a single vector from one corner, and then reflect / + // rotate that over to the other side, relative to the offset centerline) could be calculated as local vectors relative to the + // radar transform, though we'd have to then account for the offset for any radar with an asymmetric scan-zone, though that + // would be easy enough with a local quaternion. + // Actually we calculate the radar's `Quaternion.LookRotation` for the ReferenceTransform anyways, so it's probably best to + // just have vectors to 2 opposing corners of the FoV pyramid, save the quaternion, and then rotate them using it. + + if (guiDispOmni) { GUI.DrawTexture(scanRect, omniBgTexture, ScaleMode.StretchToFill, true); @@ -829,112 +1213,92 @@ private void WindowRadar(int windowID) } // Range Display and control - DisplayRange(); + if (dispRange) 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); - float dAngle = Vector3.Angle(projectedVesselFwd, referenceTransform.forward); - if (referenceTransform.InverseTransformVector(vessel.ReferenceTransform.up).x < 0) - { - dAngle = -dAngle; - } - GUIUtility.RotateAroundPivot(dAngle, scanRect.center); + GUIUtility.RotateAroundPivot(dAngle, guiMatrix * scanRect.center); GUI.DrawTexture( new Rect(scanRect.center.x - (directionSize / 2), scanRect.center.y - (directionSize / 2), directionSize, directionSize), BDArmorySetup.Instance.directionTriangleIcon, ScaleMode.StretchToFill, true); - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; - for (int i = 0; i < rCount; i++) + for (int i = 0; i < guiRCount; i++) { - bool canScan = availableRadars[i].canScan; - bool canTrackWhileScan = availableRadars[i].canTrackWhileScan; - bool islocked = availableRadars[i].locked; - float currentAngle = availableRadars[i].currentAngle; - - float radarAngle = VectorUtils.SignedAngle(projectedVesselFwd, - Vector3.ProjectOnPlane(availableRadars[i].transform.up, referenceTransform.up), - referenceTransform.right); - - if (!canScan || availableRadars[i].vessel != vessel) continue; - if ((!islocked || canTrackWhileScan)) + if (!float.IsNaN(radarCurrAngleArr[i])) { - if (!availableRadars[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 (availableRadars[i].omnidirectional && radarCount == 1) + GUIUtility.RotateAroundPivot(radarCurrAngleArr[i], guiMatrix * new Vector2((RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2, (RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2)); + if (guiFillScanR) { GUI.DrawTexture(scanRect, scanTexture, ScaleMode.StretchToFill, true); } 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)); } - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; } + // TODO: FIX THIS, currently doesn't take radar orientation in 3D space into account + // also doesn't take into account asymmetric FOVs! //if linked and directional, draw FOV lines - if (availableRadars[i].omnidirectional) continue; - float fovAngle = availableRadars[i].directionalFieldOfView / 2; + if (float.IsNaN(radarFOVAngleArr[i])) continue; float lineWidth = 2; 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)); - GUI.matrix = Matrix4x4.identity; - GUIUtility.RotateAroundPivot(dAngle - fovAngle + radarAngle, scanRect.center); - BDGUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.4f)); - GUI.matrix = Matrix4x4.identity; + GUIUtility.RotateAroundPivot(dAngle + radarFOVAngleArr[i] + radarAngleArr[i], guiMatrix * scanRect.center); + GUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.6f)); + GUI.matrix = guiMatrix; + GUIUtility.RotateAroundPivot(dAngle - radarFOVAngleArr[i] + radarAngleArr[i], guiMatrix * scanRect.center); + GUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.4f)); + GUI.matrix = guiMatrix; } - } - else + for (int i = guiRCount; i < guiSCount; i++) + { + GUIUtility.RotateAroundPivot(radarCurrAngleArr[i], guiMatrix * new Vector2((RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2, (RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2)); + if (guiFillScanI) + { + 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 = guiMatrix; + + //if linked and directional, draw FOV lines + if (float.IsNaN(radarFOVAngleArr[i])) continue; + float lineWidth = 2; + Rect verticalLineRect = new Rect(scanRect.center.x - (lineWidth / 2), 0, lineWidth, + scanRect.center.y); + GUIUtility.RotateAroundPivot(dAngle + radarFOVAngleArr[i] + radarAngleArr[i], guiMatrix * scanRect.center); + GUIUtils.DrawRectangle(verticalLineRect, new Color(1, 0, 0, 0.6f)); + GUI.matrix = guiMatrix; + GUIUtility.RotateAroundPivot(dAngle - radarFOVAngleArr[i] + radarAngleArr[i], guiMatrix * scanRect.center); + GUIUtils.DrawRectangle(verticalLineRect, new Color(1, 0, 0, 0.4f)); + GUI.matrix = guiMatrix; + } + } + else { GUI.DrawTexture(scanRect, radialBgTexture, ScaleMode.StretchToFill, true); - DisplayRange(); + if (dispRange) DisplayRange(); //don't change dist for non-range capable IRSTs - for (int i = 0; i < rCount; i++) + for (int i = 0; i < guiRCount; i++) { - bool canScan = availableRadars[i].canScan; - bool islocked = availableRadars[i].locked; - //float lockScanAngle = linkedRadars[i].lockScanAngle; - float currentAngle = availableRadars[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); + Vector2 scanIndicatorPos = scanPosArr[i]; GUI.DrawTexture(new Rect(scanIndicatorPos.x - 7, scanIndicatorPos.y - 10, 14, 20), BDArmorySetup.Instance.greenDiamondTexture, ScaleMode.StretchToFill, true); - if (!islocked || !availableRadars[i].canTrackWhileScan) continue; - Vector2 leftPos = - RadarUtils.WorldToRadarRadial( - referenceTransform.position + - (Quaternion.AngleAxis(availableRadars[i].leftLimit, referenceTransform.up) * - referenceTransform.forward), referenceTransform, scanRect, 5000, - directionalFieldOfView / 2); - Vector2 rightPos = - RadarUtils.WorldToRadarRadial( - referenceTransform.position + - (Quaternion.AngleAxis(availableRadars[i].rightLimit, referenceTransform.up) * - referenceTransform.forward), referenceTransform, scanRect, 5000, - directionalFieldOfView / 2); + if (float.IsNaN(radarCurrAngleArr[i])) continue; + Vector2 leftPos = leftPosArr[i]; + Vector2 rightPos = rightPosArr[i]; float barWidth = 2; float barHeight = 15; Color origColor = GUI.color; @@ -945,6 +1309,13 @@ private void WindowRadar(int windowID) Texture2D.whiteTexture, ScaleMode.StretchToFill, true); GUI.color = origColor; } + + for (int i = guiRCount; i < guiSCount; i++) + { + Vector2 scanIndicatorPos = scanPosArr[i]; + GUI.DrawTexture(new Rect(scanIndicatorPos.x - 7, scanIndicatorPos.y - 10, 14, 20), + BDArmorySetup.Instance.greenDiamondTexture, ScaleMode.StretchToFill, true); //FIXME? + } } //selector @@ -956,43 +1327,39 @@ 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 if (LastMissile && LastMissile.TargetAcquired) { Rect missileDataRect = new Rect(5, scanRect.height - 65, scanRect.width - 5, 60); - string missileDataString = LastMissile.GetShortName(); - missileDataString += "\nT-" + LastMissile.TimeToImpact.ToString("0"); - - if (LastMissile.ActiveRadar && Mathf.Round(Time.time * 3) % 2 == 0) - { - missileDataString += "\nACTIVE"; - } GUI.Label(missileDataRect, missileDataString, distanceStyle); } //roll indicator if (!vessel.Landed) { - Vector3 localUp = vessel.ReferenceTransform.InverseTransformDirection(referenceTransform.up); - localUp = Vector3.ProjectOnPlane(localUp, Vector3.up).normalized; - float rollAngle = -Misc.Misc.SignedAngle(-Vector3.forward, localUp, Vector3.right); - GUIUtility.RotateAroundPivot(rollAngle, scanRect.center); + GUIUtility.RotateAroundPivot(rollAngle, guiMatrix * scanRect.center); GUI.DrawTexture(scanRect, rollIndicatorTexture, ScaleMode.StretchToFill, true); - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; } - 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(); @@ -1000,9 +1367,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, Misc.Misc.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; @@ -1012,23 +1378,265 @@ private void WindowRadar(int windowID) { if (Mouse.delta.x != 0 || Mouse.delta.y != 0) { - float diff = Mouse.delta.x + Mouse.delta.y; - UpdateRadarScale(diff); + float diff = (Mathf.Abs(Mouse.delta.x) > Mathf.Abs(Mouse.delta.y) ? Mouse.delta.x : Mouse.delta.y) / BDArmorySettings.UI_SCALE_ACTUAL; + BDArmorySettings.RADAR_WINDOW_SCALE = Mathf.Clamp(BDArmorySettings.RADAR_WINDOW_SCALE + diff / RadarScreenSize, BDArmorySettings.RADAR_WINDOW_SCALE_MIN, BDArmorySettings.RADAR_WINDOW_SCALE_MAX); BDArmorySetup.ResizeRadarWindow(BDArmorySettings.RADAR_WINDOW_SCALE); } } // End Resizing code. - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRadar); - } + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRadar); + } + + // UPDATE GUI DATA + float dAngle; + float[] radarAngleArr; + float[] radarCurrAngleArr; + float[] radarFOVAngleArr; + string missileDataString; + float rollAngle; + float directionalFieldOfView; + int guiRCount = -1; // Saves the number of radars in the GUI data arrays, that way the + // cutoff between radars and IRSTs is known, This is used because the GUI data arrays + // don't match in length with rCount and iCount, having removed external radars and + // null radars. + int guiSCount = -1; // Total number of sensors in the arrays! This is specifically used + // to avoid downsizing the arrays + int arrSize = -1; + Vector2[] scanPosArr; + Vector2[] leftPosArr; + Vector2[] rightPosArr; + bool guiDispOmni = false; // This saves whether or not to display in omni, this is useful + // to know to determine whether or not we need to recalculate the ping positions + bool guiFillScanR = false; // Used for scanRect conditional + bool guiFillScanI = false; // Used for scanRect conditional + bool dispRange = false; + + private void UpdateGUIData() + { + int currIndex = 0; + + dispRange = availableRadars.Count > 0; + + int totCount = rCount + iCount - externalRadars.Count; + + // If our radarData arrays are smaller than the total count of on-board sensors + // then we re-size the arrays. + // TODO: Maybe this could be something pre-counted in OnStart() to reduce + // the number of times the arrays have to be re-sized, mostly for human players + // since Guard Mode will enable all radars/IRSTs in one shot. + if (arrSize < totCount) + { + radarAngleArr = new float[totCount]; + radarCurrAngleArr = new float[totCount]; + radarFOVAngleArr = new float[totCount]; + scanPosArr = new Vector2[totCount]; + leftPosArr = new Vector2[totCount]; + rightPosArr = new Vector2[totCount]; + arrSize = totCount; + } + + // If we've flipped from an omni-display to a b-scope or from a b-scope to an omni + // we need to recalculate all the ping positions! + if (guiDispOmni != omniDisplay) + pingPositionsDirty = true; - internal static void UpdateRadarScale(float diff) - { - float scaleDiff = ((diff / (BDArmorySetup.WindowRectRadar.width + BDArmorySetup.WindowRectRadar.height)) * 100 * .01f); - BDArmorySettings.RADAR_WINDOW_SCALE += Mathf.Abs(scaleDiff) > .01f ? scaleDiff : scaleDiff > 0 ? .01f : -.01f; - BDArmorySettings.RADAR_WINDOW_SCALE = Mathf.Clamp(BDArmorySettings.RADAR_WINDOW_SCALE, - BDArmorySettings.RADAR_WINDOW_SCALE_MIN, - BDArmorySettings.RADAR_WINDOW_SCALE_MAX); + if (omniDisplay) + { + guiDispOmni = true; + + //my ship direction icon + Vector3 projectedVesselFwd = vessel.ReferenceTransform.up.ProjectOnPlanePreNormalized(currUp).normalized; + Vector3 left = Vector3.Cross(currUp, projectedVesselFwd); + dAngle = VectorUtils.AnglePreNormalized(projectedVesselFwd, referenceTransform.forward); + if (referenceTransform.InverseTransformVector(vessel.ReferenceTransform.up).x < 0) + { + dAngle = -dAngle; + } + + Vector3 north; + if (!vessel.Landed) + north = VectorUtils.GetNorthVector(currPosition, vessel.mainBody); + else + north = Vector3.zero; + + for (int i = 0; i < rCount; i++) + { + if (availableRadars[i] == null || availableRadars[i].gameObject == null) continue; + if (!availableRadars[i].canScan || availableRadars[i].vessel != vessel) continue; + + float currentAngle = availableRadars[i].currentAngle; + + availableRadars[i].UpdateDisplayTransform(); + float radarAngle = VectorUtils.GetAngleOnPlane(availableRadars[i].currDisplayForward, projectedVesselFwd, left); + + // TODO: This does not account for the true 3D orientation of the radar! Technically, we could just + // say it is only meant to represent the current status of the sweep, but even then we'd still have + // to account for the true 3D FoV limits of the radar. + radarAngleArr[currIndex] = radarAngle; + + if (!availableRadars[i].locked || availableRadars[i].canTrackWhileScan) + { + if (!availableRadars[i].omnidirectional) + { + currentAngle += radarAngle + dAngle; + } + else if (!vessel.Landed) + { + float angleFromNorth = VectorUtils.GetAngleOnPlane(projectedVesselFwd, north, + Vector3.Cross(north, vessel.upAxis)); + currentAngle += angleFromNorth; + } + + radarCurrAngleArr[currIndex] = currentAngle; + + guiFillScanR = availableRadars[i].omnidirectional && radarCount == 1; + } + + // TODO: FIX THIS, currently doesn't take radar orientation in 3D space into account + // also doesn't take into account asymmetric FOVs! + //if linked and directional, draw FOV lines + if (availableRadars[i].omnidirectional) + { + radarFOVAngleArr[currIndex] = float.NaN; + currIndex++; + continue; + } + + radarFOVAngleArr[currIndex] = availableRadars[i].radarAzFOV * 0.5f; + + currIndex++; + } + + guiRCount = currIndex; + + for (int i = 0; i < iCount; i++) + { + if (availableIRSTs[i] == null || availableIRSTs[i].gameObject == null) continue; + if (!availableIRSTs[i].canScan || availableIRSTs[i].vessel != vessel) continue; + + float currentAngle = availableIRSTs[i].currentAngle; + + float radarAngle = VectorUtils.SignedAngle(availableIRSTs[i].irstForward, projectedVesselFwd, left); + + if (!availableIRSTs[i].omnidirectional) + { + currentAngle += radarAngle + dAngle; + } + else if (!vessel.Landed) + { + float angleFromNorth = VectorUtils.GetAngleOnPlane(projectedVesselFwd, north, + Vector3.Cross(north, vessel.upAxis)); + currentAngle += angleFromNorth; + } + + radarAngleArr[currIndex] = radarAngle; + radarCurrAngleArr[currIndex] = currentAngle; + guiFillScanI = availableIRSTs[i].omnidirectional && irstCount == 1; + + if (!dispRange && availableIRSTs[i].irstRanging) + dispRange = true; + + //if linked and directional, draw FOV lines + if (availableIRSTs[i].omnidirectional) + { + radarFOVAngleArr[currIndex] = float.NaN; + currIndex++; + continue; + } + radarFOVAngleArr[currIndex] = availableIRSTs[i].directionalFieldOfView * 0.5f; + currIndex++; + } + + guiSCount = currIndex; + } + else + { + guiDispOmni = false; + + directionalFieldOfView = (availableRadars.Count > 0) ? (availableRadars[0].radarMinMaxAzLimits[1]) : 0.5f * availableIRSTs[0].directionalFieldOfView; + Rect scanRect = new Rect(0, 0, RadarDisplayRect.width, RadarDisplayRect.height); + + for (int i = 0; i < rCount; i++) + { + if (!availableRadars[i].canScan) continue; + bool islocked = availableRadars[i].locked; + //float lockScanAngle = linkedRadars[i].lockScanAngle; + float currentAngle = availableRadars[i].currentAngle; + float indicatorAngle = currentAngle; //locked ? lockScanAngle : currentAngle; + scanPosArr[currIndex] = + RadarUtils.WorldToRadarRadial( + currPosition + + (Quaternion.AngleAxis(indicatorAngle, currUp) * currForward), + referenceTransform, scanRect, 5000, directionalFieldOfView, true); + + if (!islocked || !availableRadars[i].canTrackWhileScan) + { + radarCurrAngleArr[currIndex] = float.NaN; + currIndex++; + continue; + } + radarCurrAngleArr[currIndex] = 0f; + leftPosArr[currIndex] = + RadarUtils.WorldToRadarRadial( + currPosition + + (Quaternion.AngleAxis(availableRadars[i].leftLimit, currUp) * + currForward), referenceTransform, scanRect, 5000, + directionalFieldOfView, true); + rightPosArr[currIndex] = + RadarUtils.WorldToRadarRadial( + currPosition + + (Quaternion.AngleAxis(availableRadars[i].rightLimit, currUp) * + currForward), referenceTransform, scanRect, 5000, + directionalFieldOfView, true); + + currIndex++; + } + + guiRCount = currIndex; + + for (int i = 0; i < iCount; i++) + { + if (!availableIRSTs[i].canScan) continue; + float currentAngle = availableIRSTs[i].currentAngle; + float indicatorAngle = currentAngle; //locked ? lockScanAngle : currentAngle; + scanPosArr[currIndex] = + RadarUtils.WorldToRadarRadial( + currPosition + + (Quaternion.AngleAxis(indicatorAngle, currUp) * currForward), + referenceTransform, scanRect, 5000, directionalFieldOfView, true); + + if (!dispRange && availableIRSTs[i].irstRanging) + dispRange = true; + + currIndex++; + } + + guiSCount = currIndex; + } + + //missile data + if (LastMissile && LastMissile.TargetAcquired) + { + missileDataString = LastMissile.GetShortName(); + missileDataString += "\nT-" + LastMissile.TimeToImpact.ToString("0"); + + if (LastMissile.ActiveRadar && Mathf.Round(Time.time * 3) % 2 == 0) + { + missileDataString += "\nACTIVE"; + } + } + + //roll indicator + if (!vessel.Landed) + { + Vector3 localUp = vessel.ReferenceTransform.InverseTransformDirection(vessel.upAxis); + localUp = localUp.ProjectOnPlanePreNormalized(Vector3.up).normalized; + rollAngle = -VectorUtils.GetAngleOnPlane(localUp, -Vector3.forward, Vector3.right); + } + + if (!noData)// && iCount == 0) + UpdateDisplayedContacts(); } private void DisplayRange() @@ -1098,10 +1706,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; } } } @@ -1179,15 +1788,33 @@ 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(); + queueLinks = false; + } + 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) { - Debug.Log("[BDArmory]: 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(); @@ -1207,7 +1834,7 @@ private void UnlinkVRD(VesselRadarData vrd) while (mr.MoveNext()) { if (mr.Current == null) continue; - Debug.Log("[BDArmory]: - Unlinking radar: " + mr.Current.radarName); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.VesselRadarData]: - Unlinking radar: " + mr.Current.radarName); UnlinkRadar(mr.Current); } mr.Dispose(); @@ -1243,6 +1870,8 @@ private void UnlinkRadar(ModuleRadar mr) { externalRadars.RemoveAll(r => r == null); } + + externalLockCapabilityDirty = true; } private void RemoveEmptyVRDs() @@ -1267,12 +1896,14 @@ private void RemoveEmptyVRDs() externalVRDs.Remove(vrdr.Current); } vrdr.Dispose(); + externalLockCapabilityDirty = true; } public void UnlinkDisabledRadar(ModuleRadar mr) { RemoveRadar(mr); externalRadars.Remove(mr); + externalLockCapabilityDirty = true; SaveExternalVRDVessels(); } @@ -1299,7 +1930,7 @@ private IEnumerator RecoverUnloadedLinkedVesselRoutine(string vesselID) using (var v = BDATargetManager.LoadedVessels.GetEnumerator()) while (v.MoveNext()) { - if (v.Current == null || !v.Current.loaded || v.Current == vessel) continue; + if (v.Current == null || !v.Current.loaded || v.Current == vessel || VesselModuleRegistry.IgnoredVesselTypes.Contains(v.Current.vesselType)) continue; if (v.Current.id.ToString() != vesselID) continue; VesselRadarData vrd = v.Current.gameObject.GetComponent(); if (!vrd) continue; @@ -1308,19 +1939,16 @@ 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); - Debug.Log("[BDArmory]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + - vrd.vessel.vesselName); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.VesselRadarData]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + + vrd.vessel.vesselName); } public void UnlinkAllExternalRadars() @@ -1340,10 +1968,38 @@ 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; + MaxRadarLocksExternal = 0; RefreshAvailableLinks(); } + bool externalLockCapabilityDirty = false; + public int MaxRadarLocksExternal + { + get + { + if (externalLockCapabilityDirty) + CountExternalRadarMaxLocks(); + return field; + } + private set; + } + = 0; + + private void CountExternalRadarMaxLocks() + { + int tempMaxRadarLocksExternal = 0; + foreach (ModuleRadar radar in externalRadars) + { + if (radar == null) continue; + tempMaxRadarLocksExternal += radar.maxLocks; + } + MaxRadarLocksExternal = tempMaxRadarLocksExternal; + externalLockCapabilityDirty = false; + } + private void OpenLinkRadarWindow() { RefreshAvailableLinks(); @@ -1367,17 +2023,11 @@ private void RefreshAvailableLinks() while (v.MoveNext()) { if (v.Current == null || !v.Current.loaded || v.Current == vessel) continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(v.Current.vesselType)) continue; BDTeam team = null; - List.Enumerator mf = v.Current.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - team = mf.Current.Team; - break; - } - mf.Dispose(); - + var mf = v.Current.ActiveController().WM; + if (mf != null) team = mf.Team; if (team != weaponManager.Team) continue; VesselRadarData vrd = v.Current.gameObject.GetComponent(); if (vrd && vrd.radarCount > 0) @@ -1401,7 +2051,9 @@ public void LinkVRD(VesselRadarData vrd) LinkToRadar(mr.Current); } mr.Dispose(); + externalLockCapabilityDirty = true; SaveExternalVRDVessels(); + StartCoroutine(UpdateLocksAfterFrame()); } public void LinkToRadar(ModuleRadar mr) @@ -1422,7 +2074,7 @@ public void LinkToRadar(ModuleRadar mr) mr.AddExternalVRD(this); } - public void AddRadarContact(ModuleRadar radar, TargetSignatureData contactData, bool _locked) + public void AddRadarContact(ModuleRadar radar, TargetSignatureData contactData, bool _locked, bool receivedData = false) { bool addContact = true; @@ -1431,38 +2083,52 @@ public void AddRadarContact(ModuleRadar radar, TargetSignatureData contactData, if (rData.vessel == vessel) return; - if (rData.vessel.altitude < -20 && radar.rwrThreatType != (int)RadarWarningReceiver.RWRThreatTypes.Sonar) addContact = false; // Normal Radar Should not detect Underwater vessels - if (!rData.vessel.LandedOrSplashed && radar.rwrThreatType == (int)RadarWarningReceiver.RWRThreatTypes.Sonar) addContact = false; //Sonar should not detect Aircraft - if (rData.vessel.altitude < 0 && radar.rwrThreatType == (int)RadarWarningReceiver.RWRThreatTypes.Sonar && vessel.Splashed) addContact = true; //Sonar only detects underwater vessels // Sonar should only work when in the water - if (!vessel.Splashed && radar.rwrThreatType == (int)RadarWarningReceiver.RWRThreatTypes.Sonar) addContact = false; // Sonar should only work when in the water - if (rData.vessel.Landed && radar.rwrThreatType == (int)RadarWarningReceiver.RWRThreatTypes.Sonar) addContact = false; //Sonar should not detect land vessels + if (!receivedData) //don't prevent VRD from e.g. getting datalinked sonar data from an ally boat despite being airborne + { + if (!rData.vessel.LandedOrSplashed && radar.sonarMode != ModuleRadar.SonarModes.None) addContact = false; //Sonar should not detect Aircraft + if (rData.vessel.Splashed && radar.sonarMode != ModuleRadar.SonarModes.None && vessel.Splashed) addContact = true; //Sonar only detects underwater vessels // Sonar should only work when in the water + } if (addContact == false) return; rData.signalPersistTime = radar.signalPersistTime; rData.detectedByRadar = radar; rData.locked = _locked; + contactData.lockedByRadar = radar; rData.targetData = contactData; - rData.pingPosition = UpdatedPingPosition(contactData.position, radar); + rData.pingPosition = UpdatedPingPosition(contactData.position, directionalFieldOfView); + rData.velAngle = VectorUtils.GetAngleOnPlane(contactData.velocity, currForward, currRight); if (_locked) { radar.UpdateLockedTargetInfo(contactData); } + // Are we receiving data about a target potentially locked by another radar? bool dontOverwrite = false; + bool updateLock = true; + int replaceIndex = -1; for (int i = 0; i < displayedTargets.Count; i++) { - if (displayedTargets[i].vessel == rData.vessel) + RadarDisplayData t = displayedTargets[i]; + if (t.vessel == rData.vessel) { - if (displayedTargets[i].locked && !_locked) + // If the target we're looking for is already locked... + if (t.locked) { - dontOverwrite = true; - break; - } + // If we're locked on to a target via a different radar, don't overwrite the + // locked data if our own data is not locked! + if (!_locked) + { + dontOverwrite = true; + break; + } + // Otherwise, we can overwrite the data, but don't update locks + updateLock = false; + } replaceIndex = i; break; } @@ -1470,8 +2136,11 @@ public void AddRadarContact(ModuleRadar radar, TargetSignatureData contactData, if (replaceIndex >= 0) { + // If it is an existing target, replace the data displayedTargets[replaceIndex] = rData; - //UpdateLockedTargets(); + // And if we should update our locks, update them + if (updateLock) + UpdateLockedTargets(); return; } else if (dontOverwrite) @@ -1481,12 +2150,30 @@ public void AddRadarContact(ModuleRadar radar, TargetSignatureData contactData, } else { + // We're adding new data displayedTargets.Add(rData); UpdateLockedTargets(); return; } } + 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. @@ -1499,13 +2186,13 @@ public void TargetNext() if (displayedTargets.Count == 0) return; displayedTargetIndex = 0; TryLockTarget(displayedTargets[displayedTargetIndex]); - lockedTargetIndexes.Add(displayedTargetIndex); - UpdateLockedTargets(); + //lockedTargetIndexes.Add(displayedTargetIndex); + //UpdateLockedTargets(); return; } // We have locked target(s) Lets see if we can select the next one in the list (if it exists) displayedTargetIndex = lockedTargetIndexes[activeLockedTargetIndex]; - // Lets store the displayed target that is ative + // Lets store the displayed target that is active ModuleRadar rad = displayedTargets[displayedTargetIndex].detectedByRadar; if (lockedTargetIndexes.Count > 1) { @@ -1516,7 +2203,7 @@ public void TargetNext() { activeLockedTargetIndex = 0; } - UpdateLockedTargets(); + //UpdateLockedTargets(); } else { @@ -1537,7 +2224,7 @@ public void TargetNext() // We have a good lock. Lets update the indexes and locks lockedTargetIndexes.Add(displayedTargetIndex); rad.UnlockTargetAt(rad.currentLockIndex); - UpdateLockedTargets(); + //UpdateLockedTargets(); } } @@ -1553,8 +2240,8 @@ public void TargetPrev() if (displayedTargets.Count == 0) return; displayedTargetIndex = displayedTargets.Count - 1; TryLockTarget(displayedTargets[displayedTargetIndex]); - lockedTargetIndexes.Add(displayedTargetIndex); - UpdateLockedTargets(); + //lockedTargetIndexes.Add(displayedTargetIndex); + //UpdateLockedTargets(); return; } // We have locked target(s) Lets see if we can select the previous one in the list (if it exists) @@ -1570,7 +2257,7 @@ public void TargetPrev() { activeLockedTargetIndex = lockedTargetIndexes.Count - 1; } - UpdateLockedTargets(); + //UpdateLockedTargets(); } else { @@ -1589,43 +2276,89 @@ public void TargetPrev() TryLockTarget(displayedTargets[displayedTargetIndex]); if (!displayedTargets[displayedTargetIndex].detectedByRadar) return; // We got a good lock. Lets update the indexes and locks - lockedTargetIndexes.Add(displayedTargetIndex); + //lockedTargetIndexes.Add(displayedTargetIndex); rad.UnlockTargetAt(rad.currentLockIndex); - UpdateLockedTargets(); + //UpdateLockedTargets(); } } public bool SwitchActiveLockedTarget(Vessel vessel) // FIXME This needs to take into account the maxLocks field. { - var vesselIndex = displayedTargets.FindIndex(t => t.vessel == vessel); - if (vesselIndex != -1) + for (int i = 0; i < lockedTargetIndexes.Count; i++) { - activeLockedTargetIndex = vesselIndex; - UpdateLockedTargets(); - return true; + if (displayedTargets[lockedTargetIndexes[i]].vessel == vessel) + { + activeLockedTargetIndex = i; + return true; + } } return false; } + // NOTE: Both this method and RemoveVesselFromLockedTargets could be improved by accounting for other + // radars locking on to the target in question, as well as improved AddRadarContact behavior, primarily + // by adding a counter to RadarDisplayTarget to account for the number of radars locked on, and to have + // detectedByRadar be filled by the most capable radar of the ones reporting a lock-on. Granted, currently + // such multi-radar locks aren't a thing, and as such this hasn't been implemented. public void UnlockAllTargetsOfRadar(ModuleRadar radar) { //radar.UnlockTarget(); - displayedTargets.RemoveAll(t => t.detectedByRadar == radar); + //displayedTargets.RemoveAll(t => t.detectedByRadar == radar); -> THIS IS PRETTY EXPENSIVE, INVOLVES A LOT OF COPYING + // Instead, just set locked to false, which has the added benefit of not deleting the contact... + for (int i = 0; i < displayedTargets.Count; i++) + { + // Get local copy (since we're gonna be using it for a comparison anyways, which will create a local copy) + RadarDisplayData t = displayedTargets[i]; + if (t.detectedByRadar == radar) + { + // Set locked to false + t.locked = false; + displayedTargets[i] = t; + } + } UpdateLockedTargets(); } public void RemoveVesselFromTargets(Vessel _vessel) { + // WARNING - THIS DOES A LOT OF MOVING THINGS AROUND, ONLY USE IF NECESSARY! displayedTargets.RemoveAll(t => t.vessel == _vessel); UpdateLockedTargets(); } - public void UnlockAllTargets() + public void RemoveVesselFromLockedTargets(Vessel _vessel) + { + if (_vessel == null) + { + displayedTargets.RemoveAll(t => t.vessel == null); + UpdateLockedTargets(); + return; + } + + for (int i = 0; i < displayedTargets.Count; i++) + { + // Get local copy (since we're gonna be using it for a comparison anyways, which will create a local copy) + RadarDisplayData t = displayedTargets[i]; + if (t.vessel == _vessel) + { + // Set locked to false + t.locked = false; + displayedTargets[i] = t; + // There should only be a single instance of this vessel in `displayedTargets` + break; + } + } + UpdateLockedTargets(); + } + + public void UnlockAllTargets(bool unlockDatalinkedRadars = true) { List.Enumerator radar = weaponManager.radars.GetEnumerator(); while (radar.MoveNext()) { if (radar.Current == null) continue; + if (radar.Current.vessel != vessel) continue; + if (!unlockDatalinkedRadars && radar.Current.linkedVRDs > 0) continue; radar.Current.UnlockAllTargets(); } radar.Dispose(); @@ -1639,10 +2372,44 @@ public void UnlockCurrentTarget() rad.UnlockTargetAt(rad.currentLockIndex); } + /// + /// Unlocks the target vessel. This variant is less efficient than the index variant, however it is + /// generally more useful as it will search through displayedTargets and find the vessel. Useful in + /// instances where the index of the target in lockedTargetIndexes is not known, or where the index + /// may change due to changes in the locks. + /// + /// Vessel to unlock. + public void UnlockSelectedTarget(Vessel vessel) + { + if (!locked) return; + var vesselIndex = displayedTargets.FindIndex(t => t.vessel == vessel); + if (vesselIndex != -1) + { + ModuleRadar rad = displayedTargets[vesselIndex].detectedByRadar; + rad.UnlockTargetVessel(vessel); + } + } + + /// + /// Unlocks the target at lockedTargetIndexes[index]. NOTE! Since lockedTargetIndexes WILL change when + /// a target is locked/unlocked, this should ONLY be called in instances when you are only unlocking + /// a single target. When unlocking multiple target, use the vessel variant instead. Note this function + /// is entirely unprotected, it is the user's responsibility to ensure index is valid for + /// lockedTargetIndexes. + /// + /// Index of target in lockedTargetIndexes. + public void UnlockSelectedTarget(int index) + { + if (!locked) return; + ModuleRadar rad = displayedTargets[lockedTargetIndexes[index]].detectedByRadar; + rad.UnlockTargetVessel(displayedTargets[lockedTargetIndexes[index]].vessel); + } + private void CleanDisplayedContacts() { int count = displayedTargets.Count; - displayedTargets.RemoveAll(t => t.targetData.age > t.signalPersistTime * 2); + displayedTargets.RemoveAll(t => t.vessel == null || t.targetData.age > t.signalPersistTime * 2); + displayedIRTargets.RemoveAll(t => t.vessel == null || t.targetData.age > t.signalPersistTime * 2); if (count != displayedTargets.Count) { UpdateLockedTargets(); @@ -1650,6 +2417,11 @@ private void CleanDisplayedContacts() } private Vector2 UpdatedPingPosition(Vector3 worldPosition, ModuleRadar radar) + { + return UpdatedPingPosition(worldPosition, radar.radarMinMaxAzLimits[1]); + } + + private Vector2 UpdatedPingPosition(Vector3 worldPosition, float directionalFieldOfView) { if (rangeIndex < 0 || rangeIndex > rIncrements.Length - 1) rangeIndex = rIncrements.Length - 1; if (omniDisplay) @@ -1659,54 +2431,210 @@ private Vector2 UpdatedPingPosition(Vector3 worldPosition, ModuleRadar radar) else { return RadarUtils.WorldToRadarRadial(worldPosition, referenceTransform, RadarDisplayRect, - rIncrements[rangeIndex], radar.directionalFieldOfView / 2); + rIncrements[rangeIndex], directionalFieldOfView); + } + } + + 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; + MissileLaunchParams guiDLZData; + private bool guiDrawDLZData; + float guiDistToTarget; + // Size of 4 * jammedPositionsSize (because we display 4 jammed positions) + Vector2[] jammedPositions; + int jammedPositionsSize; - private void DrawDisplayedContacts() + private void UpdateDisplayedContacts() { - float myAlt = (float)vessel.altitude; + Vector2 displayCenter = guiDispOmni ? new Vector2(RadarDisplayRect.x * 0.5f, RadarDisplayRect.y * 0.5f) : new Vector2(RadarDisplayRect.x * 0.5f, RadarDisplayRect.y); + int currJammedIndex = 0; + guiDrawDLZData = false; - bool drewLockLabel = false; - float lockIconSize = 24 * BDArmorySettings.RADAR_WINDOW_SCALE; - - bool lockDirty = false; + int lTarInd = 0; + if (locked) + lTarInd = lockedTargetIndexes[activeLockedTargetIndex]; for (int i = 0; i < displayedTargets.Count; i++) { - if (displayedTargets[i].locked && locked) + RadarDisplayData t = displayedTargets[i]; + if (t.locked && locked) { - TargetSignatureData lockedTarget = displayedTargets[i].targetData; - //LOCKED GUI - Vector2 pingPosition; - if (omniDisplay) - { - pingPosition = RadarUtils.WorldToRadar(lockedTarget.position, referenceTransform, RadarDisplayRect, + TargetSignatureData lockedTarget = t.targetData; + /*RadarDisplayData newData = new RadarDisplayData(); + newData.detectedByRadar = displayedTargets[i].detectedByRadar; + newData.locked = displayedTargets[i].locked; + if (guiDispOmni) + newData.pingPosition = RadarUtils.WorldToRadar(lockedTarget.position, referenceTransform, RadarDisplayRect, rIncrements[rangeIndex]); + else + newData.pingPosition = RadarUtils.WorldToRadarRadial(lockedTarget.position, referenceTransform, + RadarDisplayRect, rIncrements[rangeIndex], + directionalFieldOfView); + newData.signalPersistTime = displayedTargets[i].signalPersistTime; + newData.targetData = displayedTargets[i].targetData; + newData.vessel = displayedTargets[i].vessel; + float vAngle = VectorUtils.GetAngleOnPlane(lockedTarget.velocity, currForward, currRight); + newData.velAngle = vAngle;*/ + + if (guiDispOmni) + t.pingPosition = RadarUtils.WorldToRadar(lockedTarget.position, referenceTransform, RadarDisplayRect, + rIncrements[rangeIndex]); + else + t.pingPosition = RadarUtils.WorldToRadarRadial(lockedTarget.position, referenceTransform, + RadarDisplayRect, rIncrements[rangeIndex], + directionalFieldOfView); + t.velAngle = VectorUtils.GetAngleOnPlane(lockedTarget.velocity, currForward, currRight); + + displayedTargets[i] = t; + + if (i == lTarInd && weaponManager && weaponManager.selectedWeapon != null) + { + if (weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.SLW) + { + MissileBase currMissile = weaponManager.CurrentMissile; + if (currMissile && (currMissile.TargetingMode == MissileBase.TargetingModes.Radar || currMissile.TargetingMode == MissileBase.TargetingModes.Heat || currMissile.TargetingMode == MissileBase.TargetingModes.Inertial || currMissile.TargetingMode == MissileBase.TargetingModes.Gps)) + { + guiDLZData = MissileLaunchParams.GetDynamicLaunchParams(currMissile, lockedTarget.velocity, lockedTarget.predictedPosition); + guiDistToTarget = Vector3.Distance(lockedTarget.predictedPosition, currPosition); + guiDrawDLZData = true; + } + } + } + } + else + { + //jamming + // NEW: evaluation via radarutils! + + TargetSignatureData tData = t.targetData; + + Vector2 tempPos; + if (pingPositionsDirty) + { + if (guiDispOmni) + tempPos = RadarUtils.WorldToRadar(tData.position, referenceTransform, RadarDisplayRect, + rIncrements[rangeIndex]); + else + tempPos = RadarUtils.WorldToRadarRadial(tData.position, referenceTransform, + RadarDisplayRect, rIncrements[rangeIndex], + directionalFieldOfView); } else + tempPos = t.pingPosition; + + int tempJammedIndex = -1; + // TODO: This should probably go to AddRadarContact, but that would involve a more complex + // pool-type system for jammed positions instead of this simplistic array system, unless we + // specifically wanna keep these moving jammed positions + if (tData.vesselJammer) { - pingPosition = RadarUtils.WorldToRadarRadial(lockedTarget.position, referenceTransform, - RadarDisplayRect, rIncrements[rangeIndex], - displayedTargets[i].detectedByRadar.directionalFieldOfView / 2); + float distanceToTarget = (t.detectedByRadar.currPosition - tData.position).sqrMagnitude; + float jamDistance = RadarUtils.GetVesselECMJammingDistance(tData.vessel); + if (distanceToTarget < jamDistance * jamDistance) + { + Vector2 tempRadarPos; + Vector2 dir2D; + + if (t.detectedByRadar.vessel != vessel) + { + if (guiDispOmni) + tempRadarPos = RadarUtils.WorldToRadar(t.detectedByRadar.currPosition, referenceTransform, RadarDisplayRect, + rIncrements[rangeIndex]); + else + tempRadarPos = RadarUtils.WorldToRadarRadial(t.detectedByRadar.currPosition, referenceTransform, + RadarDisplayRect, rIncrements[rangeIndex], + directionalFieldOfView); + } + else + tempRadarPos = displayCenter; + + dir2D = (tempPos - tempRadarPos).normalized; + + if (currJammedIndex > jammedPositionsSize - 1) + { + // Use the same strat as lists do and resize to twice the size if needed. + jammedPositionsSize *= 2; + // 4 times the size because we have 4 positions to store + System.Array.Resize(ref jammedPositions, jammedPositionsSize * 4); + } + + float minR = 100f / rIncrements[rangeIndex]; + dir2D = dir2D * rIncrements[rangeIndex]; + float bearingVariation = + Mathf.Clamp( + 1024e6f / // 32000 * 32000 + distanceToTarget, 0, + 80); + for (int j = 0; j < 4; j++) + jammedPositions[currJammedIndex + j] = tempRadarPos + + VectorUtils.Rotate2DVec2(dir2D * UnityEngine.Random.Range(minR, 1), UnityEngine.Random.Range(-bearingVariation, bearingVariation)); + tempJammedIndex = currJammedIndex; + currJammedIndex++; + } } - //BDGUIUtils.DrawRectangle(new Rect(pingPosition.x-(4),pingPosition.y-(4),8, 8), Color.green); - float vAngle = Vector3.Angle(Vector3.ProjectOnPlane(lockedTarget.velocity, referenceTransform.up), - referenceTransform.forward); - if (referenceTransform.InverseTransformVector(lockedTarget.velocity).x < 0) + // Update if pingPositionsDirty *or* we need to update the jammed index + if (pingPositionsDirty || tempJammedIndex > 0) { - vAngle = -vAngle; + //displayedTargets[i].pingPosition = UpdatedPingPosition(displayedTargets[i].targetData.position, displayedTargets[i].detectedByRadar); + /*RadarDisplayData newData = new RadarDisplayData(); + newData.detectedByRadar = displayedTargets[i].detectedByRadar; + newData.locked = displayedTargets[i].locked; + newData.pingPosition = tempPos; + newData.signalPersistTime = displayedTargets[i].signalPersistTime; + newData.targetData = displayedTargets[i].targetData; + newData.velAngle = displayedTargets[i].velAngle; + newData.vessel = displayedTargets[i].vessel; + newData.jammedIndex = tempJammedIndex;*/ + t.pingPosition = tempPos; + t.jammedIndex = tempJammedIndex; + displayedTargets[i] = t; } - GUIUtility.RotateAroundPivot(vAngle, pingPosition); + } + } + } + + private void DrawDisplayedContacts() + { + var guiMatrix = GUI.matrix; + float myAlt = (float)vessel.altitude; + + bool drewLockLabel = false; + float lockIconSize = 24 * BDArmorySettings.RADAR_WINDOW_SCALE; + + bool lockDirty = false; + + Vector2 Centerpoint = new Vector2((RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2, (RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2); + + for (int i = 0; i < displayedTargets.Count; i++) + { + RadarDisplayData t = displayedTargets[i]; + if (t.locked && locked) + { + TargetSignatureData lockedTarget = t.targetData; + //LOCKED GUI + Vector2 pingPosition = t.pingPosition; + + GUIUtility.RotateAroundPivot(t.velAngle, guiMatrix * pingPosition); Rect pingRect = new Rect(pingPosition.x - (lockIconSize / 2), pingPosition.y - (lockIconSize / 2), lockIconSize, lockIconSize); Texture2D txtr = (i == lockedTargetIndexes[activeLockedTargetIndex]) ? lockIconActive : lockIcon; GUI.DrawTexture(pingRect, txtr, ScaleMode.StretchToFill, true); - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; GUI.Label(new Rect(pingPosition.x + (lockIconSize * 0.35f) + 2, pingPosition.y, 100, 24), (lockedTarget.altitude / 1000).ToString("0"), distanceStyle); @@ -1721,7 +2649,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")); @@ -1735,8 +2663,8 @@ private void DrawDisplayedContacts() { //UnlockTarget(displayedTargets[i].detectedByRadar); //displayedTargets[i].detectedByRadar.UnlockTargetAtPosition(displayedTargets[i].targetData.position); - displayedTargets[i].detectedByRadar.UnlockTargetVessel(displayedTargets[i].vessel); - UpdateLockedTargets(); + t.detectedByRadar.UnlockTargetVessel(t.vessel); + //UpdateLockedTargets(); lockDirty = true; } else @@ -1750,9 +2678,9 @@ private void DrawDisplayedContacts() } } - displayedTargets[i].detectedByRadar.SetActiveLock(displayedTargets[i].targetData); + t.detectedByRadar.SetActiveLock(t.targetData); - UpdateLockedTargets(); + //UpdateLockedTargets(); } } @@ -1761,105 +2689,79 @@ private void DrawDisplayedContacts() { int lTarInd = lockedTargetIndexes[activeLockedTargetIndex]; - if (i == lTarInd && weaponManager && weaponManager.selectedWeapon != null) + if (i == lTarInd && guiDrawDLZData) { - if (weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.SLW) + float rangeToPixels = (1 / rIncrements[rangeIndex]) * RadarDisplayRect.height; + float dlzWidth = 12; + float lineWidth = 2; + float dlzX = RadarDisplayRect.width - dlzWidth - lineWidth; + + GUIUtils.DrawRectangle(new Rect(dlzX, 0, dlzWidth, RadarDisplayRect.height), Color.black); + + Rect maxRangeVertLineRect = new Rect(RadarDisplayRect.width - lineWidth, + Mathf.Clamp(RadarDisplayRect.height - (guiDLZData.maxLaunchRange * rangeToPixels), 0, + RadarDisplayRect.height), lineWidth, + Mathf.Clamp(guiDLZData.maxLaunchRange * rangeToPixels, 0, RadarDisplayRect.height)); + GUIUtils.DrawRectangle(maxRangeVertLineRect, Color.green); + + Rect maxRangeTickRect = new Rect(dlzX, maxRangeVertLineRect.y, dlzWidth, lineWidth); + GUIUtils.DrawRectangle(maxRangeTickRect, Color.green); + + Rect minRangeTickRect = new Rect(dlzX, + Mathf.Clamp(RadarDisplayRect.height - (guiDLZData.minLaunchRange * rangeToPixels), 0, + RadarDisplayRect.height), dlzWidth, lineWidth); + GUIUtils.DrawRectangle(minRangeTickRect, Color.green); + + Rect rTrTickRect = new Rect(dlzX, + Mathf.Clamp(RadarDisplayRect.height - (guiDLZData.rangeTr * rangeToPixels), 0, RadarDisplayRect.height), + dlzWidth, lineWidth); + GUIUtils.DrawRectangle(rTrTickRect, Color.green); + + Rect noEscapeLineRect = new Rect(dlzX, rTrTickRect.y, lineWidth, + minRangeTickRect.y - rTrTickRect.y); + GUIUtils.DrawRectangle(noEscapeLineRect, Color.green); + + float targetDistIconSize = 16 * BDArmorySettings.RADAR_WINDOW_SCALE; + float targetDistY; + if (!omniDisplay) { - MissileBase currMissile = weaponManager.CurrentMissile; - if (currMissile.TargetingMode == MissileBase.TargetingModes.Radar || currMissile.TargetingMode == MissileBase.TargetingModes.Heat) - { - MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(currMissile, lockedTarget.velocity, lockedTarget.predictedPosition); - float rangeToPixels = (1 / rIncrements[rangeIndex]) * RadarDisplayRect.height; - float dlzWidth = 12; - float lineWidth = 2; - float dlzX = RadarDisplayRect.width - dlzWidth - lineWidth; - - BDGUIUtils.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); - - Rect maxRangeTickRect = new Rect(dlzX, maxRangeVertLineRect.y, dlzWidth, lineWidth); - BDGUIUtils.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); - - Rect rTrTickRect = new Rect(dlzX, - Mathf.Clamp(RadarDisplayRect.height - (dlz.rangeTr * rangeToPixels), 0, RadarDisplayRect.height), - dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(rTrTickRect, Color.green); - - Rect noEscapeLineRect = new Rect(dlzX, rTrTickRect.y, lineWidth, - minRangeTickRect.y - rTrTickRect.y); - BDGUIUtils.DrawRectangle(noEscapeLineRect, Color.green); - - float targetDistIconSize = 16 * BDArmorySettings.RADAR_WINDOW_SCALE; - float targetDistY; - if (!omniDisplay) - { - targetDistY = pingPosition.y - (targetDistIconSize / 2); - } - else - { - targetDistY = RadarDisplayRect.height - - (Vector3.Distance(lockedTarget.predictedPosition, - referenceTransform.position) * rangeToPixels) - - (targetDistIconSize / 2); - } - - Rect targetDistanceRect = new Rect(dlzX - (targetDistIconSize / 2), targetDistY, - targetDistIconSize, targetDistIconSize); - GUIUtility.RotateAroundPivot(90, targetDistanceRect.center); - GUI.DrawTexture(targetDistanceRect, BDArmorySetup.Instance.directionTriangleIcon, - ScaleMode.StretchToFill, true); - GUI.matrix = Matrix4x4.identity; - } + targetDistY = pingPosition.y - (targetDistIconSize / 2); } + else + { + targetDistY = RadarDisplayRect.height - + (guiDistToTarget * rangeToPixels) - + (targetDistIconSize / 2); + } + + Rect targetDistanceRect = new Rect(dlzX - (targetDistIconSize / 2), targetDistY, + targetDistIconSize, targetDistIconSize); + GUIUtility.RotateAroundPivot(90, guiMatrix * targetDistanceRect.center); + GUI.DrawTexture(targetDistanceRect, BDArmorySetup.Instance.directionTriangleIcon, + ScaleMode.StretchToFill, true); + GUI.matrix = guiMatrix; } } } else { + TargetSignatureData tData = t.targetData; float minusAlpha = - (Mathf.Clamp01((Time.time - displayedTargets[i].targetData.timeAcquired) / - displayedTargets[i].signalPersistTime) * 2) - 1; + (Mathf.Clamp01((Time.time - tData.timeAcquired) / + t.signalPersistTime) * 2) - 1; //jamming // NEW: evaluation via radarutils! - bool jammed = false; - float distanceToTarget = (this.vessel.transform.position - displayedTargets[i].targetData.position).sqrMagnitude; - float jamDistance = RadarUtils.GetVesselECMJammingDistance(displayedTargets[i].targetData.vessel); - if (displayedTargets[i].targetData.vesselJammer && jamDistance * jamDistance > distanceToTarget) - { - jammed = true; - } + int currJammedIndex = t.jammedIndex; + bool jammed = currJammedIndex > 0; - if (pingPositionsDirty) - { - //displayedTargets[i].pingPosition = UpdatedPingPosition(displayedTargets[i].targetData.position, displayedTargets[i].detectedByRadar); - RadarDisplayData newData = new RadarDisplayData(); - newData.detectedByRadar = displayedTargets[i].detectedByRadar; - newData.locked = displayedTargets[i].locked; - newData.pingPosition = UpdatedPingPosition(displayedTargets[i].targetData.position, - displayedTargets[i].detectedByRadar); - newData.signalPersistTime = displayedTargets[i].signalPersistTime; - newData.targetData = displayedTargets[i].targetData; - newData.vessel = displayedTargets[i].vessel; - displayedTargets[i] = newData; - } - Vector2 pingPosition = displayedTargets[i].pingPosition; + Vector2 pingPosition = t.pingPosition; Rect pingRect; //draw missiles and debris as dots - if ((displayedTargets[i].targetData.targetInfo && - displayedTargets[i].targetData.targetInfo.isMissile) || - displayedTargets[i].targetData.Team == null) + if ((tData.targetInfo && + tData.targetInfo.isMissile) || + tData.Team == null) { float mDotSize = 6; pingRect = new Rect(pingPosition.x - (mDotSize / 2), pingPosition.y - (mDotSize / 2), mDotSize, @@ -1871,24 +2773,17 @@ private void DrawDisplayedContacts() GUI.color = origGUIColor; } //draw contacts with direction indicator - else if (!jammed && (displayedTargets[i].detectedByRadar.showDirectionWhileScan) && - displayedTargets[i].targetData.velocity.sqrMagnitude > 100) + else if (!jammed && (t.detectedByRadar.showDirectionWhileScan) && + tData.velocity.sqrMagnitude > 100f) { pingRect = new Rect(pingPosition.x - (lockIconSize / 2), pingPosition.y - (lockIconSize / 2), lockIconSize, lockIconSize); - float vAngle = - Vector3.Angle( - Vector3.ProjectOnPlane(displayedTargets[i].targetData.velocity, referenceTransform.up), - referenceTransform.forward); - if (referenceTransform.InverseTransformVector(displayedTargets[i].targetData.velocity).x < 0) - { - vAngle = -vAngle; - } - GUIUtility.RotateAroundPivot(vAngle, pingPosition); + float vAngle = t.velAngle; + GUIUtility.RotateAroundPivot(vAngle, guiMatrix * pingPosition); Color origGUIColor = GUI.color; GUI.color = Color.white - new Color(0, 0, 0, minusAlpha); if (weaponManager && - weaponManager.Team.IsFriendly(displayedTargets[i].targetData.Team)) + weaponManager.Team.IsFriendly(tData.Team)) { GUI.DrawTexture(pingRect, friendlyContactIcon, ScaleMode.StretchToFill, true); } @@ -1897,9 +2792,9 @@ private void DrawDisplayedContacts() GUI.DrawTexture(pingRect, radarContactIcon, ScaleMode.StretchToFill, true); } - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; GUI.Label(new Rect(pingPosition.x + (lockIconSize * 0.35f) + 2, pingPosition.y, 100, 24), - (displayedTargets[i].targetData.altitude / 1000).ToString("0"), distanceStyle); + (tData.altitude / 1000).ToString("0"), distanceStyle); GUI.color = origGUIColor; } else //draw contacts as rectangles @@ -1909,43 +2804,15 @@ private void DrawDisplayedContacts() pingSize.y); for (int d = 0; d < drawCount; d++) { - Rect jammedRect = new Rect(pingRect); - Vector3 contactPosition = displayedTargets[i].targetData.position; if (jammed) { - //jamming - Vector3 jammedPosition = transform.position + - ((displayedTargets[i].targetData.position - transform.position) - .normalized * - Random.Range(100, rIncrements[rangeIndex])); - float bearingVariation = - Mathf.Clamp( - Mathf.Pow(32000, 2) / - (displayedTargets[i].targetData.position - transform.position).sqrMagnitude, 0, - 80); - jammedPosition = transform.position + - (Quaternion.AngleAxis( - Random.Range(-bearingVariation, bearingVariation), - referenceTransform.up) * (jammedPosition - transform.position)); - if (omniDisplay) - { - pingPosition = RadarUtils.WorldToRadar(jammedPosition, referenceTransform, RadarDisplayRect, - rIncrements[rangeIndex]); - } - else - { - pingPosition = RadarUtils.WorldToRadarRadial(jammedPosition, referenceTransform, - RadarDisplayRect, rIncrements[rangeIndex], - displayedTargets[i].detectedByRadar.directionalFieldOfView / 2); - } - - jammedRect = new Rect(pingPosition.x - (pingSize.x / 2), - pingPosition.y - (pingSize.y / 2) - (pingSize.y / 3), pingSize.x, pingSize.y / 3); - contactPosition = jammedPosition; + Vector2 currJammedPos = jammedPositions[currJammedIndex + d]; + pingRect = new Rect(currJammedPos.x - (pingSize.x / 2), currJammedPos.y - (pingSize.y / 2), pingSize.x, + pingSize.y); } Color iconColor = Color.green; - float contactAlt = displayedTargets[i].targetData.altitude; + float contactAlt = tData.altitude; if (!omniDisplay && !jammed) { if (contactAlt - myAlt > 1000) @@ -1960,17 +2827,17 @@ private void DrawDisplayedContacts() if (omniDisplay) { - Vector3 localPos = referenceTransform.InverseTransformPoint(contactPosition); - localPos.y = 0; - float angleToContact = Vector3.Angle(localPos, Vector3.forward); - if (localPos.x < 0) angleToContact = -angleToContact; - GUIUtility.RotateAroundPivot(angleToContact, pingPosition); + float angleToContact = Vector2.Angle(Vector3.up, Centerpoint - pingPosition); + if (pingPosition.x < Centerpoint.x) + { + angleToContact = -angleToContact; //FIXME - inverted. Need to Flip (not mirror) angle + } + GUIUtility.RotateAroundPivot(angleToContact, guiMatrix * pingPosition); } - if (jammed || - !weaponManager.Team.IsFriendly(displayedTargets[i].targetData.Team)) + if (jammed || !weaponManager.Team.IsFriendly(tData.Team)) { - BDGUIUtils.DrawRectangle(jammedRect, iconColor - new Color(0, 0, 0, minusAlpha)); + GUIUtils.DrawRectangle(pingRect, iconColor - new Color(0, 0, 0, minusAlpha)); } else { @@ -1984,7 +2851,7 @@ private void DrawDisplayedContacts() GUI.color = origGuiColor; } - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; } } @@ -1992,22 +2859,163 @@ private void DrawDisplayedContacts() Time.time - guiInputTime > guiInputCooldown) { guiInputTime = Time.time; - TryLockTarget(displayedTargets[i]); + TryLockTarget(t, true); } - 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")); + tData.signalStrength.ToString("0.0")); + } + } + } + + } + + private void DrawDisplayedIRContacts() + { + var guiMatrix = GUI.matrix; + 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; + IRSTDisplayData t = displayedIRTargets[i]; + TargetSignatureData tData = t.targetData; + if (t.detectedByIRST.irstRanging) + { + TargetInfo tInfo = tData.targetInfo; + if (tInfo && displayedTargets.Count > 0) //if Radar enabled, don't display targets that have already been displayed + { + for (int r = 0; r < displayedTargets.Count; r++) + { + if (tInfo == 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 - tData.timeAcquired) / t.signalPersistTime) * 2f) - 1f; + + if (pingPositionsDirty) + { + //displayedTargets[i].pingPosition = UpdatedPingPosition(displayedTargets[i].targetData.position, displayedTargets[i].detectedByRadar); + /*IRSTDisplayData newData = new IRSTDisplayData(); + newData.detectedByIRST = t.detectedByIRST; + newData.magnitude = t.magnitude; + newData.pingPosition = UpdatedPingPosition(t.targetData.position, + t.detectedByIRST); + newData.signalPersistTime = t.signalPersistTime; + newData.targetData = t.targetData; + newData.vessel = t.vessel;*/ + t.pingPosition = UpdatedPingPosition(tData.position, t.detectedByIRST); + displayedIRTargets[i] = t; + } + Vector2 pingPosition = t.pingPosition; + + Rect pingRect; + + //float vAngle = Vector2.Angle(Vector3.up, pingPosition - Centerpoint); + float vAngle = 0f; + if (omniDisplay) + { + vAngle = Vector2.Angle(Vector3.up, Centerpoint - pingPosition); + if (pingPosition.x < Centerpoint.x) + { + vAngle = -vAngle; //FIXME - inverted. Need to Flip (not mirror) angle + } + } + + if ((tData.targetInfo && tData.targetInfo.isMissile) || tData.Team == null) + { + float mDotSize = (20f) / (omniDisplay ? 1f : rangeIndex + 1f); + if (mDotSize < 1f) mDotSize = 1f; + + if (omniDisplay) + { + GUIUtility.RotateAroundPivot(vAngle, guiMatrix * Centerpoint); + pingRect = new Rect(Centerpoint.x - (mDotSize * 0.5f), Centerpoint.y - (RadarDisplayRect.height * 0.5f), mDotSize, mDotSize); + } + else pingRect = new Rect(pingPosition.x - (mDotSize * 0.5f), pingPosition.y - (mDotSize * 0.5f), mDotSize, mDotSize); + + Color origGUIColor = GUI.color; + GUI.color = Color.white - new Color(0, 0, 0, minusAlpha); + GUI.DrawTexture(pingRect, omniDisplay ? t.detectedByIRST.irstRanging ? BDArmorySetup.Instance.redDotTexture : BDArmorySetup.Instance.irSpikeTexture : BDArmorySetup.Instance.redDotTexture, ScaleMode.StretchToFill, true); + GUI.color = origGUIColor; + + GUI.matrix = guiMatrix; + } + /* + 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 = + VectorUtils.Angle( + displayedIRTargets[i].targetData.velocity.ProjectOnPlanePreNormalized(referenceTransform.up), + referenceTransform.forward); + if (referenceTransform.InverseTransformVector(displayedIRTargets[i].targetData.velocity).x < 0) + { + vAngle = -vAngle; + } + GUIUtility.RotateAroundPivot(vAngle, guiMatrix*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 = guiMatrix; + 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 = (t.magnitude / (omniDisplay ? 10f : 25f)) / (omniDisplay ? 2f : rangeIndex + 1f); + if (mDotSize < 1f) mDotSize = 1f; + if (mDotSize > (omniDisplay ? 80f : 20f)) mDotSize = omniDisplay ? 80f : 20f; + + if (omniDisplay) + { + GUIUtility.RotateAroundPivot(vAngle, guiMatrix * Centerpoint); + pingRect = new Rect(Centerpoint.x - (mDotSize * 0.5f), Centerpoint.y - (RadarDisplayRect.height * 0.5f), mDotSize, mDotSize); + } + else pingRect = new Rect(pingPosition.x - (mDotSize * 0.5f), pingPosition.y - (mDotSize * 0.5f), mDotSize, mDotSize); + + Color origGUIColor = GUI.color; + GUI.color = Color.white - new Color(0, 0, 0, minusAlpha); + GUI.DrawTexture(pingRect, omniDisplay ? t.detectedByIRST.irstRanging ? BDArmorySetup.Instance.redDotTexture : BDArmorySetup.Instance.irSpikeTexture : BDArmorySetup.Instance.redDotTexture, ScaleMode.StretchToFill, true); + GUI.color = origGUIColor; + + GUI.matrix = guiMatrix; + } + + if (BDArmorySettings.DEBUG_RADAR) + { + GUI.Label(new Rect(pingPosition.x + (pingSize.x * 0.5f), pingPosition.y, 100, 24), + displayedIRTargets[i].magnitude.ToString("0.0")); } } } - pingPositionsDirty = false; } 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() @@ -2038,44 +3046,42 @@ private void UpdateInputs() ShowSelector(); SlewSelector(Vector2.up); } - - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_LOCK)) + if (radarCount > 0) { - if (showSelector) + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_LOCK)) { - TryLockViaSelector(); + if (showSelector) + { + TryLockViaSelector(); + } + ShowSelector(); } - ShowSelector(); - } - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_CYCLE_LOCK)) - { - if (locked) + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_CYCLE_LOCK)) { - CycleActiveLock(); + if (locked) + { + CycleActiveLock(); + } } - } - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_SCAN_MODE)) - { - if (!locked && radarCount > 0 && !omniDisplay) + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_NEXT)) { - availableRadars[0].boresightScan = !availableRadars[0].boresightScan; + TargetNext(); } - } - - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TURRETS)) - { - if (slaveTurrets) + else if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_PREV)) { - UnslaveTurrets(); + TargetPrev(); } - else + } + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_SCAN_MODE)) + { + if (!locked && radarCount + irstCount > 0 && !omniDisplay) { - SlaveTurrets(); + availableRadars[0].boresightScan = !availableRadars[0].boresightScan; + availableIRSTs[0].boresightScan = !availableIRSTs[0].boresightScan; } } - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_RANGE_UP)) { IncreaseRange(); @@ -2084,14 +3090,16 @@ private void UpdateInputs() { DecreaseRange(); } - - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_NEXT)) - { - TargetNext(); - } - else if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_PREV)) + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TURRETS)) { - TargetPrev(); + if (slaveTurrets) + { + UnslaveTurrets(); + } + else + { + SlaveTurrets(); + } } } @@ -2102,12 +3110,13 @@ private void TryLockViaSelector() float closestSqrMag = float.MaxValue; for (int i = 0; i < displayedTargets.Count; i++) { - float sqrMag = (displayedTargets[i].pingPosition - selectorPos).sqrMagnitude; + RadarDisplayData t = displayedTargets[i]; + float sqrMag = (t.pingPosition - selectorPos).sqrMagnitude; if (sqrMag < closestSqrMag) { - if (sqrMag < Mathf.Pow(20, 2)) + if (sqrMag < 400f) // 20 * 20) { - closestPos = displayedTargets[i].targetData.predictedPosition; + closestPos = t.targetData.predictedPosition; found = true; } } @@ -2115,9 +3124,9 @@ private void TryLockViaSelector() if (found) { - TryLockTarget(closestPos); + TryLockTarget(closestPos, true); } - else if (closestSqrMag > Mathf.Pow(40, 2)) + else if (closestSqrMag > 1600f) // (40 * 40)) { UnlockCurrentTarget(); } diff --git a/BDArmory/Radar/ViewScanResults.cs b/BDArmory/Radar/ViewScanResults.cs new file mode 100644 index 000000000..be0a2f602 --- /dev/null +++ b/BDArmory/Radar/ViewScanResults.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Radar +{ + public struct ViewScanResults + { + #region Missiles + public bool foundMissile; + public bool foundHeatMissile; + public bool foundRadarMissile; + public bool foundAntiRadiationMissile; + public bool foundGPSMissile; + public bool foundAGM; + public bool foundTorpedo; + public List incomingMissiles; // List of incoming missiles sorted by distance. + #endregion + + #region Guns + public bool firingAtMe; + public float missDistance; + public float missDeviation; + public Vector3 threatPosition; + public Vessel threatVessel; + public MissileFire threatWeaponManager; + #endregion + } + + public struct IncomingMissile + { + public MissileBase.TargetingModes guidanceType; // Missile guidance type + public float distance; // Missile distance + public float time; // Time to CPA + public Vector3 position; // Missile position + public Vessel vessel; // Missile vessel + public MissileFire weaponManager; // WM of source vessel for regular missiles or WM of missile for modular missiles. + } +} diff --git a/BDArmory/Radar/_description b/BDArmory/Radar/_description new file mode 100644 index 000000000..b07719ebb --- /dev/null +++ b/BDArmory/Radar/_description @@ -0,0 +1,2 @@ +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..b6d62ec59 --- /dev/null +++ b/BDArmory/Settings/BDAPersistentSettingsField.cs @@ -0,0 +1,267 @@ +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()) + { + try + { + 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(Vector2)) + { + settings.SetValue(field.Current.Name, ((Vector2)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); + } + } + catch + { + Debug.LogError($"[BDArmory.BDAPersistentSettingsField]: Exception triggered while trying to save field {field.Current.Name} with value {field.Current.GetValue(null)}"); + throw; + } + } + 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), 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; + } + + // Score weights have been moved to their own file. + if (fileNode.HasNode("ScoreWeights") || fileNode.HasNode("CtsScoreWeights")) + { + fileNode.RemoveNode("ScoreWeights"); + fileNode.RemoveNode("CtsScoreWeights"); + fileNode.Save(BDArmorySettings.settingsConfigURL); + } + + 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), 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), 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, string what) + { + try + { + if (type == typeof(string)) + { + return value; + } + + if (type == typeof(bool)) + { + return bool.Parse(value); + } + else if (type.IsEnum) + { + return System.Enum.Parse(type, value); + } + else if (type == typeof(float)) + { + return float.Parse(value); + } + else if (type == typeof(int)) + { + return int.Parse(value); + } + else if (type == typeof(float)) + { + return float.Parse(value); + } + else if (type == typeof(Rect)) + { + string[] strings = value.Trim(['(', ')', ' ']).Split(','); + float xVal = float.Parse(strings[0].Split(':')[1]); + float yVal = float.Parse(strings[1].Split(':')[1]); + float wVal = float.Parse(strings[2].Split(':')[1]); + float hVal = float.Parse(strings[3].Split(':')[1]); + Rect rectVal = new() + { + x = xVal, + y = yVal, + width = wVal, + height = hVal + }; + return rectVal; + } + else if (type == typeof(Vector2)) + { + char[] charsToTrim = ['(', ')', ' ']; + string[] strings = value.Trim(charsToTrim).Split(','); + float x = float.Parse(strings[0]); + float y = float.Parse(strings[1]); + return new Vector2(x, y); + } + 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(["; "], StringSplitOptions.RemoveEmptyEntries).ToList(); + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.BDAPersistantSettingsField]: Failed to parse '{value}' as a {type} for {what}: {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 80% rename from BDArmory/UI/BDAWindowSettingsField.cs rename to BDArmory/Settings/BDAWindowSettingsField.cs index 1c45074d2..039b70a41 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 @@ -24,7 +25,7 @@ public static void Save() ConfigNode settings = fileNode.GetNode("BDAWindows"); - IEnumerator field = typeof(BDArmorySetup).GetFields().AsEnumerable().GetEnumerator(); + IEnumerator field = typeof(BDArmorySetup).GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).AsEnumerable().GetEnumerator(); // Include both public and private fields. while (field.MoveNext()) { if (field.Current == null) continue; @@ -43,14 +44,14 @@ public static void Load() ConfigNode settings = fileNode.GetNode("BDAWindows"); - IEnumerator field = typeof(BDArmorySetup).GetFields().AsEnumerable().GetEnumerator(); + IEnumerator field = typeof(BDArmorySetup).GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).AsEnumerable().GetEnumerator(); // Include both public and private fields. while (field.MoveNext()) { if (field.Current == null) continue; if (!field.Current.IsDefined(typeof(BDAWindowSettingsField), false)) continue; if (!settings.HasValue(field.Current.Name)) continue; - object parsedValue = BDAPersistantSettingsField.ParseValue(field.Current.FieldType, settings.GetValue(field.Current.Name)); + object parsedValue = BDAPersistentSettingsField.ParseValue(field.Current.FieldType, settings.GetValue(field.Current.Name), field.Current.Name); if (parsedValue != null) { field.Current.SetValue(null, parsedValue); diff --git a/BDArmory/Settings/BDArmorySettings.cs b/BDArmory/Settings/BDArmorySettings.cs new file mode 100644 index 000000000..65adc22f5 --- /dev/null +++ b/BDArmory/Settings/BDArmorySettings.cs @@ -0,0 +1,501 @@ +using UnityEngine; + +using System.IO; +using System.Collections.Generic; + +namespace BDArmory.Settings +{ + public class BDArmorySettings + { + public static string oldSettingsConfigURL = Path.GetFullPath(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.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/settings.cfg")); + public static bool ready = false; + + #region Settings section toggles + [BDAPersistentSettingsField] public static bool GAMEPLAY_SETTINGS_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 = false; + [BDAPersistentSettingsField] public static bool GM_SETTINGS_TOGGLE = false; + [BDAPersistentSettingsField] public static bool ADVANCED_USER_SETTINGS = true; + #endregion + + #region Window settings + [BDAPersistentSettingsField] public static bool STRICT_WINDOW_BOUNDARIES = true; + [BDAPersistentSettingsField] public static float REMOTE_ORCHESTRATION_WINDOW_WIDTH = 225f; + [BDAPersistentSettingsField] public static bool VESSEL_SWITCHER_WINDOW_SORTING = false; + [BDAPersistentSettingsField] public static bool VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE = false; + [BDAPersistentSettingsField] public static bool VESSEL_SWITCHER_PERSIST_UI = false; + [BDAPersistentSettingsField] public static bool VESSEL_SWITCHER_WINDOW_ALIGNED = false; + [BDAPersistentSettingsField] public static float VESSEL_SPAWNER_WINDOW_WIDTH = 480f; + [BDAPersistentSettingsField] public static float VESSEL_WAYPOINT_WINDOW_WIDTH = 480f; + [BDAPersistentSettingsField] public static float EVOLUTION_WINDOW_WIDTH = 350f; + [BDAPersistentSettingsField] public static float GUI_OPACITY = 1f; // Modify the GUI opacity. + [BDAPersistentSettingsField] public static float UI_SCALE = 1f; // Global UI scaling. (Config value for when not following stock.) + [BDAPersistentSettingsField] public static bool UI_SCALE_FOLLOWS_STOCK = true; // Global UI scaling follows stock + public static float UI_SCALE_ACTUAL => UI_SCALE_FOLLOWS_STOCK ? GameSettings.UI_SCALE : UI_SCALE; // Use this one in code. + public static float PREVIOUS_UI_SCALE = 1f; // For tracking changes + #endregion + + #region General toggle settings + //[BDAPersistentSettingsField] public static bool INSTAKILL = true; //Deprecated, only affects lasers; use an Instagib mutator instead + [BDAPersistentSettingsField] public static bool AI_TOOLBAR_BUTTON = true; // Show or hide the BDA AI toolbar button. + [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 ordnance w/ Reload Module) + [BDAPersistentSettingsField] public static bool LIMITED_ORDINANCE = false; //MML ammo clamped to salvo size, no relaods + [BDAPersistentSettingsField] public static bool INFINITE_FUEL = false; //Infinite propellant + [BDAPersistentSettingsField] public static bool INFINITE_EC = false; //Infinite electric charge + [BDAPersistentSettingsField] public static bool INFINITE_COUNTERMEASURES = false; //infinite CMs + [BDAPersistentSettingsField] public static bool PERFORMANCE_OPTIONS = true; + [BDAPersistentSettingsField] public static bool BULLET_HITS = true; + [BDAPersistentSettingsField] public static bool WATER_HIT_FX = true; + [BDAPersistentSettingsField] public static bool WEAPONS_RESPECT_CROSSFEED = true; + public static bool waterHitEffect => PERFORMANCE_OPTIONS && WATER_HIT_FX; + [BDAPersistentSettingsField] public static bool EJECT_SHELLS = true; + [BDAPersistentSettingsField] public static bool LIGHTFX = true; // explosions spawn a LightFX + public static bool LightFX => PERFORMANCE_OPTIONS && LIGHTFX; + [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. + [BDAPersistentSettingsField] public static bool DISABLE_GUARDMODE_ON_SPAWN = true; // Disable guardMode on WMs when they're spawned. + + [BDAPersistentSettingsField] public static bool REMOTE_SHOOTING = false; + [BDAPersistentSettingsField] public static bool BOMB_CLEARANCE_CHECK = 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 GAPLESS_PARTICLE_EMITTERS = true; // Use gapless particle emitters. + public static bool GaplessParticleEmitters => PERFORMANCE_OPTIONS && GAPLESS_PARTICLE_EMITTERS; + [BDAPersistentSettingsField] public static bool FLARE_SMOKE = true; // Flares leave a trail of smoke. + public static bool FlareSmoke => PERFORMANCE_OPTIONS && FLARE_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 = 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. + [BDAPersistentSettingsField] public static int KERBAL_SAFETY = 1; // Try to save kerbals by ejecting/leaving seats and deploying parachutes. + [BDAPersistentSettingsField] public static bool TRACE_VESSELS_DURING_COMPETITIONS = false; // Trace vessel positions and rotations during competitions. + [BDAPersistentSettingsField] public static bool AUTO_LOG_TIME_SYNC = false; // Log time synchronisation info automatically during competitions. + [BDAPersistentSettingsField] public static float LOG_TIME_SYNC_INTERVAL = 0.2f; // Interval for logging time synchronisation information (approx). + [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 AUTOCATEGORIZE_PARTS = true; + [BDAPersistentSettingsField] public static bool SHOW_CATEGORIES = true; + [BDAPersistentSettingsField] public static bool IGNORE_TERRAIN_CHECK = false; + [BDAPersistentSettingsField] public static bool CHECK_WATER_TERRAIN = false; + [BDAPersistentSettingsField] public static bool RADAR_NOTCHING = false; + [BDAPersistentSettingsField] public static bool RADAR_ALLOW_SURFACE_WARFARE = true; + [BDAPersistentSettingsField] public static float RADAR_NOTCHING_FACTOR = 1f; + [BDAPersistentSettingsField] public static float RADAR_NOTCHING_SCR_FACTOR = 0.01f; + [BDAPersistentSettingsField] public static bool DISPLAY_PATHING_GRID = false; //laggy when the grid gets large + //[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; // do bullets/rockets get slowed down if fired into/under water + [BDAPersistentSettingsField] public static bool UNDERWATER_VISION = false; //If false, Subs and other submerged vessels fully visible to surface/air craft and vice versa without detectors? + [BDAPersistentSettingsField] public static bool PERSISTENT_FX = false; + [BDAPersistentSettingsField] public static bool LEGACY_ARMOR = false; + [BDAPersistentSettingsField] public static bool HACK_INTAKES = false; + [BDAPersistentSettingsField] public static bool COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START = false; // Close the settings window when clicking the start competition button. + [BDAPersistentSettingsField] public static bool AUTO_LOAD_TO_KSC = false; // Automatically load the last used save and go to the KSC. + [BDAPersistentSettingsField] public static bool GENERATE_CLEAN_SAVE = false; // Use a clean save instead of the persistent one when loading to the KSC. + [BDAPersistentSettingsField] public static bool REPORT_DAMAGE_NOT_PARTS_HIT = true; // Report damage to parts (including to debris) after explosions instead of parts hit. Incurs a 0.2-0.3s delay to messages. + #endregion + + #region Debug Labels + [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 + #endregion + + #region General slider settings + [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 = 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 bool COMPETITION_GM_KILL_WEAPON = false; // Competition GM will kill weaponless craft? + [BDAPersistentSettingsField] public static bool COMPETITION_GM_KILL_ENGINE = false; // Competition GM will kill engineless craft? + [BDAPersistentSettingsField] public static bool COMPETITION_GM_KILL_DISABLED = false; // Competition GM will kill craft that are disabled (no weapons or ammo, no engine [Pilot/VTOL/Ship/Sub] or no wheels [Surface]) + [BDAPersistentSettingsField] public static float COMPETITION_GM_KILL_HP = 0; // Competition GM will kill craft with low HP craft? + [BDAPersistentSettingsField] public static float COMPETITION_GM_KILL_TIME = 0; // CompetitionGM Kill time + [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 int TOURNAMENT_START_DESPITE_FAILURES_ON_ATTEMPT = 3;// When start despite failures is enabled, use it in tournaments on the Nth attempt. + [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 = 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 BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER = 128f; // Multiplier of fixedDeltaTime for the large scale steps of ballistic trajectory simulations. Large values at extreme ranges may cause small inaccuracies. 128 with 1km/s bullets at their max range seems reasonable in most cases. + [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.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 float MAX_ARMOR_LIMIT = -1; //Armor will be clamped to this limit if non-negative. + [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 calc 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 + [BDAPersistentSettingsField] public static bool NUMERIC_INPUT_SELF_UPDATE = true; // Automatically update the display string in NumericInputField after attempting to parse the value. + [BDAPersistentSettingsField] public static float NUMERIC_INPUT_DELAY = 0.5f; // Time before last input for "read and interpret" logic of NumericInputField. + [BDAPersistentSettingsField] public static Vector2 PROC_ARMOR_ALT_LIMITS = new Vector2(0.01f, 100f); // Unclamped limits of proc armour panels. + #endregion + + #region Physics constants + [BDAPersistentSettingsField] public static float GLOBAL_LIFT_MULTIPLIER = 0.25f; + [BDAPersistentSettingsField] public static float GLOBAL_DRAG_MULTIPLIER = 6f; + [BDAPersistentSettingsField] public static float RECOIL_FACTOR = 0.75f; + [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.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 HEAT_CONE_HALF_ANGLE = 2.5f; + [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 + [BDAPersistentSettingsField] public static bool KERBAL_ERA = true; + #endregion + + #region 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 + [BDAPersistentSettingsField] public static float FIRELIFETIME_IN_SECONDS = 90f; //controls fx for penetration only for landed or splashed + #endregion + + #region Radar settings + [BDAPersistentSettingsField] public static float RWR_WINDOW_SCALE_MIN = 0.50f; + [BDAPersistentSettingsField] public static float RWR_WINDOW_SCALE = 1f; + [BDAPersistentSettingsField] public static float RWR_WINDOW_SCALE_MAX = 1.50f; + [BDAPersistentSettingsField] public static float RADAR_WINDOW_SCALE_MIN = 0.50f; + [BDAPersistentSettingsField] public static float RADAR_WINDOW_SCALE = 1f; + [BDAPersistentSettingsField] public static float RADAR_WINDOW_SCALE_MAX = 1.50f; + [BDAPersistentSettingsField] public static float TARGET_WINDOW_SCALE_MIN = 0.50f; + [BDAPersistentSettingsField] public static float TARGET_WINDOW_SCALE = 1f; + [BDAPersistentSettingsField] public static float TARGET_WINDOW_SCALE_MAX = 2f; + [BDAPersistentSettingsField] public static float TARGET_CAM_RESOLUTION = 1024f; + [BDAPersistentSettingsField] public static bool BW_TARGET_CAM = true; + [BDAPersistentSettingsField] public static bool TARGET_WINDOW_INVERT_MOUSE_X = false; + [BDAPersistentSettingsField] public static bool TARGET_WINDOW_INVERT_MOUSE_Y = false; + #endregion + + #region Game modes + [BDAPersistentSettingsField] public static bool PEACE_MODE = false; + [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 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; + [BDAPersistentSettingsField] public static int ASTEROID_FIELD_NUMBER = 100; // Number of asteroids + [BDAPersistentSettingsField] public static float ASTEROID_FIELD_ALTITUDE = 2f; // Km. + [BDAPersistentSettingsField] public static float ASTEROID_FIELD_RADIUS = 5f; // Km. + [BDAPersistentSettingsField] public static bool ASTEROID_FIELD_ANOMALOUS_ATTRACTION = false; // Asteroids are attracted to vessels. + [BDAPersistentSettingsField] public static float ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH = 0.2f; // Strength of the effect. + [BDAPersistentSettingsField] public static bool ASTEROID_RAIN = false; + [BDAPersistentSettingsField] public static int ASTEROID_RAIN_NUMBER = 100; // Number of asteroids + [BDAPersistentSettingsField] public static float ASTEROID_RAIN_DENSITY = 0.5f; // Arbitrary density scale. + [BDAPersistentSettingsField] public static float ASTEROID_RAIN_ALTITUDE = 2f; // Km.k + [BDAPersistentSettingsField] public static float ASTEROID_RAIN_RADIUS = 3f; // Km. + [BDAPersistentSettingsField] public static bool ASTEROID_RAIN_FOLLOWS_CENTROID = true; + [BDAPersistentSettingsField] public static bool ASTEROID_RAIN_FOLLOWS_SPREAD = true; + [BDAPersistentSettingsField] public static float GUARD_MODE_TRIGGER_ALT = 3000; // m. + [BDAPersistentSettingsField] public static bool MUTATOR_MODE = false; + [BDAPersistentSettingsField] public static bool ZOMBIE_MODE = false; + [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"; + [BDAPersistentSettingsField] public static bool G_LIMITS = false; // Override KSP's G-force limits. If disabled, then KSP's settings are independent from BDA's. + [BDAPersistentSettingsField] public static bool PART_GLIMIT = false; // Part G-force limits. + [BDAPersistentSettingsField] public static bool KERB_GLIMIT = false; // Kerbal G-force limits. + [BDAPersistentSettingsField] public static float G_TOLERANCE = 1f; // Adjust the GToleranceMult to set Max G endurance of all kerbs to a desired amount + // G-Force Limits last adjusted from the Game Difficulty settings. + public static bool _PART_GLIMIT = false; // Part G-force limits. + public static bool _KERB_GLIMIT = false; // Kerbal G-force limits. + public static float _G_TOLERANCE = 1f; // Adjust the GToleranceMult to set Max G endurance of all kerbs to a desired amount + [BDAPersistentSettingsField] public static float AIMING_VISUAL_MALUS = 0; // Malus to visual aiming with a mk1 eyeball. + #endregion + + #region Battle Damage settings + [BDAPersistentSettingsField] public static bool BATTLEDAMAGE_TOGGLE = false; // Main battle damage toggle. + [BDAPersistentSettingsField] public static float BD_DAMAGE_CHANCE = 5; // Base chance per-hit to proc damage + [BDAPersistentSettingsField] public static float BD_DAMAGE_PENETRATION = 0.2f; // Penetration factor required to proc damage + [BDAPersistentSettingsField] public static bool BD_SUBSYSTEMS = true; // Non-critical module damage? + [BDAPersistentSettingsField] public static bool BD_TANKS = true; // Fuel tanks, batteries can leak/burn + [BDAPersistentSettingsField] public static float BD_TANK_LEAK_TIME = 20; // Leak duration + [BDAPersistentSettingsField] public static float BD_TANK_LEAK_RATE = 1; // Leak rate modifier + [BDAPersistentSettingsField] public static bool BD_AMMOBINS = true; // Can ammo bins explode? + [BDAPersistentSettingsField] public static bool BD_VOLATILE_AMMO = false; // Ammo bins guaranteed to explode when destroyed + [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_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? + [BDAPersistentSettingsField] public static bool BD_AEROPARTS = true; // Lift loss & added drag + [BDAPersistentSettingsField] public static float BD_LIFT_LOSS_RATE = 1; // Rate multiplier + [BDAPersistentSettingsField] public static bool BD_CTRL_SRF = true; // Disable ctrl srf actuatiors? + [BDAPersistentSettingsField] public static bool BD_COCKPITS = false; // Control degredation + [BDAPersistentSettingsField] public static bool BD_PILOT_KILLS = false; // Cockpit damage can kill pilots? + [BDAPersistentSettingsField] public static bool BD_FIRES_ENABLED = true; // Can fires occur + [BDAPersistentSettingsField] public static bool BD_FIRE_DOT = true; // Do fires do DoT + [BDAPersistentSettingsField] public static float BD_FIRE_DAMAGE = 5; // Do fires do DoT + [BDAPersistentSettingsField] public static bool BD_FIRE_HEATDMG = true; // Do fires add heat to parts/are fires able to cook off fuel/ammo? + [BDAPersistentSettingsField] public static bool BD_INTENSE_FIRES = false; // Do fuel tank fires DoT get bigger over time? + [BDAPersistentSettingsField] public static bool BD_FIRE_FUELEX = true; // Can fires detonate fuel tanks + [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; + #endregion + + #region Hall of Shame Settings + [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 bool HOS_ASTEROID = false; + [BDAPersistentSettingsField] public static string HOS_MUTATOR = ""; + [BDAPersistentSettingsField] public static string HOS_BADGE = ""; + #endregion + + #region Remote logging + [BDAPersistentSettingsField] public static bool REMOTE_LOGGING_VISIBLE = false; // Show/hide the remote orchestration toggle + [BDAPersistentSettingsField] public static bool REMOTE_LOGGING_ENABLED = false; // Enable/disable remote orchestration + [BDAPersistentSettingsField] public static string REMOTE_ORCHESTRATION_BASE_URL = "bdascores.herokuapp.com"; // Base URL used for orchestration (note: we can't include the https:// as it breaks KSP's serialisation routine) + [BDAPersistentSettingsField] public static string REMOTE_CLIENT_SECRET = ""; // Token used to authorize remote orchestration client + [BDAPersistentSettingsField] public static string COMPETITION_HASH = ""; // Competition hash used for orchestration + [BDAPersistentSettingsField] public static float REMOTE_INTERHEAT_DELAY = 30; // Delay between heats. + [BDAPersistentSettingsField] public static int RUNWAY_PROJECT_ROUND = 0; // RWP round index. + [BDAPersistentSettingsField] public static string REMOTE_ORCHESTRATION_NPC_SWAPPER = "Rammer"; + [BDAPersistentSettingsField] public static string REMOTE_ORC_NPCS_TEAM = ""; + #endregion + + #region 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 = 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 float VESSEL_SPAWN_REF_HEADING = 0f; // Reference heading for the first craft in circular spawns. + [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_REASSIGN_TEAMS = true; // Reassign teams on spawn, overriding teams defined in the SPH. + [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_SMART_REASSIGN_TEAMS = false; // Reassign teams on spawn, but don't override teams defined in the SPH. (Only for single-shot template spawning currently.) + [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 - 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 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 VESSEL_SPAWN_START_COMPETITION_AUTOMATICALLY = false; // Automatically start a competition after spawning succeeds. + [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_INITIAL_VELOCITY = false; // Set planes at their idle speed after dropping them at the start of a competition. + [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_CS_FOLLOWS_CENTROID = false; // The continuous spawning spawn point follows the brawl centroid with bias back to the original spawn point. + #endregion + + #region 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. + #endregion + + #region Scores + [BDAPersistentSettingsField] public static bool SHOW_SCORE_WINDOW = false; + [BDAPersistentSettingsField] public static bool SCORES_PERSIST_UI = false; + [BDAPersistentSettingsField] public static int SCORES_FONT_SIZE = 12; + [BDAPersistentSettingsField] public static int VS_NPC_SCORE_MOD = 2; + #endregion + + #region Waypoints + [BDAPersistentSettingsField] public static float WAYPOINTS_ALTITUDE = -50f; // 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 + [BDAPersistentSettingsField] public static int WAYPOINT_MAX_LAPS = 5; // Configurable max number of laps for the laps slider + #endregion + + #region Heartbleed + [BDAPersistentSettingsField] public static float HEART_BLEED_RATE = 0.01f; + [BDAPersistentSettingsField] public static float HEART_BLEED_INTERVAL = 10f; + [BDAPersistentSettingsField] public static float HEART_BLEED_THRESHOLD = 10f; + #endregion + + #region Resource steal + [BDAPersistentSettingsField] public static bool RESOURCE_STEAL_RESPECT_FLOWSTATE_IN = true; // Respect resource flow state in (stealing). + [BDAPersistentSettingsField] public static bool RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT = false; // Respect resource flow state out (stolen). + [BDAPersistentSettingsField] public static float RESOURCE_STEAL_FUEL_RATION = 0.2f; + [BDAPersistentSettingsField] public static float RESOURCE_STEAL_AMMO_RATION = 0.2f; + [BDAPersistentSettingsField] public static float RESOURCE_STEAL_CM_RATION = 0f; + #endregion + + #region Space Friction + [BDAPersistentSettingsField] public static bool SPACE_HACKS = false; + [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; + #endregion + + #region Mutator Mode + [BDAPersistentSettingsField] public static bool MUTATOR_APPLY_GLOBAL = false; + [BDAPersistentSettingsField] public static bool MUTATOR_APPLY_KILL = false; + [BDAPersistentSettingsField] public static bool MUTATOR_APPLY_TIMER = false; + [BDAPersistentSettingsField] public static float MUTATOR_DURATION = 0.5f; + [BDAPersistentSettingsField] public static List MUTATOR_LIST = new List(); + [BDAPersistentSettingsField] public static int MUTATOR_APPLY_NUM = 1; + [BDAPersistentSettingsField] public static bool MUTATOR_ICONS = false; + [BDAPersistentSettingsField] public static bool MUTATOR_APPLY_GUNGAME = false; + [BDAPersistentSettingsField] public static float VENGEANCE_DELAY = 2.5f; + [BDAPersistentSettingsField] public static float VENGEANCE_YIELD = 1.5f; + #endregion + + #region GunGame + [BDAPersistentSettingsField] public static bool GG_PERSISTANT_PROGRESSION = false; + [BDAPersistentSettingsField] public static bool GG_CYCLE_LIST = false; + //[BDAPersistentSettingsField] public static bool GG_ANNOUNCER = false; + #endregion + + #region 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, Gauntlet, etc.) + [BDAPersistentSettingsField] public static int TOURNAMENT_ROUND_TYPE = 0; // Tournament Style (Shuffled, Ranked, etc.) + [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 = -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_NPCS_PER_HEAT = 0; // NPCs Per Heat + [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. + [BDAPersistentSettingsField] public static bool AUTO_RESUME_CONTINUOUS_SPAWN = false; // Automatically load the game the last continuous spawn was running in and start running continuous spawn again. + [BDAPersistentSettingsField] public static float QUIT_MEMORY_USAGE_THRESHOLD = float.MaxValue; // Automatically quit KSP when memory usage is beyond this. (0 = disabled) + [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 TOURNAMENT_BACKUPS = false; // Store backups of unfinished tournaments. + [BDAPersistentSettingsField] public static bool AUTO_DISABLE_UI = false; // Automatically disable the UI when starting tournaments. + #endregion + + #region 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. + #endregion + + #region 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). + #endregion + + #region Scoring categories + [BDAPersistentSettingsField] public static float SCORING_HEADSHOT = 3; // Head-Shot Time Limit + [BDAPersistentSettingsField] public static float SCORING_KILLSTEAL = 5; // Kill-Steal Time Limit + #endregion + + #region Evolution settings + [BDAPersistentSettingsField] public static bool EVOLUTION_ENABLED = false; + [BDAPersistentSettingsField] public static bool SHOW_EVOLUTION_OPTIONS = false; + [BDAPersistentSettingsField] public static int EVOLUTION_ANTAGONISTS_PER_HEAT = 1; + [BDAPersistentSettingsField] public static int EVOLUTION_MUTATIONS_PER_HEAT = 1; + [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. + #endregion + + #region 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_RCS = false; //RCS evaluated in real-time based on aircraft's aspect + [BDAPersistentSettingsField] public static float ASPECTED_RCS_OVERALL_RCS_WEIGHT = 0.25f; //When ASPECTED_RCS = true, final aspected RCS will be = (1-ASPECTED_RCS_OVERALL_RCS_WEIGHT) * [Aspected RCS] + ASPECTED_RCS_OVERALL_RCS_WEIGHT * [Overall RCS] + [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 + #endregion + + #region ProjectShowdown Stuff + [BDAPersistentSettingsField] public static bool COMP_CONVENIENCE_CHECKS = false; + #endregion + } +} diff --git a/BDArmory/UI/BDInputSettingsFields.cs b/BDArmory/Settings/BDInputSettingsFields.cs similarity index 78% rename from BDArmory/UI/BDInputSettingsFields.cs rename to BDArmory/Settings/BDInputSettingsFields.cs index 1572beff3..8cef243c6 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"); @@ -39,13 +45,25 @@ public class BDInputSettingsFields public static BDInputInfo RADAR_TARGET_PREV = new BDInputInfo("Prev Target"); // VESSEL SWITCHER - public static BDInputInfo VS_SWITCH_NEXT = new BDInputInfo("page up", "Next Vessel"); - public static BDInputInfo VS_SWITCH_PREV = new BDInputInfo("page down", "Prev Vessel"); + public static BDInputInfo VS_SWITCH_NEXT = new BDInputInfo("page down", "Next Vessel"); + public static BDInputInfo VS_SWITCH_PREV = new BDInputInfo("page up", "Prev Vessel"); // TOURNAMENT public static BDInputInfo TOURNAMENT_SETUP = new BDInputInfo("Setup Tournament"); public static BDInputInfo TOURNAMENT_RUN = new BDInputInfo("Run Tournament"); + //GUI + public static BDInputInfo GUI_WM_TOGGLE = new BDInputInfo("[*]", "Toggle WM GUI"); + public static BDInputInfo GUI_AI_TOGGLE = new BDInputInfo("[/]", "Toggle AI GUI"); + + //DEBUG + public static BDInputInfo DEBUG_CLEAR_DEV_CONSOLE = new BDInputInfo("Clear Development Console"); + + // TIME SCALING + public static BDInputInfo TIME_SCALING = new BDInputInfo("Toggle Time Scaling"); + + public static BDInputInfo TEMPORARILY_SHOW_MOUSE = new BDInputInfo("right shift", "Temporarily Show Mouse"); + public static void SaveSettings() { ConfigNode fileNode = ConfigNode.Load(BDArmorySettings.settingsConfigURL); diff --git a/BDArmory/Settings/BDTISettings.cs b/BDArmory/Settings/BDTISettings.cs new file mode 100644 index 000000000..c37955d33 --- /dev/null +++ b/BDArmory/Settings/BDTISettings.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace BDArmory.Settings +{ + public class BDTISettings + { + public static string settingsConfigURL = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/settings.cfg")); + + [SettingsDataField] public static bool TEAMICONS = true; + [SettingsDataField] public static bool SHOW_SELF = false; + [SettingsDataField] public static bool TEAMNAMES = false; + [SettingsDataField] public static bool VESSELNAMES = true; + [SettingsDataField] public static bool SCORE = false; + [SettingsDataField] public static bool HEALTHBAR = false; + [SettingsDataField] public static bool THREATICON = false; + [SettingsDataField] public static bool MISSILES = false; + [SettingsDataField] public static bool MISSILE_TEXT = true; + [SettingsDataField] public static bool DEBRIS = true; + [SettingsDataField] public static bool PERSISTANT = true; + [SettingsDataField] public static bool POINTERS = true; + [SettingsDataField] public static bool TELEMETRY = false; + [SettingsDataField] public static bool STORE_TEAM_COLORS = true; // Store the team colors in the settings.cfg file. + [SettingsDataField] public static float ICONSCALE = 1.0f; + [SettingsDataField] public static float OPACITY = 0.5f; + [SettingsDataField] public static float DISTANCE_THRESHOLD = 100f; + [SettingsDataField] public static float MAX_DISTANCE_THRESHOLD = 0f; + } +} diff --git a/BDArmory/Settings/BDTISettingsFields.cs b/BDArmory/Settings/BDTISettingsFields.cs new file mode 100644 index 000000000..297d2370e --- /dev/null +++ b/BDArmory/Settings/BDTISettingsFields.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UniLinq; +using UnityEngine; + +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Settings +{ + [AttributeUsage(AttributeTargets.Field)] + public class SettingsDataField : Attribute + { + public SettingsDataField() + { + } + public static void Save() + { + ConfigNode fileNode = ConfigNode.Load(BDTISettings.settingsConfigURL); + + if (!fileNode.HasNode("IconSettings")) + { + fileNode.AddNode("IconSettings"); + } + + ConfigNode settings = fileNode.GetNode("IconSettings"); + using (IEnumerator field = typeof(BDTISettings).GetFields().AsEnumerable().GetEnumerator()) + while (field.MoveNext()) + { + if (field.Current == null) continue; + if (!field.Current.IsDefined(typeof(SettingsDataField), false)) continue; + + settings.SetValue(field.Current.Name, field.Current.GetValue(null).ToString(), true); + } + if (BDTISettings.STORE_TEAM_COLORS) + { + if (!fileNode.HasNode("TeamColors")) + { + fileNode.AddNode("TeamColors"); + } + + ConfigNode colors = fileNode.GetNode("TeamColors"); + + foreach (var keyValuePair in BDTISetup.Instance.ColorAssignments) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(keyValuePair.ToString()); + string color = $"{Mathf.RoundToInt(keyValuePair.Value.r * 255)},{Mathf.RoundToInt(keyValuePair.Value.g * 255)},{Mathf.RoundToInt(keyValuePair.Value.b * 255)},{Mathf.RoundToInt(keyValuePair.Value.a * 255)}"; + colors.SetValue(keyValuePair.Key.ToString(), color, true); + } + } + else + { + if (fileNode.HasNode("TeamColors")) + { + fileNode.RemoveNode("TeamColors"); + } + } + + fileNode.Save(BDTISettings.settingsConfigURL); + } + public static void Load() + { + ConfigNode fileNode = ConfigNode.Load(BDTISettings.settingsConfigURL); + if (!fileNode.HasNode("IconSettings")) return; + + ConfigNode settings = fileNode.GetNode("IconSettings"); + + using (IEnumerator field = typeof(BDTISettings).GetFields().AsEnumerable().GetEnumerator()) + while (field.MoveNext()) + { + if (field.Current == null) continue; + if (!field.Current.IsDefined(typeof(SettingsDataField), 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); + } + } + if (!BDTISettings.STORE_TEAM_COLORS || !fileNode.HasNode("TeamColors")) return; + ConfigNode colors = fileNode.GetNode("TeamColors"); + for (int i = 0; i < colors.CountValues; i++) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDTISettingsField]: 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] = GUIUtils.ParseColor255(colors.values[i].value); + } + else + { + BDTISetup.Instance.ColorAssignments.Add(colors.values[i].name, GUIUtils.ParseColor255(colors.values[i].value)); + } + } + } + public static object ParseValue(Type type, string value) + { + if (type == typeof(bool)) + { + return bool.Parse(value); + } + else if (type == typeof(float)) + { + return float.Parse(value); + } + else if (type == typeof(string)) + { + return value; + } + Debug.LogError("[BDArmory.BDTISettingsField]: BDAPersistentSettingsField to parse settings field of type " + type + + " and value " + value); + + return null; + } + } +} \ No newline at end of file diff --git a/BDArmory/Settings/CompSettings.cs b/BDArmory/Settings/CompSettings.cs new file mode 100644 index 000000000..17df6d6a9 --- /dev/null +++ b/BDArmory/Settings/CompSettings.cs @@ -0,0 +1,201 @@ +using UnityEngine; +using System.IO; +using System.Collections.Generic; +using System; + +namespace BDArmory.Settings +{ + public class CompSettings + { + // Settings overrides for AI/WM settings for competition rules compliance + + static readonly string CompSettingsPath = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/Comp_settings.cfg")); + static public bool CompOverridesEnabled = false; + static public bool CompVesselChecksEnabled = false; + static public bool CompPriceChecksEnabled = false; + static public bool CompBanChecksEnabled = false; + public static readonly Dictionary CompOverrides = new() + { + // FIXME there's probably a few more things that could get set here for AI/WM override if needed in specific rounds. + //AI Min/max Alt? + //AI postStallAoA? + //AI allowRamming? + //WM gunRange? + //WM multiMissileTgtNum + {"extensionCutoffTime", -1}, + {"extendDistanceAirToAir", -1}, + {"MONOCOCKPIT_VIEWRANGE", -1}, + {"DUALCOCKPIT_VIEWRANGE", -1}, + {"guardAngle", -1}, + {"collisionAvoidanceThreshold", -1}, + {"vesselCollisionAvoidanceLookAheadPeriod", -1}, + {"vesselCollisionAvoidanceStrength", -1 }, + {"idleSpeed", -1}, + {"DISABLE_SAS", 0}, //0/1 for F/T + }; + public static readonly Dictionary vesselChecks = new() + { + {"maxStacking", -1}, //wing Stacking %. No limit if -1 + {"maxPartCount", -1}, //part count. no limit if -1 + {"maxLtW", -1}, //Lift-to-Weight ratio. -1 for no limit + {"maxTWR", -1}, //Thrust-Weight ratio. -1 for no limit + {"maxEngines", 999}, //set to negative to mandate that number of engines on the craft + {"maxMass", -1}, + {"pointBuyBudget", -1}, //for comps with point buy systems for limiting armament/etc. if enabled, will check parts against partPointCosts + }; + public static readonly Dictionary partPointCosts = new() + { + //{"bahaBrowningAnm2", 1}, + }; + public static readonly Dictionary partBlacklist = new() + { + //{"bahaCloakingDevice", 2}, //flag craft containing more than allowed number of X + }; + /// + /// Load P:S AI/Wm override settings from file. + /// + public static void Load() + { + CompOverridesEnabled = false; + CompVesselChecksEnabled = false; + CompPriceChecksEnabled = false; + CompBanChecksEnabled = false; + if (!File.Exists(CompSettingsPath)) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CompSettings]: Override settings not present, skipping."); + return; + } + ConfigNode fileNode = ConfigNode.Load(CompSettingsPath); + + string settingsComment = "Settings overrides for AI/WM settings for competition rules compliance. Use -1 to disable override of that setting at competition start. MONO/DUALCOCKPIT settings set single-seat and 2+ seat cockpit view range. DISABLE_SAS uses 0/1 for False/True "; + if (!fileNode.HasNode("AIWMChecks")) + { + Initialize("AIWMChecks", CompOverrides, settingsComment, fileNode); + } + if (fileNode.HasNode("AIWMChecks")) + { + ConfigNode settings = fileNode.GetNode("AIWMChecks"); + settings.comment = settingsComment; + foreach (ConfigNode.Value fieldNode in settings.values) + { + if (float.TryParse(fieldNode.value, out float fieldValue)) + { + CompOverrides[fieldNode.name] = fieldValue; // Add or set the override. + if ((fieldNode.name == "DISABLE_SAS" && fieldValue > 0) || fieldValue >= 0) CompOverridesEnabled = true; + } + } + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log($"[BDArmory.CompSettings]: Comp AI/WM overrides loaded"); + foreach (KeyValuePair entry in CompOverrides) + { + Debug.Log($"[BDArmory.CompSettings]: {entry.Key}, value {entry.Value} added"); + } + } + } + string VCComment = "Set construction rule limits here. maxStacking is Wing Stacking% in the SPH/VAB BDA Utilities Tool GUI. Use -1 for no limit for the respective field. Setting maxEngines to a negative number will mandate a minimum engine count, e.g. maxEngines = -2 requires 2+ engines on the craft."; // Note: reading the node doesn't seem to get the comment, so we need to reset it each time. + if (!fileNode.HasNode("VesselChecks")) + { + Initialize("VesselChecks", vesselChecks, VCComment, fileNode); + } + if (fileNode.HasNode("VesselChecks")) + { + ConfigNode settings = fileNode.GetNode("VesselChecks"); + settings.comment = VCComment; + foreach (ConfigNode.Value fieldNode in settings.values) + { + if (float.TryParse(fieldNode.value, out float fieldValue)) + { + vesselChecks[fieldNode.name] = fieldValue; // Add or set the override. + switch (fieldNode.name) + { + case "maxEngines": if (fieldValue != 999) CompVesselChecksEnabled = true; break; + default: if (fieldValue >= 0) CompVesselChecksEnabled = true; break; + } + } + } + + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log($"[BDArmory.CompSettings]: Comp vessel checks loaded"); + foreach (KeyValuePair entry in vesselChecks) + { + Debug.Log($"[BDArmory.CompSettings]: {entry.Key}, value {entry.Value} added"); + } + } + } + string partCostComment = "Set parts and their point cost if pointBuyBudget > 0, e.g. 'bahaGAU-8 = 4'. Any underscores in part names need to be relaced with periods."; // Note: reading the node doesn't seem to get the comment, so we need to reset it each time. + if (!fileNode.HasNode("PartCosts")) + { + Initialize("PartCosts", partPointCosts, partCostComment, fileNode); + } + if (fileNode.HasNode("PartCosts")) + { + ConfigNode settings = fileNode.GetNode("PartCosts"); + settings.comment = partCostComment; + foreach (ConfigNode.Value fieldNode in settings.values) + { + if (float.TryParse(fieldNode.value, out float fieldValue)) + partPointCosts[fieldNode.name] = fieldValue; // Add or set the override. + } + if (partPointCosts.Keys.Count > 0) CompPriceChecksEnabled = true; + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log($"[BDArmory.CompSettings]: Comp part costs loaded"); + foreach (KeyValuePair entry in partPointCosts) + { + Debug.Log($"[BDArmory.CompSettings]: {entry.Key}, value {entry.Value} added"); + } + Debug.Log($"[BDArmory.CompSettings]: {partPointCosts.Keys.Count} parts have a pointCost"); + } + } + string blacklistComment = "Add parts to limit/ban select parts on a vessel. Identify part via part name, and max quantity that is allowed. If pointBuy is enabled, weapons/missiles not on the pricelist are autoblacklisted. A negative value will whitelist the part, and require it on the vessel in that quantity for the vessel to be legal - e.g. smallOreTank = -2 for a ruleset where the craft must have 2 ore tanks. Use an * for wildcard searches, e.g. baha* = 3 to limit craft to a max of 3 BDA parts"; + if (!fileNode.HasNode("PartBlackList")) + { + Initialize("PartBlackList", partBlacklist, blacklistComment, fileNode); + } + if (fileNode.HasNode("PartBlackList")) + { + ConfigNode settings = fileNode.GetNode("PartBlackList"); + settings.comment = blacklistComment; + foreach (ConfigNode.Value fieldNode in settings.values) + { + if (float.TryParse(fieldNode.value, out float fieldValue)) + partBlacklist[fieldNode.name] = fieldValue; + } + if (partBlacklist.Keys.Count > 0) CompBanChecksEnabled = true; + if (BDArmorySettings.DEBUG_OTHER) + { + Debug.Log($"[BDArmory.CompSettings]: Comp part blacklist loaded"); + foreach (KeyValuePair entry in partBlacklist) + { + Debug.Log($"[BDArmory.CompSettings]: {entry.Key}, value {entry.Value} added"); + } + Debug.Log($"[BDArmory.CompSettings]: {partBlacklist.Keys.Count} parts limited/banned"); + } + } + fileNode.Save(CompSettingsPath); + } + public static void Initialize(string Node, Dictionary defaultSettings, string comment, ConfigNode fileNode) + { + fileNode.AddNode(Node, comment); + var settingsNode = fileNode.GetNode(Node); + settingsNode.comment = comment; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CompSettings]: initializing {Node}, {defaultSettings.Keys.Count} fields to add"); + foreach (var fieldName in defaultSettings.Keys) + { + var fieldValue = defaultSettings[fieldName]; + try + { + settingsNode.SetValue(fieldName, fieldValue.ToString(), true); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.CompSettings]: Adding {fieldName}, value {fieldValue} to Comp_settings.cfg"); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.CompSettings]: Exception triggered while trying to save field {fieldName} with value {fieldValue}: {e.Message}"); + } + } + fileNode.Save(CompSettingsPath); + } + } +} \ No newline at end of file diff --git a/BDArmory/Settings/RWPSettings.cs b/BDArmory/Settings/RWPSettings.cs new file mode 100644 index 000000000..8b9a9d859 --- /dev/null +++ b/BDArmory/Settings/RWPSettings.cs @@ -0,0 +1,472 @@ +using System.Linq; +using System.Reflection; +using UnityEngine; + +using System.IO; +using System.Collections.Generic; +using System; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Settings +{ + public class RWPSettings + { + // Settings that are set when the RWP toggle is enabled or the slider is moved. + // Further adjustments to the settings in-game are allowed, but do not get saved to the RWP_settings.cfg file. + // This should avoid the need for many "if (SETTING || (RUNWAY_PROJECT && RUNWAY_PROJECT_ROUND == xx))" checks, though its main purpose is for resetting settings back to what they should be when toggling the RWP toggle / adjusting the RWP slider. + + static readonly string RWPSettingsPath = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/RWP_settings.cfg")); + static bool RWPEnabled = false; + static readonly Dictionary> RWPOverrides = new() + { + {0, new(){ // Global RWP settings. + // FIXME there's probably a few more things that should get set here globally for RWP and overriden if needed in specific rounds. + //{"RESTORE_KAL", true}, + //{"KERBAL_SAFETY", 1}, + //{"KERBAL_SAFETY_INVENTORY", 2}, + //{"RESET_ARMOUR", true}, + //all of the Physics Constants? + {"AUTONOMOUS_COMBAT_SEATS", false}, + {"BATTLEDAMAGE", true}, + {"DESTROY_UNCONTROLLED_WMS", true}, + {"DISABLE_RAMMING", false}, + {"DISABLE_GUARDMODE_ON_SPAWN", true}, + {"G_LIMITS", true}, // Override G-Limits from Game Difficulty and disable them + {"KERB_GLIMIT", false}, + {"PART_GLIMIT", false}, + {"G_TOLERANCE", 20.5}, // Default G-tolerance of Kerbals with KerbalGToleranceMult=1 + {"HACK_INTAKES", true}, + {"HP_THRESHOLD", 2000}, + {"INFINITE_AMMO", false}, + {"INFINITE_ORDINANCE", false}, // Note: don't set inf fuel or inf EC as those are used during autotuning and are handled differently in order to sync with the cheats menu. + {"MAX_ARMOR_LIMIT", 10}, + {"MAX_PWING_LIFT", 4.54}, + {"MAX_SAS_TORQUE", 30}, + {"OUT_OF_AMMO_KILL_TIME", 60}, + {"PWING_EDGE_LIFT", true}, + {"PWING_THICKNESS_AFFECT_MASS_HP", true}, + {"VESSEL_SPAWN_FILL_SEATS", 1}, + {"VESSEL_SPAWN_RANDOM_ORDER", true}, + {"VESSEL_SPAWN_REASSIGN_TEAMS", true}, + }}, + {17, new(){}}, + {33, new(){}}, + // Round specific overrides (also overrides global settings). + {42, new(){ // Fly the Unfriendly Skies + {"VESSEL_SPAWN_FILL_SEATS", 3}, + }}, + {44, new(){}}, + {46, new(){ // Vertigo to Jool + {"NO_ENGINES", true}, + }}, + {50, new(){ // Mach-ing Bird + {"WAYPOINTS_MODE", true}, + }}, + {53, new(){}}, + {55, new(){ // Boonta Eve Classic + {"WAYPOINTS_MODE", true}, + }}, + {60, new(){ // Legacy of the Void + {"SPACE_HACKS", true}, + {"SF_FRICTION", true}, + {"SF_GRAVITY", true}, + {"SF_DRAGMULT", 30}, + {"MAX_SAS_TORQUE", 10}, + }}, + {61, new(){ // Gun-Game + {"MUTATOR_MODE", true}, + {"MUTATOR_DURATION", 0}, + {"MUTATOR_APPLY_KILL", true}, + {"MUTATOR_APPLY_GUNGAME", true}, + {"MUTATOR_APPLY_GLOBAL", false}, + {"MUTATOR_APPLY_TIMER", false}, + {"MUTATOR_APPLY_NUM", 1}, + {"MUTATOR_ICONS", true}, + {"GG_PERSISTANT_PROGRESSION", false}, + {"GG_CYCLE_LIST", false}, + {"MUTATOR_LIST", new List{ "Brownings", "Chainguns", "Vulcans", "Mausers", "GAU-22s", "N-37s", "AT Guns", "Railguns", "GAU-8s", "Rockets" }}, + }}, + {62, new(){ // Retrofuturistic on Eve + {"ASTEROID_FIELD", true}, + {"ASTEROID_FIELD_ALTITUDE", 2000}, + {"ASTEROID_FIELD_ANOMALOUS_ATTRACTION", true}, + {"ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH", 0.5}, + {"ASTEROID_FIELD_NUMBER", 200}, + {"ASTEROID_FIELD_RADIUS", 7}, + {"VESSEL_SPAWN_CS_FOLLOWS_CENTROID", false}, + {"VESSEL_SPAWN_WORLDINDEX", 5}, // Eve + {"VESSEL_SPAWN_GEOCOORDS", new Vector2d(33.3616, -67.2242)}, // Poison Pond + {"VESSEL_SPAWN_ALTITUDE", 2500}, + }}, + {64, new(){ + {"WAYPOINTS_MODE" , true}, + {"VESSEL_SPAWN_ALTITUDE" , 2500}, + {"WAYPOINTS_ONE_AT_A_TIME" , false}, + {"WAYPOINT_GUARD_INDEX" , 0}, + {"COMPETITION_WAYPOINTS_GM_KILL_PERIOD" , 0}, + {"WAYPOINT_COURSE_INDEX" , 8}, + {"VESSEL_SPAWN_DISTANCE" , 300}, + {"COMPETITION_KILL_TIMER" , 10}, + {"COMPETITION_DURATION" , 5}, + }}, + {65, new(){ + {"MUTATOR_MODE", true}, + {"MUTATOR_DURATION", 0}, + {"MUTATOR_APPLY_GLOBAL", true}, + {"MUTATOR_APPLY_NUM", 1}, + {"MUTATOR_ICONS", false}, + {"MUTATOR_LIST", new List{ "Vengeance" }}, + {"VENGEANCE_DELAY", 3}, + {"VENGEANCE_YIELD", 1.5}, + }}, + {66, new(){ + {"OUT_OF_AMMO_KILL_TIME", 0}, + }}, + {67, new(){ + {"VESSEL_SPAWN_ALTITUDE", 3}, + {"VESSEL_SPAWN_DISTANCE", 300}, + {"VESSEL_SPAWN_DISTANCE_TOGGLE", true}, + {"PINATA_NAME", "DeadlySpacePotato"}, + {"MAX_ACTIVE_RADAR_RANGE", 700000}, + {"TOURNAMENT_VESSELS_PER_HEAT", 6}, + {"TOURNAMENT_NPCS_PER_HEAT", 1}, + }}, + {68, new(){ + {"G_TOLERANCE", 8}, + {"G_LIMITS", true}, + {"KERB_GLIMIT", true}, + {"PART_GLIMIT", true}, + }}, + {69, new(){ + {"WAYPOINTS_MODE", true}, + {"VESSEL_SPAWN_ALTITUDE", 5}, + {"WAYPOINTS_ONE_AT_A_TIME", false}, + {"WAYPOINT_GUARD_INDEX", 6}, + {"WAYPOINT_COURSE_INDEX", 8}, + {"VESSEL_SPAWN_DISTANCE", 45}, + {"COMPETITION_KILL_TIMER", 10}, + {"COMPETITION_DURATION", 5}, + {"VESSEL_SPAWN_GEOCOORDS", new Vector2d(33.911, -172.7)}, + }}, + {70,new(){ + {"ASTEROID_FIELD", true}, + {"ASTEROID_FIELD_ALTITUDE", 50000}, + {"ASTEROID_FIELD_ANOMALOUS_ATTRACTION", true}, + {"ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH", 0.05}, + {"ASTEROID_FIELD_NUMBER", 250}, + {"ASTEROID_FIELD_RADIUS", 8}, + {"COMPETITION_DISTANCE", 1000}, + {"COMPETITION_DURATION", 10}, + {"MAX_SAS_TORQUE", 9999}, + {"VESSEL_SPAWN_ALTITUDE", 50000}, + {"VESSEL_SPAWN_DISTANCE", 8500}, + {"VESSEL_RELATIVE_BULLET_CHECKS", true}, + {"MAX_ARMOR_LIMIT", 50}, + }}, + {72,new(){ + {"MAX_PWING_LIFT", 2}, + {"GRAVITY_HACKS", true}, + }}, + {73,new(){ + {"WAYPOINT_COURSE_INDEX", 1}, + {"WAYPOINTS_MODE", true}, + {"WAYPOINTS_ALTITUDE", 300}, + {"COMPETITION_KILL_TIMER", 5}, + {"VESSEL_SPAWN_ALTITUDE", 1000}, + {"WAYPOINTS_ONE_AT_A_TIME", false}, + {"COMPETITION_WAYPOINTS_GM_KILL_PERIOD", 60}, + {"COMPETITION_GM_KILL_TIME", 5}, + {"COMPETITION_GM_KILL_ENGINE", true}, + }}, + {74,new(){ + }}, + {77,new(){ + {"VESSEL_SPAWN_ALTITUDE", 5}, + {"GUARD_MODE_TRIGGER_ALT", 2000}, + }}, + {78, new(){ + {"VESSEL_SPAWN_ALTITUDE", 5 }, + {"VESSEL_SPAWN_GEOCOORDS", new Vector2d(45.6, -137.3)}, + {"VESSEL_SPAWN_WORLDINDEX", 1 } + }} + }; + public static Dictionary RWPRoundToIndex = new() { { 0, 0 } }, RWPIndexToRound = new() { { 0, 0 } }; // Helpers for the UI slider. + static readonly HashSet currentFilter = []; + + public static void SetRWP(bool enabled, int round) + { + if (enabled) + { + if (RWPEnabled) RestoreSettings(); // Revert to the original settings if we've been modifying things. + SetRWPFilters(); // Figure out what we're going to change and tag those settings. + StoreSettings(); // Store the original values of the settings that we're going to change. + SetOverrides(0); // Set global RWP settings. + if (round > 0) SetOverrides(round); // Set the round-specific settings. + } + else if (!enabled && RWPEnabled) // Disabling RWP. + { + RestoreSettings(); // Restore the non-RWP settings. + } + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) SyncWithGameSettings(); // Enable our overrides (if active). + RWPEnabled = enabled; + if (BDArmorySetup.Instance != null) BDArmorySetup.Instance.UpdateSelectedMutators(); // Update the mutators selected in the UI. + } + + static Dictionary storedSettings = []; // The base stored settings. + static Dictionary tempSettings = []; // Temporary settings for shuffling things around. + + /// + /// Store the settings that RWP is going to change. + /// + public static void StoreSettings(bool temp = false) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Storing {(temp ? "temporary" : "base")} settings."); + var settings = temp ? tempSettings : storedSettings; + settings.Clear(); + + var fields = typeof(BDArmorySettings).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var field in fields) + { + if (!currentFilter.Contains(field.Name)) + { + //Debug.Log($"[BDArmory.RWPSettings]: {field.Name} is non-RWP setting, skipping..."); + continue; + } + + settings.Add(field.Name, field.GetValue(null)); + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Stored settings: " + string.Join(", ", settings.Select(kvp => $"{kvp.Key}={kvp.Value}"))); + } + + /// + /// Restore the non-RWP settings. + /// + public static void RestoreSettings(bool temp = false) + { + var settings = temp ? tempSettings : storedSettings; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Restoring {(temp ? "temporary" : "base")} settings."); + foreach (var setting in settings.Keys) + { + var field = typeof(BDArmorySettings).GetField(setting, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + if (field == null) continue; + field.SetValue(null, settings[setting]); + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Restored settings: " + string.Join(", ", settings.Select(kvp => $"{kvp.Key}={kvp.Value}"))); + } + + /// + /// set whether or not a BDAsetting should be filtered by the store/restore setting + /// + public static void SetRWPFilters() + { + if (!RWPOverrides.ContainsKey(0)) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Setting RWP setting filters"); + if (!RWPOverrides.ContainsKey(BDArmorySettings.RUNWAY_PROJECT_ROUND)) BDArmorySettings.RUNWAY_PROJECT_ROUND = 0; // Sanity check the round number. + currentFilter.Clear(); + var fields = typeof(BDArmorySettings).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var field in fields) + { + if (field == null) continue; + if (!field.IsDefined(typeof(BDAPersistentSettingsField), false)) continue; + if (RWPOverrides[0].Keys.Contains(field.Name) || RWPOverrides[BDArmorySettings.RUNWAY_PROJECT_ROUND].Keys.Contains(field.Name)) + currentFilter.Add(field.Name); + } + } + + /// + /// Override settings based on the RWP overrides. + /// + /// The RWP round number. + public static void SetOverrides(int round) + { + if (!RWPOverrides.ContainsKey(round)) return; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Setting overrides for RWP round {round}"); + var overrides = RWPOverrides[round]; + foreach (var setting in overrides.Keys) + { + var field = typeof(BDArmorySettings).GetField(setting, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + if (field == null) + { + Debug.LogWarning($"[BDArmory.RWPSettings]: Invalid field name {setting} for RWP round {round}."); + continue; + } + try + { + field.SetValue(null, Convert.ChangeType(overrides[setting], field.FieldType)); // Convert the type to the correct type (e.g., double vs float) so unboxing works correctly. + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.RWPSettings]: Failed to set value {overrides[setting]} for {setting}: {e.Message}"); + } + } + + // Add any additional round-specific setup here. + // Note: Anything called from here has to be capable of being called during BDArmorySetup's Awake function. + switch (round) + { + case 61: + GameModes.MutatorInfo.SetupGunGame(); + break; + } + + // Update NumericInputFields for spawn values. + if (VesselSpawnerWindow.Instance != null && (overrides.ContainsKey("VESSEL_SPAWN_ALTITUDE") || overrides.ContainsKey("VESSEL_SPAWN_GEOCOORDS"))) VesselSpawnerWindow.Instance.RefreshSpawnFieldsFromSettings(); + // Update competition distance text field. + if (BDArmorySetup.Instance != null && overrides.ContainsKey("COMPETITION_DISTANCE")) BDArmorySetup.Instance.compDistGui = BDArmorySettings.COMPETITION_DISTANCE.ToString(); + } + + /// + /// Synchronise any settings between BDA and KSP that need syncing. + /// Note: This needs to be called after Awake (e.g., in Start) otherwise game-loading on scene initialisation overwrites these changes. + /// + public static void SyncWithGameSettings(bool toKSP = true, bool restoreOverrides = false) + { + if (HighLogic.CurrentGame != null) + { + var advancedParams = HighLogic.CurrentGame.Parameters.CustomParams(); + if (toKSP) + { + if (BDArmorySettings.G_LIMITS && !restoreOverrides) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Adjusting G-limits: part limits: {advancedParams.GPartLimits} -> {BDArmorySettings.PART_GLIMIT}, kerbal limits: {advancedParams.GKerbalLimits} -> {BDArmorySettings.KERB_GLIMIT}, tolerance: {advancedParams.KerbalGToleranceMult * 20.5f:0.0}g -> {BDArmorySettings.G_TOLERANCE:0.0}g"); + advancedParams.GPartLimits = BDArmorySettings.PART_GLIMIT; + advancedParams.GKerbalLimits = BDArmorySettings.KERB_GLIMIT; + advancedParams.KerbalGToleranceMult = BDArmorySettings.G_TOLERANCE / 20.5f; //Default 0.5 Courage BadS Pilot kerb has a GLimit of 20.5 + } + else if (restoreOverrides) // Restore the override values and save the game (due to changing scenes since KSP reloads these from the save file). + { + if (BDArmorySettings.G_LIMITS) // If G-limit was active and settings were changed in the Game Difficulty, but not synced to BDA due to the menu not being open, sync them now. + { + if (BDArmorySettings.PART_GLIMIT != (BDArmorySettings.PART_GLIMIT = advancedParams.GPartLimits)) BDArmorySettings._PART_GLIMIT = BDArmorySettings.PART_GLIMIT; + if (BDArmorySettings.KERB_GLIMIT != (BDArmorySettings.KERB_GLIMIT = advancedParams.GKerbalLimits)) BDArmorySettings._KERB_GLIMIT = BDArmorySettings.KERB_GLIMIT; + if (BDArmorySettings.G_TOLERANCE != (BDArmorySettings.G_TOLERANCE = BDAMath.RoundToUnit(advancedParams.KerbalGToleranceMult * 20.5f, 0.5f))) BDArmorySettings._G_TOLERANCE = BDArmorySettings.G_TOLERANCE; + BDArmorySetup.SaveConfig(); // We need to update the saved settings for these to be kept. + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Reverting to Game Difficulty G-limits for saving game state or scene change: part limits: {BDArmorySettings._PART_GLIMIT}, kerbal limits: {BDArmorySettings._KERB_GLIMIT}, tolerance: {BDArmorySettings._G_TOLERANCE:0.0}g"); + advancedParams.GPartLimits = BDArmorySettings._PART_GLIMIT; + advancedParams.GKerbalLimits = BDArmorySettings._KERB_GLIMIT; + advancedParams.KerbalGToleranceMult = BDArmorySettings._G_TOLERANCE / 20.5f; + } + else // G-Limits was disabled, revert to the last seen values from the user adjusting the Game Difficulty menu + { + advancedParams.GPartLimits = BDArmorySettings._PART_GLIMIT; + advancedParams.GKerbalLimits = BDArmorySettings._KERB_GLIMIT; + advancedParams.KerbalGToleranceMult = BDArmorySettings._G_TOLERANCE / 20.5f; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Reverting to Game Difficulty G-limits: part limits: {BDArmorySettings._PART_GLIMIT}, kerbal limits: {BDArmorySettings._KERB_GLIMIT}, tolerance: {BDArmorySettings._G_TOLERANCE:0.0}g"); + } + } + else + { + BDArmorySettings._PART_GLIMIT = advancedParams.GPartLimits; + BDArmorySettings._KERB_GLIMIT = advancedParams.GKerbalLimits; + BDArmorySettings._G_TOLERANCE = BDAMath.RoundToUnit(advancedParams.KerbalGToleranceMult * 20.5f, 0.5f); //Default 0.5 Courage BadS Pilot kerb has a GLimit of 20.5 + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.RWPSettings]: Updating BDA's Game Difficulty backing values: part limits: {BDArmorySettings._PART_GLIMIT}, kerbal limits: {BDArmorySettings._KERB_GLIMIT}, tolerance: {BDArmorySettings._G_TOLERANCE:0.0}g"); + } + } + } + + /// + /// Save RWP settings to file. + /// + /// Currently, this will save the RWPOverrides, but we don't yet have a way to update the RWPOverrides in-game. + /// For now, just manually edit the RWP_settings.cfg and reload the settings. + /// + public static void Save() + { + ConfigNode fileNode = ConfigNode.Load(RWPSettingsPath) ?? new ConfigNode(); + + if (!fileNode.HasNode("RWPSettings")) + { + fileNode.AddNode("RWPSettings"); + } + + var settingsNode = fileNode.GetNode("RWPSettings"); + foreach (var round in RWPOverrides.Keys) + { + if (!settingsNode.HasNode(round.ToString())) settingsNode.AddNode(round.ToString()); + var roundNode = settingsNode.GetNode(round.ToString()); + foreach (var fieldName in RWPOverrides[round].Keys) + { + var field = typeof(BDArmorySettings).GetField(fieldName, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + if (field == null) + { + Debug.LogWarning($"[BDArmory.RWPSettings]: Invalid field name {fieldName} for RWP round {round}."); + continue; + } + var fieldValue = RWPOverrides[round][fieldName]; + try + { + if (fieldValue.GetType() == typeof(Vector3d)) + { + roundNode.SetValue(field.Name, ((Vector3d)fieldValue).ToString("G"), true); + } + else if (fieldValue.GetType() == typeof(Vector2d)) + { + roundNode.SetValue(field.Name, ((Vector2d)fieldValue).ToString("G"), true); + } + else if (fieldValue.GetType() == typeof(Vector2)) + { + roundNode.SetValue(field.Name, ((Vector2)fieldValue).ToString("G"), true); + } + else if (fieldValue.GetType() == typeof(List)) + { + roundNode.SetValue(field.Name, string.Join("; ", (List)fieldValue), true); + } + else + { + roundNode.SetValue(field.Name, fieldValue.ToString(), true); + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.RWPSettings]: Exception triggered while trying to save field {fieldName} with value {fieldValue}: {e.Message}"); + } + } + } + fileNode.Save(RWPSettingsPath); + } + + /// + /// Load RWP settings from file. + /// + public static void Load() + { + // Set up the RWP round indices in case no settings get loaded. + var sortedRoundNumbers = RWPOverrides.Keys.ToList(); sortedRoundNumbers.Sort(); + RWPRoundToIndex = sortedRoundNumbers.ToDictionary(r => r, sortedRoundNumbers.IndexOf); + RWPIndexToRound = sortedRoundNumbers.ToDictionary(sortedRoundNumbers.IndexOf, r => r); + + if (!File.Exists(RWPSettingsPath)) return; + ConfigNode fileNode = ConfigNode.Load(RWPSettingsPath); + if (!fileNode.HasNode("RWPSettings")) return; + + ConfigNode settings = fileNode.GetNode("RWPSettings"); + + foreach (var roundNode in settings.GetNodes()) + { + int round = int.Parse(roundNode.name); + if (!RWPOverrides.ContainsKey(round)) RWPOverrides.Add(round, []); // Merge defaults with those from the file. + foreach (ConfigNode.Value fieldNode in roundNode.values) + { + var field = typeof(BDArmorySettings).GetField(fieldNode.name, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + if (field == null) + { + Debug.LogError($"[BDArmory.RWPSettings]: Unknown field {fieldNode.name} when loading RWP settings."); + continue; + } + var fieldValue = BDAPersistentSettingsField.ParseValue(field.FieldType, fieldNode.value, fieldNode.name); + RWPOverrides[round][fieldNode.name] = fieldValue; // Add or set the override. + } + } + + // Set up the RWP round indices. + sortedRoundNumbers = RWPOverrides.Keys.ToList(); sortedRoundNumbers.Sort(); + RWPRoundToIndex = sortedRoundNumbers.ToDictionary(r => r, sortedRoundNumbers.IndexOf); + RWPIndexToRound = sortedRoundNumbers.ToDictionary(sortedRoundNumbers.IndexOf, r => r); + + // Set the loaded RWP settings. + SetRWP(BDArmorySettings.RUNWAY_PROJECT, BDArmorySettings.RUNWAY_PROJECT_ROUND); + } + } +} \ No newline at end of file diff --git a/BDArmory/Shaders/BDAShaderLoader.cs b/BDArmory/Shaders/BDAShaderLoader.cs index c180b04d1..cec02d5ae 100644 --- a/BDArmory/Shaders/BDAShaderLoader.cs +++ b/BDArmory/Shaders/BDAShaderLoader.cs @@ -33,7 +33,7 @@ public string BundlePath case RuntimePlatform.LinuxPlayer: return _bundlePath + Path.DirectorySeparatorChar + - "bdarmoryshaders_macosx.bundle"; + "bdarmoryshaders_linux.bundle"; default: return _bundlePath + Path.DirectorySeparatorChar + @@ -44,16 +44,14 @@ public string BundlePath private void Awake() { - _bundlePath = KSPUtil.ApplicationRootPath + "GameData" + - Path.DirectorySeparatorChar + - "BDArmory" + Path.DirectorySeparatorChar + "AssetBundles"; + _bundlePath = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "AssetBundles")); } private void Start() { if (!_loaded) { - Debug.Log("[BDArmory] start bundle load process"); + Debug.Log("[BDArmory.BDAShaderLoader] start bundle load process"); //StartCoroutine(LoadBundleAssets()); LoadBundleAssets(); _loaded = true; @@ -62,7 +60,7 @@ private void Start() private void LoadBundleAssets() { - Debug.Log("[BDArmory] Loading bundle data"); + Debug.Log("[BDArmory.BDAShaderLoader] Loading bundle data"); AssetBundle shaderBundle = AssetBundle.LoadFromFile(BundlePath); @@ -73,7 +71,7 @@ private void LoadBundleAssets() while (shader.MoveNext()) { if (shader.Current == null) continue; - Debug.Log($"[BDArmory] Shader \"{shader.Current.name}\" loaded. Shader supported? {shader.Current.isSupported}"); + Debug.Log($"[BDArmory.BDAShaderLoader] Shader \"{shader.Current.name}\" loaded. Shader supported? {shader.Current.isSupported}"); switch (shader.Current.name) { @@ -94,19 +92,19 @@ private void LoadBundleAssets() break; default: - Debug.Log($"[BDArmory] Not expected shader : {shader.Current.name}"); + Debug.Log($"[BDArmory.BDAShaderLoader] Not expected shader : {shader.Current.name}"); break; } } shader.Dispose(); //yield return null; - Debug.Log("[BDArmory] unloading bundles"); + Debug.Log("[BDArmory.BDAShaderLoader] unloading bundles"); shaderBundle.Unload(false); // unload the raw asset bundle } else { - Debug.Log("[BDArmory] Error: Found no asset bundle to load"); + Debug.Log("[BDArmory.BDAShaderLoader] Error: Found no asset bundle to load"); } //yield return null; diff --git a/BDArmory/Shaders/rcsShader.shader b/BDArmory/Shaders/rcsShader.shader index 469113df1..5db304bf8 100644 --- a/BDArmory/Shaders/rcsShader.shader +++ b/BDArmory/Shaders/rcsShader.shader @@ -1,4 +1,4 @@ -// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' +// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Custom/rcsShader" { @@ -47,19 +47,18 @@ Shader "Custom/rcsShader" Interpolators i; i.position = UnityObjectToClipPos(v.position); i.normal = UnityObjectToWorldNormal(v.normal); - //i.uv = TRANSFORM_TEX(v.uv, _MainTex); - return i; + return i; } // Main Fragment Program float4 RCSFragmentShader(Interpolators i) : SV_TARGET { - float3 lightDir = _WorldSpaceLightPos0.xyz; - float3 lightColor = _RCSCOLOR.rgb; - float3 reflectionDir = reflect(-_LIGHTDIR, i.normal); - return DotClamped(_LIGHTDIR, reflectionDir); + float3 reflectionDir = DotClamped(_LIGHTDIR, reflect(-_LIGHTDIR, i.normal)); + half returnStr = max(0, reflectionDir); + float4 finalreturn = returnStr * _RCSCOLOR; //actually make it use _RCSCOLOR + return finalreturn; } ENDCG } //PASS } //SUBSHADER -} +} \ No newline at end of file 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 67% rename from BDArmory/Modules/ModuleTargetingCamera.cs rename to BDArmory/Targeting/ModuleTargetingCamera.cs index 728e2bde8..48dd14ccb 100644 --- a/BDArmory/Modules/ModuleTargetingCamera.cs +++ b/BDArmory/Targeting/ModuleTargetingCamera.cs @@ -1,16 +1,20 @@ using System.Collections; -using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; +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; +using System.Text; +using System; -namespace BDArmory.Modules +namespace BDArmory.Targeting { public class ModuleTargetingCamera : PartModule { @@ -29,12 +33,18 @@ public class ModuleTargetingCamera : PartModule public float gimbalLimit = 120; public bool gimbalLimitReached; + [KSPField] + public float traverseRate = 90; + [KSPField] public bool rollCameraModel = false; [KSPField(isPersistant = true)] public bool cameraEnabled; + [KSPField] + public bool colorCamera = false; + float fov { get @@ -46,6 +56,7 @@ float fov [KSPField] public string zoomFOVs = "40,15,3,1"; float[] zoomFovs; + float[] zoomTimes; [KSPField(isPersistant = true)] public int currentFovIndex; @@ -57,7 +68,7 @@ float fov public bool CoMLock; public bool radarLock; - + public Vessel lockedVessel; [KSPField(isPersistant = true)] public bool groundStabilized; @@ -65,7 +76,7 @@ float fov /// /// Point on surface that camera is focused and stabilized on. /// - public Vector3 groundTargetPosition; + public Vector3 groundTargetPosition = Vector3.zero; [KSPField(isPersistant = true)] public double savedLat; @@ -111,23 +122,17 @@ public Vector3 bodyRelativeGTP private static float adjCamImageSize = 360; internal static bool ResizingWindow; internal static bool SlewingMouseCam; - internal static bool ZoomKeysSet; - internal static bool isZooming; - internal static bool wasZooming; internal static bool SlewingButtonCam; float finalSlewSpeed; Vector2 slewInput = Vector2.zero; + public static bool IsSlewing => SlewingMouseCam; private static float gap = 2; private static float buttonHeight = 18; private static float controlsStartY = 22; private static float windowWidth = adjCamImageSize + (3 * buttonHeight) + 16 + 2 * gap; private static float windowHeight = adjCamImageSize + 23; - private AxisBinding_Single ZoomKeyP; - private AxisBinding_Single ZoomKeyS; - private AxisBinding_Single NoZoomKeyP; - private AxisBinding_Single NoZoomKeyS; Texture2D riTex; @@ -136,9 +141,7 @@ Texture2D rollIndicatorTexture get { if (!riTex) - { - riTex = GameDatabase.Instance.GetTexture("BDArmory/Textures/rollIndicator", false); - } + { riTex = GameDatabase.Instance.GetTexture("BDArmory/Textures/rollIndicator", false); } return riTex; } } @@ -150,34 +153,21 @@ Texture2D rollReferenceTexture get { if (!rrTex) - { - rrTex = GameDatabase.Instance.GetTexture("BDArmory/Textures/rollReference", false); - } + { rrTex = GameDatabase.Instance.GetTexture("BDArmory/Textures/rollReference", false); } return rrTex; } } - private MissileFire wpmr; - - public MissileFire weaponManager + public MissileFire WeaponManager { get { - if (wpmr == null || wpmr.vessel != vessel) - { - wpmr = null; - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current) - wpmr = mf.Current; - } - mf.Dispose(); - } - - return wpmr; + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; } } + private MissileFire _weaponManager; [KSPEvent(guiName = "#LOC_BDArmory_Enable", guiActive = true, guiActiveEditor = false)]//Enable public void EnableButton() @@ -207,7 +197,7 @@ public void EnableCamera() { if (!TargetingCamera.Instance) { - Debug.Log("Tried to enable targeting camera, but camera instance is null."); + Debug.Log("[BDArmory.ModuleTargetingCamera]: Tried to enable targeting camera, but camera instance is null."); return; } if (vessel.isActiveVessel) @@ -215,6 +205,7 @@ public void EnableCamera() activeCam = this; windowIsOpen = true; TargetingCamera.Instance.EnableCamera(cameraParentTransform); + TargetingCamera.Instance.color = colorCamera; TargetingCamera.Instance.nvMode = nvMode; TargetingCamera.Instance.SetFOV(fov); ResizeTargetWindow(); @@ -222,6 +213,7 @@ public void EnableCamera() cameraEnabled = true; + var weaponManager = WeaponManager; if (weaponManager) { weaponManager.mainTGP = this; @@ -245,7 +237,7 @@ public void DisableCamera() { if (!TargetingCamera.Instance) { - Debug.Log("Tried to disable targeting camera, but camera instance is null."); + Debug.Log("[BDArmory.ModuleTargetingCamera]: Tried to disable targeting camera, but camera instance is null."); return; } @@ -265,6 +257,7 @@ public void DisableCamera() } BDATargetManager.ActiveLasers.Remove(this); + var weaponManager = WeaponManager; if (weaponManager && weaponManager.mainTGP == this) { weaponManager.mainTGP = FindNextActiveCamera(); @@ -273,7 +266,7 @@ public void DisableCamera() ModuleTargetingCamera FindNextActiveCamera() { - using (List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator()) + using (var mtc = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) while (mtc.MoveNext()) { if (mtc.Current && mtc.Current.cameraEnabled) @@ -294,7 +287,7 @@ public override void OnAwake() { if (!TargetingCamera.Instance) { - (new GameObject("TargetingCameraObject")).AddComponent(); + new GameObject("TargetingCameraObject").AddComponent(); } } } @@ -302,17 +295,13 @@ public override void OnAwake() public override void OnStart(StartState state) { base.OnStart(state); - ZoomKeyP = GameSettings.AXIS_MOUSEWHEEL.primary; - ZoomKeyS = GameSettings.AXIS_MOUSEWHEEL.secondary; - NoZoomKeyP = new AxisBinding_Single(); - NoZoomKeyS = new AxisBinding_Single(); if (HighLogic.LoadedSceneIsFlight) { //GUI setup if (!camRectInitialized) { - BDArmorySetup.WindowRectTargetingCam = new Rect(Screen.width - windowWidth, Screen.height - windowHeight, windowWidth, windowHeight); + BDArmorySetup.WindowRectTargetingCam = new Rect(BDArmorySetup.WindowRectTargetingCam.x, BDArmorySetup.WindowRectTargetingCam.y, windowWidth, windowHeight); camRectInitialized = true; } @@ -327,14 +316,16 @@ public override void OnStart(StartState state) if (cameraEnabled) { - Debug.Log("[BDArmory]: saved gtp: " + bodyRelativeGTP); + Debug.Log("[BDArmory.ModuleTargetingCamera]: saved gtp: " + bodyRelativeGTP); DelayedEnable(); } + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.BetterLateThanNever, CameraTracking); } } void Disconnect(Vessel v) { + var weaponManager = WeaponManager; if (weaponManager && vessel) { if (weaponManager.vessel != vessel) @@ -342,6 +333,8 @@ void Disconnect(Vessel v) if (slaveTurrets) { weaponManager.slavingTurrets = false; + weaponManager.slavedPosition = Vector3.zero; + weaponManager.slavedTarget = TargetSignatureData.noTarget; } } } @@ -360,8 +353,8 @@ IEnumerator DelayedEnableRoutine() delayedEnabling = true; Vector3d savedGTP = bodyRelativeGTP; - Debug.Log("[BDArmory]: saved gtp: " + Misc.Misc.FormattedGeoPos(savedGTP, true)); - Debug.Log("[BDArmory]: groundStabilized: " + groundStabilized); + Debug.Log("[BDArmory.ModuleTargetingCamera]: saved gtp: " + BodyUtils.FormattedGeoPos(savedGTP, true)); + Debug.Log("[BDArmory.ModuleTargetingCamera]: groundStabilized: " + groundStabilized); while (TargetingCamera.Instance == null) { @@ -395,7 +388,7 @@ IEnumerator DelayedEnableRoutine() EnableCamera(); if (groundStabilized) { - Debug.Log("[BDArmory]: Camera delayed enabled"); + Debug.Log("[BDArmory.ModuleTargetingCamera]: Camera delayed enabled"); groundTargetPosition = VectorUtils.GetWorldSurfacePostion(savedGTP, vessel.mainBody);// vessel.mainBody.GetWorldSurfacePosition(bodyRelativeGTP.x, bodyRelativeGTP.y, bodyRelativeGTP.z); Vector3 lookVector = groundTargetPosition - cameraParentTransform.position; PointCameraModel(lookVector); @@ -403,7 +396,7 @@ IEnumerator DelayedEnableRoutine() } delayedEnabling = false; - Debug.Log("[BDArmory]: post load saved gtp: " + bodyRelativeGTP); + Debug.Log("[BDArmory.ModuleTargetingCamera]: post load saved gtp: " + bodyRelativeGTP); } void PointCameraModel(Vector3 lookVector) @@ -418,80 +411,47 @@ void PointCameraModel(Vector3 lookVector) Vector3 camUp = cameraParentTransform.up; if (eyeHolderTransform) camUp = Vector3.Cross(cameraParentTransform.forward, eyeHolderTransform.right); cameraParentTransform.rotation = Quaternion.LookRotation(lookVector, camUp); - if (vessel.isActiveVessel && activeCam == this && TargetingCamera.cameraTransform) - { - TargetingCamera.cameraTransform.rotation = Quaternion.LookRotation(cameraParentTransform.forward, worldUp); - } } } - void Update() + void UpdateCameraDirection() { - if (HighLogic.LoadedSceneIsFlight) + Vector3 worldUp = VectorUtils.GetUpDirection(cameraParentTransform.position); + if (vessel.isActiveVessel && activeCam == this && TargetingCamera.cameraTransform) { - if (cameraEnabled && TargetingCamera.ReadyForUse && vessel.IsControllable) - { - if (delayedEnabling) return; - - if (!TargetingCamera.Instance || FlightGlobals.currentMainBody == null) - { - return; - } - - if (activeCam == this) - { - if (zoomFovs != null) - { - TargetingCamera.Instance.SetFOV(fov); - } - } - - if (radarLock) - { - UpdateRadarLock(); - } - - if (groundStabilized) - { - groundTargetPosition = VectorUtils.GetWorldSurfacePostion(bodyRelativeGTP, vessel.mainBody);//vessel.mainBody.GetWorldSurfacePosition(bodyRelativeGTP.x, bodyRelativeGTP.y, bodyRelativeGTP.z); - Vector3 lookVector = groundTargetPosition - cameraParentTransform.position; - //cameraParentTransform.rotation = Quaternion.LookRotation(lookVector); - PointCameraModel(lookVector); - } + TargetingCamera.cameraTransform.rotation = Quaternion.LookRotation(cameraParentTransform.forward, worldUp); + } + } - Vector3 lookDirection = cameraParentTransform.forward; - if (Vector3.Angle(lookDirection, cameraParentTransform.parent.forward) > gimbalLimit) - { - lookDirection = Vector3.RotateTowards(cameraParentTransform.transform.parent.forward, lookDirection, gimbalLimit * Mathf.Deg2Rad, 0); - gimbalLimitReached = true; - } - else - { - gimbalLimitReached = false; - } + void Update() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (cameraEnabled && TargetingCamera.ReadyForUse && vessel.IsControllable) + { + if (delayedEnabling) return; - if (!groundStabilized || gimbalLimitReached) - { - PointCameraModel(lookDirection); - } + if (!TargetingCamera.Instance || FlightGlobals.currentMainBody == null) + { + return; + } - if (eyeHolderTransform) + if (activeCam == this) + { + if (zoomFovs != null) { - Vector3 projectedForward = Vector3.ProjectOnPlane(cameraParentTransform.forward, eyeHolderTransform.parent.up); - if (projectedForward != Vector3.zero) - { - eyeHolderTransform.rotation = Quaternion.LookRotation(projectedForward, eyeHolderTransform.parent.up); - } + TargetingCamera.Instance.SetFOV(fov); } - UpdateControls(); - UpdateSlaveData(); + UpdateCameraDirection(); } + + CameraTracking(); } } public override void OnFixedUpdate() { + base.OnFixedUpdate(); if (HighLogic.LoadedSceneIsFlight) { if (cameraEnabled && !vessel.packed && !vessel.IsControllable) @@ -504,13 +464,71 @@ public override void OnFixedUpdate() void FixedUpdate() { if (HighLogic.LoadedSceneIsFlight) + { + if (cameraEnabled && !vessel.packed) + { + if (!vessel.IsControllable) + { + DisableCamera(); + } + if (delayedEnabling) return; + } + } + } + + void CameraTracking() + { + if (cameraEnabled && TargetingCamera.ReadyForUse && vessel.IsControllable) { if (delayedEnabling) return; + if (!TargetingCamera.Instance || FlightGlobals.currentMainBody == null) return; - if (cameraEnabled) + if (radarLock) + { + UpdateRadarLock(); + } + + if (groundStabilized && !slewingToPosition) { - GetHitPoint(); + if (lockedVessel != null) + groundTargetPosition = lockedVessel.CoM; + else + groundTargetPosition = VectorUtils.GetWorldSurfacePostion(bodyRelativeGTP, vessel.mainBody);//vessel.mainBody.GetWorldSurfacePosition(bodyRelativeGTP.x, bodyRelativeGTP.y, bodyRelativeGTP.z); + + Vector3 lookVector = groundTargetPosition - cameraParentTransform.position; + //cameraParentTransform.rotation = Quaternion.LookRotation(lookVector); + PointCameraModel(lookVector); + } + + Vector3 lookDirection = cameraParentTransform.forward; + if (VectorUtils.Angle(lookDirection, cameraParentTransform.parent.forward) > gimbalLimit) + { + lookDirection = Vector3.RotateTowards(cameraParentTransform.transform.parent.forward, lookDirection, gimbalLimit * Mathf.Deg2Rad, 0); + gimbalLimitReached = true; + lockedVessel = null; + } + else + { + gimbalLimitReached = false; + } + + if (!groundStabilized || gimbalLimitReached) + { + PointCameraModel(lookDirection); + } + + if (eyeHolderTransform) + { + Vector3 projectedForward = cameraParentTransform.forward.ProjectOnPlanePreNormalized(eyeHolderTransform.parent.up); + if (projectedForward != Vector3.zero) + { + eyeHolderTransform.rotation = Quaternion.LookRotation(projectedForward, eyeHolderTransform.parent.up); + } } + + UpdateControls(); + UpdateSlaveData(); + GetHitPoint(); } } @@ -573,6 +591,7 @@ void UpdateKeyInputs() if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TGP_COM)) { CoMLock = !CoMLock; + if (!CoMLock) lockedVessel = null; } if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TGP_RADAR)) @@ -624,7 +643,7 @@ void UpdateSlewRate() { if (SlewingButtonCam) { - finalSlewSpeed = Mathf.Clamp(finalSlewSpeed + (0.5f * (fov / 60)), 0, 80 * fov / 60); + finalSlewSpeed = Mathf.Clamp(finalSlewSpeed + (0.5f * (fov / traverseRate)), 0, 80 * fov / traverseRate); SlewingButtonCam = false; } else @@ -635,14 +654,22 @@ void UpdateSlewRate() void UpdateRadarLock() { + // If CoMLock disable radar lock + if (CoMLock && lockedVessel) + { + radarLock = false; + return; + } + + var weaponManager = WeaponManager; if (weaponManager && weaponManager.vesselRadarData && weaponManager.vesselRadarData.locked) { RadarDisplayData tgt = weaponManager.vesselRadarData.lockedTargetData; - Vector3 radarTargetPos = tgt.targetData.predictedPosition; + Vector3 radarTargetPos = tgt.targetData.predictedPositionWithChaffFactor(tgt.detectedByRadar.radarChaffClutterFactor); Vector3 targetDirection = radarTargetPos - cameraParentTransform.position; //Quaternion lookRotation = Quaternion.LookRotation(radarTargetPos-cameraParentTransform.position, VectorUtils.GetUpDirection(cameraParentTransform.position)); - if (Vector3.Angle(radarTargetPos - cameraParentTransform.position, cameraParentTransform.forward) < 0.5f) + if (VectorUtils.Angle(radarTargetPos - cameraParentTransform.position, cameraParentTransform.forward) < 0.5f) { //cameraParentTransform.rotation = lookRotation; if (tgt.vessel) @@ -659,7 +686,7 @@ void UpdateRadarLock() ClearTarget(); } //lookRotation = Quaternion.RotateTowards(cameraParentTransform.rotation, lookRotation, 120*Time.fixedDeltaTime); - Vector3 rotateTwdDirection = Vector3.RotateTowards(cameraParentTransform.forward, targetDirection, 1200 * Time.fixedDeltaTime * Mathf.Deg2Rad, 0); + Vector3 rotateTwdDirection = Vector3.RotateTowards(cameraParentTransform.forward, targetDirection, traverseRate * Time.fixedDeltaTime * Mathf.Deg2Rad, 0); PointCameraModel(rotateTwdDirection); } } @@ -677,18 +704,6 @@ void OnGUI() if (SlewingMouseCam) SlewingMouseCam = false; } - if (!wasZooming && isZooming) - { - wasZooming = true; - SetZoomKeys(); - } - - if (!isZooming && wasZooming) - { - wasZooming = false; - ResetZoomKeys(); - } - if (HighLogic.LoadedSceneIsFlight && !MapView.MapIsEnabled && BDArmorySetup.GAME_UI_ENABLED && !delayedEnabling) { if (cameraEnabled && vessel.isActiveVessel && FlightGlobals.ready) @@ -696,35 +711,37 @@ void OnGUI() //window if (activeCam == this && TargetingCamera.ReadyForUse) { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectTargetingCam.position); 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); + GUI.Label(new Rect(600, 1000, 200, 30), $"Slew rate: {finalSlewSpeed:G3}"); + GUI.Label(new Rect(600, 950, 200, 30), $"ComLock: {(CoMLock ? lockedVessel != null ? lockedVessel.GetName() : "null" : "false")}"); } - 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.white); } } } @@ -740,8 +757,9 @@ void WindowTargetCam(int windowID) } windowIsOpen = true; + var guiMatrix = GUI.matrix; - GUI.DragWindow(new Rect(0, 0, BDArmorySetup.WindowRectTargetingCam.width - 18, 30)); + GUI.DragWindow(new Rect(0, 0, BDArmorySetup.WindowRectTargetingCam.width - 18, controlsStartY)); if (GUI.Button(new Rect(BDArmorySetup.WindowRectTargetingCam.width - 18, 2, 16, 16), "X", GUI.skin.button)) { DisableCamera(); @@ -768,25 +786,16 @@ void WindowTargetCam(int windowID) } if (Event.current.type == EventType.Repaint && SlewingMouseCam) { - if (Mouse.delta.x != 0 && Mouse.delta.y != 0) + if (Mouse.delta.x != 0 || Mouse.delta.y != 0) { SlewRoutine(Mouse.delta); } } - if (Event.current.type == EventType.Repaint && imageRect.Contains(Event.current.mousePosition)) - { - if (!wasZooming) isZooming = true; - } - if (Event.current.type == EventType.ScrollWheel && imageRect.Contains(Event.current.mousePosition)) { ZoomRoutine(Input.mouseScrollDelta); } - if (Event.current.type == EventType.Repaint && !imageRect.Contains(Event.current.mousePosition)) - { - if (wasZooming) isZooming = false; - } float indicatorSize = Mathf.Clamp(64 * (adjCamImageSize / camImageSize), 48, 128); float indicatorBorder = imageRect.width * 0.056f; @@ -795,8 +804,10 @@ void WindowTargetCam(int windowID) //horizon indicator float horizY = imageRect.y + imageRect.height - indicatorSize - indicatorBorder; - Vector3 hForward = Vector3.ProjectOnPlane(vesForward, upDirection); - float hAngle = -Misc.Misc.SignedAngle(hForward, vesForward, upDirection); + Vector3 hForward = vesForward.ProjectOnPlanePreNormalized(upDirection); + // Unfortunately, because `vesForward` and `upDirection` are not perpendicular to + // each other the more efficient `GetAngleOnPlane` cannot be used! + float hAngle = -VectorUtils.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); @@ -805,22 +816,22 @@ 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 = -Misc.Misc.SignedAngle(-Vector3.forward, localUp, Vector3.right); - GUIUtility.RotateAroundPivot(rollAngle, rollRect.center); + localUp = localUp.ProjectOnPlanePreNormalized(Vector3.up).normalized; + float rollAngle = -VectorUtils.GetAngleOnPlane(localUp, -Vector3.forward, Vector3.right); + GUIUtility.RotateAroundPivot(rollAngle, guiMatrix * rollRect.center); GUI.DrawTexture(rollRect, rollIndicatorTexture, ScaleMode.StretchToFill, true); - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; //target direction indicator - float angleToTarget = Misc.Misc.SignedAngle(hForward, Vector3.ProjectOnPlane(targetPointPosition - transform.position, upDirection), Vector3.Cross(upDirection, hForward)); - GUIUtility.RotateAroundPivot(angleToTarget, rollRect.center); + float angleToTarget = VectorUtils.GetAngleOnPlane((targetPointPosition - transform.position), hForward, Vector3.Cross(upDirection, hForward)); + GUIUtility.RotateAroundPivot(angleToTarget, guiMatrix * rollRect.center); GUI.DrawTexture(rollRect, BDArmorySetup.Instance.targetDirectionTexture, ScaleMode.StretchToFill, true); - GUI.matrix = Matrix4x4.identity; + GUI.matrix = guiMatrix; //resizing Rect resizeRect = new Rect(BDArmorySetup.WindowRectTargetingCam.width - 18, BDArmorySetup.WindowRectTargetingCam.height - 18, 16, 16); - GUI.DrawTexture(resizeRect, Misc.Misc.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; @@ -830,22 +841,12 @@ void WindowTargetCam(int windowID) { if (Mouse.delta.x != 0 || Mouse.delta.y != 0) { - float diff = Mouse.delta.x + Mouse.delta.y; - UpdateTargetScale(diff); + float diff = (Mathf.Abs(Mouse.delta.x) > Mathf.Abs(Mouse.delta.y) ? Mouse.delta.x : Mouse.delta.y) / BDArmorySettings.UI_SCALE_ACTUAL; + BDArmorySettings.TARGET_WINDOW_SCALE = Mathf.Clamp(BDArmorySettings.TARGET_WINDOW_SCALE + diff / camImageSize, BDArmorySettings.TARGET_WINDOW_SCALE_MIN, BDArmorySettings.TARGET_WINDOW_SCALE_MAX); ResizeTargetWindow(); } } - //ResetZoomKeys(); - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectTargetingCam); - } - - internal static void UpdateTargetScale(float diff) - { - float scaleDiff = ((diff / (BDArmorySetup.WindowRectTargetingCam.width + BDArmorySetup.WindowRectTargetingCam.height)) * 100 * .01f); - BDArmorySettings.TARGET_WINDOW_SCALE += Mathf.Abs(scaleDiff) > .01f ? scaleDiff : scaleDiff > 0 ? .01f : -.01f; - BDArmorySettings.TARGET_WINDOW_SCALE = Mathf.Clamp(BDArmorySettings.TARGET_WINDOW_SCALE, - BDArmorySettings.TARGET_WINDOW_SCALE_MIN, - BDArmorySettings.TARGET_WINDOW_SCALE_MAX); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectTargetingCam); } private void DrawSlewButtons() @@ -860,36 +861,42 @@ private void DrawSlewButtons() Rect slewRightRect = new Rect(slewStartX + (2 * buttonHeight) + (gap * 2), slewStartY + ((buttonHeight + gap) / 2), buttonHeight, buttonHeight); if (GUI.RepeatButton(slewUpRect, "^", GUI.skin.button)) { - //SlewCamera(Vector3.up); - slewInput.y = 1; + SlewCamera(Vector3.up); // OnGUI occurs after LateUpdate, so we can't set slewInput and expect it to work for the current frame. + // slewInput.y = 1; } if (GUI.RepeatButton(slewDownRect, "v", GUI.skin.button)) { - //SlewCamera(Vector3.down); - slewInput.y = -1; + SlewCamera(Vector3.down); + // slewInput.y = -1; } if (GUI.RepeatButton(slewLeftRect, "<", GUI.skin.button)) { - //SlewCamera(Vector3.left); - slewInput.x = -1; + SlewCamera(Vector3.left); + // slewInput.x = -1; } if (GUI.RepeatButton(slewRightRect, ">", GUI.skin.button)) { - //SlewCamera(Vector3.right); - slewInput.x = 1; + SlewCamera(Vector3.right); + // slewInput.x = 1; } } private void DrawZoomButtons() { - float zoomStartX = adjCamImageSize * 0.94f - (buttonHeight * 3) - (4 * gap); + float zoomGap = 9f * gap; + if (zoomTimes[currentFovIndex] >= 100f) + zoomGap += 10f * gap; + else if (zoomTimes[currentFovIndex] >= 10f) + zoomGap += 5f * gap; + + float zoomStartX = adjCamImageSize * 0.94f - (buttonHeight * 3) - zoomGap; float zoomStartY = 20 + (adjCamImageSize * 0.06f); Rect zoomOutRect = new Rect(zoomStartX, zoomStartY, buttonHeight, buttonHeight); - Rect zoomInfoRect = new Rect(zoomStartX + buttonHeight + gap, zoomStartY, buttonHeight + 4 * gap, buttonHeight); - Rect zoomInRect = new Rect(zoomStartX + buttonHeight * 2 + 5 * gap, zoomStartY, buttonHeight, buttonHeight); + Rect zoomInfoRect = new Rect(zoomStartX + buttonHeight + gap, zoomStartY, buttonHeight + zoomGap, buttonHeight); + Rect zoomInRect = new Rect(zoomStartX + buttonHeight * 2f + (zoomGap + 2f * gap), zoomStartY, buttonHeight, buttonHeight); GUI.enabled = currentFovIndex > 0; if (GUI.Button(zoomOutRect, "-", GUI.skin.button)) @@ -901,7 +908,7 @@ private void DrawZoomButtons() zoomBox.alignment = TextAnchor.UpperCenter; zoomBox.padding.top = 0; GUI.enabled = true; - GUI.Label(zoomInfoRect, (currentFovIndex + 1).ToString() + "X", zoomBox); + GUI.Label(zoomInfoRect, $"{zoomTimes[currentFovIndex]:F1} X", zoomBox); GUI.enabled = currentFovIndex < zoomFovs.Length - 1; if (GUI.Button(zoomInRect, "+", GUI.skin.button)) @@ -919,17 +926,19 @@ private void DrawSideControlButtons(Rect imageRect) GUIStyle buttonStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); buttonStyle.fontSize = 11; - float line = buttonHeight + gap; + int line = 0; + float lineHeight = buttonHeight + gap; float buttonWidth = 3 * buttonHeight + 4 * gap; //groundStablize button float startX = imageRect.width + 3 * gap; - Rect stabilizeRect = new Rect(startX, controlsStartY, buttonWidth, buttonHeight + line); + Rect stabilizeRect = new Rect(startX, controlsStartY, buttonWidth, buttonHeight + lineHeight); if (!groundStabilized) { if (GUI.Button(stabilizeRect, "Lock\nTarget", buttonStyle)) { GroundStabilize(); } + ++line; //stabilizerect is two lines tall, so account for that for later incrementing of linecount } else { @@ -939,9 +948,10 @@ private void DrawSideControlButtons(Rect imageRect) ClearTarget(); } + var weaponManager = WeaponManager; if (weaponManager) { - Rect sendGPSRect = new Rect(startX, controlsStartY + line, buttonWidth, buttonHeight); + Rect sendGPSRect = new Rect(startX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); if (GUI.Button(sendGPSRect, "Send GPS", buttonStyle)) { SendGPS(); @@ -960,7 +970,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 = Misc.Misc.FormattedGeoPos(bodyRelativeGTP, false); + string geoLabel = BodyUtils.FormattedGeoPos(bodyRelativeGTP, false); GUI.Label(geoRect, geoLabel, dataStyle); //target data @@ -979,10 +989,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) @@ -990,8 +1000,9 @@ private void DrawSideControlButtons(Rect imageRect) if (weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Missile) { MissileBase currMissile = weaponManager.CurrentMissile; - if (currMissile.TargetingMode == MissileBase.TargetingModes.Gps || - currMissile.TargetingMode == MissileBase.TargetingModes.Laser) + if (currMissile && + (currMissile.TargetingMode == MissileBase.TargetingModes.Gps || + currMissile.TargetingMode == MissileBase.TargetingModes.Laser)) { MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(currMissile, Vector3.zero, groundTargetPosition); @@ -1007,29 +1018,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(); @@ -1037,7 +1048,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); } } } @@ -1052,14 +1063,14 @@ private void DrawSideControlButtons(Rect imageRect) } //reset button - Rect resetRect = new Rect(startX, controlsStartY + (2 * line), buttonWidth, buttonHeight); + Rect resetRect = new Rect(startX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); if (GUI.Button(resetRect, "Reset", buttonStyle)) { ResetCameraButton(); } //CoM lock - Rect comLockRect = new Rect(startX, controlsStartY + 3 * line, buttonWidth, buttonHeight); + Rect comLockRect = new Rect(startX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); GUIStyle comStyle = new GUIStyle(CoMLock ? BDArmorySetup.BDGuiSkin.box : buttonStyle); comStyle.fontSize = 10; comStyle.wordWrap = false; @@ -1069,7 +1080,7 @@ private void DrawSideControlButtons(Rect imageRect) } //radar slave - Rect radarSlaveRect = new Rect(startX, controlsStartY + 4 * line, buttonWidth, buttonHeight); + Rect radarSlaveRect = new Rect(startX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); GUIStyle radarSlaveStyle = radarLock ? BDArmorySetup.BDGuiSkin.box : buttonStyle; if (GUI.Button(radarSlaveRect, "Radar", radarSlaveStyle)) { @@ -1077,7 +1088,7 @@ private void DrawSideControlButtons(Rect imageRect) } //slave turrets button - Rect slaveRect = new Rect(startX, controlsStartY + 5 * line, buttonWidth, buttonHeight); + Rect slaveRect = new Rect(startX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); if (!slaveTurrets) { if (GUI.Button(slaveRect, "Turrets", buttonStyle)) @@ -1094,7 +1105,7 @@ private void DrawSideControlButtons(Rect imageRect) } //point to gps button - Rect toGpsRect = new Rect(startX, controlsStartY + 6 * line, buttonWidth, buttonHeight); + Rect toGpsRect = new Rect(startX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); if (GUI.Button(toGpsRect, "To GPS", buttonStyle)) { PointToGPSTarget(); @@ -1102,25 +1113,39 @@ private void DrawSideControlButtons(Rect imageRect) //nv button float nvStartX = startX; - Rect nvRect = new Rect(nvStartX, controlsStartY + 7 * line, buttonWidth, buttonHeight); + Rect nvRect = new Rect(nvStartX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight); string nvLabel = nvMode ? "NV Off" : "NV On"; GUIStyle nvStyle = nvMode ? BDArmorySetup.BDGuiSkin.box : buttonStyle; if (GUI.Button(nvRect, nvLabel, nvStyle)) { ToggleNV(); } + + if (BDArmorySettings.DEBUG_RADAR) // Debug what the various cameras show in the targeting window. + { + ++line; + if (GUI.Button(new Rect(nvStartX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight), $"Near", TargetingCamera.Instance.CamEnabled[0] ? BDArmorySetup.BDGuiSkin.box : buttonStyle)) + TargetingCamera.Instance.CamEnabled[0] = !TargetingCamera.Instance.CamEnabled[0]; + if (GUI.Button(new Rect(nvStartX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight), $"Far", TargetingCamera.Instance.CamEnabled[1] ? BDArmorySetup.BDGuiSkin.box : buttonStyle)) + TargetingCamera.Instance.CamEnabled[1] = !TargetingCamera.Instance.CamEnabled[1]; + if (GUI.Button(new Rect(nvStartX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight), $"Sky", TargetingCamera.Instance.CamEnabled[2] ? BDArmorySetup.BDGuiSkin.box : buttonStyle)) + TargetingCamera.Instance.CamEnabled[2] = !TargetingCamera.Instance.CamEnabled[2]; + if (GUI.Button(new Rect(nvStartX, controlsStartY + ++line * lineHeight, buttonWidth, buttonHeight), $"Galaxy", TargetingCamera.Instance.CamEnabled[3] ? BDArmorySetup.BDGuiSkin.box : buttonStyle)) + TargetingCamera.Instance.CamEnabled[3] = !TargetingCamera.Instance.CamEnabled[3]; + } } void ResetCameraButton() { if (!resetting) { - StartCoroutine("ResetCamera"); + resetCamera = StartCoroutine(ResetCamera()); } } void SendGPS() { + var weaponManager = WeaponManager; if (groundStabilized && weaponManager) { BDATargetManager.GPSTargetList(weaponManager.Team).Add(new GPSTargetInfo(bodyRelativeGTP, "Target")); @@ -1130,12 +1155,12 @@ void SendGPS() void SlaveTurrets() { - List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mtc.MoveNext()) - { - mtc.Current.slaveTurrets = false; - } - mtc.Dispose(); + var weaponManager = WeaponManager; + using (var mtc = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mtc.MoveNext()) + { + mtc.Current.slaveTurrets = false; + } if (weaponManager && weaponManager.vesselRadarData) { @@ -1147,12 +1172,12 @@ void SlaveTurrets() void UnslaveTurrets() { - List.Enumerator mtc = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mtc.MoveNext()) - { - mtc.Current.slaveTurrets = false; - } - mtc.Dispose(); + var weaponManager = WeaponManager; + using (var mtc = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mtc.MoveNext()) + { + mtc.Current.slaveTurrets = false; + } if (weaponManager && weaponManager.vesselRadarData) { @@ -1162,14 +1187,17 @@ void UnslaveTurrets() if (weaponManager) { weaponManager.slavingTurrets = false; + weaponManager.slavedPosition = Vector3.zero; + weaponManager.slavedTarget = TargetSignatureData.noTarget; //reset and null these so hitting the slave target button on a weapon later doesn't lock it to a legacy position/target } } void UpdateSlaveData() { if (!slaveTurrets) return; + var weaponManager = WeaponManager; if (!weaponManager) return; - weaponManager.slavingTurrets = true; + if (weaponManager.slavingTurrets) return; //turrets already slaved to active radar lock weaponManager.slavedPosition = groundStabilized ? groundTargetPosition : targetPointPosition; weaponManager.slavedVelocity = Vector3.zero; weaponManager.slavedAcceleration = Vector3.zero; @@ -1191,14 +1219,14 @@ void SlewCamera(Vector3 direction) IEnumerator SlewMouseCamRoutine(Vector3 direction) { radarLock = false; - //invert the x axis. makes the mouse action more intutitve - direction.x = -direction.x; - //direction.y = -direction.y; - float velocity = Mathf.Abs(direction.x) > Mathf.Abs(direction.y) ? Mathf.Abs(direction.x) : Mathf.Abs(direction.y); + lockedVessel = null; + if (!BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_X) direction.x = -direction.x; // Invert the x-axis by default (original defaults). + if (BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_Y) direction.y = -direction.y; Vector3 rotationAxis = Matrix4x4.TRS(Vector3.zero, Quaternion.LookRotation(cameraParentTransform.forward, vessel.upAxis), Vector3.one) .MultiplyVector(Quaternion.AngleAxis(90, Vector3.forward) * direction); + float velocity = Mathf.Max(Mathf.Abs(direction.x), Mathf.Abs(direction.y)) + 0.1f * direction.sqrMagnitude; float angle = velocity / (1 + currentFovIndex) * Time.deltaTime; - if (angle / (1f + currentFovIndex) < .05f / (1f + currentFovIndex)) angle = .05f / ((1f + currentFovIndex) / 2f); + if (angle / (1f + currentFovIndex) < .01f / (1f + currentFovIndex)) angle = .01f / ((1f + currentFovIndex) / 2f); Vector3 lookVector = Quaternion.AngleAxis(angle, rotationAxis) * cameraParentTransform.forward; PointCameraModel(lookVector); @@ -1216,7 +1244,7 @@ IEnumerator SlewCamRoutine(Vector3 direction) { StopResetting(); StopPointToPosRoutine(); - + lockedVessel = null; radarLock = false; float slewRate = finalSlewSpeed; Vector3 rotationAxis = Matrix4x4.TRS(Vector3.zero, Quaternion.LookRotation(cameraParentTransform.forward, vessel.upAxis), Vector3.one).MultiplyVector(Quaternion.AngleAxis(90, Vector3.forward) * direction); @@ -1235,26 +1263,13 @@ IEnumerator SlewCamRoutine(Vector3 direction) void PointToGPSTarget() { + var weaponManager = WeaponManager; if (weaponManager && weaponManager.designatedGPSCoords != Vector3d.zero) { StartCoroutine(PointToPositionRoutine(VectorUtils.GetWorldSurfacePostion(weaponManager.designatedGPSCoords, vessel.mainBody))); } } - private void ResetZoomKeys() - { - ZoomKeysSet = false; - GameSettings.AXIS_MOUSEWHEEL.primary = ZoomKeyP; - GameSettings.AXIS_MOUSEWHEEL.secondary = ZoomKeyS; - } - - private void SetZoomKeys() - { - ZoomKeysSet = true; - GameSettings.AXIS_MOUSEWHEEL.primary = NoZoomKeyP; - GameSettings.AXIS_MOUSEWHEEL.secondary = NoZoomKeyS; - } - private void SlewRoutine(Vector2 direction) { if (SlewingMouseCam) @@ -1292,50 +1307,54 @@ void ZoomOut() //fov = zoomFovs[currentFovIndex]; } - GameObject debugSphere; - - void CreateDebugSphere() - { - debugSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); - debugSphere.GetComponent().enabled = false; - } - - void MoveDebugSphere() - { - if (!debugSphere) - { - CreateDebugSphere(); - } - debugSphere.transform.position = groundTargetPosition; - } - - void GroundStabilize() + public void GroundStabilize() { if (vessel.packed) return; StopResetting(); RaycastHit rayHit; - Ray ray = new Ray(cameraParentTransform.position + (50 * cameraParentTransform.forward), cameraParentTransform.forward); - bool raycasted = Physics.Raycast(ray, out rayHit, maxRayDistance - 50, 9076737); + Ray ray = new Ray(cameraParentTransform.position, cameraParentTransform.forward); + bool raycasted = Physics.Raycast(ray, out rayHit, maxRayDistance, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels)); if (raycasted) { - if (FlightGlobals.getAltitudeAtPos(rayHit.point) < 0) + Part p; + if (FlightGlobals.getAltitudeAtPos(rayHit.point) < 0 || ((p = rayHit.collider.GetComponentInParent()) && p.vessel == vessel)) { raycasted = false; } else { + KerbalEVA hitEVA = rayHit.collider.gameObject.GetComponentUpwards(); + if (hitEVA) + p = hitEVA.part; + + bool pCheck = false; + + if (p && p.vessel) + { + TargetInfo pInfo; + if (p.vessel != lockedVessel && (pInfo = vessel.gameObject.GetComponent()) != null && pInfo.isMissile && pInfo.MissileBaseModule.FiredByWM == WeaponManager) + { + 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()); + lockedVessel = p.vessel; + //StartCoroutine(PointToPositionRoutine(p.vessel.CoM, p.vessel, false)); + } + else + { + lockedVessel = null; } } Vector3d newGTP = VectorUtils.WorldPositionToGeoCoords(groundTargetPosition, vessel.mainBody); @@ -1368,11 +1387,6 @@ void GroundStabilize() } } } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - MoveDebugSphere(); - } } IEnumerator StabilizeNextFrame() @@ -1391,46 +1405,59 @@ void GetHitPoint() if (delayedEnabling) return; RaycastHit rayHit; - Ray ray = new Ray(cameraParentTransform.position + (50 * cameraParentTransform.forward), cameraParentTransform.forward); - if (Physics.Raycast(ray, out rayHit, maxRayDistance - 50, 9076737)) + Ray ray = new Ray(cameraParentTransform.position, cameraParentTransform.forward); + if (!Physics.Raycast(ray, out rayHit, maxRayDistance, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels))) { - targetPointPosition = rayHit.point; + targetPointPosition = cameraParentTransform.position + (maxRayDistance * cameraParentTransform.forward); + surfaceDetected = false; + return; + } - if (!surfaceDetected && groundStabilized && !gimbalLimitReached) - { - groundStabilized = true; - groundTargetPosition = rayHit.point; + Part p = rayHit.collider.GetComponentInParent(); + TargetInfo pInfo; + if (p && p.vessel == vessel || (p.vessel != lockedVessel && (pInfo = vessel.gameObject.GetComponent()) != null && pInfo.isMissile && pInfo.MissileBaseModule.FiredByWM == WeaponManager)) + { + targetPointPosition = cameraParentTransform.position + (maxRayDistance * cameraParentTransform.forward); + surfaceDetected = false; + return; + } - if (CoMLock) + targetPointPosition = rayHit.point; + if (!surfaceDetected && groundStabilized && !gimbalLimitReached) + { + groundStabilized = true; + groundTargetPosition = rayHit.point; + + if (CoMLock) + { + KerbalEVA hitEVA = rayHit.collider.gameObject.GetComponentUpwards(); + if (hitEVA) + p = hitEVA.part; + if (p && p.vessel) { - KerbalEVA hitEVA = rayHit.collider.gameObject.GetComponentUpwards(); - Part p = hitEVA ? hitEVA.part : rayHit.collider.GetComponentInParent(); - if (p && p.vessel && p.vessel.Landed) - { - groundTargetPosition = p.vessel.CoM; - } + groundTargetPosition = p.vessel.CoM; + lockedVessel = p.vessel; } - Vector3d newGTP = VectorUtils.WorldPositionToGeoCoords(groundTargetPosition, vessel.mainBody); - if (newGTP != Vector3d.zero) + else { - bodyRelativeGTP = newGTP; + lockedVessel = null; } } - - surfaceDetected = true; - - if (groundStabilized && !gimbalLimitReached && CMDropper.smokePool != null) + Vector3d newGTP = VectorUtils.WorldPositionToGeoCoords(groundTargetPosition, vessel.mainBody); + if (newGTP != Vector3d.zero) { - if (CMSmoke.RaycastSmoke(ray)) - { - surfaceDetected = false; - } + bodyRelativeGTP = newGTP; } } - else + + surfaceDetected = true; + + if (groundStabilized && !gimbalLimitReached && CMDropper.smokePool != null) { - targetPointPosition = cameraParentTransform.position + (maxRayDistance * cameraParentTransform.forward); - surfaceDetected = false; + if (CMSmoke.RaycastSmoke(ray)) + { + surfaceDetected = false; + } } } @@ -1439,6 +1466,7 @@ void ClearTarget() groundStabilized = false; } + Coroutine resetCamera; IEnumerator ResetCamera() { resetting = true; @@ -1453,9 +1481,9 @@ IEnumerator ResetCamera() currentFovIndex = 0; //fov = zoomFovs[currentFovIndex]; - while (Vector3.Angle(cameraParentTransform.forward, cameraParentTransform.parent.forward) > 0.1f) + while (VectorUtils.Angle(cameraParentTransform.forward, cameraParentTransform.parent.forward) > 0.1f) { - Vector3 newForward = Vector3.RotateTowards(cameraParentTransform.forward, cameraParentTransform.parent.forward, 60 * Mathf.Deg2Rad * Time.deltaTime, 0); + Vector3 newForward = Vector3.RotateTowards(cameraParentTransform.forward, cameraParentTransform.parent.forward, (2 / 3) * traverseRate * Mathf.Deg2Rad * Time.deltaTime, 0); //cameraParentTransform.rotation = Quaternion.LookRotation(newForward, VectorUtils.GetUpDirection(transform.position)); PointCameraModel(newForward); gimbalLimitReached = false; @@ -1483,24 +1511,46 @@ IEnumerator StopPTPRRoutine() bool stopPTPR; bool slewingToPosition; - public IEnumerator PointToPositionRoutine(Vector3 position) + public IEnumerator PointToPositionRoutine(Vector3 position, Vessel tgtVessel = null, bool clearTgt = true) { yield return StopPTPRRoutine(); stopPTPR = false; slewingToPosition = true; radarLock = false; StopResetting(); - ClearTarget(); - while (!stopPTPR && Vector3.Angle(cameraParentTransform.transform.forward, position - (cameraParentTransform.transform.position)) > 0.1f) + if (clearTgt) ClearTarget(); + if (cameraParentTransform == null) + { + slewingToPosition = false; + yield break; + } + var wait = new WaitForFixedUpdate(); + Vector3 cameraPos; + Vector3 cameraForward; + while (!stopPTPR && VectorUtils.Angle((cameraForward = cameraParentTransform.transform.forward), (tgtVessel != null ? tgtVessel.CoM : position) - (cameraPos = cameraParentTransform.transform.position)) > 0.1f) { - Vector3 newForward = Vector3.RotateTowards(cameraParentTransform.transform.forward, position - cameraParentTransform.transform.position, 90 * Mathf.Deg2Rad * Time.fixedDeltaTime, 0); + if (tgtVessel != null) + { + position = tgtVessel.CoM + tgtVessel.Velocity() * Time.fixedDeltaTime; + if ((tgtVessel.CoM - cameraPos).sqrMagnitude < maxRayDistance * maxRayDistance) + lockedVessel = tgtVessel; + else + lockedVessel = null; + } + else lockedVessel = null; + Vector3 newForward = Vector3.RotateTowards(cameraForward, position - cameraPos, traverseRate * Mathf.Deg2Rad * Time.fixedDeltaTime, 0); //cameraParentTransform.rotation = Quaternion.LookRotation(newForward, VectorUtils.GetUpDirection(transform.position)); PointCameraModel(newForward); - yield return new WaitForFixedUpdate(); + yield return wait; + if (cameraParentTransform == null) + { + slewingToPosition = false; + yield break; + } if (gimbalLimitReached) { ClearTarget(); - StartCoroutine("ResetCamera"); + resetCamera = StartCoroutine(ResetCamera()); slewingToPosition = false; yield break; } @@ -1508,25 +1558,30 @@ public IEnumerator PointToPositionRoutine(Vector3 position) if (surfaceDetected && !stopPTPR) { //cameraParentTransform.transform.rotation = Quaternion.LookRotation(position - cameraParentTransform.position, VectorUtils.GetUpDirection(transform.position)); - PointCameraModel(position - cameraParentTransform.position); + //PointCameraModel(position - cameraParentTransform.position); GroundStabilize(); } slewingToPosition = false; - yield break; } void StopResetting() { if (resetting) { - StopCoroutine("ResetCamera"); + StopCoroutine(resetCamera); resetting = false; } } void ParseFovs() { - zoomFovs = Misc.Misc.ParseToFloatArray(zoomFOVs); + zoomFovs = OtherUtils.ParseToFloatArray(zoomFOVs); + zoomTimes = new float[zoomFovs.Length]; + zoomTimes[0] = 1f; + // Just doing it based on relative size of objects + float tanoneXzoom = Mathf.Tan(Mathf.Deg2Rad * zoomFovs[0]); + for (int i = 1; i < zoomFovs.Length; i++) + zoomTimes[i] = tanoneXzoom / Mathf.Tan(Mathf.Deg2Rad * zoomFovs[i]); } void OnDestroy() @@ -1534,23 +1589,27 @@ void OnDestroy() if (HighLogic.LoadedSceneIsFlight) { windowIsOpen = false; - if (wpmr) + var weaponManager = WeaponManager; + if (weaponManager) { if (slaveTurrets) { - weaponManager.slavingTurrets = false; + weaponManager.slavingTurrets = false; //this should already be false... + weaponManager.slavedPosition = Vector3.zero; + weaponManager.slavedTarget = TargetSignatureData.noTarget; } } - - GameEvents.onVesselCreate.Remove(Disconnect); } + GameEvents.onVesselCreate.Remove(Disconnect); + SlewingMouseCam = false; + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.BetterLateThanNever, CameraTracking); } 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; @@ -1565,5 +1624,16 @@ Vector2 TargetAzimuthElevationScreenPos(Rect screenRect, Vector3 targetPosition, return new Vector2(x, y); } + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + + output.Append(Environment.NewLine); + output.AppendLine($"Targeting Camera:"); + output.AppendLine($"- Slew rate: {traverseRate}Deg./s"); + output.AppendLine($"- Max traverse: {gimbalLimit} degrees"); + output.AppendLine($"- Max range: {maxRayDistance} m"); + return output.ToString(); + } } } 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 79% rename from BDArmory/Parts/TGPCameraEffects.cs rename to BDArmory/Targeting/TGPCameraEffects.cs index 8c1ce05ee..e7f1e6f52 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 { @@ -17,14 +18,14 @@ void Awake() { grayscaleMaterial = new Material(BDAShaderLoader.GrayscaleEffectShader); grayscaleMaterial.SetTexture("_RampTex", textureRamp); - grayscaleMaterial.SetFloat("_RedPower", rampOffset); + grayscaleMaterial.SetFloat("_RedPower", 4); grayscaleMaterial.SetFloat("_RedDelta", rampOffset); } } void OnRenderImage(RenderTexture source, RenderTexture destination) { - if (BDArmorySettings.BW_TARGET_CAM || TargetingCamera.Instance.nvMode) + if (!TargetingCamera.Instance.color || TargetingCamera.Instance.nvMode) { Graphics.Blit(source, destination, grayscaleMaterial); //apply grayscale } diff --git a/BDArmory/Targeting/TargetInfo.cs b/BDArmory/Targeting/TargetInfo.cs index 8ef2ca5e6..cb27e4843 100644 --- a/BDArmory/Targeting/TargetInfo.cs +++ b/BDArmory/Targeting/TargetInfo.cs @@ -1,11 +1,18 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.UI; +using System.Linq; 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 @@ -13,16 +20,43 @@ public class TargetInfo : MonoBehaviour public BDTeam Team; public bool isMissile; public MissileBase MissileBaseModule; - public MissileFire weaponManager; - Dictionary> friendliesEngaging = new Dictionary>(); - public Dictionary detectedTime = new Dictionary(); + public MissileFire WeaponManager // Using a non-static target WM avoids continuing to target debris that has separated from the WM. + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = (vessel != null && vessel.loaded) ? vessel.ActiveController().WM : null; + if (_weaponManager != null && _weaponManager.vessel != vessel) _weaponManager = null; + return _weaponManager; + } + } + public MissileFire _weaponManager; + + Dictionary> friendliesEngaging = []; + public Dictionary detected = []; + public Dictionary detectedTime = []; public float radarBaseSignature = -1; public bool radarBaseSignatureNeedsUpdate = true; + public float[,] radarSignatureMatrix = null; + public bool radarSignatureMatrixNeedsUpdate = 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 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 { @@ -75,8 +109,7 @@ public bool isSplashed { if (!vessel) return false; if (vessel.situation == Vessel.Situations.SPLASHED) return true; - else - return false; + return false; } } @@ -93,7 +126,7 @@ public Vector3 position { get { - return vessel.vesselTransform.position; + return vessel.CoM; } } @@ -124,11 +157,13 @@ public bool isThreat { return true; } - else if (weaponManager && weaponManager.vessel.isCommandable) //Fix for GLOC'd pilots. IsControllable merely checks if plane has pilot; Iscommandable checks if they're conscious + else if (WeaponManager) { - return true; + return WeaponManager.vessel.isCommandable; //isn't debris / has command part + //return weaponManager.vessel.IsControllable; //vessel has probecore & EC/pilot && pilot is conscious + //enable this if you want exceedingly honorable pilots who hold fire if their target has GLOC'ed themselves + // GLOC'ed craft now go neutral stick, so they no longer get locked in a perma-stun deathloop } - return false; } } @@ -145,13 +180,16 @@ public bool isThreat { return false; } - else if (weaponManager && weaponManager.debilitated) + else if (WeaponManager && WeaponManager.debilitated) { return true; } return false; } } + + public List<(string, float)> debugTargetPriorities = []; // Debug info for target priorities. + void Awake() { if (!vessel) @@ -175,39 +213,22 @@ void Awake() return; } } - // IEnumerator otherInfo = vessel.gameObject.GetComponents().GetEnumerator(); - // while (otherInfo.MoveNext()) - // { - // if ((object)otherInfo.Current != this) - // { - // Destroy(this); - // return; - // } - // } Team = null; - bool foundMf = false; - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) + var mf = vessel.ActiveController().WM; + if (mf != null) { - foundMf = true; - Team = mf.Current.Team; - weaponManager = mf.Current; - break; + Team = mf.Team; // While the primary WM may change, the Team shouldn't. } - mf.Dispose(); - - if (!foundMf) + else if (vessel.IsMissile()) { - List.Enumerator ml = vessel.FindPartModulesImplementing().GetEnumerator(); - while (ml.MoveNext()) + var ml = VesselModuleRegistry.GetMissileBase(vessel, true); + if (ml != null) { isMissile = true; - MissileBaseModule = ml.Current; - Team = ml.Current.Team; - break; + MissileBaseModule = ml; + Team = ml.Team; } - ml.Dispose(); } vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; @@ -222,6 +243,8 @@ void Awake() GameEvents.onVesselPartCountChanged.Add(VesselModified); //massRoutine = StartCoroutine(MassRoutine()); // TODO: CHECK BEHAVIOUR AND SIDE EFFECTS! } + targetPartListNeedsUpdating = true; + GameEvents.onVesselDestroy.Add(CleanFriendliesEngaging); } void OnPeaceEnabled() @@ -233,26 +256,38 @@ 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); } IEnumerator UpdateRCSDelayed() { - alreadyScheduledRCSUpdate = true; - yield return new WaitForSeconds(1.0f); - //radarBaseSignatureNeedsUpdate = true; //TODO: currently disabled to reduce stuttering effects due to more demanding radar rendering! + if (radarMassAtUpdate > 0) + { + float massPercentageDifference = (radarMassAtUpdate - vessel.GetTotalMass()) / radarMassAtUpdate; + var weaponManager = WeaponManager; + 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 + radarSignatureMatrixNeedsUpdate = true; + 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) + if (vessel == null) { AboutToBeDestroyed(); } else { - if ((vessel.vesselType == VesselType.Debris) && (weaponManager == null)) + if (vessel.vesselType == VesselType.Debris && WeaponManager == null) { BDATargetManager.RemoveTarget(this); Team = null; @@ -260,6 +295,48 @@ void Update() } } + public void UpdateTargetPartList() + { + 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; + + 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).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) + { + var toRemove = friendliesEngaging.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key).ToList(); + foreach (var key in toRemove) + { friendliesEngaging.Remove(key); } + } public int NumFriendliesEngaging(BDTeam team) { if (friendliesEngaging.TryGetValue(team, out var friendlies)) @@ -270,13 +347,61 @@ public int NumFriendliesEngaging(BDTeam team) return 0; } + public float MaxThrust(Vessel v) + { + float maxThrust = 0; + float finalThrust = 0; + + var engines = VesselModuleRegistry.GetModules(v); + if (engines == null) return 0; + using (var engine = engines.GetEnumerator()) + while (engine.MoveNext()) + { + if (engine.Current == null) continue; + if (!engine.Current.EngineIgnited) continue; + + MultiModeEngine mme = engine.Current.part.FindModuleImplementing(); + if (IsAfterBurnerEngine(mme)) + { + mme.autoSwitch = false; + } + + if (mme && mme.mode != engine.Current.engineID) continue; + float engineThrust = engine.Current.maxThrust; + if (engine.Current.atmChangeFlow) + { + engineThrust *= engine.Current.flowMultiplier; + } + maxThrust += Mathf.Max(0f, engineThrust * (engine.Current.thrustPercentage / 100f)); // Don't include negative thrust percentage drives (Danny2462 drives) as they don't contribute to the thrust. + + finalThrust += engine.Current.finalThrust; + } + return maxThrust; + } + + private static bool IsAfterBurnerEngine(MultiModeEngine engine) + { + if (engine == null) + { + return false; + } + if (!engine) + { + return false; + } + return engine.primaryEngineID == "Dry" && engine.secondaryEngineID == "Wet"; + } + #region Target priority // Begin methods used for prioritizing targets public float TargetPriRange(MissileFire myMf) // 1- Target range normalized with max weapon range { - float thisDist = (position - myMf.transform.position).magnitude; + if (myMf == null) return 0; + float thisDist = (position - myMf.vessel.CoM).magnitude; float maxWepRange = 0; - using (List.Enumerator weapon = myMf.vessel.FindPartModulesImplementing().GetEnumerator()) + var weapons = VesselModuleRegistry.GetModules(myMf.vessel); + if (weapons == null) return 0; + using (var weapon = weapons.GetEnumerator()) while (weapon.MoveNext()) { if (weapon.Current == null) continue; @@ -288,21 +413,40 @@ public float TargetPriRange(MissileFire myMf) // 1- Target range normalized with public float TargetPriATA(MissileFire myMf) // Square cosine of antenna train angle { - float ataDot = Vector3.Dot(myMf.vessel.srf_vel_direction, (position - myMf.vessel.vesselTransform.position).normalized); + if (myMf == null) return 0; + float ataDot = Vector3.Dot(myMf.vessel.srf_vel_direction, (position - myMf.vessel.CoM).normalized); ataDot = (ataDot + 1) / 2; // Adjust from 0-1 instead of -1 to 1 return ataDot * ataDot; } - + public float TargetPriEngagement(MissileFire mf, double engagingAlt) // 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 if (mf.vessel.horizontalSrfSpeed < 30 && (mf.vessel.radarAltitude < 200 && engagingAlt > 800)) //if craft is flatspinning or similar, and is lower than 200m, while the aircraft targeting it is higher than 800, regard as semi-landed + { + return -0.5f; + } + 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; - float forwardAccel = Mathf.Abs((float)Vector3.Dot(vessel.acceleration, vessel.vesselTransform.up)); // Forward acceleration - return 0.1f * Mathf.Clamp(forwardAccel / bodyGravity, 0f, 10f); // Output is 0-1 (0.1 is equal to body gravity) + float maxAccel = MaxThrust(vessel) / vessel.GetTotalMass(); // This assumes that all thrust is in the same direction. + maxAccel = 0.1f * Mathf.Clamp(maxAccel / bodyGravity, 0f, 10f); + maxAccel = maxAccel == 0f ? -1f : maxAccel; // If max acceleration is zero (no engines), set to -1 for stronger target priority + return maxAccel; // Output is -1 or 0-1 (0.1 is equal to body gravity) } public float TargetPriClosureTime(MissileFire myMf) // Time to closest point of approach, normalized for one minute { - float targetDistance = Vector3.Distance(vessel.transform.position, myMf.vessel.transform.position); + if (myMf == null) return 0; + float targetDistance = Vector3.Distance(vessel.CoM, myMf.vessel.CoM); Vector3 currVel = (float)myMf.vessel.srfSpeed * myMf.vessel.Velocity().normalized; float closureTime = Mathf.Clamp((float)(1 / ((vessel.Velocity() - currVel).magnitude / targetDistance)), 0f, 60f); return 1 - closureTime / 60f; @@ -310,7 +454,7 @@ public float TargetPriATA(MissileFire myMf) // Square cosine of antenna train an public float TargetPriWeapons(MissileFire mf, MissileFire myMf) // Relative number of weapons of target compared to own weapons { - if (mf?.weaponArray == null) return 0; // The target is dead or has no weapons. + if (mf == null || mf.weaponArray == null || myMf == null) return 0; // The target is dead or has no weapons (or we're dead). float targetWeapons = mf.CountWeapons(); // Counts weapons float myWeapons = myMf.CountWeapons(); // Counts weapons // float targetWeapons = mf.weaponArray.Length - 1; // Counts weapon groups @@ -330,51 +474,38 @@ public float TargetPriFriendliesEngaging(MissileFire myMf) if (myMf == null || myMf.wingCommander == null || myMf.wingCommander.friendlies == null) return 0; float friendsEngaging = Mathf.Max(NumFriendliesEngaging(myMf.Team) - 1, 0); float teammates = myMf.wingCommander.friendlies.Count; + friendsEngaging = 1 - Mathf.Clamp(friendsEngaging / teammates, 0f, 1f); + friendsEngaging = friendsEngaging == 0f ? -1f : friendsEngaging; if (teammates > 0) - return 1 - Mathf.Clamp(friendsEngaging / teammates, 0f, 1f); // Ranges from 0 to 1 + return friendsEngaging; // Range is -1, 0 to 1. -1 if all teammates are engaging target, between 0-1 otherwise depending on number of teammates engaging else return 0; // No teammates } public float TargetPriThreat(MissileFire mf, MissileFire myMf) { + if (mf == null || myMf == null) return 0; float firingAtMe = 0; - var pilotAI = myMf.vessel.FindPartModuleImplementing(); // Get the pilot AI if the vessel has one. if (mf.vessel == myMf.incomingThreatVessel) { - if (myMf.missileIsIncoming) + if (myMf.missileIsIncoming || myMf.underFire || myMf.underAttack) firingAtMe = 1f; - else if (myMf.underFire) - { - if (pilotAI) - { - if (pilotAI.evasionThreshold > 0) // If there is an evasionThreshold, use it to calculate the threat, 0.5 is missDistance = evasionThreshold - { - float missDistance = Mathf.Clamp(myMf.incomingMissDistance, 0, pilotAI.evasionThreshold * 2f); - firingAtMe = 1f - missDistance / (pilotAI.evasionThreshold * 2f); // Ranges from 0-1 - } - else - firingAtMe = 1f; // Otherwise threat is 1 - } - else // SurfaceAI - { - firingAtMe = 1f; - } - } - } - return firingAtMe; + return firingAtMe; // Equals either 0 (not under attack) or 1 (under attack) } public float TargetPriAoD(MissileFire myMF) { - 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 + if (myMF == null) return 0; + var relativePosition = vessel.CoM - myMF.vessel.CoM; + float theta = VectorUtils.Angle(myMF.vessel.srf_vel_direction, relativePosition); + 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 { + if (mf == null || myMf == null) return 0; if (mf.vessel != null) { float targetMass = mf.vessel.GetTotalMass(); @@ -386,6 +517,51 @@ public float TargetPriMass(MissileFire mf, MissileFire myMf) // Relative mass co return 0; } } + + 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; + var targetMf = mf != null && mf.currentTarget != null ? mf.currentTarget.WeaponManager : null; + if (targetMf == null) return 0; + return (targetMf != myMf && targetMf.Team == myMf.Team) ? 1 : 0; // Not us, but on the same team. + } + + public float TargetPriProtectVIP(MissileFire mf, MissileFire myMf) // If target is attacking our VIP(s) + { + if (mf == null || myMf == null) return 0; + var targetMf = mf != null && mf.currentTarget != null ? mf.currentTarget.WeaponManager : null; + if (mf.vessel == null || targetMf == null) return 0; + bool attackingOurVIPs = targetMf.isVIP && myMf.Team == targetMf.Team; + return attackingOurVIPs ? 1 : 0; // Ranges 0 to 1, 1 if target is attacking our VIP(s), 0 if it is not + } + + public float TargetPriAttackVIP(MissileFire mf) // If target is enemy VIP + { + if (mf == null) return 0; + if (mf.vessel != null) + { + bool isVIP = mf.isVIP; + return ((isVIP == true) ? 1 : 0); // Ranges 0 to 1, 1 if target is an enemy VIP, 0 if it is not + } + else + { + return 0; + } + } // End functions used for prioritizing targets #endregion @@ -394,7 +570,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; } @@ -409,7 +585,7 @@ public void Engage(MissileFire mf) friendlies.Add(mf); } else - friendliesEngaging.Add(mf.Team, new List { mf }); + friendliesEngaging.Add(mf.Team, [mf]); } public void Disengage(MissileFire mf) @@ -429,17 +605,37 @@ void AboutToBeDestroyed() public bool IsCloser(TargetInfo otherTarget, MissileFire myMf) { - float thisSqrDist = (position - myMf.transform.position).sqrMagnitude; - float otherSqrDist = (otherTarget.position - myMf.transform.position).sqrMagnitude; + float thisSqrDist = (position - myMf.vessel.CoM).sqrMagnitude; + float otherSqrDist = (otherTarget.position - myMf.vessel.CoM).sqrMagnitude; return thisSqrDist < otherSqrDist; } + public bool SafeOrbitalIntercept(MissileFire myMf) + { + // For orbital AI craft, avoid intercepting targets if we are descending and the maneuver will bring our own periapsis to an unsafe altitude + + if (!vessel) return true; + var orbitalAI = myMf.vessel.ActiveController().OrbitalAI; + if (orbitalAI == null || !orbitalAI.pilotEnabled) + return true; + + Orbit o = myMf.vessel.orbit; + bool unsafeDescent = o.timeToPe > 0 && o.timeToPe < o.timeToAp && o.PeA < (1.2f * o.referenceBody.MinSafeAltitude()); + bool inRange = (vessel.CoM - myMf.vessel.CoM).sqrMagnitude < orbitalAI.interceptRanges.y * orbitalAI.interceptRanges.y; + Vector3 relVel = vessel.Velocity() - myMf.vessel.Velocity(); + bool killVelocityNeeded = Vector3.Dot(vessel.CoM - myMf.vessel.CoM, relVel) < 0f && + Vector3.Dot(o.GetPrograde(Planetarium.GetUniversalTime()), relVel) < 0f; // Moving away from each other in prograde direction (kill vel direction is retrograde) + + return (inRange || !(unsafeDescent && killVelocityNeeded)); + } + public void VesselModified(Vessel v) { - if (v && v == this.vessel) + if (v && v == this.vessel && isActiveAndEnabled) { if (!alreadyScheduledRCSUpdate) StartCoroutine(UpdateRCSDelayed()); + targetPartListNeedsUpdating = true; } } diff --git a/BDArmory/Targeting/TargetSignatureData.cs b/BDArmory/Targeting/TargetSignatureData.cs index beb9ea96f..e3402b9d3 100644 --- a/BDArmory/Targeting/TargetSignatureData.cs +++ b/BDArmory/Targeting/TargetSignatureData.cs @@ -1,11 +1,12 @@ using System; -using System.Collections.Generic; -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 { @@ -17,12 +18,19 @@ public struct TargetSignatureData : IEquatable public bool exists; public float timeAcquired; public float signalStrength; + public RadarWarningReceiver.RWRThreatTypes signalType; + public float notchMod; public TargetInfo targetInfo; public BDTeam Team; public Vector2 pingPosition; public VesselECMJInfo vesselJammer; public ModuleRadar lockedByRadar; public Vessel vessel; + public Part IRSource; + public bool isDecoy = false; + public float range; + //SEE TODO in CheckJamming + //public Vector3 jammedGeoPos; bool orbital; Orbit orbit; @@ -34,7 +42,7 @@ public bool Equals(TargetSignatureData other) timeAcquired == other.timeAcquired; } - public TargetSignatureData(Vessel v, float _signalStrength) + public TargetSignatureData(Vessel v, float _signalStrength, Part heatpart = null, float _notchMod = 0f, float _range = -1f) { orbital = v.InOrbit(); orbit = v.orbit; @@ -42,10 +50,12 @@ 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; + notchMod = _notchMod; + range = _range; signalStrength = _signalStrength; @@ -65,13 +75,8 @@ public TargetSignatureData(Vessel v, float _signalStrength) } else { - List.Enumerator mf = v.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - Team = mf.Current.Team; - break; - } - mf.Dispose(); + var mf = v.ActiveController().WM; + if (mf != null) Team = mf.Team; } vesselJammer = v.gameObject.GetComponent(); @@ -96,16 +101,40 @@ public TargetSignatureData(CMFlare flare, float _signalStrength) orbit = null; lockedByRadar = null; vessel = null; + IRSource = null; + isDecoy = true; + notchMod = 0f; + } + + public TargetSignatureData(CMDecoy decoy, float _signalStrength) + { + velocity = decoy.velocity; + geoPos = VectorUtils.WorldPositionToGeoCoords(decoy.transform.position, FlightGlobals.currentMainBody); + exists = true; + acceleration = Vector3.zero; + timeAcquired = Time.time; + signalStrength = _signalStrength; + targetInfo = null; + vesselJammer = null; + Team = null; + pingPosition = Vector2.zero; + orbital = false; + orbit = null; + lockedByRadar = null; + vessel = null; + IRSource = null; + isDecoy = true; + notchMod = 0f; } - public TargetSignatureData(Vector3 _velocity, Vector3 _position, Vector3 _acceleration, bool _exists, float _signalStrength) + public TargetSignatureData(Vector3 _velocity, Vector3 _position, Vector3 _acceleration, bool _exists, RadarWarningReceiver.RWRThreatTypes _signalType) { velocity = _velocity; geoPos = VectorUtils.WorldPositionToGeoCoords(_position, FlightGlobals.currentMainBody); acceleration = _acceleration; exists = _exists; timeAcquired = Time.time; - signalStrength = _signalStrength; + signalType = _signalType; targetInfo = null; vesselJammer = null; Team = null; @@ -114,6 +143,28 @@ public TargetSignatureData(Vector3 _velocity, Vector3 _position, Vector3 _accele orbit = null; lockedByRadar = null; vessel = null; + IRSource = null; + notchMod = 0f; + } + + public TargetSignatureData(Vector3 _position, Vector2 _pingPosition, bool _exists, RadarWarningReceiver.RWRThreatTypes _signalType) + { + velocity = Vector3.zero; + geoPos = VectorUtils.WorldPositionToGeoCoords(_position, FlightGlobals.currentMainBody); + acceleration = Vector3.zero; + exists = _exists; + timeAcquired = Time.time; + signalType = _signalType; + targetInfo = null; + vesselJammer = null; + Team = null; + pingPosition = _pingPosition; + orbital = false; + orbit = null; + lockedByRadar = null; + vessel = null; + IRSource = null; + notchMod = 0f; } public Vector3 position @@ -136,29 +187,70 @@ public Vector3 predictedPosition } } - public Vector3 predictedPositionWithChaffFactor + // TODO: Finish this stuff, we'll have to decide on how jamming affects positional + // accuracy in the display. For now, I'm throwing this code in VesselRadarData + // as it was before, but ideally we should be pre-calculating this, and only + // when the radar performs a sweep (so this would ideally be called in + // RadarUtils.RadarUpdateScanLock, in the modeTryLock = false branch). + /*public void CheckJamming(ModuleRadar radar) { - get + float jamDistance = RadarUtils.GetVesselECMJammingDistance(vessel); + Vector3 radarPosition = radar.currPosition; + Vector3 vectorToTarget = vessel.CoM - radar.currPosition; + float sqrDist = vectorToTarget.sqrMagnitude; + if (vesselJammer && jamDistance * jamDistance > sqrDist) { - // get chaff factor of vessel and calculate decoy distortion caused by chaff echos - float decoyFactor = 0f; - Vector3 posDistortion = Vector3.zero; + Vector3 dirToTarget = vectorToTarget / BDAMath.Sqrt(sqrDist); + + Vector3 jammedPosition = radarPosition + (dirToTarget * UnityEngine.Random.Range(100, rIncrements[rangeIndex])); + float bearingVariation = Mathf.Clamp(1024e6f / // 32000 * 32000 + sqrDist, 0, 80); + jammedPosition = radarPosition + (Quaternion.AngleAxis(Random.Range(-bearingVariation, bearingVariation), currUp) * (jammedPosition - transform.position)); + } + }*/ - if (vessel != null) + public Vector3 predictedPositionWithChaffFactor(float chaffEffectivity = 1f) + { + // get chaff factor of vessel and calculate decoy distortion caused by chaff echos + float decoyFactor = 0f; + Vector3 posDistortion = Vector3.zero; + + if (vessel != null) + { + // chaff check + decoyFactor = (1f - RadarUtils.GetVesselChaffFactor(vessel)) * (1f + notchMod); + Vector3 velOrAccel = (!vessel.InVacuum()) ? vessel.Velocity() : vessel.acceleration_immediate; + + 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); - } - } + // With ecm on better chaff effectiveness due to jammer strength + VesselECMJInfo vesseljammer = vessel.gameObject.GetComponent(); + + // 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)); - return position + (velocity * age) + posDistortion; + // 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 * (UnityEngine.Random.insideUnitSphere - jammingFactor * velOrAccel.normalized); + + // Higher speed -> missile decoyed further "behind" where the chaff drops (also means that chaff is least effective for head-on engagements) + posDistortion = signatureDistortion - Mathf.Clamp(decoyFactor * decoyFactor, 0f, 0.5f) * velOrAccel; + + // Apply effects from global settings and individual missile chaffEffectivity + //modern radar can filter out chaff easily due to doppler comparisons (chaff stationary, plane not) + //doppler comparison can be countered via jammer + chaff to illuminate the chaff with an adjusted wavelength to simulate necessary doppler shifting for a faster moving object + + //So - CE of 1: missile/radar has no doppler correction, fully fooled by chaff + // - CE of 0: missile/radar has doppler correction, not fooled at all by chaff + // - jammer on, doppler correction countered, CE:0 countered and radar gets some spoofing from chaff + chaffEffectivity = jammingFactor > 0 ? Mathf.Clamp01(chaffEffectivity + jammingFactor) : chaffEffectivity; + posDistortion *= Mathf.Max(BDArmorySettings.CHAFF_FACTOR, 0f) * chaffEffectivity; + } } + + return position + (velocity * age) + posDistortion; } public float altitude @@ -181,7 +273,7 @@ public static TargetSignatureData noTarget { get { - return new TargetSignatureData(Vector3.zero, Vector3.zero, Vector3.zero, false, 0); + return new TargetSignatureData(Vector3.zero, Vector3.zero, Vector3.zero, false, RadarWarningReceiver.RWRThreatTypes.None); } } diff --git a/BDArmory/Parts/TargetingCamera.cs b/BDArmory/Targeting/TargetingCamera.cs similarity index 80% rename from BDArmory/Parts/TargetingCamera.cs rename to BDArmory/Targeting/TargetingCamera.cs index 550e9bb39..10bd2eb86 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 { @@ -13,6 +13,7 @@ public class TargetingCamera : MonoBehaviour TGPCameraEffects camEffects; Light nvLight; public bool nvMode = false; + public bool color = false; private Texture2D reticleTex; @@ -34,6 +35,7 @@ public Texture2D ReticleTexture Camera[] cameras; public static Transform cameraTransform; + public bool[] CamEnabled = [true, true, true, true]; bool cameraEnabled; @@ -83,6 +85,7 @@ public void SetFOV(float fov) for (int i = 0; i < cameras.Length; i++) { + if (cameras[i] == null) continue; cameras[i].fieldOfView = fov; } currentFOV = fov; @@ -96,22 +99,22 @@ void VesselChange(Vessel v) } bool moduleFound = false; - List.Enumerator mtc = v.FindPartModulesImplementing().GetEnumerator(); - while (mtc.MoveNext()) - { - Debug.Log("[BDArmory] : Vessel switched to vessel with targeting camera. Refreshing camera state."); - - if (mtc.Current.cameraEnabled) - { - mtc.Current.DelayedEnable(); - } - else + using (var mtc = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (mtc.MoveNext()) { - mtc.Current.DisableCamera(); + if (mtc.Current == null) continue; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.TargetingCamera]: Vessel switched to vessel with targeting camera. Refreshing camera state."); + + if (mtc.Current.cameraEnabled) + { + mtc.Current.DelayedEnable(); + } + else + { + mtc.Current.DisableCamera(); + } + moduleFound = true; } - moduleFound = true; - } - mtc.Dispose(); if (!moduleFound) { @@ -131,6 +134,7 @@ public void EnableCamera(Transform parentTransform) for (int i = 0; i < cameras.Length; i++) { + if (cameras[i] == null) continue; cameras[i].enabled = false; } @@ -141,8 +145,8 @@ public void EnableCamera(Transform parentTransform) void RenderCameras() { - cameras[3].Render(); - cameras[2].Render(); + if (cameras[3] != null && CamEnabled[3]) cameras[3].Render(); // Galaxy cam + if (cameras[2] != null && CamEnabled[2]) cameras[2].Render(); // Sky cam Color origAmbientColor = RenderSettings.ambientLight; if (nvMode) @@ -150,8 +154,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 && CamEnabled[1]) cameras[1].Render(); // Far cam + if (cameras[0] != null && CamEnabled[0]) cameras[0].Render(); // Near cam nvLight.enabled = false; if (nvMode) @@ -162,7 +166,7 @@ void RenderCameras() void LateUpdate() { - if (cameraEnabled) + if (cameraEnabled && HighLogic.LoadedSceneIsFlight) { if (cameras == null || cameras[0] == null) { @@ -185,6 +189,7 @@ public void DisableCamera() { for (int i = 0; i < cameras.Length; i++) { + if (cameras[i] == null) continue; cameras[i].enabled = false; } } @@ -196,7 +201,7 @@ void SetupCamera(Transform parentTransform) { if (!parentTransform) { - Debug.Log("Targeting camera tried setup but parent transform is null"); + Debug.Log("[BDArmory.TargetingCamera]: Targeting camera tried setup but parent transform is null"); return; } @@ -205,7 +210,7 @@ void SetupCamera(Transform parentTransform) cameraTransform = (new GameObject("targetCamObject")).transform; } - Debug.Log("Setting target camera parent"); + //Debug.Log("[BDArmory.TargetingCamera]: Setting target camera parent"); cameraTransform.parent = parentTransform; cameraTransform.localPosition = Vector3.zero; cameraTransform.localRotation = Quaternion.identity; @@ -213,8 +218,10 @@ void SetupCamera(Transform parentTransform) if (targetCamRenderTexture == null) { int res = Mathf.RoundToInt(BDArmorySettings.TARGET_CAM_RESOLUTION); - targetCamRenderTexture = new RenderTexture(res, res, 24); - targetCamRenderTexture.antiAliasing = 1; + targetCamRenderTexture = new RenderTexture(res, res, (int)RenderTextureFormat.RGBAUShort) + { + antiAliasing = 1 + }; targetCamRenderTexture.Create(); } @@ -304,7 +311,7 @@ private Camera FindCamera(string cameraName) return cam; } } - Debug.Log("Couldn't find " + cameraName); + Debug.Log("[BDArmory.TargetingCamera]: Couldn't find " + cameraName); return null; } @@ -312,6 +319,15 @@ void OnDestroy() { ReadyForUse = false; GameEvents.onVesselChange.Remove(VesselChange); + if (cameras != null) + { + foreach (var camera in cameras) + { + if (camera != null && camera.gameObject != null) + { Destroy(camera.gameObject); } + } + } + if (targetCamRenderTexture != null) targetCamRenderTexture.Release(); } public static bool IsTGPCamera(Camera c) diff --git a/BDArmory/Targeting/_description b/BDArmory/Targeting/_description new file mode 100644 index 000000000..bcec0edcf --- /dev/null +++ b/BDArmory/Targeting/_description @@ -0,0 +1 @@ +Targeting modules and utils. \ No newline at end of file diff --git a/BDArmory/UI/BDAEditorAnalysisWindow.cs b/BDArmory/UI/BDAEditorAnalysisWindow.cs index 798eaf82a..98ecf4872 100644 --- a/BDArmory/UI/BDAEditorAnalysisWindow.cs +++ b/BDArmory/UI/BDAEditorAnalysisWindow.cs @@ -1,12 +1,14 @@ 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.Settings; +using BDArmory.Utils; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.EditorAny, false)] @@ -16,11 +18,12 @@ internal class BDAEditorAnalysisWindow : MonoBehaviour private ApplicationLauncherButton toolbarButton = null; private bool showRcsWindow = false; - private string windowTitle = "BDArmory Radar Cross Section Analysis (Worst Three Aspects)"; + private string windowTitle = !Settings.BDArmorySettings.ASPECTED_RCS ? "BDArmory Radar Cross Section Analysis (Worst Three Aspects)" : "BDArmory Radar Cross Section Analysis (Front/Side/Rear)"; private Rect windowRect = new Rect(300, 150, 650, 500); private bool takeSnapshot = false; private float rcsReductionFactor; + private float rcsOverride = -1; private float rcsGCF = 1.0f; private ModuleRadar[] radars; @@ -35,11 +38,12 @@ internal class BDAEditorAnalysisWindow : MonoBehaviour void Awake() { + if (Instance != null) Destroy(Instance); + Instance = this; } void Start() { - Instance = this; AddToolbarButton(); RadarUtils.SetupResources(); @@ -53,7 +57,7 @@ private void FillRadarList() // first pass, then sort for (int i = 0; i < radars.Length; i++) { - if (string.IsNullOrEmpty(radars[i].radarName)) radars[i].radarName = radars[i].part?.partInfo?.title; + if (string.IsNullOrEmpty(radars[i].radarName)) radars[i].radarName = (radars[i].part == null ? null : radars[i].part.partInfo == null ? null : radars[i].part.partInfo.title); GUIContent gui = new GUIContent(radars[i].radarName); } Array.Sort(radars, delegate (ModuleRadar r1, ModuleRadar r2) { return r1.radarName.CompareTo(r2.radarName); }); @@ -72,6 +76,31 @@ private void FillRadarList() private void OnEditorShipModifiedEvent(ShipConstruct data) { + if (data is null) return; + delayedTakeSnapShot = true; + if (!delayedTakeSnapShotInProgress) + StartCoroutine(DelayedTakeSnapShot(data)); + } + + private bool delayedTakeSnapShot = false; + private bool delayedTakeSnapShotInProgress = false; + IEnumerator DelayedTakeSnapShot(ShipConstruct ship) + { + delayedTakeSnapShotInProgress = true; + var wait = new WaitForFixedUpdate(); + while (delayedTakeSnapShot) // Wait until ship modified events stop coming. + { + delayedTakeSnapShot = false; + 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; } @@ -80,6 +109,7 @@ private void OnDestroy() { GameEvents.onEditorShipModified.Remove(OnEditorShipModifiedEvent); RadarUtils.CleanupResources(); + HideToolbarGUINow(); if (toolbarButton) { @@ -88,27 +118,22 @@ private void OnDestroy() } } - IEnumerator ToolbarButtonRoutine() + void AddToolbarButton() { - if (toolbarButton || (!HighLogic.LoadedSceneIsEditor)) yield break; - while (!ApplicationLauncher.Ready) - { - yield return null; - } - - AddToolbarButton(); + if (!HighLogic.LoadedSceneIsEditor) return; + StartCoroutine(ToolbarButtonRoutine()); } - - void AddToolbarButton() + IEnumerator ToolbarButtonRoutine() { - if (HighLogic.LoadedSceneIsEditor) + if (toolbarButton) // Update the callbacks for the current instance. { - if (toolbarButton == null) - { - Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon_rcs", false); - toolbarButton = ApplicationLauncher.Instance.AddModApplication(ShowToolbarGUI, HideToolbarGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB, buttonTexture); - } + toolbarButton.onTrue = ShowToolbarGUI; + toolbarButton.onFalse = HideToolbarGUI; + yield break; } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. + Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon_rcs", false); + toolbarButton = ApplicationLauncher.Instance.AddModApplication(ShowToolbarGUI, HideToolbarGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB, buttonTexture); } public void ShowToolbarGUI() @@ -117,10 +142,22 @@ public void ShowToolbarGUI() takeSnapshot = true; } - public void HideToolbarGUI() + // Doing it this way prevents OnGUI events from below the window from being triggered by the window disappearing. + public void HideToolbarGUI() => StartCoroutine(HideToolbarGUIAtEndOfFrame()); + bool waitingForEndOfFrame = false; + IEnumerator HideToolbarGUIAtEndOfFrame() + { + if (waitingForEndOfFrame) yield break; + waitingForEndOfFrame = true; + yield return new WaitForEndOfFrame(); + waitingForEndOfFrame = false; + HideToolbarGUINow(); + } + void HideToolbarGUINow() { showRcsWindow = false; takeSnapshot = false; + GUIUtils.PreventClickThrough(windowRect, "BDARCSLOCK", true); } void Dummy() @@ -130,22 +167,22 @@ void OnGUI() { if (showRcsWindow) { - windowRect = GUI.Window(this.GetInstanceID(), windowRect, WindowRcs, windowTitle, BDArmorySetup.BDGuiSkin.window); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, windowRect.position); + windowRect = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), windowRect, WindowRcs, windowTitle, BDArmorySetup.BDGuiSkin.window); } - - PreventClickThrough(); } void WindowRcs(int windowID) { + GUIUtils.PreventClickThrough(windowRect, "BDARCSLOCK"); if (GUI.Button(new Rect(windowRect.width - 18, 2, 16, 16), "X")) { - HideToolbarGUI(); + toolbarButton.SetFalse(); } - GUI.Label(new Rect(10, 40, 200, 20), $"Az {RadarUtils.worstRCSAspects[0, 0]}, El {RadarUtils.worstRCSAspects[0, 1]}", BDArmorySetup.BDGuiSkin.box); - GUI.Label(new Rect(220, 40, 200, 20), $"Az {RadarUtils.worstRCSAspects[1, 0]}, El {RadarUtils.worstRCSAspects[1, 1]}", BDArmorySetup.BDGuiSkin.box); - GUI.Label(new Rect(430, 40, 200, 20), $"Az {RadarUtils.worstRCSAspects[2, 0]}, El {RadarUtils.worstRCSAspects[2, 1]}", BDArmorySetup.BDGuiSkin.box); + GUI.Label(new Rect(10, 40, 200, 20), $"Az {RadarUtils.editorRCSAspects[0, 0].ToString("0")}, El {RadarUtils.editorRCSAspects[0, 1].ToString("0")}", BDArmorySetup.BDGuiSkin.box); + GUI.Label(new Rect(220, 40, 200, 20), $"Az {RadarUtils.editorRCSAspects[1, 0].ToString("0")}, El {RadarUtils.editorRCSAspects[1, 1].ToString("0")}", BDArmorySetup.BDGuiSkin.box); + GUI.Label(new Rect(430, 40, 200, 20), $"Az {RadarUtils.editorRCSAspects[2, 0].ToString("0")}, El {RadarUtils.editorRCSAspects[2, 1].ToString("0")}", BDArmorySetup.BDGuiSkin.box); if (takeSnapshot) takeRadarSnapshot(); @@ -155,14 +192,19 @@ void WindowRcs(int windowID) GUI.DrawTexture(new Rect(220, 70, 200, 200), RadarUtils.GetTexture2, ScaleMode.StretchToFill); GUI.DrawTexture(new Rect(430, 70, 200, 200), RadarUtils.GetTexture3, ScaleMode.StretchToFill); - GUI.Label(new Rect(10, 275, 200, 20), string.Format("{0:0.00}", RadarUtils.worstRCSAspects[0, 2]) + " m^2", BDArmorySetup.BDGuiSkin.label); - GUI.Label(new Rect(220, 275, 200, 20), string.Format("{0:0.00}", RadarUtils.worstRCSAspects[1, 2]) + " m^2", BDArmorySetup.BDGuiSkin.label); - GUI.Label(new Rect(430, 275, 200, 20), string.Format("{0:0.00}", RadarUtils.worstRCSAspects[2, 2]) + " m^2", BDArmorySetup.BDGuiSkin.label); + float editorUIRCS0 = (!Settings.BDArmorySettings.ASPECTED_RCS) ? RadarUtils.editorRCSAspects[0, 2] : (RadarUtils.editorRCSAspects[0, 2] * (1 - Settings.BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT) + RadarUtils.rcsTotal * Settings.BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT); + float editorUIRCS1 = (!Settings.BDArmorySettings.ASPECTED_RCS) ? RadarUtils.editorRCSAspects[1, 2] : (RadarUtils.editorRCSAspects[1, 2] * (1 - Settings.BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT) + RadarUtils.rcsTotal * Settings.BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT); + float editorUIRCS2 = (!Settings.BDArmorySettings.ASPECTED_RCS) ? RadarUtils.editorRCSAspects[2, 2] : (RadarUtils.editorRCSAspects[2, 2] * (1 - Settings.BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT) + RadarUtils.rcsTotal * Settings.BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT); + + GUI.Label(new Rect(10, 275, 200, 20), RadarUtils.RCSString(editorUIRCS0), BDArmorySetup.BDGuiSkin.label); + GUI.Label(new Rect(220, 275, 200, 20), RadarUtils.RCSString(editorUIRCS1), BDArmorySetup.BDGuiSkin.label); + GUI.Label(new Rect(430, 275, 200, 20), RadarUtils.RCSString(editorUIRCS2), BDArmorySetup.BDGuiSkin.label); + GUIStyle style = BDArmorySetup.BDGuiSkin.label; style.fontStyle = FontStyle.Bold; - GUI.Label(new Rect(10, 300, 600, 20), "Base radar cross section for vessel: " + string.Format("{0:0.00} m^2 (without ECM/countermeasures)", RadarUtils.rcsTotal), style); - GUI.Label(new Rect(10, 320, 600, 20), "Total radar cross section for vessel: " + string.Format("{0:0.00} m^2 (with RCS reduction/stealth/ground clutter)", RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF), style); + GUI.Label(new Rect(10, 300, 600, 20), "Base radar cross section for vessel: " + RadarUtils.RCSString(RadarUtils.rcsTotal) + " (without ECM/countermeasures)", style); + GUI.Label(new Rect(10, 320, 600, 20), "Total radar cross section for vessel: " + RadarUtils.RCSString(rcsOverride > 0 ? rcsOverride * rcsGCF : RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF) + " (with RCS reduction/stealth/ground clutter)", style); style.fontStyle = FontStyle.Normal; GUI.Label(new Rect(10, 380, 600, 20), "** (Range evaluation not accounting for ECM/countermeasures)", style); @@ -177,7 +219,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(); @@ -203,7 +245,7 @@ void WindowRcs(int windowID) for (float distance = selected_radar.radarMaxDistanceDetect; distance >= 0; distance--) { text_detection = $"Detection: undetectable by this radar."; - if (selected_radar.radarDetectionCurve.Evaluate(distance) <= (RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) + if (selected_radar.radarDetectionCurve.Evaluate(distance) <= (rcsOverride > 0 ? rcsOverride * rcsGCF : RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) { text_detection = $"Detection: detected at {distance} km and closer"; break; @@ -220,7 +262,7 @@ void WindowRcs(int windowID) text_locktrack = $"Lock/Track: untrackable by this radar."; for (float distance = selected_radar.radarMaxDistanceLockTrack; distance >= 0; distance--) { - if (selected_radar.radarLockTrackCurve.Evaluate(distance) <= (RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) + if (selected_radar.radarLockTrackCurve.Evaluate(distance) <= (rcsOverride > 0 ? rcsOverride * rcsGCF : RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) { text_locktrack = $"Lock/Track: tracked at {distance} km and closer"; break; @@ -239,14 +281,14 @@ void WindowRcs(int windowID) previous_index = selected_index; GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref windowRect); + GUIUtils.RepositionWindow(ref windowRect); } void WindowRcsLegacy(int windowID) { if (GUI.Button(new Rect(windowRect.width - 18, 2, 16, 16), "X")) { - HideToolbarGUI(); + toolbarButton.SetFalse(); } GUI.Label(new Rect(10, 40, 200, 20), "Frontal", BDArmorySetup.BDGuiSkin.box); @@ -272,14 +314,14 @@ void WindowRcsLegacy(int windowID) else GUI.DrawTexture(new Rect(430, 70, 200, 200), RadarUtils.GetTextureVentral45, ScaleMode.StretchToFill); - GUI.Label(new Rect(10, 275, 200, 20), string.Format("{0:0.00}", Mathf.Max(RadarUtils.rcsFrontal, RadarUtils.rcsFrontal45)) + " m^2", BDArmorySetup.BDGuiSkin.label); - GUI.Label(new Rect(220, 275, 200, 20), string.Format("{0:0.00}", Mathf.Max(RadarUtils.rcsLateral, RadarUtils.rcsLateral45)) + " m^2", BDArmorySetup.BDGuiSkin.label); - GUI.Label(new Rect(430, 275, 200, 20), string.Format("{0:0.00}", Mathf.Max(RadarUtils.rcsVentral, RadarUtils.rcsVentral45)) + " m^2", BDArmorySetup.BDGuiSkin.label); + GUI.Label(new Rect(10, 275, 200, 20), string.Format("{0:0.00}", Mathf.Max(RadarUtils.rcsFrontal, RadarUtils.rcsFrontal45)) + " m²", BDArmorySetup.BDGuiSkin.label); + GUI.Label(new Rect(220, 275, 200, 20), string.Format("{0:0.00}", Mathf.Max(RadarUtils.rcsLateral, RadarUtils.rcsLateral45)) + " m²", BDArmorySetup.BDGuiSkin.label); + GUI.Label(new Rect(430, 275, 200, 20), string.Format("{0:0.00}", Mathf.Max(RadarUtils.rcsVentral, RadarUtils.rcsVentral45)) + " m²", BDArmorySetup.BDGuiSkin.label); GUIStyle style = BDArmorySetup.BDGuiSkin.label; style.fontStyle = FontStyle.Bold; - GUI.Label(new Rect(10, 300, 600, 20), "Base radar cross section for vessel: " + string.Format("{0:0.00} m^2 (without ECM/countermeasures)", RadarUtils.rcsTotal), style); - GUI.Label(new Rect(10, 320, 600, 20), "Total radar cross section for vessel: " + string.Format("{0:0.00} m^2 (with RCS reduction/stealth/ground clutter)", RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF), style); + GUI.Label(new Rect(10, 300, 600, 20), "Base radar cross section for vessel: " + string.Format("{0:0.00} m² (without ECM/countermeasures)", RadarUtils.rcsTotal), style); + GUI.Label(new Rect(10, 320, 600, 20), "Total radar cross section for vessel: " + string.Format("{0:0.00} m² (with RCS reduction/stealth/ground clutter)", rcsOverride > 0 ? rcsOverride * rcsGCF : RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF), style); style.fontStyle = FontStyle.Normal; GUI.Label(new Rect(10, 380, 600, 20), "** (Range evaluation not accounting for ECM/countermeasures)", style); @@ -294,7 +336,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(); @@ -320,7 +362,7 @@ void WindowRcsLegacy(int windowID) for (float distance = selected_radar.radarMaxDistanceDetect; distance >= 0; distance--) { text_detection = $"Detection: undetectable by this radar."; - if (selected_radar.radarDetectionCurve.Evaluate(distance) <= (RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) + if (selected_radar.radarDetectionCurve.Evaluate(distance) <= (rcsOverride > 0 ? rcsOverride * rcsGCF : RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) { text_detection = $"Detection: detected at {distance} km and closer"; break; @@ -337,7 +379,7 @@ void WindowRcsLegacy(int windowID) text_locktrack = $"Lock/Track: untrackable by this radar."; for (float distance = selected_radar.radarMaxDistanceLockTrack; distance >= 0; distance--) { - if (selected_radar.radarLockTrackCurve.Evaluate(distance) <= (RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) + if (selected_radar.radarLockTrackCurve.Evaluate(distance) <= (rcsOverride > 0 ? rcsOverride * rcsGCF : RadarUtils.rcsTotal * rcsReductionFactor * rcsGCF)) { text_locktrack = $"Lock/Track: tracked at {distance} km and closer"; break; @@ -356,7 +398,7 @@ void WindowRcsLegacy(int windowID) previous_index = selected_index; GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref windowRect); + GUIUtils.RepositionWindow(ref windowRect); } void takeRadarSnapshot() @@ -367,12 +409,14 @@ void takeRadarSnapshot() // Encapsulate editor ShipConstruct into a vessel: Vessel v = new Vessel(); v.parts = EditorLogic.fetch.ship.Parts; + v.vesselType = VesselType.Plane; // Tell KSP that it's not debris (which we ignore in the snapshot). // RadarUtils.RenderVesselRadarSnapshot(v, EditorLogic.RootPart.transform); //first rendering for true RCS - RadarUtils.RenderVesselRadarSnapshot(v, EditorLogic.RootPart.transform, true); //create renders + RadarUtils.RenderVesselRadarSnapshot(v, EditorLogic.RootPart.transform, null, true); //create renders takeSnapshot = false; // get RCS reduction measures (stealth/low observability) rcsReductionFactor = 1.0f; + int rcsCount = 0; List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator(); while (parts.MoveNext()) @@ -384,49 +428,14 @@ void takeRadarSnapshot() { rcsReductionFactor *= rcsJammer.rcsReductionFactor; rcsCount++; + if (rcsOverride < rcsJammer.rcsOverride) rcsOverride = rcsJammer.rcsOverride; } } } parts.Dispose(); if (rcsCount > 0) - rcsReductionFactor = Mathf.Clamp((rcsReductionFactor * rcsCount), 0.0f, 1); //same formula as in VesselECMJInfo must be used here! - } - - /// - /// Lock the model if our own window is shown and has cursor focus to prevent click-through. - /// Code adapted from FAR Editor GUI - /// - private void PreventClickThrough() - { - bool cursorInGUI = false; - EditorLogic EdLogInstance = EditorLogic.fetch; - if (!EdLogInstance) - { - return; - } - if (showRcsWindow) - { - cursorInGUI = windowRect.Contains(GetMousePos()); - } - if (cursorInGUI) - { - if (!CameraMouseLook.GetMouseLook()) - EdLogInstance.Lock(false, false, false, "BDARCSLOCK"); - else - EdLogInstance.Unlock("BDARCSLOCK"); - } - else if (!cursorInGUI) - { - EdLogInstance.Unlock("BDARCSLOCK"); - } - } - - private Vector3 GetMousePos() - { - Vector3 mousePos = Input.mousePosition; - mousePos.y = Screen.height - mousePos.y; - return mousePos; + rcsReductionFactor = Mathf.Max((rcsReductionFactor * rcsCount), 0.0f); //same formula as in VesselECMJInfo must be used here! } } //EditorRCsWindow } diff --git a/BDArmory/UI/BDAEditorArmorWindow.cs b/BDArmory/UI/BDAEditorArmorWindow.cs new file mode 100644 index 000000000..61e92bbf2 --- /dev/null +++ b/BDArmory/UI/BDAEditorArmorWindow.cs @@ -0,0 +1,1492 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System; +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)] + internal class BDAEditorArmorWindow : MonoBehaviour + { + public static BDAEditorArmorWindow Instance = null; + private ApplicationLauncherButton toolbarButton = null; + + private bool showArmorWindow = false; + 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; + private GUIContent[] armorGUI; + private GUIContent armorBoxText; + 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 wingLoadingWet; + private float wingLoadingDry; + private float WLRatioWet; + private float WLRatioDry; + private List vesselResources; + private List vesselResourceIDs; + private Rect vesselResourceBoxRect = new(10, 0, 280, 0); + private bool CalcArmor = false; + private bool shipModifiedfromCalcArmor = false; + private bool SetType = false; + private bool SetThickness = false; + private string selectedArmor = "None"; + private bool ArmorStats = false; + private bool resourcePick = false; + private float ArmorDensity = 0; + private float ArmorStrength = 200; + private float ArmorHardness = 300; + 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; + private float maxThickness = 60; + private bool Visualizer = false; + private bool HPvisualizer = false; + private bool HullVisualizer = false; + private bool LiftVisualizer = false; + private bool TreeVisualizer = false; + private bool oldVisualizer = false; + private bool oldHPvisualizer = false; + private bool oldHullVisualizer = false; + private bool oldLiftVisualizer = false; + private bool oldTreeVisualizer = false; + private bool refreshVisualizer = false; + private bool refreshHPvisualizer = false; + private bool refreshHullvisualizer = true; + private bool refreshLiftvisualizer = false; + private bool refreshTreevisualizer = false; + private string hullmat = "Aluminium"; + + private float steelValue = 1; + private float armorValue = 1; + private float relValue = 1; + private float exploValue; + + //comp rules compliance stuff + float maxStacking = -1; + int maxPartCount = -1; + float maxLtW = -1; + float maxTWR = -1; + float maxMass = -1; + int maxEngines = 999; + int pointBuyBudget = -1; + + Dictionary thicknessField; + void Awake() + { + if (Instance != null) Destroy(Instance); + Instance = this; + } + + void Start() + { + AddToolbarButton(); + thicknessField = new Dictionary + { + {"Thickness", gameObject.AddComponent().Initialise(0, 10, 0, 1500) }, // FIXME should use maxThickness instead of 1500 here. + }; + vesselResourceIDs = new List(); + vesselResources = new List(); + GameEvents.onEditorShipModified.Add(OnEditorShipModifiedEvent); + GameEvents.onEditorPartPlaced.Add(OnEditorPartPlacedEvent); + GameEvents.onEditorPartDeleted.Add(OnEditorPartPlacedEvent); + /* + 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; + if (ArmorDuctility > 0.25f) + { + yieldStrength *= 0.7f; + } + 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 + SetupLegalityValues(); + } + + private void FillArmorList() + { + armorGUI = new GUIContent[ArmorInfo.armors.Count]; + for (int i = 0; i < ArmorInfo.armors.Count; i++) + { + GUIContent gui = new GUIContent(ArmorInfo.armors[i].name.Length <= 17 ? ArmorInfo.armors[i].name : ArmorInfo.armors[i].name.Remove(14) + "..."); + armorGUI[i] = gui; + } + armorBoxText = new GUIContent(); + 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].localizedName.Length <= 17 ? HullInfo.materials[i].localizedName : HullInfo.materials[i].localizedName.Remove(14) + "..."); + hullGUI[i] = gui; + } + + hullBoxText = new GUIContent(); + hullBoxText.text = StringUtils.Localize("#LOC_BDArmory_Armor_HullType"); + } + + public void SetupLegalityValues() + { + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS || BDArmorySettings.RUNWAY_PROJECT) + { + if (CompSettings.CompVesselChecksEnabled) + { + if (CompSettings.vesselChecks.TryGetValue("maxStacking", out float ms) && ms > 0) maxStacking = ms; + if (CompSettings.vesselChecks.TryGetValue("maxPartCount", out float mpc) && mpc > 0) maxPartCount = Mathf.RoundToInt(mpc); + if (CompSettings.vesselChecks.TryGetValue("maxLtW", out float ltw) && mpc > 0) maxLtW = ltw; + if (CompSettings.vesselChecks.TryGetValue("maxTWR", out float twr) && mpc > 0) maxTWR = twr; + if (CompSettings.vesselChecks.TryGetValue("maxMass", out float m) && m > 0) maxMass = m; + if (CompSettings.vesselChecks.TryGetValue("maxEngines", out float me) && me != 999) maxEngines = Mathf.RoundToInt(me); + } + if (CompSettings.CompPriceChecksEnabled && CompSettings.vesselChecks.TryGetValue("pointBuyBudget", out float pb) && pb > 0) pointBuyBudget = Mathf.RoundToInt(pb); + } + } + + private void OnEditorShipModifiedEvent(ShipConstruct data) + { + if (data is null) return; + delayedRefreshVisuals = true; + if (!delayedRefreshVisualsInProgress) + StartCoroutine(DelayedRefreshVisuals(data)); + } + + private bool delayedRefreshVisuals = false; + private bool delayedRefreshVisualsInProgress = false; + IEnumerator DelayedRefreshVisuals(ShipConstruct ship) + { + delayedRefreshVisualsInProgress = true; + 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 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}")); + if (BDArmorySettings.DEBUG_ARMOR) Debug.LogWarning($"[BDArmory.BDAEditorArmorWindow]: Ship HP failed to settle within {countLimit} frames.{(string.IsNullOrEmpty(reason) ? "" : $" {reason}")}"); + } + delayedRefreshVisualsInProgress = false; + + if (showArmorWindow) + { + if (!shipModifiedfromCalcArmor) + { + CalcArmor = true; + } + if (Visualizer || HPvisualizer || HullVisualizer || LiftVisualizer || TreeVisualizer) + { + refreshVisualizer = true; + refreshHPvisualizer = true; + refreshHullvisualizer = true; + refreshLiftvisualizer = true; + refreshTreevisualizer = true; + } + shipModifiedfromCalcArmor = false; + CalculateArmorMass(); + + var oldResources = vesselResources.ToHashSet(); + vesselResources.Clear(); + using (var part = EditorLogic.fetch.ship.parts.GetEnumerator()) + while (part.MoveNext()) + { + foreach (PartResource res in part.Current.Resources) + { + if (!vesselResources.Contains(res.info)) + { + vesselResources.Add(res.info); + } + } + } + var newResources = vesselResources.ToHashSet(); + newResources.ExceptWith(oldResources); // Newly added resources. + var resourceIDs = newResources.Select(res => res.id).ToHashSet(); //add all resources to VRID by default so default drymass is true drymass, until specific resouces filtered + resourceIDs.ExceptWith(vesselResourceIDs.ToHashSet()); // Only newly added resources that aren't already added to the IDs list. + if (resourceIDs.Count > 0) + vesselResourceIDs.AddRange(resourceIDs); + + if (!FerramAerospace.hasFAR) + CalculateTotalLift(); // Re-calculate lift and wing loading on armor change + //Debug.Log("[ArmorTool] Recalculating mass/lift"); + } + DoVesselLegalityChecks(false); + } + + private void OnEditorPartPlacedEvent(Part data) + { + DoVesselLegalityChecks(true); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 78) + { + data.sameVesselCollision = true; + } + } + + private void OnDestroy() + { + GameEvents.onEditorShipModified.Remove(OnEditorShipModifiedEvent); + GameEvents.onEditorPartPlaced.Remove(OnEditorPartPlacedEvent); + GameEvents.onEditorPartDeleted.Remove(OnEditorPartPlacedEvent); + HideToolbarGUINow(); + if (toolbarButton) + { + ApplicationLauncher.Instance.RemoveModApplication(toolbarButton); + toolbarButton = null; + } + } + + void AddToolbarButton() + { + if (!HighLogic.LoadedSceneIsEditor || BDArmorySettings.LEGACY_ARMOR) return; + StartCoroutine(ToolbarButtonRoutine()); + } + IEnumerator ToolbarButtonRoutine() + { + if (toolbarButton) // Update the callbacks for the current instance. + { + toolbarButton.onTrue = ShowToolbarGUI; + toolbarButton.onFalse = HideToolbarGUI; + yield break; + } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. + Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon_Armor", false); + toolbarButton = ApplicationLauncher.Instance.AddModApplication(ShowToolbarGUI, HideToolbarGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB, buttonTexture); + } + + public void ShowToolbarGUI() + { + showArmorWindow = true; + OnEditorShipModifiedEvent(EditorLogic.fetch.ship); // Trigger updating of stuff. + } + + public void HideToolbarGUI() => StartCoroutine(HideToolbarGUIAtEndOfFrame()); + bool waitingForEndOfFrame = false; + IEnumerator HideToolbarGUIAtEndOfFrame() + { + if (waitingForEndOfFrame) yield break; + waitingForEndOfFrame = true; + yield return new WaitForEndOfFrame(); + waitingForEndOfFrame = false; + HideToolbarGUINow(); + } + void HideToolbarGUINow() + { + showArmorWindow = false; + CalcArmor = false; + Visualizer = false; + HPvisualizer = false; + HullVisualizer = false; + LiftVisualizer = false; + TreeVisualizer = false; + if (thicknessField != null && thicknessField.ContainsKey("Thickness")) thicknessField["Thickness"].tryParseValueNow(); + Visualize(); + GUIUtils.PreventClickThrough(windowRect, "BDAArmorLOCK", true); + } + + void Dummy() + { } + + void OnGUI() + { + if (showArmorWindow) + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, windowRect.position); + windowRect = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), windowRect, WindowArmor, windowTitle, BDArmorySetup.BDGuiSkin.window); + } + if (TreeVisualizer) + { + Part rootPart = EditorLogic.RootPart; + if (rootPart == null) return; + using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == rootPart) + GUIUtils.DrawTextureOnWorldPos(parts.Current.transform.position, BDArmorySetup.Instance.redDotTexture, new Vector2(48, 48), 0); + else + { + GUIUtils.DrawTextureOnWorldPos(parts.Current.transform.position, BDArmorySetup.Instance.redDotTexture, new Vector2(16, 16), 0); + Color VisualizerColor = Color.HSVToRGB(((1 - Mathf.Clamp(Mathf.Abs(Vector3.Distance(parts.Current.attPos, Vector3.zero)), 0.1f, 1)) / 1) / 3, 1, 1); + //will result in any part that has been offset more than a meter showing up with a red line + GUIUtils.DrawLineBetweenWorldPositions(parts.Current.transform.position, parts.Current.parent.transform.position, 3, VisualizerColor); + } + } + } + } + + void WindowArmor(int windowID) + { + GUIUtils.PreventClickThrough(windowRect, "BDAArmorLOCK"); + if (GUI.Button(new Rect(windowRect.width - 18, 2, 16, 16), "X")) + { + toolbarButton.SetFalse(); + } + if (CalcArmor) + { + CalcArmor = false; + SetType = false; + CalculateArmorMass(); + } + + GUIStyle style = BDArmorySetup.BDGuiSkin.label; + + if (useNumField != (useNumField = GUI.Toggle(new Rect(windowRect.width - 36, 2, 16, 16), useNumField, "#", useNumField ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { + if (!useNumField && thicknessField != null && thicknessField.ContainsKey("Thickness")) thicknessField["Thickness"].tryParseValueNow(); + } + + float line = 1.5f; + + style.fontStyle = FontStyle.Normal; + + 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; + TreeVisualizer = false; + } + } + line += 1.25f; + + + if (!BDArmorySettings.RESET_ARMOUR) + { + 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; + TreeVisualizer = false; + } + } + line += 1.25f; + } + + if (!BDArmorySettings.RESET_HULL) + { + 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; + TreeVisualizer = 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; + TreeVisualizer = false; + } + } + line += 1.25f; + } + + //if (BDArmorySettings.RUNWAY_PROJECT) + { + if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), StringUtils.Localize("#LOC_BDArmory_partTreeVisualizer"), TreeVisualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + TreeVisualizer = !TreeVisualizer; + if (TreeVisualizer) + { + Visualizer = false; + HullVisualizer = false; + HPvisualizer = false; + LiftVisualizer = false; + } + } + line += 1.25f; + } + + line += 0.25f; + + if ((refreshHPvisualizer || HPvisualizer != oldHPvisualizer) || (refreshVisualizer || Visualizer != oldVisualizer) || (refreshHullvisualizer || HullVisualizer != oldHullVisualizer) || (refreshLiftvisualizer || LiftVisualizer != oldLiftVisualizer) || (refreshTreevisualizer || TreeVisualizer != oldTreeVisualizer)) + { + Visualize(); + } + + if (!BDArmorySettings.RESET_ARMOUR) + { + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorThickness")}: {Thickness} mm", style); + line++; + if (!useNumField) + { + Thickness = GUI.HorizontalSlider(new Rect(20, line * lineHeight, 260, lineHeight), Thickness, 0, maxThickness); + //Thickness /= 5; + Thickness = Mathf.Round(Thickness); + //Thickness *= 5; + line++; + } + else + { + var field = thicknessField["Thickness"]; + field.tryParseValue(GUI.TextField(new Rect(20, line * lineHeight, 260, lineHeight), field.possibleValue, 4, field.style)); + Thickness = Mathf.Min((float)field.currentValue, maxThickness); // FIXME Mathf.Min shouldn't be necessary if the maxValue of the thicknessField has been updated for maxThickness + line++; + } + 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++; + } + 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")}:", style); + line++; + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $" - {StringUtils.Localize("#autoLOC_6001895")}: {wingLoadingWet:0.00} ({WLRatioWet:F2} kg/m2)", style); + line++; + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $" - {StringUtils.Localize("#autoLOC_6001896")}: {wingLoadingDry:0.00} ({WLRatioDry:F2} kg/m2)", style); + line++; + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorLiftStacking")}: {totalLiftStackRatio:0.0%}", style); + line++; +#if DEBUG + line += 0.5f; + 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 + } + if (!FerramAerospace.hasFAR) + { + line += 0.5f; + resourcePick = GUI.Toggle(new Rect(10, line++ * lineHeight, 280, lineHeight), resourcePick, StringUtils.Localize("#LOC_BDArmory_DryMassWhitelist"), resourcePick ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + if (resourcePick) + { + vesselResourceBoxRect.y = line * lineHeight - 2; + GUI.Box(vesselResourceBoxRect, "", BDArmorySetup.BDGuiSkin.box); // l,r,t,b = 3,3,3,3 with slight overlap of the toggle + int pos = 0; + using (var res = vesselResources.GetEnumerator()) + while (res.MoveNext()) + { + if (res.Current.density == 0) continue; //don't show massless resouces for drymass blacklist + if (res.Current.name.Contains("Intake")) continue; //don't include intake air, since that will always be present + int resID = res.Current.id; + var buttonName = res.Current.displayName.Length <= 17 ? res.Current.displayName : res.Current.displayName.Remove(14) + "..."; + if (GUI.Button(new Rect(pos % 2 == 0 ? 13 : 152f, (line + (int)(pos / 2)) * lineHeight + 1, 135, lineHeight), $"{buttonName}", vesselResourceIDs.Contains(resID) ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box)) // match BDGUIComboBox's layout + { + if (!vesselResourceIDs.Contains(resID)) + { + vesselResourceIDs.Add(resID); //resource counted as wet mass + } + else + { + vesselResourceIDs.Remove(resID); //resouce to be counted as drymass + } + CalculateTotalLift(); + } + pos++; + } + vesselResourceBoxRect.height = Mathf.CeilToInt(pos / 2f) * lineHeight + 6; + line += Mathf.CeilToInt(pos / 2f) + 0.25f; + } + } + float StatLines = 0; + float armorLines = 0; + if (!BDArmorySettings.RESET_ARMOUR) + { + line += 0.5f; + if (Thickness != oldThickness) + { + oldThickness = Thickness; + SetThickness = true; + maxThickness = 10; + thicknessField["Thickness"].maxValue = maxThickness; + CalculateArmorMass(); + } + //GUI.Label(new Rect(40, line * lineHeight, 300, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorSelect"), style); + if (!armorslist) + { + FillArmorList(); + 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) + { + armorLines += armorBox.Height / lineHeight; + } + if (selected_index != previous_index) + { + if (selected_index != -1) + { + selectedArmor = ArmorInfo.armors[selected_index].name; + SetType = true; + CalculateArmorMass(); + CalculateArmorStats(); + } + previous_index = selected_index; + CalculateArmorMass(); + } + + if (GameSettings.ADVANCED_TWEAKABLES) + { + line += 0.5f; + 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) + { + if (selectedArmor != "None") + { + 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), $"{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), $"{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), StringUtils.Localize("#LOC_BDArmory_ArmorShatterWarning"), style); + StatLines++; + } + } + if (selectedArmor != "Mild Steel" && selectedArmor != "None") + { + GUI.Label(new Rect(10, (line + armorLines + StatLines) * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_EquivalentThickness")}: {Thickness / relValue:G3} mm", style); + line++; + } + } + } + } + float HullLines = 0; + if (!BDArmorySettings.RESET_HULL) + { + line += 0.5f; + if (!hullslist) + { + 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) + { + hullmat = HullInfo.materials[selected_mat].name; + CalculateArmorMass(true); + } + previous_mat = selected_mat; + } + } + line += 0.5f; + if ((BDArmorySettings.RUNWAY_PROJECT || BDArmorySettings.COMP_CONVENIENCE_CHECKS) && (CompSettings.CompBanChecksEnabled || CompSettings.CompPriceChecksEnabled || CompSettings.CompVesselChecksEnabled)) + { + if (GUI.Button(new Rect(10, (line + armorLines + StatLines + HullLines) * lineHeight, 280, lineHeight), StringUtils.Localize("#LOC_BDArmory_checkVessel"), BDArmorySetup.ButtonStyle)) + { + DoVesselLegalityChecks(true, true); + } + line += 1.5f; + } + GUI.DragWindow(); + height = Mathf.Lerp(height, (line + armorLines + StatLines + HullLines) * lineHeight, 0.15f); + windowRect.height = height; + GUIUtils.RepositionWindow(ref windowRect); + } + + void CalculateArmorMass(bool vesselmass = false) + { + if (EditorLogic.RootPart == null) + return; + + bool modified = false; + var selectedArmorIndex = ArmorInfo.armors.FindIndex(t => t.name == selectedArmor); + if (selectedArmorIndex < 0) + return; + 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 (!vesselmass) + { + if (armor.maxSupportedArmor > maxThickness) + { + maxThickness = armor.maxSupportedArmor; + thicknessField["Thickness"].maxValue = maxThickness; + } + if (SetType || SetThickness) + { + if (SetThickness) + { + if (armor.ArmorTypeNum > 1) + { + armor.Armor = Mathf.Clamp(Thickness, 0, armor.maxSupportedArmor); + } + } + if (SetType) + { + armor.ArmorTypeNum = selectedArmorIndex + 1; + if (armor.ArmorThickness > 10) + { + if (armor.ArmorTypeNum < 2) + { + armor.ArmorTypeNum = 2; //don't set armor type "none" for armor panels + } + if (armor.maxSupportedArmor > maxThickness) + { + maxThickness = armor.maxSupportedArmor; + thicknessField["Thickness"].maxValue = maxThickness; + } + } + } + armor.ArmorModified(null, null); + modified = true; + } + 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 = HullInfo.materials.FindIndex(t => t.name == hullmat) + 1; + armor.HullModified(null, null); + modified = true; + } + + } + } + CalcArmor = false; + if ((SetType || SetThickness) && (Visualizer || HPvisualizer)) + { + refreshVisualizer = true; + } + SetType = false; + SetThickness = false; + ArmorCost = ArmorInfo.armors[selectedArmorIndex].Cost; + ArmorDensity = ArmorInfo.armors[selectedArmorIndex].Density; + ArmorDiffusivity = ArmorInfo.armors[selectedArmorIndex].Diffusivity; + ArmorDuctility = ArmorInfo.armors[selectedArmorIndex].Ductility; + 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; + + var totalMass = EditorLogic.fetch.ship.GetTotalMass(); + 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 + } + } + wingLoadingWet = totalLift / totalMass; //convert to kg/m2. 1 LiftingArea is ~ 3.51m2, or ~285kg/m2 + totalLiftArea = totalLift * 3.52f; + WLRatioWet = totalMass * 1000 / totalLiftArea; + float dMass = totalMass - EditorLogic.fetch.ship.parts.SelectMany(p => p.Resources, (p, r) => r).Where(res => vesselResourceIDs.Contains(res.info.id)).Select(res => (float)res.amount * res.info.density).Sum(); + + wingLoadingDry = totalLift / dMass; + WLRatioDry = (dMass * 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(); + if (!HighLogic.LoadedSceneIsEditor) yield break; + 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 || LiftVisualizer) + { + using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current.name.Contains("conformaldecals")) continue; + HitpointTracker a = parts.Current.GetComponent(); + if (a != null) + { + Color VisualizerColor = Color.HSVToRGB((Mathf.Clamp(a.Hitpoints, 100, 1600) / 1600) / 3, 1, 1); + if (Visualizer) + { + VisualizerColor = Color.HSVToRGB(a.ArmorTypeNum / (ArmorInfo.armors.Count + 1), (a.Armor / maxThickness), 1f); + } + if (HullVisualizer) + { + 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); + if (BDArmorySettings.MAX_PWING_LIFT > 0 && parts.Current.name.Contains("B9.Aero.Wing.Procedural") && wing.deflectionLiftCoeff > BDArmorySettings.MAX_PWING_LIFT) + { + VisualizerColor = Color.magenta; + } + } + else + VisualizerColor = Color.HSVToRGB(0, 0, 0.5f); + } + var r = parts.Current.GetComponentsInChildren(); + { + if (!a.RegisterProcWingShader && parts.Current.name.Contains("B9.Aero.Wing.Procedural")) //procwing defaultshader left null on start so current shader setup can be grabbed at visualizer runtime + { + for (int s = 0; s < r.Length; s++) + { + if (r[s].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + int key = r[s].material.GetInstanceID(); + a.defaultShader.Add(key, r[s].material.shader); + //Debug.Log("[Visualizer] " + parts.Current.name + " shader is " + r[s].material.shader.name); + if (r[s].material.HasProperty("_Color")) + { + a.defaultColor.Add(key, r[s].material.color); + } + } + a.RegisterProcWingShader = true; + } + for (int i = 0; i < r.Length; i++) + { + if (r[i].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + if (!a.defaultShader.ContainsKey(r[i].material.GetInstanceID())) continue; // Don't modify shaders that we don't have defaults for as we can't then replace them. + if (r[i].material.shader.name.Contains("Alpha")) continue; + r[i].material.shader = Shader.Find("KSP/Unlit"); + if (r[i].material.HasProperty("_Color")) + { + r[i].material.SetColor("_Color", VisualizerColor); + } + } + } + //Debug.Log("[VISUALIZER] modding shaders on " + parts.Current.name);//can confirm that procwings aren't getting shaders applied, yet they're still getting applied. + //at least this fixes the procwings widgets getting colored + } + } + } + if (!Visualizer && !HPvisualizer && !HullVisualizer && !LiftVisualizer) + { + using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts.MoveNext()) + { + HitpointTracker armor = parts.Current.GetComponent(); + if (parts.Current.name.Contains("conformaldecals")) continue; + //so, this gets called when GUI closed, without touching the hp/armor visualizer at all. + //Now, on GUI close, it runs the latter half of visualize to shut off any visualizer effects and reset stuff. + //Procs wings turn orange at this point... oh. That's why: The visualizer reset is grabbing a list of shaders and colors at *part spawn!* + //pWings use dynamic shaders to paint themselves, so it's not reapplying the latest shader /color config, but the initial one, the one from the part icon + var r = parts.Current.GetComponentsInChildren(); + if (!armor.RegisterProcWingShader && parts.Current.name.Contains("B9.Aero.Wing.Procedural")) //procwing defaultshader left null on start so current shader setup can be grabbed at visualizer runtime + { + for (int s = 0; s < r.Length; s++) + { + if (r[s].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + int key = r[s].material.GetInstanceID(); + armor.defaultShader.Add(key, r[s].material.shader); + //Debug.Log("[Visualizer] " + parts.Current.name + " shader is " + r[s].material.shader.name); + if (r[s].material.HasProperty("_Color")) + { + armor.defaultColor.Add(key, r[s].material.color); + } + } + armor.RegisterProcWingShader = true; + } + //Debug.Log("[VISUALIZER] applying shader to " + parts.Current.name); + for (int i = 0; i < r.Length; i++) + { + try + { + if (r[i].GetComponentInParent() != parts.Current) continue; // Don't recurse to child parts. + int key = r[i].material.GetInstanceID(); + if (!armor.defaultShader.ContainsKey(key)) + { + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.BDAEditorArmorWindow]: {r[i].material.name} ({key}) not found in defaultShader for part {parts.Current.partInfo.name} on {parts.Current.vessel.vesselName}"); // Enable this to see what materials aren't getting RCS shaders applied to them. + continue; + } + if (r[i].material.shader != armor.defaultShader[key]) + { + if (armor.defaultShader[key] != null) + { + r[i].material.shader = armor.defaultShader[key]; + } + if (armor.defaultColor.ContainsKey(key)) + { + if (armor.defaultColor[key] != null) + { + if (parts.Current.name.Contains("B9.Aero.Wing.Procedural")) + { + r[i].material.SetColor("_MainTex", armor.defaultColor[key]); + //LayeredSpecular has _MainTex, _Emissive, _SpecColor,_RimColor, _TemperatureColor, and _BurnColor + // source: https://github.com/tetraflon/B9-PWings-Modified/blob/master/B9%20PWings%20Fork/shaders/SpecularLayered.shader + //This works.. occasionally. Sometimes it will properly reset pwing tex/color, most of the time it doesn't. need to test later + } + else + { + r[i].material.SetColor("_Color", armor.defaultColor[key]); + } + } + else + { + if (parts.Current.name.Contains("B9.Aero.Wing.Procedural")) + { + //r[i].material.SetColor("_Emissive", Color.white); + r[i].material.SetColor("_MainTex", Color.white); + } + else + { + r[i].material.SetColor("_Color", Color.white); + } + } + } + } + } + catch + { + //Debug.Log("[BDAEditorArmorWindow]: material on " + parts.Current.name + "could not find default shader/color"); + } + } + } + } + oldVisualizer = Visualizer; + oldHPvisualizer = HPvisualizer; + oldHullVisualizer = HullVisualizer; + oldLiftVisualizer = LiftVisualizer; + oldTreeVisualizer = TreeVisualizer; + refreshVisualizer = false; + refreshHPvisualizer = false; + refreshHullvisualizer = false; + } + + float priceCkeckoout = 0; + Dictionary> partLimitCheck = new Dictionary>(); + string boughtParts = ""; + string engineparts = ""; + int engineCount = 0; + string blacklistedParts = ""; + bool nonCockpitWM = false; + bool nonCockpitAI = false; + //bool nonRootCockpit = false; + bool notOnPriceList = false; + int oversizedPWings = 0; + float maxThrust = 0; + int weaponmanagers = 0; + int AIs = 0; + ScreenMessage vessellegality = new ScreenMessage("", 7.0f, ScreenMessageStyle.LOWER_CENTER); + + void DoVesselLegalityChecks(bool refreshParts, bool buttonTest = false) + { + if ((BDArmorySettings.RUNWAY_PROJECT || BDArmorySettings.COMP_CONVENIENCE_CHECKS) && (CompSettings.CompBanChecksEnabled || CompSettings.CompPriceChecksEnabled || CompSettings.CompVesselChecksEnabled)) + { + if (refreshParts) + { + priceCkeckoout = 0; + partLimitCheck.Clear(); + boughtParts = ""; + engineparts = ""; + engineCount = 0; + maxThrust = 0; + blacklistedParts = ""; + nonCockpitWM = false; + nonCockpitAI = false; + //nonRootCockpit = false; + weaponmanagers = 0; + AIs = 0; + oversizedPWings = 0; + + foreach (var part in EditorLogic.fetch.ship.Parts) //grab a list of parts and their quantity + { + if (partLimitCheck.TryGetValue(part.name, out var qty)) + qty.Add(part); + else + partLimitCheck.Add(part.name, new List { part }); + } + //begin evaluation + if (CompSettings.CompBanChecksEnabled) //do we have more limited parts than allowed? + { + foreach (var part in CompSettings.partBlacklist) + { + string partName = part.Key; + int listedpartCount = 0; + if (partName.Contains("*")) + { + partName = partName.Trim('*'); + + foreach (var kvp in partLimitCheck) + { + if (kvp.Key.Contains(partName)) + listedpartCount += kvp.Value.Count; + } + } + else + if (partLimitCheck.TryGetValue(part.Key, out var qty)) + listedpartCount = qty.Count; + if (CompSettings.partBlacklist.TryGetValue(part.Key, out float bQ)) + { + if (bQ >= 0 && listedpartCount > bQ) + { + if (!string.IsNullOrEmpty(blacklistedParts)) blacklistedParts += " | "; + blacklistedParts += $"{partName} parts({listedpartCount}/{bQ})"; //is the part on the black list? if so, add to string for messaging illegal parts + } + if (bQ < 0 && listedpartCount < Mathf.Abs(bQ)) + { + if (!string.IsNullOrEmpty(blacklistedParts)) blacklistedParts += " | "; + blacklistedParts += $"{partName} missing({listedpartCount}/{Mathf.Abs(bQ)})"; //is the part on the white list? if so, add to string for messaging missing parts + } + } + } + } + //could just eval the placed part, but that doesn't cover symmetry or subassumblies + foreach (var kvp in partLimitCheck) + { + notOnPriceList = false; + + if (CompSettings.CompPriceChecksEnabled && pointBuyBudget > 0)// budget check + { + if (CompSettings.partPointCosts.TryGetValue(kvp.Key, out float pb)) //if the part is in the pricing list + { + if (!string.IsNullOrEmpty(boughtParts)) boughtParts += " | "; + boughtParts += $"{kvp.Value.Count}x {kvp.Value[0].partInfo.title}({kvp.Value.Count * pb})"; //make a note for later + priceCkeckoout += (kvp.Value.Count * pb); //and tally total budget spent so far + } + else + { + notOnPriceList = true; + } + } + foreach (var partModule in kvp.Value[0].Modules) //weapon whitelist/engine count + { + if (partModule == null) continue; + switch (partModule.moduleName) + { + case "ModuleEngines": + case "ModuleEnginesFX": + { + if (engineparts.Contains(kvp.Value[0].partInfo.title)) break; //don't grab both moduleEngines for dual-mode engines and double-count them + if (CompSettings.CompVesselChecksEnabled && maxEngines < 999 || maxTWR > 0) + { + if (maxEngines < 999) + { + if (!string.IsNullOrEmpty(engineparts)) engineparts += " | "; + engineparts += $"{kvp.Value.Count}x {kvp.Value[0].partInfo.title}"; + engineCount += kvp.Value.Count; + Debug.Log($"[VesselCheckDebug] found {kvp.Value.Count} {kvp.Value[0].partInfo.title}"); + } + if (maxTWR > 0) maxThrust += (kvp.Value[0].FindModuleImplementing().maxThrust * kvp.Value[0].FindModuleImplementing().thrustPercentage) * kvp.Value.Count; + } + break; + } + case "ModuleWeapon": + case "MissileBase": + case "MissileLauncher": + { + if (pointBuyBudget > 0 && notOnPriceList) //if a weapon isn't on the price list, it's banned + { + if (!string.IsNullOrEmpty(blacklistedParts)) blacklistedParts += " | "; + blacklistedParts += $"{kvp.Value[0].partInfo.title}({kvp.Value.Count}/0)"; + } + break; + } + case "MissileFire": + { + weaponmanagers += kvp.Value.Count; + if (weaponmanagers > 1) //only 1 WM per vessel. TODO - remember to change this out if Doc ever gets mothership sub-WMs implemented fully + { + if (!string.IsNullOrEmpty(blacklistedParts)) blacklistedParts += " | "; + blacklistedParts += $"{kvp.Value[0].partInfo.title}(WMs: {kvp.Value.Count}/1)"; + } + /* + if (kvp.Value[0].parent != EditorLogic.fetch.ship.Parts[0] || kvp.Value[0] != EditorLogic.fetch.ship.Parts[0]) + { + nonCockpitWM = true; + } + */ + var isChair = kvp.Value[0].FindModuleImplementing(); + if (isChair != null) + { + break; + } + ModuleCommand AIParent = null; + if (kvp.Value[0].parent) AIParent = kvp.Value[0].parent.FindModuleImplementing(); + if (AIParent == null) + { + nonCockpitWM = true; + } + break; + } + case "BDModulePilotAI": + case "BDModuleSurfaceAI": + case "BDModuleVTOLAI": + case "BDModuleOrbitalAI": + { + AIs += kvp.Value.Count; + if (AIs > 1) //only 1 WM per vessel. TODO - remember to change this out if Doc ever gets mothership sub-WMs implemented fully + { + if (!string.IsNullOrEmpty(blacklistedParts)) blacklistedParts += " | "; + blacklistedParts += $"{kvp.Value[0].partInfo.title}(AI: {kvp.Value.Count}/1)"; + } + //editorLogic.fetch.ship.parts[0] doesn't account for re-rooting the craft. fetch.ship also doesn't support .rootpart + //if (kvp.Value[0].parent != EditorLogic.fetch.ship.Parts[0] || kvp.Value[0] != EditorLogic.fetch.ship.Parts[0]) + var isChair = kvp.Value[0].FindModuleImplementing(); + if (isChair != null) + { + break; + } + ModuleCommand AIParent = null; + if (kvp.Value[0].parent) AIParent = kvp.Value[0].parent.FindModuleImplementing(); + if (AIParent == null) + { + nonCockpitAI = true; + } + break; + } + case "ModuleCommand": + { + int crewCount = kvp.Value[0].FindModuleImplementing().minimumCrew; + if (crewCount <= 0) + { + if (!string.IsNullOrEmpty(blacklistedParts)) blacklistedParts += " | "; + blacklistedParts += $"{kvp.Value[0].partInfo.title}(Probecore: {kvp.Value.Count}/0)"; + } + /* + if (kvp.Value[0] != EditorLogic.fetch.ship.Parts[0]) + { + nonRootCockpit = true; + } + */ + break; + } + } + } + } + if (BDArmorySettings.MAX_PWING_LIFT > 0) + { + foreach (var part in EditorLogic.fetch.ship.Parts) //not ideal, but this needs to fire onVesselModified, not onPartPlaced, but linking this to Visualizer's parts eval only updates when that does + { + if (part.name.Contains("B9.Aero.Wing.Procedural.Type")) + { + ModuleLiftingSurface wing = part.GetComponent(); + if (wing != null && wing.deflectionLiftCoeff > 0f) + { + if (wing.deflectionLiftCoeff > BDArmorySettings.MAX_PWING_LIFT) + oversizedPWings++; + } + } + } + } + } + StringBuilder evaluationstring = new StringBuilder(); + if (CompSettings.CompVesselChecksEnabled) + { + CalculateTotalLift(); //update wing lading/lift stack values if GUI not open + if (maxPartCount > 0 && EditorLogic.fetch.ship.Parts.Count > maxPartCount) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolPartCount")} ({EditorLogic.fetch.ship.Parts.Count}/{maxPartCount})"); //"Part count exceeded!" + if (engineCount > 0) + { + if (maxEngines >= 0 && engineCount > maxEngines) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolEngineCount")} ({engineCount}/{maxEngines}) - {engineparts}"); //Too Many Engines:" + if (maxEngines < 0 && engineCount < Mathf.Abs(maxEngines)) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolEngineCountFloor")} ({engineCount}/{Mathf.Abs(maxEngines)})"); //"Too Few Engines:" + } + if (maxTWR > 0 && Math.Round(((maxThrust / (PhysicsGlobals.GravitationalAcceleration * FlightGlobals.GetHomeBody().GeeASL) * EditorLogic.fetch.ship.GetTotalMass())), 2) > maxLtW) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolTWR")} {Math.Round(maxThrust / (EditorLogic.fetch.ship.GetTotalMass() * (PhysicsGlobals.GravitationalAcceleration * FlightGlobals.GetHomeBody().GeeASL)), 2)}/{maxTWR}"); //"TWR Exceeded:" + if (maxLtW > 0 && wingLoadingWet > maxLtW) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolLTW")} {wingLoadingWet}/{maxLtW}"); //"LTW Exceeded:" + if (maxStacking > 0 && totalLiftStackRatio * 100 > maxStacking) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorLiftStacking")}: {Mathf.RoundToInt(totalLiftStackRatio * 100)}/{maxStacking}%"); //"Lift Stacking" + if (maxMass > 0 && EditorLogic.fetch.ship.GetTotalMass() > maxMass) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolMaxMass")} {EditorLogic.fetch.ship.GetTotalMass()}/{maxMass}"); //"Maxx Limit Exceeded:" + //max Dimensions? + } + if (CompSettings.CompPriceChecksEnabled && pointBuyBudget > 0) + { + if (priceCkeckoout > pointBuyBudget) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolMaxPoints")} ({priceCkeckoout}/{pointBuyBudget}) - {boughtParts}"); //Point Limit Exceeded: + } + if (CompSettings.CompVesselChecksEnabled || CompSettings.CompBanChecksEnabled) + { + if (!string.IsNullOrEmpty(blacklistedParts)) + evaluationstring.AppendLine($"{StringUtils.Localize("#LOC_BDArmory_ArmorToolIllegalParts")} - {blacklistedParts}"); //"Illegal Parts:" + } + + if (nonCockpitAI || nonCockpitWM) // || nonRootCockpit) + { + string commandStatus = ""; + if (nonCockpitAI) commandStatus += StringUtils.Localize("#LOC_BDArmory_Settings_DebugAI"); //"AI" + if (nonCockpitWM) + { + if (!string.IsNullOrEmpty(commandStatus)) commandStatus += ", "; + commandStatus += StringUtils.Localize("#LOC_BDArmory_WMWindow_title"); //"BDA Weapon Manager" + } + commandStatus += $" {(StringUtils.Localize("#LOC_BDArmory_ArmorToolNonCockpit"))}"; //"not attached to cockpit" + //if (nonRootCockpit) + //{ + // commandStatus += ", which is not a cockpit."; + //} + evaluationstring.AppendLine(commandStatus); + } + + if (buttonTest) + { + if (evaluationstring.Length == 0) + evaluationstring.AppendLine(StringUtils.Localize("#LOC_BDArmory_ArmorToolVesselLegal")); //"Vessel Legal!" + } + if (oversizedPWings > 0) + evaluationstring.AppendLine($"{oversizedPWings} {StringUtils.Localize("#LOC_BDArmory_ArmorToolOversizedPWings")}"); //"pWings exceedeing max Lift - check Lift Visualize" + ScreenMessages.RemoveMessage(vessellegality); + vessellegality.textInstance = null; + vessellegality.message = evaluationstring.ToString(); + vessellegality.style = ScreenMessageStyle.UPPER_CENTER; + + ScreenMessages.PostScreenMessage(vessellegality); + //todo - draw a GUI line to each illegal part? + } + } + + private void CalculateArmorStats() + { + if (selectedArmor == "Mild Steel") + { + relValue = 1; + } + else + { + /*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, 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 = BDAMath.RoundToUnit(armorValue / steelValue, 0.1f); + exploValue = ArmorStrength * (1 + ArmorDuctility) * (ArmorDensity / 1000); + } + } + } +} \ No newline at end of file diff --git a/BDArmory/UI/BDAEditorCategory.cs b/BDArmory/UI/BDAEditorCategory.cs index 3e7cde7de..c03802c5d 100644 --- a/BDArmory/UI/BDAEditorCategory.cs +++ b/BDArmory/UI/BDAEditorCategory.cs @@ -1,13 +1,22 @@ -using System.Collections; +using System; using System.Collections.Generic; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.CounterMeasure; -using BDArmory.Modules; +using System.Collections; +using System.IO; +using System.Linq; 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)] @@ -19,6 +28,8 @@ public class BDAEditorCategory : MonoBehaviour public const string AutoBDACategoryKey = "autobdacategory"; public const int SubcategoryGroup = 412440121; + static readonly string customCategoriesConfigURL = "GameData/BDArmory/PluginData/CustomCategories.cfg"; + /// /// Adding to this dictionary before the category buttons are created will add more bda categories. /// @@ -81,13 +92,14 @@ public class BDAEditorCategory : MonoBehaviour private void Awake() { Instance = this; + LoadCustomCategories(); bool partsDetected = false; using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) while (parts.MoveNext()) { 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); @@ -96,11 +108,20 @@ private void Awake() } // Part autocategorization if (partsDetected) + { + // Set our category to 1 more than the highest category enum value. Note: this breaks type safety for this enum, but doesn't seem to cause issues. + PartCategories BDAPartCategory = (PartCategories)(Enum.GetValues(typeof(PartCategories)).Cast().Max() + 1); + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) while (parts.MoveNext()) { if (parts.Current.partConfig == null || parts.Current.partPrefab == null) continue; + if (parts.Current.TechHidden || parts.Current.TechRequired == "Unresearchable") + { + parts.Current.partConfig.RemoveValue(BDACategoryKey); + continue; + } if (parts.Current.partConfig.HasValue(BDACategoryKey)) parts.Current.partConfig.AddValue(AutoBDACategoryKey, parts.Current.partConfig.GetValue(BDACategoryKey)); else @@ -144,7 +165,7 @@ private void Awake() else if (parts.Current.partPrefab.FindModuleImplementing() != null) { parts.Current.partConfig.AddValue(AutoBDACategoryKey, "Missile turrets"); - } + } else if (parts.Current.partPrefab.FindModuleImplementing() != null) { parts.Current.partConfig.AddValue(AutoBDACategoryKey, "Radars"); @@ -172,7 +193,12 @@ private void Awake() parts.Current.partConfig.AddValue(AutoBDACategoryKey, "Ammo"); } } + if (parts.Current.category == PartCategories.none && parts.Current.partConfig.HasValue(AutoBDACategoryKey)) // BDA parts that don't fall into the main KSP categories. + { + parts.Current.category = BDAPartCategory; // Set their category to our custom value so that searching works (since search ignores the "none" category). + } } + } } private void OnDestroy() @@ -180,6 +206,40 @@ private void OnDestroy() GameEvents.onGUIEditorToolbarReady.Remove(LoadBDArmoryCategory); } + void LoadCustomCategories() + { + var configURL = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, customCategoriesConfigURL)); + var fileNode = ConfigNode.Load(configURL); + if (fileNode == null) // If the file doesn't exist, create it, but don't overwrite it if it does exist. + { + fileNode = new ConfigNode(); + if (!Directory.GetParent(configURL).Exists) + { Directory.GetParent(configURL).Create(); } + fileNode.AddNode("CustomCategories"); + fileNode.Save(configURL); + } + if (!fileNode.HasNode("CustomCategories")) + { + fileNode.AddNode("CustomCategories"); + } + string customCategoriesComment = "Add custom BDA subcategories using the format: CategoryName = CategoryIconPath (e.g., Misc = BDArmory/Textures/Misc). Only non-preexisting categories will be added. Empty categories won't be shown."; + ConfigNode customCategories = fileNode.GetNode("CustomCategories"); + customCategories.comment = customCategoriesComment; + foreach (ConfigNode.Value category in customCategories.values) + { + // Note: we use CategoryIcons for the check as Categories gets pared down depending on the parts actually available. + if (CategoryIcons.ContainsKey(category.name)) continue; + Categories.Add(category.name); + if (!GameDatabase.Instance.ExistsTexture(category.value)) + { + Debug.LogWarning($"[BDArmory.BDAEditorCategory]: Icon for {category.name} not found at {category.value}. Using default icon."); + category.value = "BDArmory/Textures/icon"; + } + Debug.Log($"[BDArmory.BDAEditorCategory]: Adding category {category.name} with icon {category.value}."); + CategoryIcons.Add(category.name, category.value); + } + } + public static string GetTexturePath(string category) { if (CategoryIcons.TryGetValue(category, out string value)) @@ -254,8 +314,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() @@ -269,6 +329,11 @@ private void CreateBDAPartBar() { if (parts.Current == null || !parts.Current.partPrefab || parts.Current.partConfig == null) continue; + if (parts.Current.TechHidden || parts.Current.TechRequired == "Unresearchable") + { + parts.Current.partConfig.RemoveValue(BDACategoryKey); + continue; + } string cat = ""; if (parts.Current.partConfig.TryGetValue(BDArmorySettings.AUTOCATEGORIZE_PARTS ? AutoBDACategoryKey : BDACategoryKey, ref cat)) { @@ -278,7 +343,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"); @@ -293,7 +358,8 @@ private void CreateBDAPartBar() BDAPartBar = BDAPartBarContainer.AddComponent(); BDAPartBar.name = "BDAPartBar"; BDAPartBarContainer.transform.SetParent(PartCategorizer.Instance.transform, false); - BDAPartBar.anchoredPosition = EditorPanels.Instance.partsEditorModes.panelTransform.anchoredPosition + new Vector2(-212, -126); + BDAPartBar.anchoredPosition = EditorPanels.Instance.partsEditorModes.panelTransform.anchoredPosition;// + new Vector2(-212, -126); + var panelTop = EditorPanels.Instance.partsEditorModes.transform.position.y - 1; // BDA part category bar background // DOESN'T WORK, NOTHING WORKS. :( @@ -324,7 +390,8 @@ private void CreateBDAPartBar() button.btnToggleGeneric.onTrueBtn.RemoveAllListeners(); button.btnToggleGeneric.SetGroup(412440121); button.transform.SetParent(BDAPartBar, false); - button.transform.position = new Vector3(BDACategory.button.transform.position.x + 34, 424, 750) + button_offset * SubcategoryButtons.Count; + // button.transform.position = new Vector3(BDACategory.button.transform.position.x + 34, 424, 750) + button_offset * SubcategoryButtons.Count; + button.transform.position = new Vector3(BDACategory.button.transform.position.x + 34 * GameSettings.UI_SCALE, panelTop - button_offset.y, 750) + button_offset * SubcategoryButtons.Count; categorizer_button.DeleteSubcategory(); SubcategoryButtons.Add(button); // Gotta use a saved value, because the enumerator changes the value during the run @@ -347,7 +414,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": { @@ -386,12 +453,12 @@ void OnGUI() bool shouldOpen = BDArmorySettings.SHOW_CATEGORIES && FilterByFunctionCategory.button.activeButton.Value && BDACategory.button.activeButton.Value; if (shouldOpen && !expanded) { - ExpandPartSelector(offset); + ExpandPartSelector(offset * GameSettings.UI_SCALE); expanded = true; } else if (!shouldOpen && expanded) { - ExpandPartSelector(-offset); + ExpandPartSelector(-offset * GameSettings.UI_SCALE); expanded = false; } @@ -403,6 +470,7 @@ void OnGUI() } if (SettingsOpen) { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, SettingsWindow.position); SettingsWindow = GUI.Window(9476026, SettingsWindow, DrawSettingsWindow, "", BDArmorySetup.BDGuiSkin.window); } diff --git a/BDArmory/UI/BDATargetManager.cs b/BDArmory/UI/BDATargetManager.cs index bd26926ab..c0ed224c2 100644 --- a/BDArmory/UI/BDATargetManager.cs +++ b/BDArmory/UI/BDATargetManager.cs @@ -1,17 +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 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 KSP.UI.Screens; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; namespace BDArmory.UI { @@ -22,17 +27,22 @@ public class BDATargetManager : MonoBehaviour private static Dictionary> GPSTargets; public static List ActiveLasers; public static List FiredMissiles; + public static List FiredBullets; + public static List FiredRockets; 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; - public static bool hasAddedButton; + static string gpsTargetsCfg; void Awake() { + gpsTargetsCfg = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/gpsTargets.cfg")); GameEvents.onGameStateLoad.Add(LoadGPSTargets); GameEvents.onGameStateSave.Add(SaveGPSTargets); LoadedBuildings = new List(); @@ -62,6 +72,7 @@ void OnDestroy() GameEvents.onVesselGoOffRails.Remove(AddVessel); GameEvents.onVesselCreate.Remove(AddVessel); GameEvents.onVesselDestroy.Remove(CleanVesselList); + DestructibleBuilding.OnLoaded.Remove(AddBuilding); } void Start() @@ -79,9 +90,8 @@ void Start() ActiveLasers = new List(); FiredMissiles = new List(); - - //AddToolbarButton(); - StartCoroutine(ToolbarButtonRoutine()); + FiredBullets = new List(); + FiredRockets = new List(); } public static List GPSTargetList(BDTeam team) @@ -129,55 +139,23 @@ void CleanVesselList(Vessel v) LoadedVessels.RemoveAll(ves => ves.loaded == false); } - void AddToolbarButton() - { - if (HighLogic.LoadedSceneIsFlight) - { - if (!hasAddedButton) - { - Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon", false); - ApplicationLauncher.Instance.AddModApplication(ShowToolbarGUI, HideToolbarGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.FLIGHT, buttonTexture); - hasAddedButton = true; - } - } - } - - IEnumerator ToolbarButtonRoutine() - { - if (hasAddedButton) yield break; - if (!HighLogic.LoadedSceneIsFlight) yield break; - while (!ApplicationLauncher.Ready) - { - yield return null; - } - - AddToolbarButton(); - } - - public void ShowToolbarGUI() - { - BDArmorySetup.windowBDAToolBarEnabled = true; - } - - public void HideToolbarGUI() - { - BDArmorySetup.windowBDAToolBarEnabled = false; - } - - void Dummy() - { } - 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) @@ -214,12 +192,12 @@ public static void RegisterLaserPoint(ModuleTargetingCamera cam) /// Gets the laser target painter with the least angle off boresight. Set the missileBase as the reference missilePosition. /// /// The laser target painter. - public static ModuleTargetingCamera GetLaserTarget(MissileBase ml, bool parentOnly) + public static ModuleTargetingCamera GetLaserTarget(MissileBase ml, bool parentOnly, BDTeam team) { - return GetModuleTargeting(parentOnly, ml.GetForwardTransform(), ml.MissileReferenceTransform.position, ml.maxOffBoresight, ml.vessel, ml.SourceVessel); + return GetModuleTargeting(parentOnly, ml.GetForwardTransform(), ml.MissileReferenceTransform.position, ml.maxOffBoresight, ml.vessel, ml.SourceVessel, team); } - private static ModuleTargetingCamera GetModuleTargeting(bool parentOnly, Vector3 missilePosition, Vector3 position, float maxOffBoresight, Vessel vessel, Vessel sourceVessel) + private static ModuleTargetingCamera GetModuleTargeting(bool parentOnly, Vector3 missilePosition, Vector3 position, float maxOffBoresight, Vessel vessel, Vessel sourceVessel, BDTeam team) { ModuleTargetingCamera finalCam = null; float smallestAngle = 360; @@ -227,14 +205,18 @@ private static ModuleTargetingCamera GetModuleTargeting(bool parentOnly, Vector3 while (cam.MoveNext()) { if (cam.Current == null) continue; + var wm = cam.Current.WeaponManager; + if (cam.Current == null || wm == null) continue; + if (wm.Team != team) continue; if (parentOnly && !(cam.Current.vessel == vessel || cam.Current.vessel == sourceVessel)) continue; if (!cam.Current.cameraEnabled || !cam.Current.groundStabilized || !cam.Current.surfaceDetected || cam.Current.gimbalLimitReached) continue; - float angle = Vector3.Angle(missilePosition, cam.Current.groundTargetPosition - position); + float angle = VectorUtils.Angle(missilePosition, cam.Current.groundTargetPosition - position); + float tgtRadius = Mathf.Max(wm.currentTarget ? wm.currentTarget.Vessel.GetRadius() : 20, 20); if (!(angle < maxOffBoresight) || !(angle < smallestAngle) || !CanSeePosition(cam.Current.groundTargetPosition, vessel.transform.position, - (vessel.transform.position + missilePosition))) continue; + (vessel.transform.position + missilePosition), tgtRadius)) continue; smallestAngle = angle; finalCam = cam.Current; @@ -243,9 +225,9 @@ private static ModuleTargetingCamera GetModuleTargeting(bool parentOnly, Vector3 return finalCam; } - public static bool CanSeePosition(Vector3 groundTargetPosition, Vector3 vesselPosition, Vector3 missilePosition) - { - if ((groundTargetPosition - vesselPosition).sqrMagnitude < Mathf.Pow(20, 2)) + public static bool CanSeePosition(Vector3 groundTargetPosition, Vector3 vesselPosition, Vector3 missilePosition, float threshold) + { + if ((groundTargetPosition - vesselPosition).sqrMagnitude < 400) // 20 * 20 { return false; } @@ -254,9 +236,14 @@ 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, 557057)) + if (Physics.Raycast(ray, out rayHit, dist, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels))) { - if ((rayHit.point - groundTargetPosition).sqrMagnitude < 200) + bool pCheck = false; + Part p = rayHit.collider.GetComponentInParent(); + if (p && p.vessel && (p.vessel.CoM - groundTargetPosition).sqrMagnitude < 100) + pCheck = true; + + if ((rayHit.point - groundTargetPosition).sqrMagnitude < (pCheck ? threshold * threshold : 200)) //200 is a max vessel width of 14m. Trivially easy to exceed, even in pure Stock, which would prevent tgtCams from seeing said large vessel { return true; } @@ -275,45 +262,182 @@ 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 = float.PositiveInfinity; + 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 thisSqrdistance = VectorUtils.SqrDist(part.Current.transform.position, sensorPosition); + if (distance > thisSqrdistance) + { + distance = thisSqrdistance; + closestPart = part.Current; + } + } + IRPart = closestPart; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.BDATargetManager] closest heatsource found: " + closestPart.name + ", heat: " + (float)(closestPart.thermalInternalFluxPrevious + closestPart.skinTemperature)); + } + if (closestPart != null) + { + TargetInfo tInfo; + distance = BDAMath.Sqrt(distance); + if (tInfo = v.gameObject.GetComponent()) + { + if (tInfo.isMissile) + { + heatScore = tInfo.MissileBaseModule.MissileState == MissileBase.MissileStates.Boost ? 1500 : tInfo.MissileBaseModule.MissileState == MissileBase.MissileStates.Cruise ? 1000 : minHeat; //make missiles actually return a heatvalue unless post thrust + heatScore = Mathf.Max(heatScore, minHeat * frontAspectModifier); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.BDATargetManager] missile heatScore: " + heatScore); + return new Tuple(heatScore, IRPart); + } + 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 = GetOccludedSensorScore(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 = GetOccludedSensorScore(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("[BDArmory.BDATargetManager] final heatSignature: " + heatScore); + return new Tuple(heatScore, IRPart); + } + static float GetOccludedSensorScore(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, bool occludeHeat = true) + { + 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 + if (occludeHeat) 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("[BDArmory.BDATargetManager] 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 + if ((OcclusionFactor > 0) || propEngine) heatScore *= frontAspectModifier; //enginePlume getting assigned frontAspectMod regardless of orientation? if outside 50deg exhaust cone would already have an OcclusioNFactor > 0 return heatScore; } /// /// Find a flare closest in heat signature to passed heat signature /// - public static TargetSignatureData GetFlareTarget(Ray ray, float scanRadius, float highpassThreshold, bool allAspect, float heatSignature, float biasLevel) + public static TargetSignatureData GetFlareTarget(Ray ray, float scanRadius, float highpassThreshold, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, FloatCurve lockedSensorVelocityMagnitudeBias, float lockedSensorMinAngularVelocity, TargetSignatureData heatTarget, Vector3 heatTargetAngularVel, float heatTargetAngularVelMag) { TargetSignatureData flareTarget = TargetSignatureData.noTarget; + float heatSignature = heatTarget.signalStrength; float bestScore = 0f; + Vector3 down = -VectorUtils.GetUpDirection(ray.origin); + using (List.Enumerator flare = BDArmorySetup.Flares.GetEnumerator()) while (flare.MoveNext()) { if (!flare.Current) continue; - - float angle = Vector3.Angle(flare.Current.transform.position - ray.origin, ray.direction); + Vector3 relativePosFlare = flare.Current.transform.position - ray.origin; + float angle = VectorUtils.Angle(relativePosFlare, ray.direction); if (angle < scanRadius) { + float score = flare.Current.thermal * Mathf.Clamp01(15 / angle); // Reduce score on anything outside 15 deg of look ray // Add bias targets closer to center of seeker FOV - score *= Mathf.Clamp(-1f * ((biasLevel - 1f) / (scanRadius * scanRadius)) * angle * angle + biasLevel, 1f, biasLevel); // Equal to biasLevel for angle==0, 1 for angle==scanRadius + score *= GetSeekerBias(angle, Vector3.Cross(relativePosFlare, flare.Current.velocity) / relativePosFlare.sqrMagnitude, heatTargetAngularVel, heatTargetAngularVelMag, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity); - score *= (1400 * 1400) / Mathf.Clamp((flare.Current.transform.position - ray.origin).sqrMagnitude, 90000, 36000000); - score *= Mathf.Clamp(Vector3.Angle(flare.Current.transform.position - ray.origin, -VectorUtils.GetUpDirection(ray.origin)) / 90, 0.5f, 1.5f); + score *= (1400 * 1400) / Mathf.Clamp(relativePosFlare.sqrMagnitude, 90000, 36000000); + score *= Mathf.Clamp(VectorUtils.Angle(relativePosFlare, down) / 90, 0.5f, 1.5f); if (BDArmorySettings.DUMB_IR_SEEKERS) // Pick the hottest flare hotter than heatSignature { @@ -337,12 +461,78 @@ public static TargetSignatureData GetFlareTarget(Ray ray, float scanRadius, floa return flareTarget; } - public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel missileVessel, Ray ray, float priorHeatScore, float scanRadius, float highpassThreshold, bool allAspect, MissileFire mf = null, bool favorGroundTargets = false) + public static TargetSignatureData GetDecoyTarget(Ray ray, float scanRadius, float highpassThreshold, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, FloatCurve lockedSensorVelocityMagnitudeBias, float lockedSensorMinAngularVelocity, TargetSignatureData noiseTarget, Vector3 noiseTargetAngularVel, float noiseTargetAngularVelMag) + { + TargetSignatureData decoyTarget = TargetSignatureData.noTarget; + float AcousticSignature = noiseTarget.signalStrength; + float bestScore = 0f; + + using (List.Enumerator decoy = BDArmorySetup.Decoys.GetEnumerator()) + while (decoy.MoveNext()) + { + if (!decoy.Current) continue; + + Vector3 relativePosDecoy = decoy.Current.transform.position - ray.origin; + float angle = VectorUtils.Angle(relativePosDecoy, ray.direction); + if (angle < scanRadius) + { + float score = decoy.Current.acousticSig * Mathf.Clamp01(15 / angle); // Reduce score on anything outside 15 deg of look ray + + // Add bias targets closer to center of seeker FOV + score *= GetSeekerBias(angle, Vector3.Cross(relativePosDecoy, decoy.Current.velocity) / relativePosDecoy.sqrMagnitude, noiseTargetAngularVel, noiseTargetAngularVelMag, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity); + + score *= (1400 * 1400) / Mathf.Clamp(relativePosDecoy.sqrMagnitude, 90000, 36000000); + score *= Mathf.Clamp(VectorUtils.Angle(relativePosDecoy, -VectorUtils.GetUpDirection(ray.origin)) / 90, 0.5f, 1.5f); + + if (BDArmorySettings.DUMB_IR_SEEKERS) // Pick the hottest flare hotter than heatSignature + { + if ((score > AcousticSignature) && (score > bestScore)) + { + decoyTarget = new TargetSignatureData(decoy.Current, score); + bestScore = score; + } + } + else + { + if ((score > 0f) && (Mathf.Abs(score - AcousticSignature) < Mathf.Abs(bestScore - AcousticSignature))) // Pick the closest flare to target + { + decoyTarget = new TargetSignatureData(decoy.Current, score); + bestScore = score; + } + } + } + } + + return decoyTarget; + } + + public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel missileVessel, Ray ray, TargetSignatureData priorHeatTarget, float scanRadius, float highpassThreshold, float frontAspectHeatModifier, bool uncagedLock, bool targetCoM, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, FloatCurve lockedSensorVelocityMagnitudeBias, float lockedSensorMinAngularVelocity, MissileFire mf = null, TargetInfo desiredTarget = null, bool IFF = true) { - float minMass = 0.05f; //otherwise the RAMs have trouble shooting down incoming missiles - float biasLevel = 1.2f; // Bias level for targets/flares closer to seeker centerline + float minMass = missileVessel.InNearVacuum() ? 0f : 0.05f; // FIXME, RAMs need min mass of 0.05, but orbital KKVs mass < 0.05 + + bool priorHeatTargetExists = priorHeatTarget.exists; + TargetSignatureData finalData = TargetSignatureData.noTarget; float finalScore = 0; + float priorHeatScore = priorHeatTarget.signalStrength; // Technically should be gated behind exists, but with us mis-using signalStrength to represent RWR values, and RWR.none being -1 this is fine + Tuple IRSig; + + Vector3 relativePosPriorHeatTarget; + Vector3 priorHeatTargetAngularVel; + float priorHeatTargetAngularVelMag; + + if (priorHeatTargetExists) + { + relativePosPriorHeatTarget = priorHeatTarget.position - ray.origin; + priorHeatTargetAngularVel = Vector3.Cross(relativePosPriorHeatTarget, priorHeatTarget.velocity) / relativePosPriorHeatTarget.sqrMagnitude; + priorHeatTargetAngularVelMag = priorHeatTargetAngularVel.magnitude; + } + else + { + relativePosPriorHeatTarget = Vector3.zero; + priorHeatTargetAngularVel = Vector3.zero; + priorHeatTargetAngularVelMag = 0f; + } foreach (Vessel vessel in LoadedVessels) { @@ -352,35 +542,38 @@ 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] {missileVessel.GetName()} 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; - + { + if (mf != null) + { + tInfo = vessel.gameObject.AddComponent(); + } + else + continue; //return finalData; //shouldn't this be continue, so a non-target, non-WM vessel doesn't prevent scanning viable vessels in the area? + } // 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))) + !(mf && tInfo && tInfo.isMissile && (tInfo.MissileBaseModule.MissileState == MissileBase.MissileStates.Boost || tInfo.MissileBaseModule.MissileState == MissileBase.MissileStates.Cruise))) { if (vessel.GetTotalMass() < minMass) - { continue; - } } - // Abort if target is friendly. if (mf != null) { - if (mf.Team.IsFriendly(tInfo.Team)) + if (IFF && mf.Team.IsFriendly(tInfo.Team)) continue; } - // Abort if target is a missile that we've shot if (tInfo.isMissile) { @@ -388,37 +581,37 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss continue; } - float angle = Vector3.Angle(vessel.CoM - ray.origin, ray.direction); - if (angle < scanRadius) + Vector3 relativePosVessel = vessel.CoM - ray.origin; + //float angle = VectorUtils.Angle(vessel.CoM - ray.origin, ray.direction); at very close ranges for very narrow sensor Fovs this will cause a problem if the heatsource is an engine plume + float angle = VectorUtils.Angle((priorHeatTargetExists && priorHeatTarget.vessel == vessel) ? relativePosPriorHeatTarget : relativePosVessel, ray.direction); + if ((angle < scanRadius) || (uncagedLock && !priorHeatTargetExists)) // Allow allAspect=true missiles to find target outside of seeker FOV before launch { - if (RadarUtils.TerrainCheck(ray.origin, vessel.transform.position)) + if (RadarUtils.TerrainCheck(ray.origin, vessel.CoM, vessel.mainBody)) continue; - if (!allAspect) + if (!uncagedLock) { - if (!Misc.Misc.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); - - // Add bias targets closer to center of seeker FOV - score *= Mathf.Clamp(-1f * ((biasLevel - 1f) / (scanRadius * scanRadius)) * angle * angle + biasLevel, 1f, biasLevel); // Equal to biasLevel for angle==0, 1 for angle==scanRadius - - 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 + 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(15f / angle); + float relativePosSqrMag = relativePosVessel.sqrMagnitude; + score *= (1400 * 1400) / Mathf.Max(relativePosSqrMag, 90000); // Clamp below 300m + + Vector3 angularVel = Vector3.Cross(relativePosVessel, vessel.Velocity()) / relativePosSqrMag; + + // Add bias targets closer to center of seeker FOV, only once missile seeker can see target + if ((priorHeatScore > 0f) && (angle < scanRadius)) + score *= GetSeekerBias(angle, angularVel, priorHeatTargetAngularVel, priorHeatTargetAngularVelMag, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity); + score *= Mathf.Clamp(VectorUtils.Angle(relativePosVessel, -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 { if (Mathf.Abs(score - priorHeatScore) < Mathf.Abs(finalScore - priorHeatScore)) { finalScore = score; - finalData = new TargetSignatureData(vessel, score); + finalData = new TargetSignatureData(vessel, score, targetCoM ? null : IRSig.Item2); } } else // Otherwise, pick the highest heat score @@ -426,28 +619,29 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss if (score > finalScore) { finalScore = score; - finalData = new TargetSignatureData(vessel, score); + finalData = new TargetSignatureData(vessel, score, targetCoM ? null : IRSig.Item2); } } + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDATargetManager] heatscore of {vessel.GetName()} at angle {angle}° is {score}"); } + // else Debug.Log($"[BDArmory.BDATargetManager] ignoring {vessel.GetName()} at angle {angle}°, which is beyond scanRadius {scanRadius}°"); } - - // 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, priorHeatScore, biasLevel); + flareData = GetFlareTarget(ray, scanRadius, highpassThreshold, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity, priorHeatTarget, priorHeatTargetAngularVel, priorHeatTargetAngularVelMag); + float flareEft = 1; + var mB = missileVessel.GetComponent(); + if (mB != null) flareEft = mB.flareEffectivity; + flareData.signalStrength *= flareEft; flareSuccess = ((!flareData.Equals(TargetSignatureData.noTarget)) && (flareData.signalStrength > highpassThreshold)); } - - // No targets above highpassThreshold if (finalScore < highpassThreshold) { finalData = TargetSignatureData.noTarget; - if (flareSuccess) // return matching flare return flareData; else //else return the target: @@ -457,81 +651,391 @@ 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; - - if (flareSuccess) // return matching flare return flareData; else //else return the target: return finalData; } + private static float GetSeekerBias(float anglePos, Vector3 angularVel, Vector3 prevAngularVel, float prevAngularVelMagnitude, FloatCurve seekerBiasCurvePosition, FloatCurve seekerBiasCurveVelocity, FloatCurve lockedSensorVelocityMagnitudeBias, float lockedSensorMinAngularVelocity) + { + float angularVelMagnitude = angularVel.magnitude; + float seekerAngularVelocity = VectorUtils.AnglePreNormalized(angularVel, prevAngularVel, angularVelMagnitude, prevAngularVelMagnitude); + + angularVelMagnitude = Mathf.Max(angularVelMagnitude, lockedSensorMinAngularVelocity * Mathf.Deg2Rad); + prevAngularVelMagnitude = Mathf.Max(prevAngularVelMagnitude, lockedSensorMinAngularVelocity * Mathf.Deg2Rad); + + float seekerBias = Mathf.Clamp01(seekerBiasCurvePosition.Evaluate(anglePos)) * Mathf.Clamp01(seekerBiasCurveVelocity.Evaluate(seekerAngularVelocity)) * Mathf.Clamp01(lockedSensorVelocityMagnitudeBias.Evaluate(1f - Mathf.Abs((angularVelMagnitude - prevAngularVelMagnitude) / prevAngularVelMagnitude))); + + return seekerBias; + } + + public static Tuple GetVesselAcousticSignature(Vessel v, Vector3 sensorPosition = default(Vector3)) //not bothering with thermocline modelling at this time + { + float noiseScore = 1f; + Part NoisePart = null; + bool hasEngines = false; + bool hasPumps = false; + TargetInfo ti = RadarUtils.GetVesselRadarSignature(v); + hottestPart.Clear(); + if (!v.Splashed) return new Tuple(0, null); + var engineModules = VesselModuleRegistry.GetModules(v); + if (engineModules.Count > 0) + { + hasEngines = true; + using (var engines = engineModules.GetEnumerator()) + while (engines.MoveNext()) + { + if (engines.Current == null) continue; + if (!engines.Current.EngineIgnited) continue; + // Props don't generate thrust above Mach 1--will catch props that don't use Firespitter + float thisScore = engines.Current.GetCurrentThrust(); //pumps, fuel flow, cavitation, noise from ICE/turbine/etc. + //if (engines.Current.velCurve.Evaluate(1.1f) <= 0 && v.horizontalSrfSpeed > ) //Propellers cause cavitation, noisy + noiseScore = Mathf.Max(noiseScore, thisScore); + } + } + var pumpModules = VesselModuleRegistry.GetModules(v); + if (pumpModules.Count > 0) + { + hasPumps = true; + using (var pump = pumpModules.GetEnumerator()) + while (pump.MoveNext()) + { + if (pump.Current == null) continue; + if (!pump.Current.isActiveAndEnabled) continue; + float thisScore = (float)pump.Current.maxEnergyTransfer / 1000; //pumps, coolant gurgling, etc + noiseScore = Mathf.Max(noiseScore, thisScore); + } + } + //any other noise-making modules it would be sensible to add? + if (sensorPosition != default(Vector3)) //Audio source found; now lets determine how much of the craft is occluding it + { + if (hasEngines) + { + using (var engines = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (engines.MoveNext()) + { + if (engines.Current == null || !engines.Current.EngineIgnited) continue; + float thisScore = engines.Current.GetCurrentThrust() / 5; //pumps, fuel flow, cavitation, noise from ICE/turbine/etc. + if (thisScore < noiseScore * 1.05f && thisScore > noiseScore * 0.95f) + { + hottestPart.Add(engines.Current.part); + } + } + } + if (hasPumps) + { + using (var pump = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (pump.MoveNext()) + { + if (pump.Current == null || !pump.Current.isActiveAndEnabled) continue; + float thisScore = (float)pump.Current.maxEnergyTransfer / 500; //pumps, coolant gurgling, etc + if (thisScore < noiseScore * 1.05f && thisScore > noiseScore * 0.95f) + { + hottestPart.Add(pump.Current.part); + } + } + } + Part closestPart = null; + Transform thrustTransform = null; + 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; + } + } + NoisePart = closestPart; + } + if (closestPart != null) + { + if (ti.targetEngineList.Contains(closestPart)) + { + string transformName = closestPart.GetComponent() ? closestPart.GetComponent().thrustVectorTransformName : "thrustTransform"; + thrustTransform = closestPart.FindModelTransform(transformName); + } + // Set thrustTransform as noise source position for engines + Vector3 NoisePosition = thrustTransform ? thrustTransform.position : closestPart.transform.position; + Ray partRay = new Ray(NoisePosition, sensorPosition - NoisePosition); //trace from source to sensor + + // First evaluate occluded heat score, then if the closestPart is a non-prop engine, evaluate the plume temperature + float occludedPartScore = GetOccludedSensorScore(v, closestPart, NoisePosition, noiseScore, partRay, hits, distance, thrustTransform, false, false, 1, false); + + noiseScore = occludedPartScore; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.BDATargetManager] {v.vesselName}'s noiseScore post occlusion: {noiseScore.ToString("0.0")}"); + + } + } + } + VesselECMJInfo jammer = v.gameObject.GetComponent(); + if (jammer != null) + { + noiseScore += jammer.jammerStrength / 2; //acoustic spam to overload sensor/obsfucate exact position, while effective against *Active* sonar, is going make you light up like a christmas tree on Passive soanr + } + using (var sonar = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (sonar.MoveNext()) + { + if (sonar.Current == null || !sonar.Current.radarEnabled || sonar.Current.sonarMode != ModuleRadar.SonarModes.Active) continue; + float ping = sensorPosition != default(Vector3) ? Vector3.Distance(sonar.Current.transform.position, sensorPosition) / 1000 : 0; + if (ping < sonar.Current.radarMaxDistanceDetect * 2) + { + float sonarMalus = 1000 - ((ping / (sonar.Current.radarMaxDistanceDetect * 2)) * 1000); //more return from closer enemy active sonar + noiseScore += sonarMalus; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.BDATargetManager] {v.vesselName}'s active sonar contributing {sonarMalus.ToString("0.0")} to noiseScore"); + } + break; + } + noiseScore += (ti.radarBaseSignature / 10f) * (float)(v.speed * (v.speed / 15f)); //the bigger something is, or the faster it's moving through the water, the larger the acoustic sig + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.BDATargetManager] final noiseScore for {v.vesselName}: " + noiseScore); + return new Tuple(noiseScore, NoisePart); + } + + public static TargetSignatureData GetAcousticTarget(Vessel sourceVessel, Vessel missileVessel, Ray ray, TargetSignatureData priorNoiseTarget, float scanRadius, float highpassThreshold, bool targetCoM, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, FloatCurve lockedSensorVelocityMagnitudeBias, float lockedSensorMinAngularVelocity, MissileFire mf = null, TargetInfo desiredTarget = null, bool IFF = true) + { + TargetSignatureData finalData = TargetSignatureData.noTarget; + float finalScore = 0; + Tuple AcousticSig; + float priorNoiseScore = priorNoiseTarget.signalStrength; + //if (!sourceVessel.Splashed) return finalData; //technically this should be uncommented, but a hack to allow air-dropped passive acoustic torps + + Vector3 relativePosPriorNoiseTarget = priorNoiseTarget.position - ray.origin; + Vector3 priorNoiseTargetAngularVel = Vector3.Cross(relativePosPriorNoiseTarget, priorNoiseTarget.velocity) / relativePosPriorNoiseTarget.sqrMagnitude; + float priorNoiseTargetAngularVelMag = priorNoiseTargetAngularVel.magnitude; + + foreach (Vessel vessel in LoadedVessels) + { + if (vessel == null) + continue; + if (!vessel || !vessel.loaded) + continue; + if (vessel == sourceVessel || vessel == missileVessel) + continue; + if (!vessel.Splashed) + continue; + if (vessel.vesselType == VesselType.Debris) + continue; + if (mf != null && mf.guardMode && (desiredTarget == null || desiredTarget.Vessel != vessel)) + continue; + + TargetInfo tInfo = vessel.gameObject.GetComponent(); + + if (tInfo == null) + { + var WM = vessel.ActiveController().WM; + if (WM != null) + { + tInfo = vessel.gameObject.AddComponent(); + } + else + return finalData; + } + + // Abort if target is friendly. + if (mf != null) + { + if (IFF && mf.Team.IsFriendly(tInfo.Team)) + continue; + } + + // Abort if target is a missile that we've shot + if (tInfo.isMissile) + { + if (tInfo.MissileBaseModule.SourceVessel == sourceVessel) + continue; + } + + Vector3 relativePosVessel = vessel.CoM - ray.origin; + float angle = VectorUtils.Angle(relativePosVessel, ray.direction); + + if ((angle < scanRadius)) + { + if (RadarUtils.TerrainCheck(ray.origin, vessel.CoM)) + continue; + AcousticSig = GetVesselAcousticSignature(vessel, missileVessel.CoM); + float score = AcousticSig.Item1; + if (missileVessel.altitude > -100) + score *= Mathf.Pow(0.8f, (vessel.CoM - ray.origin).magnitude / 1450); //some reflection losses at surface, using 0.8 as arbitrary value. technically should take depth/seafloor depth into account + // else // //below thermocline, subject to Deep Sound Channel and basically 0 propagation loss + + Vector3 angularVel = Vector3.Cross(relativePosVessel, vessel.Velocity()) / relativePosVessel.sqrMagnitude; + // Add bias targets closer to center of seeker FOV, only once missile seeker can see target + if ((priorNoiseScore > 0f) && (angle < scanRadius)) + score *= GetSeekerBias(angle, angularVel, priorNoiseTargetAngularVel, priorNoiseTargetAngularVelMag, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity); + //not messing about with thermocline at this time. + score *= Mathf.Clamp(VectorUtils.Angle(relativePosVessel, -VectorUtils.GetUpDirection(ray.origin)) / 90, 0.5f, 1.5f); + + if ((finalScore > 0f) && (score > 0f) && (priorNoiseScore > 0)) // If we were passed a target noise score, look for the most similar non-zero noise score after picking a target + { + if (Mathf.Abs(score - priorNoiseScore) < Mathf.Abs(finalScore - priorNoiseScore)) + { + finalScore = score; + finalData = new TargetSignatureData(vessel, score, targetCoM ? null : AcousticSig.Item2); + } + } + else // Otherwise, pick the highest noise score + { + if (score > finalScore) + { + finalScore = score; + finalData = new TargetSignatureData(vessel, score, targetCoM ? null : AcousticSig.Item2); + } + } + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.BDATargetManager.GetAcousticTarget] soundScore of {vessel.GetName()} at angle {angle}° is {score}"); + } + } + + // see if there are audio spoofers decoying us: + bool decoySuccess = false; + TargetSignatureData decoyData = TargetSignatureData.noTarget; + if (priorNoiseScore > 0) // Acoustic decoys can only decoy if we already had a target + { + decoyData = GetDecoyTarget(ray, scanRadius, highpassThreshold, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity, priorNoiseTarget, priorNoiseTargetAngularVel, priorNoiseTargetAngularVelMag); + decoyData.signalStrength *= missileVessel.GetComponent().flareEffectivity; + decoySuccess = ((!decoyData.Equals(TargetSignatureData.noTarget)) && (decoyData.signalStrength > highpassThreshold)); + } + + + // No targets above highpassThreshold + if (finalScore < highpassThreshold) + { + finalData = TargetSignatureData.noTarget; + + if (decoySuccess) // return matching acoustic spoofer + return decoyData; + else //else return the target: + return finalData; + } + + // See if an acoustic spoof decoy is closer in score to priornoiseScore than finalScore + if (priorNoiseScore > 0) + decoySuccess = (Mathf.Abs(decoyData.signalStrength - priorNoiseScore) < Mathf.Abs(finalScore - priorNoiseScore)) && decoySuccess; + else if (BDArmorySettings.DUMB_IR_SEEKERS) //convert to a missile .cfg option for earlier-gen IR missiles? + decoySuccess = (decoyData.signalStrength > finalScore) && decoySuccess; + else + decoySuccess = false; + + if (decoySuccess) // return matching flare + return decoyData; + else //else return the target: + return finalData; + } + + + void UpdateDebugLabels() { debugString.Length = 0; + debugStringLineCount = 0; using (var team = TargetDatabase.GetEnumerator()) while (team.MoveNext()) { - debugString.Append($"Team {team.Current.Key} targets:"); - debugString.Append(Environment.NewLine); + 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.Append($"- A target with no vessel reference."); - debugString.Append(Environment.NewLine); + debugString.AppendLine($"- A target with no vessel reference."); } else { - debugString.Append($"- {targetInfo.Vessel.vesselName} Engaged by {targetInfo.TotalEngaging()}"); - debugString.Append(Environment.NewLine); + debugString.AppendLine($"- {targetInfo.Vessel.vesselName} Engaged by {targetInfo.TotalEngaging()}"); } } else { - debugString.Append($"- null target info."); - debugString.Append(Environment.NewLine); + debugString.AppendLine($"- null target info."); } + ++debugStringLineCount; } } - debugString.Append(Environment.NewLine); - debugString.Append($"Heat Signature: {GetVesselHeatSignature(FlightGlobals.ActiveVessel):#####}"); - debugString.Append(Environment.NewLine); - - debugString.Append($"Radar Signature: " + RadarUtils.GetVesselRadarSignature(FlightGlobals.ActiveVessel).radarModifiedSignature); - debugString.Append(Environment.NewLine); - - debugString.Append($"Chaff multiplier: " + RadarUtils.GetVesselChaffFactor(FlightGlobals.ActiveVessel)); - debugString.Append(Environment.NewLine); - - debugString.Append($"ECM Jammer Strength: " + FlightGlobals.ActiveVessel.gameObject.GetComponent()?.jammerStrength); - debugString.Append(Environment.NewLine); - - debugString.Append($"ECM Lockbreak Strength: " + FlightGlobals.ActiveVessel.gameObject.GetComponent()?.lockBreakStrength); - debugString.Append(Environment.NewLine); - - debugString.Append($"Radar Lockbreak Factor: " + RadarUtils.GetVesselRadarSignature(FlightGlobals.ActiveVessel).radarLockbreakFactor); - debugString.Append(Environment.NewLine); + var activeVessel = FlightGlobals.ActiveVessel; + if (activeVessel != null) + { + Vector3 position = activeVessel.ReferenceTransform.position; + Vector3 vesselTransformUp = activeVessel.ReferenceTransform.up; + Vector3 vesselTransformForward = activeVessel.ReferenceTransform.forward; + Vector3 forward = position + 100f * vesselTransformUp; + Vector3 aft = position - 100f * vesselTransformUp; + Vector3 side = position + 100f * activeVessel.ReferenceTransform.right; + Vector3 top = position - 100f * vesselTransformForward; + Vector3 bottom = position + 100f * vesselTransformForward; + + + debugString.Append(Environment.NewLine); + debugString.AppendLine($"Base Acoustic Signature: {GetVesselAcousticSignature(activeVessel).Item1.ToString("0.00")}"); + debugString.AppendLine($"Base Heat Signature: {GetVesselHeatSignature(activeVessel, Vector3.zero):#####}, For/Aft: " + + GetVesselHeatSignature(activeVessel, forward).Item1.ToString("0") + "/" + + GetVesselHeatSignature(activeVessel, aft).Item1.ToString("0") + ", Side: " + + GetVesselHeatSignature(activeVessel, side).Item1.ToString("0") + ", Top/Bot: " + + GetVesselHeatSignature(activeVessel, top).Item1.ToString("0") + "/" + + GetVesselHeatSignature(activeVessel, bottom).Item1.ToString("0")); + var radarSig = RadarUtils.GetVesselRadarSignature(activeVessel); + if ((radarSig.radarBaseSignature == radarSig.radarMassAtUpdate) && (!VesselModuleRegistry.IgnoredVesselTypes.Contains(activeVessel.vesselType) && activeVessel.IsControllable)) + RadarUtils.ForceUpdateRadarCrossSections(); + string aspectedText = ""; + if (BDArmorySettings.ASPECTED_RCS) + { + aspectedText += ", For/Aft: " + RadarUtils.RCSString(RadarUtils.GetVesselRadarSignatureAtAspect(radarSig, forward, 100f)) + "/" + RadarUtils.RCSString(RadarUtils.GetVesselRadarSignatureAtAspect(radarSig, aft, 100f)); + aspectedText += ", Side: " + RadarUtils.RCSString(RadarUtils.GetVesselRadarSignatureAtAspect(radarSig, side, 100f)); + aspectedText += ", Top/Bot: " + RadarUtils.RCSString(RadarUtils.GetVesselRadarSignatureAtAspect(radarSig, top, 100f)) + "/" + RadarUtils.RCSString(RadarUtils.GetVesselRadarSignatureAtAspect(radarSig, bottom, 100f)); + } + debugString.AppendLine($"Radar Signature: " + RadarUtils.RCSString(radarSig.radarModifiedSignature) + aspectedText); + debugString.AppendLine($"Chaff multiplier: " + RadarUtils.GetVesselChaffFactor(activeVessel).ToString("0.0")); + + var ecmjInfo = activeVessel.gameObject.GetComponent(); + var cloakInfo = 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: " + 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 += 10; + + var wm = activeVessel.ActiveController().WM; + if (wm != null && wm.currentTarget != null) + { + debugString.Append(Environment.NewLine); + debugString.AppendLine($"Target Priorities:"); + foreach (var item in wm.currentTarget.debugTargetPriorities) + debugString.AppendLine($" - {item.Item1}: {item.Item2:0.00}"); + debugStringLineCount += wm.currentTarget.debugTargetPriorities.Count + 1; + } + } } public void SaveGPSTargets(ConfigNode saveNode = null) { string saveTitle = HighLogic.CurrentGame.Title; - Debug.Log("[BDArmory]: Save title: " + saveTitle); - ConfigNode fileNode = ConfigNode.Load("GameData/BDArmory/gpsTargets.cfg"); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.BDATargetManager]: Save title: " + saveTitle); + ConfigNode fileNode = ConfigNode.Load(gpsTargetsCfg); if (fileNode == null) { fileNode = new ConfigNode(); fileNode.AddNode("BDARMORY"); - fileNode.Save("GameData/BDArmory/gpsTargets.cfg"); + if (!Directory.GetParent(gpsTargetsCfg).Exists) + { Directory.GetParent(gpsTargetsCfg).Create(); } + fileNode.Save(gpsTargetsCfg); } if (fileNode != null && fileNode.HasNode("BDARMORY")) @@ -580,14 +1084,14 @@ public void SaveGPSTargets(ConfigNode saveNode = null) string targetString = GPSListToString(); gpsNode.SetValue("Targets", targetString, true); - fileNode.Save("GameData/BDArmory/gpsTargets.cfg"); - Debug.Log("[BDArmory]: ==== Saved BDA GPS Targets ===="); + fileNode.Save(gpsTargetsCfg); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.BDATargetManager]: ==== Saved BDA GPS Targets ===="); } } void LoadGPSTargets(ConfigNode saveNode) { - ConfigNode fileNode = ConfigNode.Load("GameData/BDArmory/gpsTargets.cfg"); + ConfigNode fileNode = ConfigNode.Load(gpsTargetsCfg); string saveTitle = HighLogic.CurrentGame.Title; if (fileNode != null && fileNode.HasNode("BDARMORY")) @@ -603,15 +1107,15 @@ void LoadGPSTargets(ConfigNode saveNode) string targetString = gpsNode.GetValue("Targets"); if (targetString == string.Empty) { - Debug.Log("[BDArmory]: ==== BDA GPS Target string was empty! ===="); + Debug.Log("[BDArmory.BDATargetManager]: ==== BDA GPS Target string was empty! ===="); return; } StringToGPSList(targetString); - Debug.Log("[BDArmory]: ==== Loaded BDA GPS Targets ===="); + Debug.Log("[BDArmory.BDATargetManager]: ==== Loaded BDA GPS Targets ===="); } else { - Debug.Log("[BDArmory]: ==== No BDA GPS Targets value found! ===="); + Debug.Log("[BDArmory.BDATargetManager]: ==== No BDA GPS Targets value found! ===="); } } } @@ -669,18 +1173,21 @@ public List Load() //format: very mangled json :( private string GPSListToString() { - return Misc.Misc.JsonCompat(JsonUtility.ToJson(new SerializableGPSData(GPSTargets))); + return OtherUtils.JsonCompat(JsonUtility.ToJson(new SerializableGPSData(GPSTargets))); } private void StringToGPSList(string listString) { try { - GPSTargets = JsonUtility.FromJson(Misc.Misc.JsonDecompat(listString)).Load(); + GPSTargets = JsonUtility.FromJson(OtherUtils.JsonDecompat(listString)).Load(); - Debug.Log("[BDArmory]: Loaded GPS Targets."); + Debug.Log("[BDArmory.BDATargetManager]: Loaded GPS Targets."); + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.BDATargetManager]: Exception thrown in StringToGPSList: " + e.Message + "\n" + e.StackTrace); } - catch { } } IEnumerator CleanDatabaseRoutine() @@ -711,7 +1218,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, bool initialSetup = false) { if (!v) return; if (!reporter) return; @@ -719,41 +1226,64 @@ public static void ReportVessel(Vessel v, MissileFire reporter) TargetInfo info = v.gameObject.GetComponent(); if (!info) { - List.Enumerator mf = v.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) + MissileFire mf = ActiveController.GetActiveController(v).WM; + if (mf != null) { - if (mf.Current == null) continue; - if (reporter.Team.IsEnemy(mf.Current.Team)) + if (reporter.Team.IsEnemy(mf.Team)) { info = v.gameObject.AddComponent(); info.detectedTime[reporter.Team] = Time.time; - break; + if (radar) + { + info.detected[reporter.Team] = true; + } } } - mf.Dispose(); - - List.Enumerator ml = v.FindPartModulesImplementing().GetEnumerator(); - while (ml.MoveNext()) + else { - if (ml.Current == null) continue; - if (ml.Current.HasFired) - { - if (reporter.Team.IsEnemy(ml.Current.Team)) + using (var ml = VesselModuleRegistry.GetModules(v).GetEnumerator()) + while (ml.MoveNext()) { - info = v.gameObject.AddComponent(); - info.detectedTime[reporter.Team] = Time.time; - break; + if (ml.Current == null) continue; + if (ml.Current.HasFired) + { + if (reporter.Team.IsEnemy(ml.Current.Team)) + { + info = v.gameObject.AddComponent(); + info.detectedTime[reporter.Team] = Time.time; + if (radar) + { + info.detected[reporter.Team] = true; + } + break; + } + } } - } } - ml.Dispose(); } - + if (initialSetup) return; // add target to database 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; } } @@ -777,14 +1307,8 @@ public static List TargetList(BDTeam team) public static void ClearDatabase() { - using (var teamDB = TargetDatabase.GetEnumerator()) - while (teamDB.MoveNext()) - { - using (var targetList = teamDB.Current.Value.GetEnumerator()) - while (targetList.MoveNext()) - targetList.Current.detectedTime.Clear(); - teamDB.Current.Value.Clear(); - } + if (TargetDatabase is null) return; + TargetDatabase.Clear(); } public static TargetInfo GetAirToAirTarget(MissileFire mf) @@ -798,6 +1322,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; @@ -826,6 +1353,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; @@ -854,8 +1382,10 @@ public static TargetInfo GetClosestFriendly(MissileFire mf) using (List.Enumerator target = TargetList(mf.Team).GetEnumerator()) while (target.MoveNext()) { - if (target.Current == null || !target.Current.Vessel || target.Current.weaponManager == mf) continue; - if (finalTarget == null || (target.Current.IsCloser(finalTarget, mf))) + if (target.Current == null || !target.Current.Vessel) continue; + var targetMf = target.Current.WeaponManager; + if (targetMf == null || targetMf == mf) continue; + if (finalTarget == null || target.Current.IsCloser(finalTarget, mf)) { finalTarget = target.Current; } @@ -870,7 +1400,9 @@ public static TargetInfo GetTargetFromWeaponManager(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; - if (target.Current.Vessel && target.Current.weaponManager == mf) + var targetMf = target.Current.WeaponManager; + if (targetMf == null) continue; + if (target.Current.Vessel && targetMf == mf) { return target.Current; } @@ -886,7 +1418,9 @@ public static TargetInfo GetClosestTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; - if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !target.Current.isMissile) + 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 && target.Current.SafeOrbitalIntercept(mf)) { if (finalTarget == null || (target.Current.IsCloser(finalTarget, mf))) { @@ -905,7 +1439,9 @@ public static List GetAllTargetsExcluding(List excluding while (target.MoveNext()) { if (target.Current == null) continue; - if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !excluding.Contains(target.Current)) + 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) && target.Current.SafeOrbitalIntercept(mf)) { finalTargets.Add(target.Current); } @@ -920,8 +1456,10 @@ public static TargetInfo GetLeastEngagedTarget(MissileFire mf) using (List.Enumerator target = TargetList(mf.Team).GetEnumerator()) while (target.MoveNext()) { - if (target.Current == null) continue; - if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !target.Current.isMissile && target.Current.isThreat) + 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 && target.Current.SafeOrbitalIntercept(mf)) { if (finalTarget == null || target.Current.NumFriendliesEngaging(mf.Team) < finalTarget.NumFriendliesEngaging(mf.Team)) { @@ -942,11 +1480,15 @@ public static TargetInfo GetClosestTargetWithBiasAndHysteresis(MissileFire mf) using (var target = TargetList(mf.Team).GetEnumerator()) while (target.MoveNext()) { - if (target.Current != null && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !target.Current.isMissile && target.Current.isThreat && !target.Current.isLandedOrSurfaceSplashed) + 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 && target.Current.SafeOrbitalIntercept(mf)) { - float theta = Vector3.Angle(mf.vessel.srf_vel_direction, target.Current.transform.position - mf.vessel.transform.position); + float theta = VectorUtils.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; @@ -958,26 +1500,39 @@ public static TargetInfo GetClosestTargetWithBiasAndHysteresis(MissileFire mf) } // Select a target based on target priority settings + static List<(string, float)> debugTargetScores = []; public static TargetInfo GetHighestPriorityTarget(MissileFire mf) { TargetInfo finalTarget = null; float finalTargetScore = 0f; + debugTargetScores.Clear(); using (var target = TargetList(mf.Team).GetEnumerator()) while (target.MoveNext()) { - if (target.Current != null && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !target.Current.isMissile && target.Current.isThreat && !target.Current.isLandedOrSurfaceSplashed) + if (target.Current == null) continue; + var targetMf = target.Current.WeaponManager; + if (targetMf == 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 && target.Current.SafeOrbitalIntercept(mf)) { float targetScore = (target.Current == mf.currentTarget ? mf.targetBias : 1f) * ( 1f + mf.targetWeightRange * target.Current.TargetPriRange(mf) + + mf.targetWeightAirPreference * target.Current.TargetPriEngagement(targetMf, mf.vessel.radarAltitude) + 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.targetWeightWeaponNumber * target.Current.TargetPriWeapons(targetMf, mf) + + mf.targetWeightMass * target.Current.TargetPriMass(targetMf, mf) + + mf.targetWeightDamage * target.Current.TargetPriDmg(targetMf) + mf.targetWeightFriendliesEngaging * target.Current.TargetPriFriendliesEngaging(mf) + - mf.targetWeightThreat * target.Current.TargetPriThreat(target.Current.weaponManager, mf) + - mf.targetWeightAoD * target.Current.TargetPriAoD(mf)); + mf.targetWeightThreat * target.Current.TargetPriThreat(targetMf, mf) + + mf.targetWeightAoD * target.Current.TargetPriAoD(mf) + + mf.targetWeightProtectTeammate * target.Current.TargetPriProtectTeammate(targetMf, mf) + + mf.targetWeightProtectVIP * target.Current.TargetPriProtectVIP(targetMf, mf) + + mf.targetWeightAttackVIP * target.Current.TargetPriAttackVIP(targetMf)); + if (BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) debugTargetScores.Add((target.Current.Vessel.GetName(), targetScore)); if (finalTarget == null || targetScore > finalTargetScore) { finalTarget = target.Current; @@ -985,8 +1540,12 @@ public static TargetInfo GetHighestPriorityTarget(MissileFire mf) } } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDTargeting]: Selected " + (finalTarget != null ? finalTarget.Vessel.GetDisplayName() : "null") + " with target score of " + finalTargetScore.ToString("0.00")); + if ((BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) && finalTarget != null) + { + finalTarget.debugTargetPriorities = [.. debugTargetScores.OrderByDescending(s => s.Item2)]; + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.BDATargetManager]: {mf.vessel.vesselName} Selected {(finalTarget != null ? finalTarget.Vessel.GetName() : "null")} with target score of {finalTargetScore:0.00} amongst {string.Join(", ", finalTarget.debugTargetPriorities.Select(s => $"{s.Item1}: {s.Item2:0.00}"))}, {TargetList(mf.Team).Count} total potential targets"); + } mf.UpdateTargetPriorityUI(finalTarget); return finalTarget; @@ -1001,13 +1560,21 @@ public static TargetInfo GetMissileTarget(MissileFire mf, bool targetingMeOnly = while (target.MoveNext()) { if (target.Current == null) 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) { if (targetingMeOnly) { - if (Vector3.SqrMagnitude(target.Current.MissileBaseModule.TargetPosition - mf.vessel.CoM) > 60 * 60) + if (!RadarUtils.MissileIsThreat(target.Current.MissileBaseModule, mf)) + { + continue; + } + } + else + { + if (!RadarUtils.MissileIsThreat(target.Current.MissileBaseModule, mf, false)) { continue; } @@ -1015,11 +1582,11 @@ public static TargetInfo GetMissileTarget(MissileFire mf, bool targetingMeOnly = } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.LogWarning("checking target missile - doesn't have missile module"); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.LogWarning("[BDArmory.BDATargetManager]: checking target missile - doesn't have missile module"); } - if (((finalTarget == null && target.Current.NumFriendliesEngaging(mf.Team) < 2) || (finalTarget != null && target.Current.NumFriendliesEngaging(mf.Team) < finalTarget.NumFriendliesEngaging(mf.Team)))) + if (((finalTarget == null && target.Current.NumFriendliesEngaging(mf.Team) < 2) || (finalTarget != null && target.Current.NumFriendliesEngaging(mf.Team) < finalTarget.NumFriendliesEngaging(mf.Team) && target.Current.IsCloser(finalTarget, mf)))) { finalTarget = target.Current; } @@ -1034,7 +1601,8 @@ public static TargetInfo GetUnengagedMissileTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; - if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && target.Current.isMissile && target.Current.isThreat) + 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) { @@ -1053,6 +1621,7 @@ public static TargetInfo GetClosestMissileTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == 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) { bool isHostile = false; @@ -1070,7 +1639,35 @@ public static TargetInfo GetClosestMissileTarget(MissileFire mf) return finalTarget; } - //checks to see if a friendly is too close to the gun trajectory to fire them + public static TargetInfo GetClosestMissileThreat(MissileFire mf) + { + TargetInfo finalTarget = null; + using (List.Enumerator target = TargetList(mf.Team).GetEnumerator()) + while (target.MoveNext()) + { + if (target.Current == null) continue; + if (mf.PDMslTgts.Contains(target.Current)) continue; + //Debug.Log($"[BDArmory.BDAtargetManager - {(mf.vessel != null ? mf.vessel.GetName() : "null")}] closestMissileThreat, checking {target.Current.Vessel.name}"); + if (target.Current && target.Current.Vessel && target.Current.isMissile && mf.CanSeeTarget(target.Current)) + { + //Debug.Log($"[BDArmory.BDAtargetManager - {(mf.vessel != null ? mf.vessel.GetName() : "null")}] closestMissileThreat, {target.Current.Vessel.name} is missile..."); + if (RadarUtils.MissileIsThreat(target.Current.MissileBaseModule, mf, false)) + { + //if (target.Current.NumFriendliesEngaging(mf.Team) >= 0) continue; + if (finalTarget == null || target.Current.IsCloser(finalTarget, mf)) + { + finalTarget = target.Current; + //Debug.Log($"[BDArmory.BDAtargetManager - {(mf.vessel != null ? mf.vessel.GetName() : "null")}] and is threat."); + } + } + } + } + return finalTarget; + } + + + + //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; @@ -1079,15 +1676,13 @@ public static bool CheckSafeToFireGuns(MissileFire weaponManager, Vector3 aimDir using (var friendlyTarget = FlightGlobals.Vessels.GetEnumerator()) while (friendlyTarget.MoveNext()) { - if (friendlyTarget.Current == null || friendlyTarget.Current == weaponManager.vessel) - continue; - var wms = friendlyTarget.Current.FindPartModuleImplementing(); - if (wms != null && wms.Team != weaponManager.Team) - continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(friendlyTarget.Current.vesselType)) continue; + if (friendlyTarget.Current == null || friendlyTarget.Current == weaponManager.vessel) continue; + var wm = friendlyTarget.Current.ActiveController().WM; + if (wm == null || wm.Team != weaponManager.Team) continue; Vector3 targetDistance = friendlyTarget.Current.CoM - weaponManager.vessel.CoM; float friendlyPosDot = Vector3.Dot(targetDistance, aimDirection); - if (friendlyPosDot <= 0) - continue; + if (friendlyPosDot <= 0) continue; float friendlyDistance = targetDistance.magnitude; float friendlyPosDotNorm = friendlyPosDot / friendlyDistance; //scale down the dot to be a 0-1 so we can check it againts cosUnsafeAngle @@ -1099,10 +1694,10 @@ 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()); } } } -} +} \ No newline at end of file diff --git a/BDArmory/UI/BDATeamIcons.cs b/BDArmory/UI/BDATeamIcons.cs new file mode 100644 index 000000000..9664fe50d --- /dev/null +++ b/BDArmory/UI/BDATeamIcons.cs @@ -0,0 +1,513 @@ +using System.Collections.Generic; +using UnityEngine; +using BDArmory.Competition; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class BDATeamIcons : MonoBehaviour + { + public BDATeamIcons Instance; + + public Material IconMat; + + void Awake() + { + if (Instance) + { + Destroy(this); + } + else + Instance = this; + } + GUIStyle IconUIStyle; + GUIStyle DropshadowStyle; + GUIStyle mIStyle; + Color Teamcolor; + Color Missilecolor; + float Opacity; + int textScale = 10; + float oldIconScale; + + private void Start() + { + textScale = Mathf.Max(10, Mathf.CeilToInt(10 * BDTISettings.ICONSCALE)); + IconUIStyle = new GUIStyle(); + IconUIStyle.fontStyle = FontStyle.Bold; + IconUIStyle.fontSize = textScale; + IconUIStyle.normal.textColor = XKCDColors.Red;//replace with BDATISetup defined value varable. + + DropshadowStyle = new GUIStyle(); + DropshadowStyle.fontStyle = FontStyle.Bold; + DropshadowStyle.fontSize = textScale; + DropshadowStyle.normal.textColor = Color.black; + + mIStyle = new GUIStyle(); + mIStyle.fontStyle = FontStyle.Normal; + mIStyle.fontSize = textScale; + Missilecolor = XKCDColors.Yellow; + + IconMat = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + + UpdateStyles(true); + TimingManager.LateUpdateAdd(TimingManager.TimingStage.BetterLateThanNever, UpdateUI); + } + + void OnDestroy() + { + TimingManager.LateUpdateRemove(TimingManager.TimingStage.BetterLateThanNever, UpdateUI); + } + + private void DrawOnScreenIcon(Vector3 worldPos, Texture texture, Vector2 size, Color Teamcolor, bool ShowPointer) + { + Teamcolor.a *= BDTISetup.iconOpacity; + if (Event.current.type.Equals(EventType.Repaint)) + { + bool offscreen = false; + Vector3 screenPos = GUIUtils.GetMainCamera().WorldToViewportPoint(worldPos); + if (screenPos.z < 0) + { + offscreen = true; + screenPos.x *= -1; + screenPos.y *= -1; + } + if (screenPos.x != Mathf.Clamp01(screenPos.x)) + { + offscreen = true; + } + if (screenPos.y != Mathf.Clamp01(screenPos.y)) + { + offscreen = true; + } + float xPos = (screenPos.x * Screen.width) - (0.5f * size.x); + float yPos = ((1 - screenPos.y) * Screen.height) - (0.5f * size.y); + float xtPos = 1 * (Screen.width / 2); + float ytPos = 1 * (Screen.height / 2); + + if (!offscreen) + { + IconMat.SetColor("_TintColor", Teamcolor); + IconMat.mainTexture = texture; + Rect iconRect = new Rect(xPos, yPos, size.x, size.y); + Graphics.DrawTexture(iconRect, texture, IconMat); + } + else + { + if (BDTISettings.POINTERS) + { + Vector2 head; + Vector2 tail; + + head.x = xPos; + head.y = yPos; + tail.x = xtPos; + tail.y = ytPos; + float angle = Vector2.Angle(Vector3.up, tail - head); + if (tail.x < head.x) + { + angle = -angle; + } + if (ShowPointer && BDTISettings.POINTERS) + { + DrawPointer(calculateRadialCoords(head, tail, angle, 0.75f), angle, 4, Teamcolor); + } + } + } + + } + } + private void DrawThreatIndicator(Vector3 vesselPos, Vector3 targetPos, Color Teamcolor) + { + Teamcolor.a *= BDTISetup.iconOpacity; + if (Event.current.type.Equals(EventType.Repaint)) + { + Vector3 screenPos = GUIUtils.GetMainCamera().WorldToViewportPoint(vesselPos); + Vector3 screenTPos = GUIUtils.GetMainCamera().WorldToViewportPoint(targetPos); + if (screenTPos.z > 0) + { + float xPos = (screenPos.x * Screen.width); + float yPos = ((1 - screenPos.y) * Screen.height); + float xtPos = (screenTPos.x * Screen.width); + float ytPos = ((1 - screenTPos.y) * Screen.height); + + Vector2 head; + Vector2 tail; + + head.x = xPos; + head.y = yPos; + tail.x = xtPos; + tail.y = ytPos; + float angle = Vector2.Angle(Vector3.up, tail - head); + if (tail.x < head.x) + { + angle = -angle; + } + DrawPointer(tail, (angle - 180), 2, Teamcolor); + } + } + } + public Vector2 calculateRadialCoords(Vector2 RadialCoord, Vector2 Tail, float angle, float edgeDistance) + { + float theta = Mathf.Abs(angle); + if (theta > 90) + { + theta -= 90; + } + theta = theta * Mathf.Deg2Rad; //needs to be in radians for Mathf. trig + float Cos = Mathf.Cos(theta); + float Sin = Mathf.Sin(theta); + + if (RadialCoord.y >= Tail.y) + { + if (RadialCoord.x >= Tail.x) // set up Quads 3-4 + { + RadialCoord.x = (Cos * (edgeDistance * Tail.x)) + Tail.x; + } + else + { + RadialCoord.x = Tail.x - ((Cos * edgeDistance) * Tail.x); + } + RadialCoord.y = (Sin * (edgeDistance * Tail.y)) + Tail.y; + } + else + { + if (RadialCoord.x >= Tail.x) // set up Quads 1-2 + { + RadialCoord.x = (Sin * (edgeDistance * Tail.x)) + Tail.x; + } + else + { + RadialCoord.x = Tail.x - ((Sin * edgeDistance) * Tail.x); + } + RadialCoord.y = Tail.y - ((Cos * edgeDistance) * Tail.y); + } + return RadialCoord; + } + public static void DrawPointer(Vector2 Pointer, float angle, float width, Color color) + { + Camera cam = GUIUtils.GetMainCamera(); + + if (cam == null) return; + + var guiMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.identity; + float length = 60; + + Rect upRect = new Rect(Pointer.x - (width / 2), Pointer.y - length, width, length); + GUIUtility.RotateAroundPivot(-angle + 180, Pointer); + GUIUtils.DrawRectangle(upRect, color); + GUI.matrix = guiMatrix; + } + + readonly List<(Vector3, Texture2D, Vector2, Color, bool)> onScreenIcons = []; // (position, texture, size, color, showPointer) + readonly List<(Rect, string, Color, GUIStyle)> onScreenLabels = []; // (position, content, style, shadow style) (shadow is the rect offset by Vector2.one if not null) + readonly List<(Vector3, Texture2D, Vector2, float)> texturesToDraw = []; // (position, texture, size, wobble) + readonly List<(Vector3, Vector3, Color)> threatIndicators = []; // (vessel, target, color) + readonly List<(Rect, Color)> healthBars = []; // (position, color) + void UpdateUI() + { + onScreenIcons.Clear(); + onScreenLabels.Clear(); + texturesToDraw.Clear(); + threatIndicators.Clear(); + healthBars.Clear(); + if ((HighLogic.LoadedSceneIsFlight && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDTISettings.TEAMICONS) || HighLogic.LoadedSceneIsFlight && !BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDTISettings.TEAMICONS && BDTISettings.PERSISTANT) + { + float size = 40; + UpdateStyles(); + float minDistanceSqr = BDTISettings.DISTANCE_THRESHOLD * BDTISettings.DISTANCE_THRESHOLD; + float maxDistanceSqr = BDTISettings.MAX_DISTANCE_THRESHOLD * BDTISettings.MAX_DISTANCE_THRESHOLD; + using var vessel = FlightGlobals.Vessels.GetEnumerator(); + while (vessel.MoveNext()) + { + if (vessel.Current == null || vessel.Current.packed || !vessel.Current.loaded) continue; + if (BDTISettings.MISSILES) + { + using var ml = VesselModuleRegistry.GetModules(vessel.Current).GetEnumerator(); + while (ml.MoveNext()) + { + if (ml.Current == null) continue; + 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 = vessel.Current.vesselTransform.position; + float distSqr = (tPos - sPos).sqrMagnitude; + if (distSqr >= minDistanceSqr && distSqr <= maxDistanceSqr) + { + onScreenIcons.Add((vessel.Current.CoM, BDTISetup.Instance.TextureIconMissile, new Vector2(20, 20), Missilecolor, true)); + if (GUIUtils.WorldToGUIPos(ml.Current.vessel.CoM, out Vector2 guiPos)) + { + var dist = BDAMath.Sqrt(distSqr); + onScreenLabels.Add((new(guiPos.x - 12, guiPos.y + 10, 100, 32), dist > 1e3f ? $"{1e-3f * dist:0.00}km" : $"{dist:0.0}m", Missilecolor, null)); + if (BDTISettings.MISSILE_TEXT) + { + Color iconUI = BDTISetup.Instance.ColorAssignments.ContainsKey(ml.Current.Team.Name) ? BDTISetup.Instance.ColorAssignments[ml.Current.Team.Name] : Color.gray; + iconUI.a = Opacity * BDTISetup.textOpacity; + onScreenLabels.Add((new(guiPos.x + 24 * BDTISettings.ICONSCALE, guiPos.y - 4, 100, 32), ml.Current.vessel.vesselName, iconUI, DropshadowStyle)); + } + } + } + } + } + } + + if (!vessel.Current.loaded || vessel.Current.packed || vessel.Current.isActiveVessel) continue; + if (BDTISettings.DEBRIS) + { + if (vessel.Current == null) continue; + if (vessel.Current.vesselType != VesselType.Debris) continue; + if (vessel.Current.LandedOrSplashed) continue; + + Vector3 sPos = FlightGlobals.ActiveVessel.vesselTransform.position; + Vector3 tPos = vessel.Current.vesselTransform.position; + float distSqr = (tPos - sPos).sqrMagnitude; + if (distSqr >= minDistanceSqr && distSqr <= maxDistanceSqr) + { + texturesToDraw.Add((vessel.Current.CoM, BDTISetup.Instance.TextureIconDebris, new Vector2(20, 20), 0)); + } + } + } + using var teamManagers = BDTISetup.Instance.weaponManagers.GetEnumerator(); + while (teamManagers.MoveNext()) + { + using var wm = teamManagers.Current.Value.GetEnumerator(); + while (wm.MoveNext()) + { + if (wm.Current == null) continue; + if (!BDTISetup.Instance.ColorAssignments.ContainsKey(wm.Current.Team.Name)) continue; // Ignore entries that haven't been updated yet. + Color teamcolor = BDTISetup.Instance.ColorAssignments[wm.Current.Team.Name]; + teamcolor.a = Opacity; + Teamcolor = teamcolor; + teamcolor.a *= BDTISetup.textOpacity; + size = wm.Current.vessel.vesselType == VesselType.Debris ? 20 : 40; + if (wm.Current.vessel.isActiveVessel) + { + if (BDTISettings.THREATICON) + { + if (wm.Current.currentTarget == null) continue; + Vector3 sPos = FlightGlobals.ActiveVessel.CoM; + Vector3 tPos = wm.Current.currentTarget.Vessel.CoM; + float relPosSqr = (tPos - sPos).sqrMagnitude; + if (relPosSqr >= minDistanceSqr && relPosSqr <= maxDistanceSqr) + { + threatIndicators.Add((wm.Current.vessel.CoM, wm.Current.currentTarget.Vessel.CoM, Teamcolor)); + } + } + if (BDTISettings.SHOW_SELF) + { + onScreenIcons.Add(( + wm.Current.vessel.CoM, + GetIconForVessel(wm.Current.vessel), + new Vector2(size * BDTISettings.ICONSCALE, size * BDTISettings.ICONSCALE), + Teamcolor, + true + )); + if (BDTISettings.VESSELNAMES) + { + if (GUIUtils.WorldToGUIPos(wm.Current.vessel.CoM, out Vector2 guiPos)) + { + onScreenLabels.Add((new(guiPos.x + 24 * BDTISettings.ICONSCALE, guiPos.y - 4, 100, 32), wm.Current.vessel.vesselName, teamcolor, DropshadowStyle)); + } + } + } + } + else + { + Vector3 selfPos = FlightGlobals.ActiveVessel.CoM; + Vector3 targetPos = wm.Current.vessel.CoM; + Vector3 targetRelPos = targetPos - selfPos; + float distSqr = targetRelPos.sqrMagnitude; + if (distSqr >= minDistanceSqr && distSqr <= maxDistanceSqr) //TODO - look into having vessel icons be based on vesel visibility? (So don't draw icon for undetected stealth plane, etc?) + { + onScreenIcons.Add(( + wm.Current.vessel.CoM, + GetIconForVessel(wm.Current.vessel), + new Vector2(size * BDTISettings.ICONSCALE, size * BDTISettings.ICONSCALE), + Teamcolor, + true + )); + if (BDTISettings.THREATICON) + { + if (wm.Current.currentTarget != null) + { + if (!wm.Current.currentTarget.Vessel.isActiveVessel) + { + threatIndicators.Add((wm.Current.vessel.CoM, wm.Current.currentTarget.Vessel.CoM, Teamcolor)); + } + } + } + if (GUIUtils.WorldToGUIPos(wm.Current.vessel.CoM, out Vector2 guiPos)) + { + if (BDTISettings.VESSELNAMES) + { + string vName = wm.Current.vessel.vesselName; + onScreenLabels.Add((new(guiPos.x + 24 * BDTISettings.ICONSCALE, guiPos.y - 4, 100, 32), vName, teamcolor, DropshadowStyle)); + } + if (BDTISettings.TEAMNAMES) + { + onScreenLabels.Add((new(guiPos.x + 16 * BDTISettings.ICONSCALE, guiPos.y - 19 * BDTISettings.ICONSCALE, 100, 32), $"Team: {wm.Current.Team.Name}", teamcolor, DropshadowStyle)); + } + + if (BDTISettings.SCORE) + { + int Score = 0; + + if (BDACompetitionMode.Instance.Scores.ScoreData.TryGetValue(wm.Current.vessel.vesselName, out var scoreData)) + Score = scoreData.hits; + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + if (ContinuousSpawning.Instance.continuousSpawningScores.TryGetValue(wm.Current.vessel.vesselName, out var ctsScoreData)) + Score += ctsScoreData.cumulativeHits; + } + + onScreenLabels.Add((new(guiPos.x + 16 * BDTISettings.ICONSCALE, guiPos.y + 14 * BDTISettings.ICONSCALE, 100, 32), "Score: " + Score, teamcolor, DropshadowStyle)); + } + float dist = BDAMath.Sqrt(distSqr); + string UIdistStr = dist > 1000f ? $"{1e-3f * dist:0.00}km" : $"{dist:0.0}m"; + if (BDTISettings.HEALTHBAR) + { + float hpPercent = Mathf.Clamp01(wm.Current.currentHP / wm.Current.totalHP); + if (hpPercent > 0) + { + Rect barRect = new(guiPos.x - 32 * BDTISettings.ICONSCALE, guiPos.y + 30 * BDTISettings.ICONSCALE, 64 * BDTISettings.ICONSCALE, 12); + Rect healthRect = new(guiPos.x - 30 * BDTISettings.ICONSCALE, guiPos.y + 32 * BDTISettings.ICONSCALE, 60 * hpPercent * BDTISettings.ICONSCALE, 8); + Color temp = XKCDColors.Grey; + temp.a = Opacity * BDTISetup.iconOpacity; + healthBars.Add((barRect, temp)); + temp = Color.HSVToRGB(85f * hpPercent / 255, 1f, 1f); + temp.a = Opacity * BDTISetup.iconOpacity; + healthBars.Add((healthRect, temp)); + + } + onScreenLabels.Add((new(guiPos.x - 12, guiPos.y + 45 * BDTISettings.ICONSCALE, 100, 32), UIdistStr, teamcolor, DropshadowStyle)); + } + else + { + onScreenLabels.Add((new(guiPos.x - 12, guiPos.y + 20 * BDTISettings.ICONSCALE, 100, 32), UIdistStr, teamcolor, DropshadowStyle)); + } + if (BDTISettings.TELEMETRY) + { + string selectedWeapon = "Using: " + wm.Current.selectedWeaponString; + string AIstate = wm.Current.AI != null ? $"Pilot {wm.Current.AI.currentStatus}" : "No AI"; + + onScreenLabels.Add((new(guiPos.x + 32 * BDTISettings.ICONSCALE, guiPos.y + 32, 200, 32), selectedWeapon, teamcolor, DropshadowStyle)); + onScreenLabels.Add((new(guiPos.x + 32 * BDTISettings.ICONSCALE, guiPos.y + 48, 200, 32), AIstate, teamcolor, DropshadowStyle)); + if (wm.Current.isFlaring || wm.Current.isChaffing || wm.Current.isECMJamming) + { + onScreenLabels.Add((new(guiPos.x + 32 * BDTISettings.ICONSCALE, guiPos.y + 64, 200, 32), "Deploying Counter-Measures", teamcolor, DropshadowStyle)); + } + onScreenLabels.Add((new(guiPos.x - 96 * BDTISettings.ICONSCALE, guiPos.y + 64, 100, 32), $"Speed: {wm.Current.vessel.speed:0.0}m/s", teamcolor, DropshadowStyle)); + onScreenLabels.Add((new(guiPos.x - 96 * BDTISettings.ICONSCALE, guiPos.y + 80, 100, 32), $"Alt: {wm.Current.vessel.altitude:0.0}m", teamcolor, DropshadowStyle)); + onScreenLabels.Add((new(guiPos.x - 96 * BDTISettings.ICONSCALE, guiPos.y + 96, 100, 32), $"Throttle: {Mathf.CeilToInt(wm.Current.vessel.ctrlState.mainThrottle * 100)}%", teamcolor, DropshadowStyle)); + } + } + } + } + } + } + } + } + + void OnGUI() + { + if ((HighLogic.LoadedSceneIsFlight && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDTISettings.TEAMICONS) || HighLogic.LoadedSceneIsFlight && !BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled && BDTISettings.TEAMICONS && BDTISettings.PERSISTANT) + { + // Ordering: textures (debris), icons, health bars, threat indicators, text. + foreach (var (position, texture, size, wobble) in texturesToDraw) GUIUtils.DrawTextureOnWorldPos(position, texture, size, wobble); + foreach (var (position, icon, size, color, showPointer) in onScreenIcons) DrawOnScreenIcon(position, icon, size, color, showPointer); + foreach (var (rect, color) in healthBars) GUIUtils.DrawRectangle(rect, color); + foreach (var (from, to, color) in threatIndicators) DrawThreatIndicator(from, to, color); + foreach (var (rect, content, color, shadowStyle) in onScreenLabels) + { + if (shadowStyle != null) GUI.Label(new(rect.position + Vector2.one, rect.size), content, shadowStyle); + IconUIStyle.normal.textColor = color; + GUI.Label(rect, content, IconUIStyle); + } + } + } + + void UpdateStyles(bool forceUpdate = false) + { + // Update opacity for DropshadowStyle, mIStyle, Missilecolor. IconUIStyle opacity + // is updated in OnGUI(). + if (forceUpdate || Opacity != BDTISettings.OPACITY) + { + Opacity = BDTISettings.OPACITY; + + Teamcolor.a = Opacity; + Color temp; + temp = DropshadowStyle.normal.textColor; + temp.a = Opacity * BDTISetup.textOpacity; + DropshadowStyle.normal.textColor = temp; + temp = mIStyle.normal.textColor; + temp.a = Opacity * BDTISetup.textOpacity; + mIStyle.normal.textColor = temp; + Missilecolor.a = Opacity; + } + if (forceUpdate || BDTISettings.ICONSCALE != oldIconScale) + { + textScale = Mathf.Max(10, Mathf.CeilToInt(10 * BDTISettings.ICONSCALE)); //Would BD_UI_SCALE make more sense here? + oldIconScale = BDTISettings.ICONSCALE; + + IconUIStyle.fontSize = textScale; + DropshadowStyle.fontSize = textScale; + mIStyle.fontSize = textScale; + } + } + + Texture2D GetIconForVessel(Vessel v) + { + Texture2D icon; + if ((v.vesselType == VesselType.Ship && !v.Splashed) || v.vesselType == VesselType.Plane) + { + icon = BDTISetup.Instance.TextureIconPlane; + } + else if (v.vesselType == VesselType.Base || v.vesselType == VesselType.Lander) + { + icon = BDTISetup.Instance.TextureIconBase; + } + else if (v.vesselType == VesselType.Rover) + { + icon = BDTISetup.Instance.TextureIconRover; + } + else if (v.vesselType == VesselType.Probe) + { + icon = BDTISetup.Instance.TextureIconProbe; + } + else if (v.vesselType == VesselType.Ship && v.Splashed) + { + icon = BDTISetup.Instance.TextureIconShip; + if (v.vesselType == VesselType.Ship && v.altitude < -10) + { + icon = BDTISetup.Instance.TextureIconSub; + } + } + else if (v.vesselType == VesselType.Debris) + { + icon = BDTISetup.Instance.TextureIconDebris; + Color temp = XKCDColors.Grey; + temp.a = Opacity; + Teamcolor = temp; + temp.a *= BDTISetup.textOpacity; + IconUIStyle.normal.textColor = temp; + } + else + { + icon = BDTISetup.Instance.TextureIconGeneric; + } + return icon; + } + } +} 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 new file mode 100644 index 000000000..0956f6583 --- /dev/null +++ b/BDArmory/UI/BDAmmoSelector.cs @@ -0,0 +1,317 @@ +using System.Collections.Generic; +using UnityEngine; +using static UnityEngine.GUILayout; + +using BDArmory.Bullets; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons; +using System.Collections; + +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.EditorAny, false)] + public class BDAmmoSelector : MonoBehaviour + { + public static BDAmmoSelector Instance; + private Rect windowRect = new Rect(350, 100, 350, 20); + + const float width = 350; + const float margin = 5; + const float buttonHeight = 20; + + private bool open = false; + private bool save = false; + private float height = 20; + + private string beltString = string.Empty; + private string GUIstring = string.Empty; + private string lastGUIstring = string.Empty; + string countString = string.Empty; + private int roundCounter = 0; + int labelLines = 1; + + private Vector2 windowLocation; + private ModuleWeapon selectedWeapon; + + public string SelectedAmmoType; //presumably Aubranium can use this to filter allowed/banned ammotypes + + public List AList = new List(); + public List BList = new List(); + public List ammoDesc = new List(); + private BulletInfo bulletInfo; + public string guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); + + GUIStyle labelStyle; + GUIStyle titleStyle; + private Vector2 scrollInfoVector; + void Start() + { + labelStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); + labelStyle.alignment = TextAnchor.UpperLeft; + labelStyle.normal.textColor = Color.white; + + titleStyle = new GUIStyle(); + titleStyle.normal.textColor = BDArmorySetup.BDGuiSkin.window.normal.textColor; + titleStyle.font = BDArmorySetup.BDGuiSkin.window.font; + titleStyle.fontSize = BDArmorySetup.BDGuiSkin.window.fontSize; + titleStyle.fontStyle = BDArmorySetup.BDGuiSkin.window.fontStyle; + titleStyle.alignment = TextAnchor.UpperCenter; + } + + public void Open(ModuleWeapon weapon, Vector2 position) + { + open = true; + selectedWeapon = weapon; + windowLocation = position; + beltString = string.Empty; + GUIstring = string.Empty; + countString = string.Empty; + lastGUIstring = string.Empty; + roundCounter = 0; + applyWeaponGroupTo = new string[] { StringUtils.Localize("#LOC_BDArmory_thisWeapon"), StringUtils.Localize("#LOC_BDArmory_SymmetricWeapons"), $"{StringUtils.Localize("#autoLOC_900712")} {weapon.part.partInfo.title}" }; + _applyWeaponGroupTo = applyWeaponGroupTo[_applyWeaponGroupToIndex]; + if (weapon.ammoBelt != "def") + { + beltString = weapon.ammoBelt; + BList = BDAcTools.ParseNames(beltString); + for (int i = 0; i < BList.Count; i++) + { + BulletInfo binfo = BulletInfo.bullets[BList[i].ToString()]; + if (BList[i] != lastGUIstring) + { + GUIstring += countString.ToString(); + GUIstring += (string.IsNullOrEmpty(binfo.DisplayName) ? binfo.name : binfo.DisplayName); + lastGUIstring = (string.IsNullOrEmpty(binfo.DisplayName) ? binfo.name : binfo.DisplayName); + roundCounter = 1; + countString = "; "; + } + else + { + roundCounter++; + countString = " X" + roundCounter + "; "; + } + } + } + AList = BDAcTools.ParseNames(weapon.bulletType); + + for (int a = 0; a < AList.Count; a++) + { + bulletInfo = BulletInfo.bullets[AList[a].ToString()]; + guiAmmoTypeString = ""; + if (bulletInfo.projectileCount >= 2) + { + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Shot") + " "; + } + if (bulletInfo.apBulletMod >= 1.1) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_AP") + " "; + } + if (bulletInfo.apBulletMod < 1.1 && bulletInfo.apBulletMod > 0.8f) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_SAP") + " "; + } + if (bulletInfo.nuclear) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Nuclear") + " "; + } + if (bulletInfo.tntMass > 0 && !bulletInfo.nuclear) + { + if (bulletInfo.fuzeType.ToLower() == "flak" || bulletInfo.fuzeType.ToLower() == "proximity") + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Flak") + " "; + } + else if (bulletInfo.explosive.ToLower() == "Shaped") + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; + } + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Explosive") + " "; + } + if (bulletInfo.incendiary) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Incendiary") + " "; + } + if (bulletInfo.EMP && !bulletInfo.nuclear) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_EMP") + " "; + } + if (bulletInfo.beehive) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Beehive") + " "; + } + if (bulletInfo.tntMass <= 0 && bulletInfo.apBulletMod <= 0.8) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); + } + ammoDesc.Add(guiAmmoTypeString); + } + } + + // Doing it this way prevents OnGUI events from below the window from being triggered by the window disappearing. + void CloseWindow() => StartCoroutine(CloseWindowAtEndOfFrame()); + bool waitingForEndOfFrame = false; + IEnumerator CloseWindowAtEndOfFrame() + { + if (waitingForEndOfFrame) yield break; + waitingForEndOfFrame = true; + yield return new WaitForEndOfFrame(); + waitingForEndOfFrame = false; + CloseWindowNow(); + } + void CloseWindowNow() + { + open = false; + GUIUtils.PreventClickThrough(windowRect, "BDABELTLOCK", true); + } + + string[] applyWeaponGroupTo; + string _applyWeaponGroupTo; + int _applyWeaponGroupToIndex = 0; + protected virtual void OnGUI() + { + if (save) + { + save = false; + + switch (_applyWeaponGroupToIndex) + { + case 0: + SetBeltInfo(selectedWeapon); + break; + case 1: // symmetric parts + SetBeltInfo(selectedWeapon); + foreach (Part p in selectedWeapon.part.symmetryCounterparts) + { + var wpn = p.GetComponent(); + if (wpn == null) continue; + if (wpn.GetShortName() != selectedWeapon.GetShortName()) continue; + SetBeltInfo(wpn); + } + break; + case 2: // all weapons of the same type + foreach (Part p in EditorLogic.fetch.ship.parts) + { + if (p.name == selectedWeapon.part.name) + { + var wpn = p.GetComponent(); + if (wpn == null) continue; + if (wpn.GetShortName() != selectedWeapon.GetShortName()) continue; + SetBeltInfo(wpn); + } + } + break; + } + } + if (open) + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, windowRect.position); + windowRect = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), windowRect, AmmoSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); + } + } + private void SetBeltInfo(ModuleWeapon weapon) + { + weapon.ammoBelt = beltString; + if (!string.IsNullOrEmpty(beltString)) + weapon.useCustomBelt = true; + else + weapon.useCustomBelt = false; + } + private void AmmoSelectorWindow(int id) + { + GUIUtils.PreventClickThrough(windowRect, "BDABELTLOCK"); + float line = 0.5f; + string labelString = GUIstring.ToString() + countString.ToString(); + 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 - 26, 2, 24, 24), "X", BDArmorySetup.CloseButtonStyle)) + { + beltString = string.Empty; + GUIstring = string.Empty; + countString = string.Empty; + lastGUIstring = string.Empty; + CloseWindow(); + } + line++; + GUI.Label(new Rect(margin, line * buttonHeight, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_Ammo_Weapon") + " " + selectedWeapon.part.partInfo.title, labelStyle); + line++; + 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)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(width - 2 * margin), Height(labelLines * buttonHeight))) + { + scrollInfoVector = scrollViewScope.scrollPosition; + GUILayout.Label(labelString, labelStyle, Width(width - 50 - 2 * margin)); + } + EndArea(); + line++; + + float ammolines = 0.1f; + for (int i = 0; i < AList.Count; i++) + { + string ammoname = string.IsNullOrEmpty(BulletInfo.bullets[AList[i]].DisplayName) ? BulletInfo.bullets[AList[i]].name : BulletInfo.bullets[AList[i]].DisplayName; + if (GUI.Button(new Rect(margin * 2, (line + labelLines + ammolines) * buttonHeight, (width - 4 * margin), buttonHeight), ammoname, BDArmorySetup.ButtonStyle)) + { + beltString += BulletInfo.bullets[AList[i]].name; + beltString += "; "; + if (lastGUIstring != ammoname) + { + GUIstring += countString.ToString(); + GUIstring += ammoname; + lastGUIstring = ammoname; + roundCounter = 1; + countString = "; "; + } + else + { + roundCounter++; + countString = " X" + roundCounter + "; "; + } + } + ammolines++; + if (ammoDesc[i] != null) + { + GUI.Label(new Rect(margin * 4, (line + labelLines + ammolines) * buttonHeight, (width - 8 * margin), buttonHeight), ammoDesc[i], labelStyle); + ammolines += 1.1f; + } + } + if (GUI.Button(new Rect(margin * 5, (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), StringUtils.Localize("#LOC_BDArmory_reset"), BDArmorySetup.ButtonStyle)) + { + beltString = string.Empty; + GUIstring = string.Empty; + countString = string.Empty; + lastGUIstring = string.Empty; + labelLines = 1; + roundCounter = 1; + } + 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"), BDArmorySetup.ButtonStyle)) + { + save = true; + CloseWindow(); + } + line += 1.5f; + + GUI.Label(new Rect(margin * 5, (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), $"{StringUtils.Localize("#LOC_BDArmory_applyTo")} {_applyWeaponGroupTo}"); + line += 0.2f; + if (_applyWeaponGroupToIndex != (_applyWeaponGroupToIndex = Mathf.RoundToInt(GUI.HorizontalSlider(new Rect((margin * 5) + (width - (10 * margin)) / 2, (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), + _applyWeaponGroupToIndex, 0, 2)))) _applyWeaponGroupTo = applyWeaponGroupTo[_applyWeaponGroupToIndex]; + line += 1.5f; + height = Mathf.Lerp(height, (line + labelLines + ammolines) * buttonHeight, 0.15f); + windowRect.height = height; + + GUI.DragWindow(); + GUIUtils.RepositionWindow(ref windowRect); + } + + private void Awake() + { + if (Instance) Destroy(Instance); + Instance = this; + windowRect = new Rect((Screen.width / 2) - (width / 2), (Screen.height / 2) - (height / 2), width, height); + } + + private void OnDestroy() + { + CloseWindowNow(); + } + } +} diff --git a/BDArmory/UI/BDArmoryAIGUI.cs b/BDArmory/UI/BDArmoryAIGUI.cs new file mode 100644 index 000000000..ede78ed04 --- /dev/null +++ b/BDArmory/UI/BDArmoryAIGUI.cs @@ -0,0 +1,2402 @@ +using KSP.UI.Screens; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System; +using UnityEngine; +using static UnityEngine.GUILayout; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Extensions; + +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] + public class BDArmoryAIGUI : MonoBehaviour + { + //toolbar gui + public static bool infoLinkEnabled = false; + public static bool contextTipsEnabled = false; + public static bool NumFieldsEnabled = false; + public static bool windowBDAAIGUIEnabled; + internal static bool resizingWindow = false; + internal static bool autoResizingWindow = true; + internal static int _guiCheckIndex = -1; + + public static ApplicationLauncherButton button; + + float WindowWidth = 500; + float WindowHeight = 350; + float contentHeight = 0; + float height = 0; + const float ColumnWidth = 350; + const float _buttonSize = 26; + const float _windowMargin = 4; + const float contentTop = 10; + const float entryHeight = 20; + public bool checkForAI = false; // Flag to indicate that a new check for AI needs to happen (instead of responding to every event). + + int Drivertype = 0; + int broadsideDir = 0; + int pidMode = 0; + int rollTowards = 0; + public AIUtils.VehicleMovementType[] VehicleMovementTypes = (AIUtils.VehicleMovementType[])Enum.GetValues(typeof(AIUtils.VehicleMovementType)); // Get the VehicleMovementType as an array of enum values. + public BDModuleOrbitalAI.PIDModeTypes[] PIDModeTypes = (BDModuleOrbitalAI.PIDModeTypes[])Enum.GetValues(typeof(BDModuleOrbitalAI.PIDModeTypes)); // Get the PID mode as an array of enum values. + public BDModuleOrbitalAI.RollModeTypes[] RollModeTypes = (BDModuleOrbitalAI.RollModeTypes[])Enum.GetValues(typeof(BDModuleOrbitalAI.RollModeTypes)); // Get the roll mode as an array of enum values. + + public AIType activeAIType = AIType.None; + public BDGenericAIBase ActiveAI; // Note: we don't use the usual ActiveController pattern as we need more control for the numeric input fields. + List AIs = []; // A list of the AIs for use in the Editor. + + Dictionary scrollViewVectors = []; + private Vector2 scrollInfoVector; + + public static BDArmoryAIGUI Instance; + public static bool buttonSetup; + + Dictionary inputFields; + + GUIStyle BoldLabel; + GUIStyle Label; + GUIStyle Title; + GUIStyle contextLabel; + GUIStyle contextLabelRight; + GUIStyle infoLinkStyle; + bool stylesConfigured = false; + + + void Awake() + { + if (Instance != null) Destroy(Instance); + Instance = this; + } + + void Start() + { + if (BDArmorySettings.AI_TOOLBAR_BUTTON) AddToolbarButton(); + + BDArmorySetup.WindowRectAI = new Rect(BDArmorySetup.WindowRectAI.x, BDArmorySetup.WindowRectAI.y, WindowWidth, BDArmorySetup.WindowRectAI.height); + WindowHeight = Mathf.Max(BDArmorySetup.WindowRectAI.height, 305); + + if (HighLogic.LoadedSceneIsFlight) + { + GetAI(); + GameEvents.onVesselChange.Add(OnVesselChange); + GameEvents.onVesselPartCountChanged.Add(OnVesselModified); + GameEvents.onPartDestroyed.Add(OnPartDestroyed); + } + else if (HighLogic.LoadedSceneIsEditor) + { + GetAIEditor(); + GameEvents.onEditorLoad.Add(OnEditorLoad); + 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() + { + if (!HighLogic.LoadedSceneIsFlight && !HighLogic.LoadedSceneIsEditor) return; + StartCoroutine(ToolbarButtonRoutine()); + } + IEnumerator ToolbarButtonRoutine() + { + if (buttonSetup) // Reconfigure the callbacks to use the current instance. + { + button.onTrue = ShowAIGUI; + button.onFalse = HideAIGUI; + yield break; + } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. + Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon_ai", false); + 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 RemoveToolbarButton() + { + if (button == null) return; + if (!HighLogic.LoadedSceneIsFlight && !HighLogic.LoadedSceneIsEditor) return; + ApplicationLauncher.Instance.RemoveModApplication(button); + button = null; + buttonSetup = false; + } + + public void ToggleAIGUI() + { + if (windowBDAAIGUIEnabled) HideAIGUI(); + else ShowAIGUI(); + } + + public void ShowAIGUI() + { + windowBDAAIGUIEnabled = true; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, windowBDAAIGUIEnabled); + if (HighLogic.LoadedSceneIsFlight) GetAI(); + else GetAIEditor(); + if (button != null) button.SetTrue(false); + } + + // Doing it this way prevents OnGUI events from below the window from being triggered by the window disappearing. + public void HideAIGUI() => StartCoroutine(HideAIGUIAtEndOfFrame()); + bool waitingForEndOfFrame = false; + IEnumerator HideAIGUIAtEndOfFrame() + { + if (waitingForEndOfFrame) yield break; + waitingForEndOfFrame = true; + yield return new WaitForEndOfFrame(); + waitingForEndOfFrame = false; + HideAIGUINow(); + } + void HideAIGUINow() + { + windowBDAAIGUIEnabled = false; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, windowBDAAIGUIEnabled); + BDAWindowSettingsField.Save(); // Save window settings. + if (button != null) button.SetFalse(false); + if (HighLogic.LoadedSceneIsEditor) GUIUtils.PreventClickThrough(BDArmorySetup.WindowRectAI, "AIGUI lock", true); + AIs.Clear(); + AISelectionComboBox = null; + } + + void Dummy() + { } + + void Update() + { + if (!(HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor)) return; + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.GUI_AI_TOGGLE)) + { + ToggleAIGUI(); + } + if (!windowBDAAIGUIEnabled) return; + if (checkForAI) // Only happens during flight. + { + GetAI(); + checkForAI = false; + } + } + + void OnVesselChange(Vessel v) + { + if (!windowBDAAIGUIEnabled) return; + if (v == null) return; + if (v.isActiveVessel) + { + GetAI(); + } + } + + void OnVesselModified(Vessel v) // Active AI was on a part that got detached from the active vessel. + { + if (!windowBDAAIGUIEnabled || activeAIType == AIType.None) return; + if (v == null) return; + if (v.isActiveVessel && (ActiveAI == null || ActiveAI.vessel != v)) // Was an active vessel with an AI, but the AI is now gone or on another vessel. + { + activeAIType = AIType.None; + checkForAI = true; + } + } + + void OnPartDestroyed(Part p) + { + if (!windowBDAAIGUIEnabled || activeAIType == AIType.None) return; + if (ActiveAI == null) // We had an AI, but now it's gone... + { + activeAIType = AIType.None; + checkForAI = true; + } + } + + void OnEditorLoad(ShipConstruct ship, CraftBrowserDialog.LoadType loadType) + { + GetAIEditor(); + } + + private void OnEditorPartPlacedEvent(Part p) + { + if (!windowBDAAIGUIEnabled) return; + if (p == null) return; + GetAIEditor(); // We need to check if we have a new AI or if the ordering has changed. + } + + private void OnEditorPartDeletedEvent(Part p) + { + if (!windowBDAAIGUIEnabled) return; + if (activeAIType != AIType.None) // If we had an active AI, we need to check to see if it's disappeared. + GetAIEditor(); // We can't just check the part as it's now null. + } + + 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() + { + ActiveAI = null; + activeAIType = AIType.None; + inputFields = null; + AISelectionComboBox = null; + 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. + ActiveAI = FlightGlobals.ActiveVessel.ActiveController().AI as BDGenericAIBase; + activeAIType = ActiveAI == null ? AIType.None : ActiveAI.aiType; + SetInputFields(activeAIType); + SetChooseOptionSliders(); + } + + void GetAIEditor() + { + if (_getAIEditorCoroutine != null) StopCoroutine(_getAIEditorCoroutine); + _getAIEditorCoroutine = StartCoroutine(GetAIEditorCoroutine()); + } + Coroutine _getAIEditorCoroutine; + IEnumerator GetAIEditorCoroutine() + { + AISelectionComboBox = null; // Clear the combobox to reset it. + 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. + var ship = EditorLogic.fetch.ship; + if (ship != null && ship.Parts != null) + { + AIs = [.. ship.Parts.SelectMany(part => part.FindModulesImplementing()).Where(ai => ai != null)]; + if (AIs.Count > 0) + { + var rootPart = ship.Parts.First(); while (rootPart.parent != null) rootPart = rootPart.parent; + VesselModuleRegistry.SortByProximityToRootIBDAI(ref AIs, rootPart); + if (ActiveAI == AIs.First() as BDGenericAIBase) yield break; // It's the same AI, do nothing. + ActiveAI = AIs.First() as BDGenericAIBase; // Switch back to the primary AI. + activeAIType = ActiveAI.aiType; + SetInputFields(activeAIType); + SetChooseOptionSliders(); + yield break; + } + } + + // No AIs were found, clear everything. + activeAIType = AIType.None; + ActiveAI = null; + inputFields = null; + AIs.Clear(); + } + + /// + /// Get the proximity to the root part. + /// + /// + /// Proximity to the root part or int.MaxValue if not connected to the root part. + public static int ProximityToRoot(Part part, Part root) + { + int proximity = 0; + Part currentPart = part; + while (currentPart != null && currentPart != root) + { + currentPart = currentPart.parent; + ++proximity; + } + if (currentPart == null) + return int.MaxValue; + return proximity; + } + + /// + /// Get the value, minValue and maxValue of a UI_FloatRange derived control. + /// + /// The type of the AI. + /// The AI. + /// The name of the field to look at. + /// value, minValue, maxValue, (rounding, sigFig, withZero) + (float, float, float, (float, float, bool, bool)) GetAIFieldLimits(AIType aiType, BDGenericAIBase gAI, string fieldName) + { + float value = 0, minValue = 0, maxValue = 0, rounding = 0, sigFig = 0; + bool withZero = false, reducedPrecisionAtMin = false; + try + { + (float, float, float, float, bool, bool) GetLimits(UI_FloatRange uic) + { + if (uic is UI_FloatSemiLogRange) (minValue, maxValue, rounding, sigFig, withZero, reducedPrecisionAtMin) = (uic as UI_FloatSemiLogRange).GetLimits(); + else if (uic is UI_FloatLogRange) (minValue, maxValue, rounding, sigFig) = (uic as UI_FloatLogRange).GetLimits(); // (min, max, 0, steps) + else if (uic is UI_FloatPowerRange) (minValue, maxValue, rounding, sigFig) = (uic as UI_FloatPowerRange).GetLimits(); + else + { + minValue = uic.minValue; + maxValue = uic.maxValue; + rounding = uic.stepIncrement; + } + return (minValue, maxValue, rounding, sigFig, withZero, reducedPrecisionAtMin); + } + switch (aiType) + { + case AIType.PilotAI: + { + var AI = gAI as BDModulePilotAI; + var uic = (HighLogic.LoadedSceneIsFlight ? AI.Fields[fieldName].uiControlFlight : AI.Fields[fieldName].uiControlEditor) as UI_FloatRange; + (minValue, maxValue, rounding, sigFig, withZero, reducedPrecisionAtMin) = GetLimits(uic); + value = (float)typeof(BDModulePilotAI).GetField(fieldName).GetValue(AI); + } + break; + case AIType.SurfaceAI: + { + var AI = gAI as BDModuleSurfaceAI; + var uic = (HighLogic.LoadedSceneIsFlight ? AI.Fields[fieldName].uiControlFlight : AI.Fields[fieldName].uiControlEditor) as UI_FloatRange; + (minValue, maxValue, rounding, sigFig, withZero, reducedPrecisionAtMin) = GetLimits(uic); + value = (float)typeof(BDModuleSurfaceAI).GetField(fieldName).GetValue(AI); + } + break; + case AIType.VTOLAI: + { + var AI = gAI as BDModuleVTOLAI; + var uic = (HighLogic.LoadedSceneIsFlight ? AI.Fields[fieldName].uiControlFlight : AI.Fields[fieldName].uiControlEditor) as UI_FloatRange; + (minValue, maxValue, rounding, sigFig, withZero, reducedPrecisionAtMin) = GetLimits(uic); + value = (float)typeof(BDModuleVTOLAI).GetField(fieldName).GetValue(AI); + } + break; + case AIType.OrbitalAI: + { + var AI = gAI as BDModuleOrbitalAI; + var uic = (HighLogic.LoadedSceneIsFlight ? AI.Fields[fieldName].uiControlFlight : AI.Fields[fieldName].uiControlEditor) as UI_FloatRange; + (minValue, maxValue, rounding, sigFig, withZero, reducedPrecisionAtMin) = GetLimits(uic); + value = (float)typeof(BDModuleOrbitalAI).GetField(fieldName).GetValue(AI); + } + break; + } + } + catch (Exception e) + { + var errorMsg = e.Message; +#if DEBUG + errorMsg += $"\n{e.StackTrace}"; +#endif + Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Failed to retrieve field limits from {fieldName} on AI of type {aiType}: {errorMsg}"); + } + return (value, minValue, maxValue, (rounding, sigFig, withZero, reducedPrecisionAtMin)); + } + + /// + /// Get the current field limits from the inputFields data (which may change due to UpToEleven). + /// Note: if the values require significant post-processing (e.g., Log10), then it may be better to just use fixed values. + /// + /// + /// minValue, maxValue, rounding, sig.fig., with zero + (float, float, float, float, bool, bool) GetFieldLimits(string fieldName) + { + if (!inputFields.ContainsKey(fieldName)) return (0, 0, 0, 0, false, false); + var field = inputFields[fieldName]; + return ((float)field.minValue, (float)field.maxValue, field.rounding, field.sigFig, field.withZero, field.reducedPrecisionAtMin); + } + + /// + /// Set the inputFields entries for the given AI type. + /// Note: only UI_FloatRange derived entries should be included here. + /// + /// The type of the currently active AI. + void SetInputFields(AIType aiType) + { + // Note: We use nameof(AI.field) to get the fieldname to avoid typos. + switch (aiType) + { + case AIType.PilotAI: + { + var AI = ActiveAI as BDModulePilotAI; + if (AI == null) + { + Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Mismatch between AI type and actual AI."); + activeAIType = AIType.None; + inputFields = null; + return; + } + inputFields = new List { + nameof(AI.steerMult), + nameof(AI.steerKiAdjust), + nameof(AI.steerDamping), + nameof(AI.steerDampingPitch), + nameof(AI.steerDampingYaw), + nameof(AI.steerDampingRoll), + nameof(AI.DynamicDampingMin), + nameof(AI.DynamicDampingMax), + nameof(AI.dynamicSteerDampingFactor), + nameof(AI.DynamicDampingPitchMin), + nameof(AI.DynamicDampingPitchMax), + nameof(AI.dynamicSteerDampingPitchFactor), + nameof(AI.DynamicDampingYawMin), + nameof(AI.DynamicDampingYawMax), + nameof(AI.dynamicSteerDampingYawFactor), + nameof(AI.DynamicDampingRollMin), + nameof(AI.DynamicDampingRollMax), + nameof(AI.dynamicSteerDampingRollFactor), + nameof(AI.threeAxisPIDPitchMult), + nameof(AI.threeAxisPIDPitchKi), + nameof(AI.threeAxisPIDPitchDamping), + nameof(AI.threeAxisPIDYawMult), + nameof(AI.threeAxisPIDYawKi), + nameof(AI.threeAxisPIDYawDamping), + nameof(AI.threeAxisPIDRollMult), + nameof(AI.threeAxisPIDRollKi), + nameof(AI.threeAxisPIDRollDamping), + + nameof(AI.autoTuningOptionNumSamples), + nameof(AI.autoTuningOptionFastResponseRelevance), + nameof(AI.autoTuningOptionInitialLearningRate), + nameof(AI.autoTuningOptionInitialRollRelevance), + nameof(AI.autoTuningAltitude), + nameof(AI.autoTuningSpeed), + nameof(AI.autoTuningRecenteringDistance), + + nameof(AI.defaultAltitude), + nameof(AI.minAltitude), + nameof(AI.maxAltitude), + nameof(AI.bombingAltitude), + + nameof(AI.maxSpeed), + nameof(AI.takeOffSpeed), + nameof(AI.minSpeed), + nameof(AI.strafingSpeed), + nameof(AI.idleSpeed), + nameof(AI.ABPriority), + nameof(AI.ABOverrideThreshold), + nameof(AI.brakingPriority), + + nameof(AI.maxSteer), + nameof(AI.lowSpeedSwitch), + nameof(AI.maxSteerAtMaxSpeed), + nameof(AI.cornerSpeed), + nameof(AI.altitudeSteerLimiterFactor), + nameof(AI.altitudeSteerLimiterAltitude), + nameof(AI.maxBank), + nameof(AI.waypointPreRollTime), + nameof(AI.waypointYawAuthorityTime), + nameof(AI.maxAllowedGForce), + nameof(AI.maxAllowedAoA), + nameof(AI.postStallAoA), + nameof(AI.ImmelmannTurnAngle), + nameof(AI.ImmelmannPitchUpBias), + + nameof(AI.minEvasionTime), + nameof(AI.evasionNonlinearity), + nameof(AI.evasionThreshold), + nameof(AI.evasionTimeThreshold), + nameof(AI.evasionMinRangeThreshold), + nameof(AI.collisionAvoidanceThreshold), + nameof(AI.vesselCollisionAvoidanceLookAheadPeriod), + nameof(AI.vesselCollisionAvoidanceStrength), + nameof(AI.vesselStandoffDistance), + nameof(AI.extendDistanceAirToAir), + nameof(AI.extendAngleAirToAir), + nameof(AI.extendDistanceAirToGroundGuns), + nameof(AI.extendDistanceAirToGround), + nameof(AI.extendTargetVel), + nameof(AI.extendTargetAngle), + nameof(AI.extendTargetDist), + nameof(AI.extendAbortTime), + nameof(AI.extendMinGainRate), + + nameof(AI.turnRadiusTwiddleFactorMin), + nameof(AI.turnRadiusTwiddleFactorMax), + nameof(AI.terrainAvoidanceCriticalAngle), + nameof(AI.controlSurfaceDeploymentTime), + nameof(AI.postTerrainAvoidanceCoolDownDuration), + nameof(AI.waypointTerrainAvoidance), + + nameof(AI.controlSurfaceLag), + }.ToDictionary(key => key, key => + { + var (value, minValue, maxValue, meta) = GetAIFieldLimits(aiType, ActiveAI, key); + return gameObject.AddComponent().Initialise(0, value, minValue, maxValue, meta); + }); + showSection[Section.UpToEleven] = AI.UpToEleven; + } + break; + case AIType.SurfaceAI: + { + var AI = ActiveAI as BDModuleSurfaceAI; + if (AI == null) + { + Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Mismatch between AI type and actual AI."); + activeAIType = AIType.None; + inputFields = null; + return; + } + inputFields = new List { + nameof(AI.MaxSlopeAngle), + nameof(AI.CruiseSpeed), + nameof(AI.MaxSpeed), + nameof(AI.MaxDrift), + nameof(AI.TargetPitch), + nameof(AI.BankAngle), + nameof(AI.WeaveFactor), + nameof(AI.steerMult), + nameof(AI.steerDamping), + nameof(AI.MinEngagementRange), + nameof(AI.MaxEngagementRange), + nameof(AI.AvoidMass), + }.ToDictionary(key => key, key => + { + var (value, minValue, maxValue, meta) = GetAIFieldLimits(aiType, ActiveAI, key); + return gameObject.AddComponent().Initialise(0, value, minValue, maxValue, meta); + }); + showSection[Section.UpToEleven] = AI.UpToEleven; + } + break; + case AIType.VTOLAI: + { + var AI = ActiveAI as BDModuleVTOLAI; + if (AI == null) + { + Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Mismatch between AI type and actual AI."); + activeAIType = AIType.None; + inputFields = null; + return; + } + inputFields = new List { + nameof(AI.steerMult), + nameof(AI.steerKiAdjust), + nameof(AI.steerDamping), + nameof(AI.defaultAltitude), + nameof(AI.CombatAltitude), + nameof(AI.minAltitude), + nameof(AI.MaxSpeed), + nameof(AI.CombatSpeed), + nameof(AI.MaxPitchAngle), + nameof(AI.MaxBankAngle), + nameof(AI.WeaveFactor), + nameof(AI.MinEngagementRange), + nameof(AI.MaxEngagementRange), + }.ToDictionary(key => key, key => + { + var (value, minValue, maxValue, meta) = GetAIFieldLimits(aiType, ActiveAI, key); + return gameObject.AddComponent().Initialise(0, value, minValue, maxValue, meta); + }); + showSection[Section.UpToEleven] = AI.UpToEleven; + } + break; + case AIType.OrbitalAI: + { + var AI = ActiveAI as BDModuleOrbitalAI; + if (AI == null) + { + Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Mismatch between AI type and actual AI."); + activeAIType = AIType.None; + inputFields = null; + return; + } + inputFields = new List { + nameof(AI.steerMult), + nameof(AI.steerKiAdjust), + nameof(AI.steerDamping), + nameof(AI.steerMaxError), + nameof(AI.MinEngagementRange), + nameof(AI.ForceFiringRange), + nameof(AI.ManeuverSpeed), + nameof(AI.minFiringSpeed), + nameof(AI.firingSpeed), + nameof(AI.firingAngularVelocityLimit), + nameof(AI.minEvasionTime), + nameof(AI.evasionThreshold), + nameof(AI.evasionTimeThreshold), + nameof(AI.evasionErraticness), + nameof(AI.evasionMinRangeThreshold), + nameof(AI.collisionAvoidanceThreshold), + nameof(AI.vesselCollisionAvoidanceLookAheadPeriod), + }.ToDictionary(key => key, key => + { + var (value, minValue, maxValue, meta) = GetAIFieldLimits(aiType, ActiveAI, key); + return gameObject.AddComponent().Initialise(0, value, minValue, maxValue, meta); + }); + } + break; + default: + inputFields = null; + break; + } + } + + public void SyncInputFieldsNow(bool fromInputFields) + { + if (inputFields == null) return; + if (fromInputFields) + { + // Try to parse all the fields immediately so that they're up to date. + foreach (var field in inputFields.Keys) + { inputFields[field].tryParseValueNow(); } + } + switch (activeAIType) + { + case AIType.PilotAI: SetInputFieldValues(ActiveAI as BDModulePilotAI, fromInputFields); break; + case AIType.SurfaceAI: SetInputFieldValues(ActiveAI as BDModuleSurfaceAI, fromInputFields); break; + case AIType.VTOLAI: SetInputFieldValues(ActiveAI as BDModuleVTOLAI, fromInputFields); break; + case AIType.OrbitalAI: SetInputFieldValues(ActiveAI as BDModuleOrbitalAI, fromInputFields); break; + default: return; + } + } + + void SetInputFieldValues(T AI, bool fromInputFields) where T : BDGenericAIBase + { + if (AI == null) return; + if (fromInputFields) + { + foreach (var field in inputFields.Keys) + { + try + { + var fieldInfo = AI.GetType().GetField(field); + if (fieldInfo != null) + { fieldInfo.SetValue(AI, Convert.ChangeType(inputFields[field].currentValue, fieldInfo.FieldType)); } + else // Check if it's a property instead of a field. + { + var propInfo = AI.GetType().GetProperty(field); + propInfo.SetValue(AI, Convert.ChangeType(inputFields[field].currentValue, propInfo.PropertyType)); + } + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Failed to set current value of {field}: " + e.Message); } + } + } + else + { + foreach (var field in inputFields.Keys) + { + try + { + var fieldInfo = AI.GetType().GetField(field); + if (fieldInfo != null) + { inputFields[field].SetCurrentValue(Convert.ToDouble(fieldInfo.GetValue(AI))); } + else // Check if it's a property instead of a field. + { + var propInfo = AI.GetType().GetProperty(field); + inputFields[field].SetCurrentValue(Convert.ToDouble(propInfo.GetValue(AI))); + } + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDArmoryAIGUI]: Failed to set current value of {field}: " + e.Message + "\n" + e.StackTrace); } + } + } + } + + public void SetChooseOptionSliders() + { + if (ActiveAI == null) return; + switch (activeAIType) + { + case AIType.SurfaceAI: + { + var AI = ActiveAI as BDModuleSurfaceAI; + Drivertype = VehicleMovementTypes.IndexOf(AI.SurfaceType); + broadsideDir = AI.orbitDirections.IndexOf(AI.OrbitDirectionName); + } + break; + case AIType.VTOLAI: + { + var AI = ActiveAI as BDModuleVTOLAI; + broadsideDir = AI.orbitDirections.IndexOf(AI.OrbitDirectionName); + } + break; + case AIType.OrbitalAI: + { + var AI = ActiveAI as BDModuleOrbitalAI; + pidMode = AI.pidModes.IndexOf(AI.pidMode); + rollTowards = AI.rollTowardsModes.IndexOf(AI.rollTowards); + } + break; + } + } + + #region GUI + + void OnGUI() + { + if (!BDArmorySetup.GAME_UI_ENABLED) return; + + if (!windowBDAAIGUIEnabled || (!HighLogic.LoadedSceneIsFlight && !HighLogic.LoadedSceneIsEditor)) return; + if (!stylesConfigured) ConfigureStyles(); + if (HighLogic.LoadedSceneIsFlight) BDArmorySetup.SetGUIOpacity(); + if (resizingWindow && Event.current.type == EventType.MouseUp) { resizingWindow = false; } + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectAI.position); + BDArmorySetup.WindowRectAI = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), BDArmorySetup.WindowRectAI, WindowAIGUI, "", BDArmorySetup.BDGuiSkin.window);//"BDA Weapon Manager" + if (HighLogic.LoadedSceneIsFlight) BDArmorySetup.SetGUIOpacity(false); + } + + void ConfigureStyles() + { + Label = new GUIStyle(); + Label.alignment = TextAnchor.UpperLeft; + Label.normal.textColor = Color.white; + + contextLabelRight = new GUIStyle(); + contextLabelRight.alignment = TextAnchor.UpperRight; + contextLabelRight.normal.textColor = Color.white; + + contextLabel = new GUIStyle(Label); + + 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; + } + + enum Section { UpToEleven, PID, Altitude, Speed, Control, Evasion, Terrain, Ramming, Combat, Misc, FixedAutoTuneFields, VehicleType }; // Sections and other important toggles. + static Dictionary showSection = Enum.GetValues(typeof(Section)).Cast
().ToDictionary(s => s, s => false); + readonly Dictionary sectionHeights = []; + const float contentBorder = 0.2f * entryHeight; + const float contentMargin = 10; + const float contentInnerMargin = contentMargin + contentBorder; + const float columnIndent = 100; + const float labelWidth = 200; + const float sliderIndent = contentInnerMargin + labelWidth; + + Rect TitleButtonRect(float offset, float width = 1) + { + return new Rect((ColumnWidth * 2) - _windowMargin - (offset * _buttonSize), _windowMargin, width * _buttonSize, _buttonSize); + } + Rect SubsectionRect(float line) + { + return new Rect(contentMargin, contentTop + line * entryHeight, columnIndent, entryHeight); + } + Rect SettinglabelRect(float lines) + { + return new Rect(contentInnerMargin, lines * entryHeight, labelWidth, entryHeight); + } + Rect SettingSliderRect(float lines, float contentWidth) + { + return new Rect(sliderIndent, (lines + 0.2f) * entryHeight, contentWidth - 2 * contentMargin - labelWidth, entryHeight); + } + Rect SettingTextRect(float lines, float contentWidth) + { + return new Rect(sliderIndent, lines * entryHeight, contentWidth - 2 * contentMargin - labelWidth, entryHeight); + } + Rect ContextLabelRect(float lines, float contentWidth) => SettingTextRect(lines, contentWidth); + Rect ContextLabelRect(float lines) + { + return new Rect(sliderIndent, lines * entryHeight, 100, entryHeight); + } + Rect ContextLabelRectRight(float lines, float contentWidth) + { + return new Rect(contentWidth - columnIndent - 2 * contentMargin, lines * entryHeight, columnIndent, entryHeight); + } + + Rect ToggleButtonRect(float lines, float contentWidth) + { + return new Rect(contentInnerMargin, lines * entryHeight, contentWidth - 2 * contentInnerMargin, entryHeight); + } + + Rect ToggleButtonRects(float lines, float pos, float of, float contentWidth) + { + var gap = contentInnerMargin / 2f; + return new Rect(contentInnerMargin + pos / of * (contentWidth - gap * (of - 1f) - 2f * contentInnerMargin) + pos * gap, lines * entryHeight, 1f / of * (contentWidth - gap * (of - 1f) - 2f * contentInnerMargin), entryHeight); + } + + enum ContentType { FloatSlider, SemiLogSlider, FloatLogSlider, Toggle, Button }; + float ContentEntry(ContentType contentType, float line, float width, ref float value, string fieldName, string baseLOC, string formattedValue, bool splitContext = false) + { + switch (contentType) + { + case ContentType.FloatSlider: + { + GUI.Label(SettinglabelRect(line), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}") + ": " + formattedValue, Label); + if (!NumFieldsEnabled) + { + var (min, max, rounding, _, _, _) = GetFieldLimits(fieldName); + if (value != (value = GUI.HorizontalSlider(SettingSliderRect(line, width), value, min, max)) && rounding > 0) + value = BDAMath.RoundToUnit(value, rounding); + } + else + { + var field = inputFields[fieldName]; + field.tryParseValue(GUI.TextField(SettingTextRect(line, width), field.possibleValue, 8, field.style)); + value = (float)field.currentValue; + } + if (contextTipsEnabled) + { + if (splitContext) + { + GUI.Label(ContextLabelRect(++line), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_ContextLow"), Label); + GUI.Label(ContextLabelRectRight(line, width), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_ContextHigh"), contextLabelRight); + } + else GUI.Label(ContextLabelRect(++line, width), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_Context"), contextLabel); + } + ++line; + } + break; + case ContentType.SemiLogSlider: + { + GUI.Label(SettinglabelRect(line), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}") + ": " + formattedValue, Label); + if (!NumFieldsEnabled) + { + var (min, max, rounding, sigFig, withZero, reducedPrecisionAtMin) = GetFieldLimits(fieldName); + if (!cacheSemiLogLimits.ContainsKey(fieldName)) { cacheSemiLogLimits[fieldName] = null; } + var cache = cacheSemiLogLimits[fieldName]; + if (value != (value = GUIUtils.HorizontalSemiLogSlider(SettingSliderRect(line, width), value, min, max, sigFig, withZero, reducedPrecisionAtMin, ref cache)) && rounding > 0) + value = BDAMath.RoundToUnit(value, rounding); + } + else + { + var field = inputFields[fieldName]; + field.tryParseValue(GUI.TextField(SettingTextRect(line, width), field.possibleValue, 8, field.style)); + value = (float)field.currentValue; + } + if (contextTipsEnabled) + { + if (splitContext) + { + GUI.Label(ContextLabelRect(++line), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_ContextLow"), Label); + GUI.Label(ContextLabelRectRight(line, width), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_ContextHigh"), contextLabelRight); + } + else GUI.Label(ContextLabelRect(++line, width), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_Context"), contextLabel); + } + ++line; + } + break; + case ContentType.FloatLogSlider: + { + GUI.Label(SettinglabelRect(line), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}") + ": " + formattedValue, Label); + if (!NumFieldsEnabled) + { + var (min, max, _, steps, _, _) = GetFieldLimits(fieldName); + if (!cacheSemiLogLimits.ContainsKey(fieldName)) { cacheSemiLogLimits[fieldName] = null; } + var cache = cacheSemiLogLimits[fieldName]; + value = GUIUtils.HorizontalFloatLogSlider(SettingSliderRect(line, width), value, min, max, (int)steps, ref cache); + } + else + { + var field = inputFields[fieldName]; + field.tryParseValue(GUI.TextField(SettingTextRect(line, width), field.possibleValue, 8, field.style)); + value = (float)field.currentValue; + } + if (contextTipsEnabled) + { + if (splitContext) + { + GUI.Label(ContextLabelRect(++line), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_ContextLow"), Label); + GUI.Label(ContextLabelRectRight(line, width), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_ContextHigh"), contextLabelRight); + } + else GUI.Label(ContextLabelRect(++line, width), StringUtils.Localize($"#LOC_BDArmory_AIWindow_{baseLOC}_Context"), contextLabel); + } + ++line; + } + break; + case ContentType.Toggle: + { + line += 1.25f; + } + break; + case ContentType.Button: + { + line += 1.25f; + } + break; + } + return line; + } + readonly Dictionary cacheSemiLogLimits = []; + + BDGUIComboBox AISelectionComboBox; + int AISelectionIndex = -1; + void UpdateAISelectionComboBox(Rect rect) + { + if (!(HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight)) return; + var primaryAI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ActiveVessel != null) ? FlightGlobals.ActiveVessel.ActiveController().AI : null; + var ais = HighLogic.LoadedSceneIsEditor ? AIs : VesselModuleRegistry.GetIBDAIControls(FlightGlobals.ActiveVessel); + GUIContent[] listContent = [.. ais.Select(ai => new GUIContent(ai == primaryAI ? $"* {ai.aiType} *" : $"{ai.aiType}"))]; + if (listContent.Length > 0) + { + AISelectionComboBox = new BDGUIComboBox(rect, rect, new GUIContent(ActiveAI as IBDAIControl == primaryAI ? $"* {ActiveAI.aiType} *" : $"{ActiveAI.aiType}"), listContent, (listContent.Length + 1) * _buttonSize + 2 * _windowMargin, BDArmorySetup.BDGuiSkin.button, 1); + AISelectionIndex = AISelectionComboBox.SetSelectedItemIndex(ais.FindIndex(ai => ai.pilotEnabled)); + } + else + AISelectionComboBox = null; + } + + void WindowAIGUI(int windowID) + { + if (HighLogic.LoadedSceneIsEditor) GUIUtils.PreventClickThrough(BDArmorySetup.WindowRectAI, "AIGUI lock"); + float windowColumns = 2; + float contentIndent = contentMargin + columnIndent; + float contentWidth = 2 * ColumnWidth - 2 * contentMargin - columnIndent; + + GUI.DragWindow(new Rect(contentIndent + _windowMargin, _windowMargin, contentWidth + _windowMargin - 4 * _buttonSize, _windowMargin + _buttonSize)); + GUI.Label(new Rect(contentIndent, contentTop, contentWidth - 4 * _buttonSize, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_title"), Title); + #region AI Selection + if (ActiveAI != null) + { + if (AISelectionComboBox == null) UpdateAISelectionComboBox(new Rect(contentMargin, _windowMargin, columnIndent, _buttonSize)); + if (AISelectionComboBox != null && AISelectionIndex != (AISelectionIndex = AISelectionComboBox.Show())) + { + ActiveAI = ( + HighLogic.LoadedSceneIsEditor ? AIs[AISelectionIndex] + : VesselModuleRegistry.GetIBDAIControls(FlightGlobals.ActiveVessel).Skip(AISelectionIndex).First() + ) as BDGenericAIBase; + activeAIType = ActiveAI.aiType; + SetInputFields(activeAIType); + } + } + #endregion + + if (GUI.Button(TitleButtonRect(1), "X", windowBDAAIGUIEnabled ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box)) //Exit Button + { + if (button) button.SetFalse(); + else HideAIGUI(); // In case the button is disabled. + } + if (GUI.Button(TitleButtonRect(2), "i", infoLinkEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) //Infolink button + { infoLinkEnabled = !infoLinkEnabled; } + if (GUI.Button(TitleButtonRect(3), "?", contextTipsEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) //Context labels button + { contextTipsEnabled = !contextTipsEnabled; } + if (GUI.Button(TitleButtonRect(4), "#", NumFieldsEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) //Numeric fields button + { + NumFieldsEnabled = !NumFieldsEnabled; + SyncInputFieldsNow(!NumFieldsEnabled); + } + + float minHeight = 0; + if (activeAIType == AIType.None || ActiveAI == null) + { + GUI.Label(new Rect(contentMargin, contentTop + (1.75f * entryHeight), contentWidth, entryHeight), + StringUtils.Localize("#LOC_BDArmory_AIWindow_NoAI"), Title);// "No AI found." + } + else + { + height = Mathf.Lerp(height, contentHeight, 0.15f); + contentHeight = 0; + switch (activeAIType) + { + case AIType.PilotAI: + { + var AI = ActiveAI as BDModulePilotAI; + if (AI == null) { Debug.LogError($"[BDArmory.BDArmoryAIGUI]: AI module mismatch!"); activeAIType = AIType.None; break; } + + if (AISelectionComboBox == null || !AISelectionComboBox.IsOpen) + { // Section buttons + float line = 1.5f; + showSection[Section.PID] = GUI.Toggle(SubsectionRect(line), showSection[Section.PID], StringUtils.Localize("#LOC_BDArmory_AIWindow_PID"), showSection[Section.PID] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"PiD" + + line += 1.2f; + showSection[Section.Altitude] = GUI.Toggle(SubsectionRect(line), showSection[Section.Altitude], StringUtils.Localize("#LOC_BDArmory_AIWindow_Altitudes"), showSection[Section.Altitude] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Altitude" + + line += 1.2f; + showSection[Section.Speed] = GUI.Toggle(SubsectionRect(line), showSection[Section.Speed], StringUtils.Localize("#LOC_BDArmory_AIWindow_Speeds"), showSection[Section.Speed] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Speed" + + line += 1.2f; + showSection[Section.Control] = GUI.Toggle(SubsectionRect(line), showSection[Section.Control], StringUtils.Localize("#LOC_BDArmory_AIWindow_Control"), showSection[Section.Control] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Control" + + line += 1.2f; + showSection[Section.Evasion] = GUI.Toggle(SubsectionRect(line), showSection[Section.Evasion], StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeExtend"), showSection[Section.Evasion] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Evasion" + + line += 1.2f; + showSection[Section.Terrain] = GUI.Toggle(SubsectionRect(line), showSection[Section.Terrain], StringUtils.Localize("#LOC_BDArmory_AIWindow_Terrain"), showSection[Section.Terrain] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Terrain" + + line += 1.2f; + showSection[Section.Ramming] = GUI.Toggle(SubsectionRect(line), showSection[Section.Ramming], StringUtils.Localize("#LOC_BDArmory_AIWindow_Ramming"), showSection[Section.Ramming] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Ramming" + + line += 1.2f; + showSection[Section.Misc] = GUI.Toggle(SubsectionRect(line), showSection[Section.Misc], StringUtils.Localize("#LOC_BDArmory_AIWindow_Misc"), showSection[Section.Misc] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" + + line += 1.5f; + if (showSection[Section.UpToEleven] != (AI.UpToEleven = GUI.Toggle(SubsectionRect(line), AI.UpToEleven, + AI.UpToEleven ? StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_disabledText"), + AI.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)))//"Misc" + { + SetInputFields(activeAIType); + } + + #region Store/Restore + line += 1.5f; + GUIStyle saveStyle = BDArmorySetup.BDGuiSkin.button; + if (GUI.Button(SubsectionRect(line), "Save", saveStyle)) + { + AI.StoreSettings(); + } + + if (AI.Events["RestoreSettings"].active == true) + { + line += 1f; + GUIStyle restoreStyle = BDArmorySetup.BDGuiSkin.button; + if (GUI.Button(SubsectionRect(line), "Restore", restoreStyle)) + { + AI.RestoreSettings(); + } + } + #endregion + + minHeight = contentTop + (line + 1f) * entryHeight + _windowMargin; + } + + if (showSection[Section.PID] || showSection[Section.Altitude] || showSection[Section.Speed] || showSection[Section.Control] || showSection[Section.Evasion] || showSection[Section.Terrain] || showSection[Section.Ramming] || showSection[Section.Misc]) + { + scrollViewVectors[AIType.PilotAI] = GUI.BeginScrollView(new Rect(contentIndent, contentTop + entryHeight * 1.5f, ColumnWidth * 2 - contentIndent, WindowHeight - entryHeight * 1.5f - 2 * contentTop), scrollViewVectors.GetValueOrDefault(AIType.PilotAI), new Rect(0, 0, contentWidth - contentMargin * 2, height + contentTop)); + + GUI.BeginGroup(new Rect(contentMargin, 0, contentWidth - contentMargin * 2, height + 2 * contentBorder), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box + + contentWidth -= 24 + contentBorder; + + if (showSection[Section.PID]) + { + float pidLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.PID); + GUI.BeginGroup(new Rect(contentBorder, pidLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + pidLines += 0.25f; + + GUI.Label(SettinglabelRect(pidLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PID"), BoldLabel); + if (AI.threeAxisPID) + { + if (AI.threeAxisPID != (AI.threeAxisPID = GUI.Toggle(ToggleButtonRects(pidLines, 0, 1, contentWidth), AI.threeAxisPID, StringUtils.Localize("#LOC_BDArmory_AI_3AxisPID"), AI.threeAxisPID ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + pidLines += 1.25f; + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDPitchMult, nameof(AI.threeAxisPIDPitchMult), "3AxisPIDPitchMult", $"{AI.threeAxisPIDPitchMult:0.0}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDPitchKi, nameof(AI.threeAxisPIDPitchKi), "3AxisPIDPitchKi", $"{AI.threeAxisPIDPitchKi:0.00}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDPitchDamping, nameof(AI.threeAxisPIDPitchDamping), "3AxisPIDPitchDamping", $"{AI.threeAxisPIDPitchDamping:0.00}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDYawMult, nameof(AI.threeAxisPIDYawMult), "3AxisPIDYawMult", $"{AI.threeAxisPIDYawMult:0.0}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDYawKi, nameof(AI.threeAxisPIDYawKi), "3AxisPIDYawKi", $"{AI.threeAxisPIDYawKi:0.00}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDYawDamping, nameof(AI.threeAxisPIDYawDamping), "3AxisPIDYawDamping", $"{AI.threeAxisPIDYawDamping:0.00}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDRollMult, nameof(AI.threeAxisPIDRollMult), "3AxisPIDRollMult", $"{AI.threeAxisPIDRollMult:0.0}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDRollKi, nameof(AI.threeAxisPIDRollKi), "3AxisPIDRollKi", $"{AI.threeAxisPIDRollKi:0.00}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.threeAxisPIDRollDamping, nameof(AI.threeAxisPIDRollDamping), "3AxisPIDRollDamping", $"{AI.threeAxisPIDRollDamping:0.00}", splitContext: true); + } + else + { + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.steerMult, nameof(AI.steerMult), "SteerPower", $"{AI.steerMult:0.0}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.steerKiAdjust, nameof(AI.steerKiAdjust), "SteerKi", $"{AI.steerKiAdjust:0.00}", splitContext: true); + if (!AI.threeAxisSteerDamping && !AI.dynamicSteerDamping) + { + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.steerDamping, nameof(AI.steerDamping), "SteerDamping", $"{AI.steerDamping:0.00}", splitContext: true); + } + if (AI.threeAxisPID != (AI.threeAxisPID = GUI.Toggle(ToggleButtonRects(pidLines, 0, 3, contentWidth), AI.threeAxisPID, StringUtils.Localize("#LOC_BDArmory_AI_3AxisPID"), AI.threeAxisPID ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + if (AI.threeAxisSteerDamping != (AI.threeAxisSteerDamping = GUI.Toggle(ToggleButtonRects(pidLines, 1, 3, contentWidth), AI.threeAxisSteerDamping, StringUtils.Localize("#LOC_BDArmory_AI_3AxisSteerDamping"), AI.threeAxisSteerDamping ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + if (AI.dynamicSteerDamping != (AI.dynamicSteerDamping = GUI.Toggle(ToggleButtonRects(pidLines, 2, 3, contentWidth), AI.dynamicSteerDamping, StringUtils.Localize("#LOC_BDArmory_AI_DynamicDamping"), AI.dynamicSteerDamping ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + pidLines += 1.25f; + if (!AI.threeAxisSteerDamping && AI.dynamicSteerDamping) + { + GUI.Label(SettinglabelRect(pidLines++), StringUtils.Localize("#LOC_BDArmory_AI_DynamicDamping") + $": {AI.dynSteerDampingValue}", Label); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingMin, nameof(AI.DynamicDampingMin), "DynDampMin", $"{AI.DynamicDampingMin:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingMax, nameof(AI.DynamicDampingMax), "DynDampMax", $"{AI.DynamicDampingMax:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.dynamicSteerDampingFactor, nameof(AI.dynamicSteerDampingFactor), "DynDampMult", $"{AI.dynamicSteerDampingFactor:0.0}"); + } + if (AI.threeAxisSteerDamping) + { + // Pitch + if (AI.dynamicSteerDamping) + { + if (AI.dynamicDampingPitch != (AI.dynamicDampingPitch = GUI.Toggle(ToggleButtonRect(pidLines, contentWidth), AI.dynamicDampingPitch, StringUtils.Localize("#LOC_BDArmory_AI_DynamicDampingPitch"), AI.dynamicDampingPitch ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + pidLines += 1.25f; + } + if (AI.dynamicSteerDamping && AI.dynamicDampingPitch) + { + GUI.Label(SettinglabelRect(pidLines++), StringUtils.Localize("#LOC_BDArmory_AI_DynamicDampingPitch") + $": {AI.dynSteerDampingPitchValue}", Label); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingPitchMin, nameof(AI.DynamicDampingPitchMin), "DynDampMin", $"{AI.DynamicDampingPitchMin:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingPitchMax, nameof(AI.DynamicDampingPitchMax), "DynDampMax", $"{AI.DynamicDampingPitchMax:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.dynamicSteerDampingPitchFactor, nameof(AI.dynamicSteerDampingPitchFactor), "DynDampMult", $"{AI.dynamicSteerDampingPitchFactor:0.0}"); + } + else + { + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.steerDampingPitch, nameof(AI.steerDampingPitch), "SteerDampingPitch", $"{AI.steerDampingPitch:0.00}", splitContext: true); + } + // Yaw + if (AI.dynamicSteerDamping) + { + if (AI.dynamicDampingYaw != (AI.dynamicDampingYaw = GUI.Toggle(ToggleButtonRect(pidLines, contentWidth), AI.dynamicDampingYaw, StringUtils.Localize("#LOC_BDArmory_AI_DynamicDampingYaw"), AI.dynamicDampingYaw ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + pidLines += 1.25f; + } + if (AI.dynamicSteerDamping && AI.dynamicDampingYaw) + { + GUI.Label(SettinglabelRect(pidLines++), StringUtils.Localize("#LOC_BDArmory_AI_DynamicDampingYaw") + $": {AI.dynSteerDampingYawValue}", Label); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingYawMin, nameof(AI.DynamicDampingYawMin), "DynDampMin", $"{AI.DynamicDampingYawMin:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingYawMax, nameof(AI.DynamicDampingYawMax), "DynDampMax", $"{AI.DynamicDampingYawMax:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.dynamicSteerDampingYawFactor, nameof(AI.dynamicSteerDampingYawFactor), "DynDampMult", $"{AI.dynamicSteerDampingYawFactor:0.0}"); + } + else + { + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.steerDampingYaw, nameof(AI.steerDampingYaw), "SteerDampingYaw", $"{AI.steerDampingYaw:0.00}", splitContext: true); + } + // Roll + if (AI.dynamicSteerDamping) + { + if (AI.dynamicDampingRoll != (AI.dynamicDampingRoll = GUI.Toggle(ToggleButtonRect(pidLines, contentWidth), AI.dynamicDampingRoll, StringUtils.Localize("#LOC_BDArmory_AI_DynamicDampingRoll"), AI.dynamicDampingRoll ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) + { AI.OnPIDTogglesChanged(); } + pidLines += 1.25f; + } + if (AI.dynamicSteerDamping && AI.dynamicDampingRoll) + { + GUI.Label(SettinglabelRect(pidLines++), StringUtils.Localize("#LOC_BDArmory_AI_DynamicDampingRoll") + $": {AI.dynSteerDampingRollValue}", Label); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingRollMin, nameof(AI.DynamicDampingRollMin), "DynDampMin", $"{AI.DynamicDampingRollMin:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.DynamicDampingRollMax, nameof(AI.DynamicDampingRollMax), "DynDampMax", $"{AI.DynamicDampingRollMax:0.0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.dynamicSteerDampingRollFactor, nameof(AI.dynamicSteerDampingRollFactor), "DynDampMult", $"{AI.dynamicSteerDampingRollFactor:0.0}"); + } + else + { + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.steerDampingRoll, nameof(AI.steerDampingRoll), "SteerDampingRoll", $"{AI.steerDampingRoll:0.00}", splitContext: true); + } + } + } + + #region AutoTune + if (AI.AutoTune != GUI.Toggle(ToggleButtonRect(pidLines, contentWidth), AI.AutoTune, StringUtils.Localize("#LOC_BDArmory_AI_PID_AutoTune"), AI.AutoTune ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + AI.AutoTune = !AI.AutoTune; // Only actually toggle it when needed as the setter does extra stuff. + } + pidLines += 1.25f; + if (AI.AutoTune) // Auto-tuning + { + pidLines += 0.25f; + if (HighLogic.LoadedSceneIsEditor) + { + if (!string.IsNullOrEmpty(AI.autoTuningLossLabel)) // Not auto-tuning, but have been previously => show a summary of the last results. + GUI.Label(new Rect(contentInnerMargin + labelWidth / 8, pidLines++ * entryHeight, labelWidth, entryHeight), + StringUtils.Localize("#LOC_BDArmory_AI_PID_AutoTuning_Summary") + $": {AI.autoTuningSummary}", Label); + } + else + { + GUI.Label(SettinglabelRect(pidLines++), StringUtils.Localize("#LOC_BDArmory_AI_PID_AutoTuning_Loss") + $": {AI.autoTuningLossLabel}", Label); + GUI.Label(SettinglabelRect(pidLines++), $"\tParams: {AI.autoTuningLossLabel2}", Label); + GUI.Label(SettinglabelRect(pidLines++), $"\tField: {AI.autoTuningLossLabel3}", Label); + } + + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.autoTuningOptionNumSamples, nameof(AI.autoTuningOptionNumSamples), "PIDAutoTuningNumSamples", $"{AI.autoTuningOptionNumSamples:0}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.autoTuningOptionFastResponseRelevance, nameof(AI.autoTuningOptionFastResponseRelevance), "PIDAutoTuningFastResponseRelevance", $"{AI.autoTuningOptionFastResponseRelevance:G3}", splitContext: true); + pidLines = ContentEntry(ContentType.FloatLogSlider, pidLines, contentWidth, ref AI.autoTuningOptionInitialLearningRate, nameof(AI.autoTuningOptionInitialLearningRate), "PIDAutoTuningInitialLearningRate", $"{AI.autoTuningOptionInitialLearningRate:G3}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.autoTuningOptionInitialRollRelevance, nameof(AI.autoTuningOptionInitialRollRelevance), "PIDAutoTuningInitialRollRelevance", $"{AI.autoTuningOptionInitialRollRelevance:G3}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.autoTuningAltitude, nameof(AI.autoTuningAltitude), "PIDAutoTuningAltitude", $"{AI.autoTuningAltitude:0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.autoTuningSpeed, nameof(AI.autoTuningSpeed), "PIDAutoTuningSpeed", $"{AI.autoTuningSpeed:0}"); + pidLines = ContentEntry(ContentType.FloatSlider, pidLines, contentWidth, ref AI.autoTuningRecenteringDistance, nameof(AI.autoTuningRecenteringDistance), "PIDAutoTuningRecenteringDistance", $"{AI.autoTuningRecenteringDistance:0}km"); + + showSection[Section.FixedAutoTuneFields] = GUI.Toggle(ToggleButtonRects(pidLines, 0, 2, contentWidth), showSection[Section.FixedAutoTuneFields], StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields"), showSection[Section.FixedAutoTuneFields] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + AI.autoTuningOptionClampMaximums = GUI.Toggle(ToggleButtonRects(pidLines, 1, 2, contentWidth), AI.autoTuningOptionClampMaximums, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums"), AI.autoTuningOptionClampMaximums ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + pidLines += 1.25f; + + if (showSection[Section.FixedAutoTuneFields]) + { + bool resetAutoTuning = false; + if (AI.threeAxisPID) // Full 3-Axis PID + { + if (AI.autoTuningOptionFixedPp != (AI.autoTuningOptionFixedPp = GUI.Toggle(ToggleButtonRects(pidLines, 0, 9, contentWidth), AI.autoTuningOptionFixedPp, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Pp"), AI.autoTuningOptionFixedPp ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedIp != (AI.autoTuningOptionFixedIp = GUI.Toggle(ToggleButtonRects(pidLines, 1, 9, contentWidth), AI.autoTuningOptionFixedIp, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Ip"), AI.autoTuningOptionFixedIp ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDp != (AI.autoTuningOptionFixedDp = GUI.Toggle(ToggleButtonRects(pidLines, 2, 9, contentWidth), AI.autoTuningOptionFixedDp, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Dp"), AI.autoTuningOptionFixedDp ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedPy != (AI.autoTuningOptionFixedPy = GUI.Toggle(ToggleButtonRects(pidLines, 3, 9, contentWidth), AI.autoTuningOptionFixedPy, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Py"), AI.autoTuningOptionFixedPy ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedIy != (AI.autoTuningOptionFixedIy = GUI.Toggle(ToggleButtonRects(pidLines, 4, 9, contentWidth), AI.autoTuningOptionFixedIy, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Iy"), AI.autoTuningOptionFixedIy ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDy != (AI.autoTuningOptionFixedDy = GUI.Toggle(ToggleButtonRects(pidLines, 5, 9, contentWidth), AI.autoTuningOptionFixedDy, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Dy"), AI.autoTuningOptionFixedDy ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedPr != (AI.autoTuningOptionFixedPr = GUI.Toggle(ToggleButtonRects(pidLines, 6, 9, contentWidth), AI.autoTuningOptionFixedPr, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Pr"), AI.autoTuningOptionFixedPr ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedIr != (AI.autoTuningOptionFixedIr = GUI.Toggle(ToggleButtonRects(pidLines, 7, 9, contentWidth), AI.autoTuningOptionFixedIr, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Ir"), AI.autoTuningOptionFixedIr ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDr != (AI.autoTuningOptionFixedDr = GUI.Toggle(ToggleButtonRects(pidLines, 8, 9, contentWidth), AI.autoTuningOptionFixedDr, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_Dr"), AI.autoTuningOptionFixedDr ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + else if (!AI.dynamicSteerDamping) + { + if (!AI.threeAxisSteerDamping) // Normal PID + { + if (AI.autoTuningOptionFixedP != (AI.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(pidLines, 0, 3, contentWidth), AI.autoTuningOptionFixedP, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P"), AI.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedI != (AI.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(pidLines, 1, 3, contentWidth), AI.autoTuningOptionFixedI, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I"), AI.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedD != (AI.autoTuningOptionFixedD = GUI.Toggle(ToggleButtonRects(pidLines, 2, 3, contentWidth), AI.autoTuningOptionFixedD, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D"), AI.autoTuningOptionFixedD ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + else // PID with 3-axis static damping + { + if (AI.autoTuningOptionFixedP != (AI.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(pidLines, 0, 5, contentWidth), AI.autoTuningOptionFixedP, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P"), AI.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedI != (AI.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(pidLines, 1, 5, contentWidth), AI.autoTuningOptionFixedI, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I"), AI.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDP != (AI.autoTuningOptionFixedDP = GUI.Toggle(ToggleButtonRects(pidLines, 2, 5, contentWidth), AI.autoTuningOptionFixedDP, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch"), AI.autoTuningOptionFixedDP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDY != (AI.autoTuningOptionFixedDY = GUI.Toggle(ToggleButtonRects(pidLines, 3, 5, contentWidth), AI.autoTuningOptionFixedDY, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw"), AI.autoTuningOptionFixedDY ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDR != (AI.autoTuningOptionFixedDR = GUI.Toggle(ToggleButtonRects(pidLines, 4, 5, contentWidth), AI.autoTuningOptionFixedDR, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll"), AI.autoTuningOptionFixedDR ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + } + else + { + if (!AI.threeAxisSteerDamping) // PID with dynamic damping + { + if (AI.autoTuningOptionFixedP != (AI.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(pidLines, 0, 5, contentWidth), AI.autoTuningOptionFixedP, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P"), AI.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedI != (AI.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(pidLines, 1, 5, contentWidth), AI.autoTuningOptionFixedI, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I"), AI.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDOff != (AI.autoTuningOptionFixedDOff = GUI.Toggle(ToggleButtonRects(pidLines, 2, 5, contentWidth), AI.autoTuningOptionFixedDOff, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OffTarget"), AI.autoTuningOptionFixedDOff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDOn != (AI.autoTuningOptionFixedDOn = GUI.Toggle(ToggleButtonRects(pidLines, 3, 5, contentWidth), AI.autoTuningOptionFixedDOn, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_OnTarget"), AI.autoTuningOptionFixedDOn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDF != (AI.autoTuningOptionFixedDF = GUI.Toggle(ToggleButtonRects(pidLines, 4, 5, contentWidth), AI.autoTuningOptionFixedDF, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_D_Factor"), AI.autoTuningOptionFixedDF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + else // PID with 3-axis dynamic damping (or mixed) + { + int buttonCount = 2 + (AI.dynamicDampingPitch ? 3 : 1) + (AI.dynamicDampingRoll ? 3 : 1) + (AI.dynamicDampingYaw ? 3 : 1); + int buttonIndex = -1; + if (AI.autoTuningOptionFixedP != (AI.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedP, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_P"), AI.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedI != (AI.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedI, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_I"), AI.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.dynamicDampingPitch) + { + if (AI.autoTuningOptionFixedDPOff != (AI.autoTuningOptionFixedDPOff = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDPOff, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OffTarget"), AI.autoTuningOptionFixedDPOff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDPOn != (AI.autoTuningOptionFixedDPOn = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDPOn, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_OnTarget"), AI.autoTuningOptionFixedDPOn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDPF != (AI.autoTuningOptionFixedDPF = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDPF, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch_Factor"), AI.autoTuningOptionFixedDPF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + else + { + if (AI.autoTuningOptionFixedDP != (AI.autoTuningOptionFixedDP = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDP, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DPitch"), AI.autoTuningOptionFixedDP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + if (AI.dynamicDampingYaw) + { + if (AI.autoTuningOptionFixedDYOff != (AI.autoTuningOptionFixedDYOff = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDYOff, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OffTarget"), AI.autoTuningOptionFixedDYOff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDYOn != (AI.autoTuningOptionFixedDYOn = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDYOn, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_OnTarget"), AI.autoTuningOptionFixedDYOn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDYF != (AI.autoTuningOptionFixedDYF = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDYF, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw_Factor"), AI.autoTuningOptionFixedDYF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + else + { + if (AI.autoTuningOptionFixedDY != (AI.autoTuningOptionFixedDY = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDY, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DYaw"), AI.autoTuningOptionFixedDY ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + if (AI.dynamicDampingRoll) + { + if (AI.autoTuningOptionFixedDROff != (AI.autoTuningOptionFixedDROff = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDROff, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OffTarget"), AI.autoTuningOptionFixedDROff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDROn != (AI.autoTuningOptionFixedDROn = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDROn, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_OnTarget"), AI.autoTuningOptionFixedDROn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + if (AI.autoTuningOptionFixedDRF != (AI.autoTuningOptionFixedDRF = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDRF, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll_Factor"), AI.autoTuningOptionFixedDRF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + else + { + if (AI.autoTuningOptionFixedDR != (AI.autoTuningOptionFixedDR = GUI.Toggle(ToggleButtonRects(pidLines, ++buttonIndex, buttonCount, contentWidth), AI.autoTuningOptionFixedDR, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixed_DRoll"), AI.autoTuningOptionFixedDR ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetAutoTuning = true; + } + } + } + if (resetAutoTuning && HighLogic.LoadedSceneIsFlight) AI.pidAutoTuning.ResetGradient(); + pidLines += 1.25f; + } + } + else if (!string.IsNullOrEmpty(AI.autoTuningLossLabel)) // Not auto-tuning, but have been previously => show a summary of the last results. + { + GUI.Label(new Rect(contentInnerMargin + labelWidth / 8, pidLines * entryHeight, labelWidth, entryHeight), + StringUtils.Localize("#LOC_BDArmory_AI_PID_AutoTuning_Summary") + $": {AI.autoTuningSummary}", Label); + pidLines += 1.25f; + } + #endregion + + GUI.EndGroup(); + sectionHeights[Section.PID] = Mathf.Lerp(sectionHeight, pidLines, 0.15f); + pidLines += 0.1f; + contentHeight += pidLines * entryHeight; + } + if (showSection[Section.Altitude]) + { + float altLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Altitude); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + altLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + altLines += 0.25f; + + GUI.Label(SettinglabelRect(altLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Altitudes"), BoldLabel);//"Altitudes" + + var oldDefaultAlt = AI.defaultAltitude; + altLines = ContentEntry(ContentType.FloatSlider, altLines, contentWidth, ref AI.defaultAltitude, nameof(AI.defaultAltitude), "DefaultAltitude", $"{AI.defaultAltitude:0}m"); + if (AI.defaultAltitude != oldDefaultAlt) + { + AI.ClampFields("defaultAltitude"); + inputFields["minAltitude"].SetCurrentValue(AI.minAltitude); + inputFields["maxAltitude"].SetCurrentValue(AI.maxAltitude); + } + + var oldMinAlt = AI.minAltitude; + altLines = ContentEntry(ContentType.FloatSlider, altLines, contentWidth, ref AI.minAltitude, nameof(AI.minAltitude), "MinAltitude", $"{AI.minAltitude:0}m"); + if (AI.minAltitude != oldMinAlt) + { + AI.ClampFields("minAltitude"); + inputFields["defaultAltitude"].SetCurrentValue(AI.defaultAltitude); + inputFields["maxAltitude"].SetCurrentValue(AI.maxAltitude); + } + + AI.hardMinAltitude = GUI.Toggle(ToggleButtonRects(altLines, 0, 2, contentWidth), AI.hardMinAltitude, + StringUtils.Localize("#LOC_BDArmory_AI_HardMinAltitude"), AI.hardMinAltitude ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle);//"Hard Min Altitude" + AI.maxAltitudeToggle = GUI.Toggle(ToggleButtonRects(altLines, 1, 2, contentWidth), AI.maxAltitudeToggle, + StringUtils.Localize("#LOC_BDArmory_AIWindow_MaxAltitude"), AI.maxAltitudeToggle ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle);//"max altitude AGL" + altLines += 1.25f; + + if (AI.maxAltitudeToggle) + { + var oldMaxAlt = AI.maxAltitude; + altLines = ContentEntry(ContentType.FloatSlider, altLines, contentWidth, ref AI.maxAltitude, nameof(AI.maxAltitude), "MaxAltitude", $"{AI.maxAltitude:0}m"); + if (AI.maxAltitude != oldMaxAlt) + { + AI.ClampFields("maxAltitude"); + inputFields["minAltitude"].SetCurrentValue(AI.minAltitude); + inputFields["defaultAltitude"].SetCurrentValue(AI.defaultAltitude); + } + } + altLines = ContentEntry(ContentType.FloatSlider, altLines, contentWidth, ref AI.bombingAltitude, nameof(AI.bombingAltitude), "BombingAltitude", $"{AI.bombingAltitude:0}m"); + + AI.divebombing = GUI.Toggle(ToggleButtonRects(altLines, 0, 2, contentWidth), AI.divebombing, +StringUtils.Localize("#LOC_BDArmory_AIWindow_DiveBomb"), AI.divebombing ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle);//"Hard Min Altitude" + altLines += 1.25f; + GUI.EndGroup(); + sectionHeights[Section.Altitude] = Mathf.Lerp(sectionHeight, altLines, 0.15f); + altLines += 0.1f; + contentHeight += altLines * entryHeight; + } + if (showSection[Section.Speed]) + { + float spdLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Speed); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + spdLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + spdLines += 0.25f; + + GUI.Label(SettinglabelRect(spdLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Speeds"), BoldLabel);//"Speed" + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.maxSpeed, nameof(AI.maxSpeed), "MaxSpeed", $"{AI.maxSpeed:0}m/s"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.takeOffSpeed, nameof(AI.takeOffSpeed), "TakeOffSpeed", $"{AI.takeOffSpeed:0}m/s"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.minSpeed, nameof(AI.minSpeed), "MinSpeed", $"{AI.minSpeed:0}m/s"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.strafingSpeed, nameof(AI.strafingSpeed), "StrafingSpeed", $"{AI.strafingSpeed:0}m/s"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.idleSpeed, nameof(AI.idleSpeed), "IdleSpeed", $"{AI.idleSpeed:0}m/s"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.ABPriority, nameof(AI.ABPriority), "ABPriority", $"{AI.ABPriority:0}%"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.ABOverrideThreshold, nameof(AI.ABOverrideThreshold), "ABOverrideThreshold", $"{AI.ABOverrideThreshold:0}m/s"); + spdLines = ContentEntry(ContentType.FloatSlider, spdLines, contentWidth, ref AI.brakingPriority, nameof(AI.brakingPriority), "BrakingPriority", $"{AI.brakingPriority:0}%"); + + GUI.EndGroup(); + sectionHeights[Section.Speed] = Mathf.Lerp(sectionHeight, spdLines, 0.15f); + spdLines += 0.1f; + contentHeight += spdLines * entryHeight; + } + if (showSection[Section.Control]) + { + float ctrlLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Control); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + ctrlLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + ctrlLines += 0.25f; + + GUI.Label(SettinglabelRect(ctrlLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Control"), BoldLabel);//"Control" + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.maxSteer, nameof(AI.maxSteer), "LowSpeedSteerLimiter", $"{AI.maxSteer:0.00}"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.lowSpeedSwitch, nameof(AI.lowSpeedSwitch), "LowSpeedLimiterSpeed", $"{AI.lowSpeedSwitch:0}m/s"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.maxSteerAtMaxSpeed, nameof(AI.maxSteerAtMaxSpeed), "HighSpeedSteerLimiter", $"{AI.maxSteerAtMaxSpeed:0.00}"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.cornerSpeed, nameof(AI.cornerSpeed), "HighSpeedLimiterSpeed", $"{AI.cornerSpeed:0}m/s"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.altitudeSteerLimiterFactor, nameof(AI.altitudeSteerLimiterFactor), "AltitudeSteerLimiterFactor", $"{AI.altitudeSteerLimiterFactor:0}"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.altitudeSteerLimiterAltitude, nameof(AI.altitudeSteerLimiterAltitude), "AltitudeSteerLimiterAltitude", $"{AI.altitudeSteerLimiterAltitude:0}m"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.maxBank, nameof(AI.maxBank), "BankLimiter", $"{AI.maxBank:0}°"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.waypointPreRollTime, nameof(AI.waypointPreRollTime), "WaypointPreRollTime", $"{AI.waypointPreRollTime:0.00}s"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.waypointYawAuthorityTime, nameof(AI.waypointYawAuthorityTime), "WaypointYawAuthorityTime", $"{AI.waypointYawAuthorityTime:0.0}s"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.maxAllowedGForce, nameof(AI.maxAllowedGForce), "MaxAllowedGForce", $"{AI.maxAllowedGForce:0.0}g"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.maxAllowedAoA, nameof(AI.maxAllowedAoA), "MaxAllowedAoA", $"{AI.maxAllowedAoA:0.0}°"); + if (!(BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)) + { ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.postStallAoA, nameof(AI.postStallAoA), "PostStallAoA", $"{AI.postStallAoA:0.0}"); } + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.ImmelmannTurnAngle, nameof(AI.ImmelmannTurnAngle), "ImmelmannTurnAngle", $"{AI.ImmelmannTurnAngle:0}°"); + ctrlLines = ContentEntry(ContentType.FloatSlider, ctrlLines, contentWidth, ref AI.ImmelmannPitchUpBias, nameof(AI.ImmelmannPitchUpBias), "ImmelmannPitchUpBias", $"{AI.ImmelmannPitchUpBias:0}°/s"); + + GUI.EndGroup(); + sectionHeights[Section.Control] = Mathf.Lerp(sectionHeight, ctrlLines, 0.15f); + ctrlLines += 0.1f; + contentHeight += ctrlLines * entryHeight; + } + if (showSection[Section.Evasion]) + { + float evadeLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Evasion); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + evadeLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + evadeLines += 0.25f; + + #region Evasion + GUI.Label(SettinglabelRect(evadeLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Evade"), BoldLabel); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.minEvasionTime, nameof(AI.minEvasionTime), "MinEvasionTime", $"{AI.minEvasionTime:0.00}s"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.evasionThreshold, nameof(AI.evasionThreshold), "EvasionThreshold", $"{AI.evasionThreshold:0}m"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.evasionTimeThreshold, nameof(AI.evasionTimeThreshold), "EvasionTimeThreshold", $"{AI.evasionTimeThreshold:0.00}s"); + evadeLines = ContentEntry(ContentType.SemiLogSlider, evadeLines, contentWidth, ref AI.evasionMinRangeThreshold, nameof(AI.evasionMinRangeThreshold), "EvasionMinRangeThreshold", AI.evasionMinRangeThreshold < 1000 ? $"{AI.evasionMinRangeThreshold:0}m" : $"{AI.evasionMinRangeThreshold / 1000:0}km"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.evasionNonlinearity, nameof(AI.evasionNonlinearity), "EvasionNonlinearity", $"{AI.evasionNonlinearity:0.0}°"); + + AI.evasionIgnoreMyTargetTargetingMe = GUI.Toggle(ToggleButtonRect(evadeLines, contentWidth), AI.evasionIgnoreMyTargetTargetingMe, StringUtils.Localize("#LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe"), AI.evasionIgnoreMyTargetTargetingMe ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + evadeLines += 1.25f; + + AI.evasionMissileKinematic = GUI.Toggle(ToggleButtonRect(evadeLines, contentWidth), AI.evasionMissileKinematic, StringUtils.Localize("#LOC_BDArmory_AI_EvasionMissileKinematic"), AI.evasionMissileKinematic ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + evadeLines += 1.25f; + #endregion + + #region Craft Avoidance + evadeLines += 0.5f; + GUI.Label(SettinglabelRect(evadeLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Avoidance"), BoldLabel); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.collisionAvoidanceThreshold, nameof(AI.collisionAvoidanceThreshold), "CollisionAvoidanceThreshold", $"{AI.collisionAvoidanceThreshold:0}m"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.vesselCollisionAvoidanceLookAheadPeriod, nameof(AI.vesselCollisionAvoidanceLookAheadPeriod), "CollisionAvoidanceLookAheadPeriod", $"{AI.vesselCollisionAvoidanceLookAheadPeriod:0.0}s"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.vesselCollisionAvoidanceStrength, nameof(AI.vesselCollisionAvoidanceStrength), "CollisionAvoidanceStrength", $"{AI.vesselCollisionAvoidanceStrength:0.0} ({AI.vesselCollisionAvoidanceStrength / Time.fixedDeltaTime:0}°/s)"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.vesselStandoffDistance, nameof(AI.vesselStandoffDistance), "StandoffDistance", $"{AI.vesselStandoffDistance:0}m"); + #endregion + + #region Extending + if (AI.canExtend) + { + evadeLines += 0.5f; + GUI.Label(SettinglabelRect(evadeLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Extend"), BoldLabel); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendDistanceAirToAir, nameof(AI.extendDistanceAirToAir), "ExtendDistanceAirToAir", $"{AI.extendDistanceAirToAir:0}m"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendAngleAirToAir, nameof(AI.extendAngleAirToAir), "ExtendAngleAirToAir", $"{AI.extendAngleAirToAir:0}°"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendDistanceAirToGroundGuns, nameof(AI.extendDistanceAirToGroundGuns), "ExtendDistanceAirToGroundGuns", $"{AI.extendDistanceAirToGroundGuns:0}m"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendDistanceAirToGround, nameof(AI.extendDistanceAirToGround), "ExtendDistanceAirToGround", $"{AI.extendDistanceAirToGround:0}m"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendTargetVel, nameof(AI.extendTargetVel), "ExtendTargetVel", $"{AI.extendTargetVel:0.0}"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendTargetAngle, nameof(AI.extendTargetAngle), "ExtendTargetAngle", $"{AI.extendTargetAngle:0}°"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendTargetDist, nameof(AI.extendTargetDist), "ExtendTargetDist", $"{AI.extendTargetDist:0}m"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendAbortTime, nameof(AI.extendAbortTime), "ExtendAbortTime", $"{AI.extendAbortTime:0}s"); + evadeLines = ContentEntry(ContentType.FloatSlider, evadeLines, contentWidth, ref AI.extendMinGainRate, nameof(AI.extendMinGainRate), "ExtendMinGainRate", $"{AI.extendMinGainRate:0}m/s"); + } + AI.canExtend = GUI.Toggle(ToggleButtonRect(evadeLines, contentWidth), AI.canExtend, StringUtils.Localize("#LOC_BDArmory_AI_ExtendToggle"), AI.canExtend ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + evadeLines += 1.25f; + #endregion + + GUI.EndGroup(); + sectionHeights[Section.Evasion] = Mathf.Lerp(sectionHeight, evadeLines, 0.15f); + evadeLines += 0.1f; + contentHeight += evadeLines * entryHeight; + } + if (showSection[Section.Terrain]) + { + float gndLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Terrain); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + gndLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + gndLines += 0.25f; + + GUI.Label(SettinglabelRect(gndLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Terrain"), BoldLabel);//"Speed" + + var oldMinTwiddle = AI.turnRadiusTwiddleFactorMin; + gndLines = ContentEntry(ContentType.FloatSlider, gndLines, contentWidth, ref AI.turnRadiusTwiddleFactorMin, nameof(AI.turnRadiusTwiddleFactorMin), "TerrainAvoidanceMin", $"{AI.turnRadiusTwiddleFactorMin:0.0}"); + if (AI.turnRadiusTwiddleFactorMin != oldMinTwiddle) + { + AI.OnMinUpdated(null, null); + var field = inputFields["turnRadiusTwiddleFactorMax"]; + field.SetCurrentValue(AI.turnRadiusTwiddleFactorMax); + } + + var oldMaxTwiddle = AI.turnRadiusTwiddleFactorMax; + gndLines = ContentEntry(ContentType.FloatSlider, gndLines, contentWidth, ref AI.turnRadiusTwiddleFactorMax, nameof(AI.turnRadiusTwiddleFactorMax), "TerrainAvoidanceMax", $"{AI.turnRadiusTwiddleFactorMax:0.0}"); + if (AI.turnRadiusTwiddleFactorMax != oldMaxTwiddle) + { + AI.OnMaxUpdated(null, null); + var field = inputFields["turnRadiusTwiddleFactorMin"]; + field.SetCurrentValue(AI.turnRadiusTwiddleFactorMin); + } + + var oldTerrainAvoidanceCriticalAngle = AI.terrainAvoidanceCriticalAngle; + gndLines = ContentEntry(ContentType.FloatSlider, gndLines, contentWidth, ref AI.terrainAvoidanceCriticalAngle, nameof(AI.terrainAvoidanceCriticalAngle), "InvertedTerrainAvoidanceCriticalAngle", $"{AI.terrainAvoidanceCriticalAngle:0}°"); + if (AI.terrainAvoidanceCriticalAngle != oldTerrainAvoidanceCriticalAngle) { AI.OnTerrainAvoidanceCriticalAngleChanged(); } + + gndLines = ContentEntry(ContentType.FloatSlider, gndLines, contentWidth, ref AI.controlSurfaceDeploymentTime, nameof(AI.controlSurfaceDeploymentTime), "TerrainAvoidanceVesselReactionTime", $"{AI.controlSurfaceDeploymentTime:0.0}s"); + gndLines = ContentEntry(ContentType.FloatSlider, gndLines, contentWidth, ref AI.postTerrainAvoidanceCoolDownDuration, nameof(AI.postTerrainAvoidanceCoolDownDuration), "TerrainAvoidancePostAvoidanceCoolDown", $"{AI.postTerrainAvoidanceCoolDownDuration:0.00}s"); + gndLines = ContentEntry(ContentType.FloatSlider, gndLines, contentWidth, ref AI.waypointTerrainAvoidance, nameof(AI.waypointTerrainAvoidance), "WaypointTerrainAvoidance", $"{AI.waypointTerrainAvoidance:0.00}"); + + GUI.EndGroup(); + sectionHeights[Section.Terrain] = Mathf.Lerp(sectionHeight, gndLines, 0.15f); + gndLines += 0.1f; + contentHeight += gndLines * entryHeight; + } + if (showSection[Section.Ramming]) + { + float ramLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Ramming); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + ramLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + ramLines += 0.25f; + + GUI.Label(SettinglabelRect(ramLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Ramming"), BoldLabel);//"Ramming" + + AI.allowRamming = GUI.Toggle(ToggleButtonRect(ramLines, contentWidth), AI.allowRamming, + StringUtils.Localize("#LOC_BDArmory_AI_AllowRamming"), AI.allowRamming ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Allow Ramming" + ramLines += 1.25f; + + if (AI.allowRamming) + { + AI.allowRammingGroundTargets = GUI.Toggle(ToggleButtonRect(ramLines, contentWidth), AI.allowRammingGroundTargets, + StringUtils.Localize("#LOC_BDArmory_AI_AllowRammingGroundTargets"), AI.allowRammingGroundTargets ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Include Ground Targets" + ramLines += 1.25f; + ramLines = ContentEntry(ContentType.FloatSlider, ramLines, contentWidth, ref AI.controlSurfaceLag, nameof(AI.controlSurfaceLag), "ControlSurfaceLag", $"{AI.controlSurfaceLag:0.00}s"); + } + + GUI.EndGroup(); + sectionHeights[Section.Ramming] = Mathf.Lerp(sectionHeight, ramLines, 0.15f); + ramLines += 0.1f; + contentHeight += ramLines * entryHeight; + } + if (showSection[Section.Misc]) + { + float miscLines = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Misc); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + miscLines * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + miscLines += 0.25f; + + GUI.Label(SettinglabelRect(miscLines++), StringUtils.Localize("#LOC_BDArmory_AI_Orbit"), BoldLabel);//"orbit" + AI.ClockwiseOrbit = GUI.Toggle(ToggleButtonRect(miscLines, contentWidth), AI.ClockwiseOrbit, + AI.ClockwiseOrbit ? StringUtils.Localize("#LOC_BDArmory_AI_Orbit_Starboard") : StringUtils.Localize("#LOC_BDArmory_AI_Orbit_Port"), + AI.ClockwiseOrbit ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + miscLines += 1.25f; + if (contextTipsEnabled) GUI.Label(ContextLabelRect(miscLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Orbit_Context"), Label);//"orbit direction" + + GUI.Label(SettinglabelRect(miscLines++), StringUtils.Localize("#LOC_BDArmory_AI_Standby"), BoldLabel);//"Standby" + AI.standbyMode = GUI.Toggle(ToggleButtonRect(miscLines, contentWidth), + AI.standbyMode, AI.standbyMode ? StringUtils.Localize("#LOC_BDArmory_On") : StringUtils.Localize("#LOC_BDArmory_Off"), AI.standbyMode ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + miscLines += 1.25f; + if (contextTipsEnabled) GUI.Label(ContextLabelRect(miscLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Standby_Context"), Label);//"Activate when target in guard range" + + GUI.Label(SettinglabelRect(miscLines++), StringUtils.Localize("#LOC_BDArmory_ControlSurfaceSettings"), BoldLabel);//"Control Surface Settings" + if (GUI.Button(ToggleButtonRect(miscLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_StoreControlSurfaceSettings"), BDArmorySetup.BDGuiSkin.button)) + { + AI.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 (AI.Events["RestoreControlSurfaceSettings"].active == true) + { + GUIStyle restoreStyle = BDArmorySetup.BDGuiSkin.button; + if (GUI.Button(ToggleButtonRect(miscLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_RestoreControlSurfaceSettings"), restoreStyle)) + { + AI.RestoreControlSurfaceSettings(); + } + miscLines += 1.25f; + } + + GUI.EndGroup(); + sectionHeights[Section.Misc] = Mathf.Lerp(sectionHeight, miscLines, 0.15f); + miscLines += 0.1f; + contentHeight += miscLines * entryHeight; + } + + GUI.EndGroup(); + GUI.EndScrollView(); + } + + if (infoLinkEnabled) + { + windowColumns = 3; + + GUI.Label(new Rect(contentMargin + ColumnWidth * 2, contentTop, ColumnWidth - contentMargin, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" + BeginArea(new Rect(contentMargin + ColumnWidth * 2, contentTop + entryHeight * 1.5f, ColumnWidth - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - contentMargin), Height(WindowHeight - entryHeight * 1.5f - 2 * contentTop))) + { + scrollInfoVector = scrollViewScope.scrollPosition; + + if (showSection[Section.PID]) //these autoalign, so if new entries need to be added, they can just be slotted in + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PID"), BoldLabel, Width(ColumnWidth - contentMargin * 4 - 20)); //PID label + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //Pid desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_SteerPower"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //steer mult desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_SteerKi"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //steer ki desc. + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_Steerdamp"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //steer damp description + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_Dyndamp"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //dynamic damping desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_AutoTune") + (AI.AutoTune ? StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_PidHelp_AutoTune_details") : ""), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //auto-tuning desc + } + if (showSection[Section.Altitude]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_Altitudes"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //Altitude label + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //altitude description + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_Def"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //default alt desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_min"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //min alt desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_max"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //max alt desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_AltHelp_bombing"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //bombing alt desc + } + if (showSection[Section.Speed]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_Speeds"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //Speed header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //speed explanation + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_min"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //min+max speed desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_takeoff"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //takeoff speed + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_gnd"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //strafe speed + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_idle"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //idle speed + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABpriority"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //AB priority + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_ABOverrideThreshold"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //AB override threshold + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_SpeedHelp_BrakingPriority"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //Braking priority + } + if (showSection[Section.Control]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_Control"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //conrrol header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //control desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_limiters"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //low + high speed limiters + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_bank"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //max bank desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_clamps"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //max G + max AoA + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_modeSwitches"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //post-stall + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_ControlHelp_Immelmann"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //Immelmann turn angle + bias + } + if (showSection[Section.Evasion]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeExtend"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //evade header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //evade description + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Evade"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //evade dist/ time/ time threshold + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Nonlinearity"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //evade/extend nonlinearity + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Dodge"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //collision avoid + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_standoff"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //standoff distance + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_Extend"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //extend distances + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendVars"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //extend target dist/angle/vel + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendVel"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //extend target velocity + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendAngle"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //extend target angle + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendDist"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //extend target dist + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendAbortTime"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //extend abort time + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_EvadeHelp_ExtendToggle"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //evade/extend toggle + } + if (showSection[Section.Terrain]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_Terrain"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //Terrain avoid header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_TerrainHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //terrain avoid desc + } + if (showSection[Section.Ramming]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AI_Ramming"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //ramming header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_RamHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20));// ramming desc + } + if (showSection[Section.Misc]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_Misc"), BoldLabel, Width(ColumnWidth - (contentMargin * 4) - 20)); //misc header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_miscHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //misc desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_orbitHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //orbit dir + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Pilot_standbyHelp"), infoLinkStyle, Width(ColumnWidth - (contentMargin * 4) - 20)); //standby + } + } + EndArea(); + } + } + break; + case AIType.SurfaceAI: + { + var AI = ActiveAI as BDModuleSurfaceAI; + if (AI == null) { Debug.LogError($"[BDArmory.BDArmoryAIGUI]: AI module mismatch!"); activeAIType = AIType.None; break; } + + if (AISelectionComboBox == null || !AISelectionComboBox.IsOpen) + { // Section buttons + float line = 1.5f; + showSection[Section.PID] = GUI.Toggle(SubsectionRect(line), showSection[Section.PID], StringUtils.Localize("#LOC_BDArmory_AIWindow_PID"), showSection[Section.PID] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"PiD" + + line += 1.2f; + showSection[Section.Speed] = GUI.Toggle(SubsectionRect(line), showSection[Section.Speed], StringUtils.Localize("#LOC_BDArmory_AIWindow_Speeds"), showSection[Section.Speed] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Speed" + + line += 1.2f; + showSection[Section.Control] = GUI.Toggle(SubsectionRect(line), showSection[Section.Control], StringUtils.Localize("#LOC_BDArmory_AIWindow_Control"), showSection[Section.Control] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Control" + + line += 1.2f; + showSection[Section.Combat] = GUI.Toggle(SubsectionRect(line), showSection[Section.Combat], StringUtils.Localize("#LOC_BDArmory_AIWindow_Combat"), showSection[Section.Combat] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Combat" + + line += 1.5f; + if (showSection[Section.UpToEleven] != (AI.UpToEleven = GUI.Toggle(SubsectionRect(line), AI.UpToEleven, + AI.UpToEleven ? StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_disabledText"), + AI.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)))//"Misc" + { + SetInputFields(activeAIType); + } + + minHeight = contentTop + (line + 1f) * entryHeight + _windowMargin; + } + + { // Controls panel + scrollViewVectors[AIType.SurfaceAI] = GUI.BeginScrollView( + new Rect(contentMargin + 100, contentTop + entryHeight * 1.5f, (ColumnWidth * 2) - 100 - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop), + scrollViewVectors.GetValueOrDefault(AIType.SurfaceAI), + new Rect(0, 0, ColumnWidth * 2 - 120 - contentMargin * 2, height + contentTop) + ); + + GUI.BeginGroup(new Rect(contentMargin, 0, ColumnWidth * 2 - 120 - contentMargin * 2, height + 2 * contentBorder), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box + + contentWidth -= 24 + contentBorder; + + { // Vehicle Type + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.VehicleType); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + GUI.Label(SettinglabelRect(line), StringUtils.Localize("#LOC_BDArmory_AIWindow_VehicleType") + $": {AI.SurfaceTypeName}", Label); + if (Drivertype != (Drivertype = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(line++, contentWidth), Drivertype, 0, VehicleMovementTypes.Length - 1)))) + { + AI.SurfaceTypeName = VehicleMovementTypes[Drivertype].ToString(); + AI.ChooseOptionsUpdated(null, null); + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_VehicleType_Context"), contextLabel); + } + + GUI.EndGroup(); + sectionHeights[Section.VehicleType] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + + if (AI.SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + if (showSection[Section.PID]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.PID); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerMult, nameof(AI.steerMult), "SteerPower", $"{AI.steerMult:0.0}", true); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerDamping, nameof(AI.steerDamping), "SteerDamping", $"{AI.steerDamping:0.0}", true); + + GUI.EndGroup(); + sectionHeights[Section.PID] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Speed]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Speed); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.CruiseSpeed, nameof(AI.CruiseSpeed), "CruiseSpeed", $"{AI.CruiseSpeed:0}m/s"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxSpeed, nameof(AI.MaxSpeed), "MaxSpeed", $"{AI.MaxSpeed:0}m/s"); + + GUI.EndGroup(); + sectionHeights[Section.Speed] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Control]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Control); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxSlopeAngle, nameof(AI.MaxSlopeAngle), "MaxSlopeAngle", $"{AI.MaxSlopeAngle:0}°"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxDrift, nameof(AI.MaxDrift), "MaxDrift", $"{AI.MaxDrift:0}°"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.TargetPitch, nameof(AI.TargetPitch), "TargetPitch", $"{AI.TargetPitch:0.0}°"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.BankAngle, nameof(AI.BankAngle), "BankAngle", $"{AI.BankAngle:0}°"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.AvoidMass, nameof(AI.AvoidMass), "MinObstacleMass", $"{AI.AvoidMass:0}t"); + + if (broadsideDir != (broadsideDir = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(line, contentWidth), broadsideDir, 0, AI.orbitDirections.Length - 1)))) + { + AI.SetBroadsideDirection(AI.orbitDirections[broadsideDir]); + AI.ChooseOptionsUpdated(null, null); + } + GUI.Label(SettinglabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PreferredBroadsideDirection") + $": {AI.OrbitDirectionName}", Label); + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context"), contextLabel); + } + + AI.ManeuverRCS = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.ManeuverRCS, + StringUtils.Localize("#LOC_BDArmory_AIWindow_ManeuverRCS") + " : " + (AI.ManeuverRCS ? StringUtils.Localize("#LOC_BDArmory_AI_ManeuverRCS_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_ManeuverRCS_disabledText")), + AI.ManeuverRCS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_ManeuverRCS_Context"), contextLabel); + } + + GUI.EndGroup(); + sectionHeights[Section.Control] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + } + if (showSection[Section.Combat]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Combat); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MinEngagementRange, nameof(AI.MinEngagementRange), "MinEngagementRange", $"{AI.MinEngagementRange:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxEngagementRange, nameof(AI.MaxEngagementRange), "MaxEngagementRange", $"{AI.MaxEngagementRange:0}m"); + if (AI.SurfaceType == AIUtils.VehicleMovementType.Submarine) + { line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.CombatAltitude, nameof(AI.CombatAltitude), "CombatAltitude", $"{AI.CombatAltitude:0}m"); } + if (AI.SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.WeaveFactor, nameof(AI.WeaveFactor), "WeaveFactor", $"{AI.WeaveFactor:0.0}"); + if (AI.SurfaceType == AIUtils.VehicleMovementType.Land) + { + AI.maintainMinRange = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.maintainMinRange, + StringUtils.Localize("#LOC_BDArmory_AIWindow_MaintainEngagementRange") + " : " + (AI.maintainMinRange ? StringUtils.Localize("#LOC_BDArmory_true") : StringUtils.Localize("#LOC_BDArmory_false")), + AI.maintainMinRange ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Maintain Min range" + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_MaintainEngagementRange_Context"), contextLabel); + } + } + + AI.BroadsideAttack = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.BroadsideAttack, + StringUtils.Localize("#LOC_BDArmory_AIWindow_BroadsideAttack") + " : " + (AI.BroadsideAttack ? StringUtils.Localize("#LOC_BDArmory_AI_BroadsideAttack_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_BroadsideAttack_disabledText")), + AI.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//Broadside Attack" + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_BroadsideAttack_Context"), contextLabel); + } + } + + GUI.EndGroup(); + sectionHeights[Section.Combat] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + + GUI.EndGroup(); + GUI.EndScrollView(); + } + + if (infoLinkEnabled) + { + windowColumns = 3; + + GUI.Label(new Rect(contentMargin + ColumnWidth * 2, contentTop, ColumnWidth - contentMargin, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" + BeginArea(new Rect(contentMargin + ColumnWidth * 2, contentTop + entryHeight * 1.5f, ColumnWidth - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - contentMargin), Height(WindowHeight - entryHeight * 1.5f - 2 * contentTop))) + { + scrollInfoVector = scrollViewScope.scrollPosition; + + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Type"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //Pid desc + if (AI.SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + if (showSection[Section.PID]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_SteerPower"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //steer mult desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_SteerDamping"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //steer damp desc + } + if (showSection[Section.Speed]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Speeds"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //cruise, flank speed desc + } + if (showSection[Section.Control]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Slopes"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //tgt pitch, slope angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Drift"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //drift angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Bank"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //bank angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_AvoidMass"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //avoid mass desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Orientation"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //attack vector, broadside desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_RCS"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //RCS desc + } + } + if (showSection[Section.Combat]) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Engagement"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //engage ranges desc + if (AI.SurfaceType == AIUtils.VehicleMovementType.Submarine) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Altitude"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //sub cruise/combat depth + } + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_Weave"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //weave factor desc + if (AI.SurfaceType == AIUtils.VehicleMovementType.Land) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Surface_MaintainMinRange"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); //maintain min range desc + } + } + } + EndArea(); + } + } + break; + case AIType.VTOLAI: + { + var AI = ActiveAI as BDModuleVTOLAI; + if (AI == null) { Debug.LogError($"[BDArmory.BDArmoryAIGUI]: AI module mismatch!"); activeAIType = AIType.None; break; } + + if (AISelectionComboBox == null || !AISelectionComboBox.IsOpen) + { // Section buttons + float line = 1.5f; + showSection[Section.PID] = GUI.Toggle(SubsectionRect(line), showSection[Section.PID], StringUtils.Localize("#LOC_BDArmory_AIWindow_PID"), showSection[Section.PID] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"PiD" + + line += 1.2f; + showSection[Section.Altitude] = GUI.Toggle(SubsectionRect(line), showSection[Section.Altitude], StringUtils.Localize("#LOC_BDArmory_AIWindow_Altitudes"), showSection[Section.Altitude] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Altitude" + + line += 1.2f; + showSection[Section.Speed] = GUI.Toggle(SubsectionRect(line), showSection[Section.Speed], StringUtils.Localize("#LOC_BDArmory_AIWindow_Speeds"), showSection[Section.Speed] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Speed" + + line += 1.2f; + showSection[Section.Control] = GUI.Toggle(SubsectionRect(line), showSection[Section.Control], StringUtils.Localize("#LOC_BDArmory_AIWindow_Control"), showSection[Section.Control] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Control" + + line += 1.2f; + showSection[Section.Combat] = GUI.Toggle(SubsectionRect(line), showSection[Section.Combat], StringUtils.Localize("#LOC_BDArmory_AIWindow_Combat"), showSection[Section.Combat] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Combat" + + line += 1.5f; + if (showSection[Section.UpToEleven] != (AI.UpToEleven = GUI.Toggle(SubsectionRect(line), AI.UpToEleven, + AI.UpToEleven ? StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_UnclampTuning_disabledText"), + AI.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)))//"Misc" + { + SetInputFields(activeAIType); + } + + minHeight = contentTop + (line + 1f) * entryHeight + _windowMargin; + } + + if (showSection[Section.PID] || showSection[Section.Altitude] || showSection[Section.Speed] || showSection[Section.Control] || showSection[Section.Combat]) // Controls panel + { + scrollViewVectors[AIType.VTOLAI] = GUI.BeginScrollView( + new Rect(contentMargin + 100, contentTop + entryHeight * 1.5f, (ColumnWidth * 2) - 100 - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop), + scrollViewVectors.GetValueOrDefault(AIType.VTOLAI), + new Rect(0, 0, ColumnWidth * 2 - 120 - contentMargin * 2, height + contentTop) + ); + + GUI.BeginGroup(new Rect(contentMargin, 0, ColumnWidth * 2 - 120 - contentMargin * 2, height + 2 * contentBorder), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box + + contentWidth -= 24 + contentBorder; + + if (showSection[Section.PID]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.PID); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerMult, nameof(AI.steerMult), "SteerPower", $"{AI.steerMult:0.0}", true); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerKiAdjust, nameof(AI.steerKiAdjust), "SteerKi", $"{AI.steerKiAdjust:0.00}", true); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerDamping, nameof(AI.steerDamping), "SteerDamping", $"{AI.steerDamping:0.0}", true); + + GUI.EndGroup(); + sectionHeights[Section.PID] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Altitude]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Altitude); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.defaultAltitude, nameof(AI.defaultAltitude), "DefaultAltitude", $"{AI.defaultAltitude:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.CombatAltitude, nameof(AI.CombatAltitude), "CombatAltitude", $"{AI.CombatAltitude:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.bombingAltitude, nameof(AI.bombingAltitude), "BombingAltitude", $"{AI.bombingAltitude:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.minAltitude, nameof(AI.minAltitude), "MinAltitude", $"{AI.minAltitude:0}m"); + + GUI.EndGroup(); + sectionHeights[Section.Altitude] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Speed]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Speed); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxSpeed, nameof(AI.MaxSpeed), "MaxSpeed", $"{AI.MaxSpeed:0}m/s"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.CombatSpeed, nameof(AI.CombatSpeed), "CombatSpeed", $"{AI.CombatSpeed:0}m/s"); + + GUI.EndGroup(); + sectionHeights[Section.Speed] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Control]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Control); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxPitchAngle, nameof(AI.MaxPitchAngle), "MaxPitchAngle", $"{AI.MaxPitchAngle:0}°"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxBankAngle, nameof(AI.MaxBankAngle), "MaxBankAngle", $"{AI.MaxBankAngle:0}°"); + + GUI.Label(SettinglabelRect(line), StringUtils.Localize("#LOC_BDArmory_AIWindow_PreferredBroadsideDirection") + ": " + AI.OrbitDirectionName, Label); + if (broadsideDir != (broadsideDir = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(line++, contentWidth), broadsideDir, 0, AI.orbitDirections.Length - 1)))) + { + AI.SetBroadsideDirection(AI.orbitDirections[broadsideDir]); + AI.ChooseOptionsUpdated(null, null); + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PreferredBroadsideDirection_Context"), contextLabel); + } + + AI.ManeuverRCS = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.ManeuverRCS, + StringUtils.Localize("#LOC_BDArmory_AIWindow_ManeuverRCS") + " : " + (AI.ManeuverRCS ? StringUtils.Localize("#LOC_BDArmory_AI_ManeuverRCS_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_ManeuverRCS_disabledText")), + AI.ManeuverRCS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_ManeuverRCS_Context"), contextLabel); + } + + GUI.EndGroup(); + sectionHeights[Section.Control] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Combat]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Combat); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.WeaveFactor, nameof(AI.WeaveFactor), "WeaveFactor", $"{AI.WeaveFactor:0.0}"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MinEngagementRange, nameof(AI.MinEngagementRange), "MinEngagementRange", $"{AI.MinEngagementRange:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.MaxEngagementRange, nameof(AI.MaxEngagementRange), "MaxEngagementRange", $"{AI.MaxEngagementRange:0}m"); + + AI.BroadsideAttack = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.BroadsideAttack, + StringUtils.Localize("#LOC_BDArmory_AIWindow_BroadsideAttack") + " : " + (AI.BroadsideAttack ? StringUtils.Localize("#LOC_BDArmory_AI_BroadsideAttack_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_BroadsideAttack_disabledText")), + AI.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_BroadsideAttack_Context"), contextLabel); + } + + GUI.EndGroup(); + sectionHeights[Section.Combat] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + + GUI.EndGroup(); + GUI.EndScrollView(); + } + + if (infoLinkEnabled) + { + windowColumns = 3; + + GUI.Label(new Rect(contentMargin + ColumnWidth * 2, contentTop, ColumnWidth - contentMargin, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" + BeginArea(new Rect(contentMargin + ColumnWidth * 2, contentTop + entryHeight * 1.5f, ColumnWidth - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - contentMargin), Height(WindowHeight - entryHeight * 1.5f - 2 * contentTop))) + { + scrollInfoVector = scrollViewScope.scrollPosition; + + // FIXME + if (showSection[Section.PID]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_VTOL_PID"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Altitude]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_VTOL_Altitudes"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Speed]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_VTOL_Speeds"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Control]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_VTOL_Control"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Combat]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_VTOL_Combat"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + } + EndArea(); + } + } + break; + case AIType.OrbitalAI: + { + var AI = ActiveAI as BDModuleOrbitalAI; + if (AI == null) { Debug.LogError($"[BDArmory.BDArmoryAIGUI]: AI module mismatch!"); activeAIType = AIType.None; break; } + + if (AISelectionComboBox == null || !AISelectionComboBox.IsOpen) + { // Section buttons + float line = 1.5f; + showSection[Section.PID] = GUI.Toggle(SubsectionRect(line), showSection[Section.PID], StringUtils.Localize("#LOC_BDArmory_AIWindow_PID"), showSection[Section.PID] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"PiD" + + line += 1.2f; + showSection[Section.Combat] = GUI.Toggle(SubsectionRect(line), showSection[Section.Combat], StringUtils.Localize("#LOC_BDArmory_AIWindow_Combat"), showSection[Section.Combat] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Combat" + + line += 1.2f; + showSection[Section.Speed] = GUI.Toggle(SubsectionRect(line), showSection[Section.Speed], StringUtils.Localize("#LOC_BDArmory_AIWindow_Speeds"), showSection[Section.Speed] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Speed" + + line += 1.2f; + showSection[Section.Control] = GUI.Toggle(SubsectionRect(line), showSection[Section.Control], StringUtils.Localize("#LOC_BDArmory_AIWindow_Control"), showSection[Section.Control] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Control" + + line += 1.2f; + showSection[Section.Evasion] = GUI.Toggle(SubsectionRect(line), showSection[Section.Evasion], StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeExtend"), showSection[Section.Evasion] ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Evasion" + + minHeight = contentTop + (line + 1f) * entryHeight + _windowMargin; + } + + if (showSection[Section.PID] || showSection[Section.Combat] || showSection[Section.Speed] || showSection[Section.Control] || showSection[Section.Evasion]) // Controls panel + { + scrollViewVectors[AIType.OrbitalAI] = GUI.BeginScrollView( + new Rect(contentMargin + 100, contentTop + entryHeight * 1.5f, (ColumnWidth * 2) - 100 - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop), + scrollViewVectors.GetValueOrDefault(AIType.OrbitalAI), + new Rect(0, 0, ColumnWidth * 2 - 120 - contentMargin * 2, height + contentTop) + ); + + GUI.BeginGroup(new Rect(contentMargin, 0, ColumnWidth * 2 - 120 - contentMargin * 2, height + 2 * contentBorder), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box + + contentWidth -= 24 + contentBorder; + + if (showSection[Section.PID]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.PID); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + // PID Mode + GUI.Label(SettinglabelRect(line), StringUtils.Localize("#LOC_BDArmory_AIWindow_OrbitalPIDActive") + $": {AI.pidMode}", Label); + if (pidMode != (pidMode = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(line++, contentWidth), pidMode, 0, PIDModeTypes.Length - 1)))) + { + AI.pidMode = PIDModeTypes[pidMode].ToString(); + AI.ChooseOptionsUpdated(null, null); + } + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerMult, nameof(AI.steerMult), "SteerPower", $"{AI.steerMult:0.0}", true); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerKiAdjust, nameof(AI.steerKiAdjust), "SteerKi", $"{AI.steerKiAdjust:0.00}", true); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerDamping, nameof(AI.steerDamping), "SteerDamping", $"{AI.steerDamping:0.0}", true); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.steerMaxError, nameof(AI.steerMaxError), "SteerMaxError", $"{AI.steerMaxError:0.0}", true); + + GUI.EndGroup(); + sectionHeights[Section.PID] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Combat]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Combat); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + AI.BroadsideAttack = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.BroadsideAttack, + StringUtils.Localize("#LOC_BDArmory_AIWindow_BroadsideAttack") + " : " + (AI.BroadsideAttack ? StringUtils.Localize("#LOC_BDArmory_AI_BroadsideAttack_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_BroadsideAttack_disabledText")), + AI.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//Broadside Attack" + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_BroadsideAttack_Context"), contextLabel); + } + + GUI.Label(SettinglabelRect(line), StringUtils.Localize("#LOC_BDArmory_AIWindow_RollMode") + $": {AI.rollTowards}", Label); + if (rollTowards != (rollTowards = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(line++, contentWidth), rollTowards, 0, RollModeTypes.Length - 1)))) + { + AI.rollTowards = RollModeTypes[rollTowards].ToString(); + AI.ChooseOptionsUpdated(null, null); + } + + var oldMinEngagementRange = AI.MinEngagementRange; + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.MinEngagementRange, nameof(AI.MinEngagementRange), "MinEngagementRange", $"{AI.MinEngagementRange:0}m"); + if (AI.MinEngagementRange != oldMinEngagementRange) + { + AI.OnMinUpdated(null, null); + var field = inputFields["ForceFiringRange"]; + field.SetCurrentValue(AI.ForceFiringRange); + } + + var oldForceFiringRange = AI.ForceFiringRange; + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.ForceFiringRange, nameof(AI.ForceFiringRange), "ForceFiringRange", $"{AI.ForceFiringRange:0}m"); + if (AI.ForceFiringRange != oldForceFiringRange) + { + AI.OnMaxUpdated(null, null); + var field = inputFields["MinEngagementRange"]; + field.SetCurrentValue(AI.MinEngagementRange); + } + + AI.allowRamming = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.allowRamming, + StringUtils.Localize("#LOC_BDArmory_AI_AllowRamming"), AI.allowRamming ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Allow Ramming" + line += 1.25f; + + GUI.EndGroup(); + sectionHeights[Section.Combat] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Speed]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Speed); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + var oldManeuverSpeed = AI.ManeuverSpeed; + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.ManeuverSpeed, nameof(AI.ManeuverSpeed), "ManeuverSpeed", $"{AI.ManeuverSpeed:0}m/s"); + if (AI.ManeuverSpeed != oldManeuverSpeed) + { + AI.OnMaxUpdated(null, null); + var field = inputFields["FiringSpeed"]; + field.SetCurrentValue(AI.firingSpeed); + } + + var oldMinFiringSpeed = AI.minFiringSpeed; + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.minFiringSpeed, nameof(AI.minFiringSpeed), "minFiringSpeed", $"{AI.minFiringSpeed:0}m/s"); + if (AI.minFiringSpeed != oldMinFiringSpeed) + { + AI.OnMinUpdated(null, null); + var field = inputFields["FiringSpeed"]; + field.SetCurrentValue(AI.firingSpeed); + } + + var oldFiringSpeed = AI.firingSpeed; + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.firingSpeed, nameof(AI.firingSpeed), "FiringSpeed", $"{AI.firingSpeed:0}m/s"); + if (AI.firingSpeed != oldFiringSpeed) + { + AI.OnMaxUpdated(null, null); + var field = inputFields["minFiringSpeed"]; + field.SetCurrentValue(AI.minFiringSpeed); + + AI.OnMinUpdated(null, null); + var field2 = inputFields["ManeuverSpeed"]; + field2.SetCurrentValue(AI.ManeuverSpeed); + } + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.firingAngularVelocityLimit, nameof(AI.firingAngularVelocityLimit), "FiringAngularVelocityLimit", $"{AI.firingAngularVelocityLimit:0}deg/s"); + + GUI.EndGroup(); + sectionHeights[Section.Speed] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Control]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Control); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + AI.FiringRCS = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.FiringRCS, + StringUtils.Localize("#LOC_BDArmory_AIWindow_FiringRCS") + " : " + (AI.FiringRCS ? StringUtils.Localize("#LOC_BDArmory_AI_FiringRCS_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_FiringRCS_disabledText")), + AI.FiringRCS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_FiringRCS_Context"), contextLabel); + } + + AI.ManeuverRCS = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.ManeuverRCS, + StringUtils.Localize("#LOC_BDArmory_AIWindow_ManeuverRCS") + " : " + (AI.ManeuverRCS ? StringUtils.Localize("#LOC_BDArmory_AI_ManeuverRCS_enabledText") : StringUtils.Localize("#LOC_BDArmory_AI_ManeuverRCS_disabledText")), + AI.ManeuverRCS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_ManeuverRCS_Context"), contextLabel); + } + + AI.ReverseThrust = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.ReverseThrust, + StringUtils.Localize("#LOC_BDArmory_AIWindow_ReverseEngines") + " : " + (AI.ReverseThrust ? StringUtils.Localize("#LOC_BDArmory_Enabled") : StringUtils.Localize("#LOC_BDArmory_Disabled")), + AI.ReverseThrust ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_ReverseEngines_Context"), contextLabel); + } + + AI.EngineRCSRotation = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.EngineRCSRotation, + StringUtils.Localize("#LOC_BDArmory_AIWindow_EngineRCSRotation") + " : " + (AI.EngineRCSRotation ? StringUtils.Localize("#LOC_BDArmory_Enabled") : StringUtils.Localize("#LOC_BDArmory_Disabled")), + AI.EngineRCSRotation ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_EngineRCSRotation_Context"), contextLabel); + } + + AI.EngineRCSTranslation = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.EngineRCSTranslation, + StringUtils.Localize("#LOC_BDArmory_AIWindow_EngineRCSTranslation") + " : " + (AI.EngineRCSTranslation ? StringUtils.Localize("#LOC_BDArmory_Enabled") : StringUtils.Localize("#LOC_BDArmory_Disabled")), + AI.EngineRCSTranslation ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_EngineRCSTranslation_Context"), contextLabel); + } + + GUI.EndGroup(); + sectionHeights[Section.Control] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + if (showSection[Section.Evasion]) + { + float line = 0.2f; + var sectionHeight = sectionHeights.GetValueOrDefault(Section.Evasion); + GUI.BeginGroup(new Rect(contentBorder, contentHeight + line * entryHeight, contentWidth, sectionHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line += 0.25f; + + GUI.Label(SettinglabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Evade"), BoldLabel); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.minEvasionTime, nameof(AI.minEvasionTime), "MinEvasionTime", $"{AI.minEvasionTime:0.00}s"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.evasionThreshold, nameof(AI.evasionThreshold), "EvasionThreshold", $"{AI.evasionThreshold:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.evasionTimeThreshold, nameof(AI.evasionTimeThreshold), "EvasionTimeThreshold", $"{AI.evasionTimeThreshold:0.0}s"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.evasionErraticness, nameof(AI.evasionErraticness), "EvasionErraticness", $"{AI.evasionErraticness:0.00}"); + line = ContentEntry(ContentType.SemiLogSlider, line, contentWidth, ref AI.evasionMinRangeThreshold, nameof(AI.evasionMinRangeThreshold), "EvasionMinRangeThreshold", AI.evasionMinRangeThreshold < 1000 ? $"{AI.evasionMinRangeThreshold:0}m" : $"{AI.evasionMinRangeThreshold / 1000:0}km"); + AI.evasionRCS = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.evasionRCS, + StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionRCS") + " : " + (AI.evasionRCS ? StringUtils.Localize("#LOC_BDArmory_Enabled") : StringUtils.Localize("#LOC_BDArmory_Disabled")), + AI.evasionRCS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionRCS_Context"), contextLabel); + } + + AI.evasionEngines = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.evasionEngines, + StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionEngines") + " : " + (AI.evasionEngines ? StringUtils.Localize("#LOC_BDArmory_Enabled") : StringUtils.Localize("#LOC_BDArmory_Disabled")), + AI.evasionEngines ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionEngines_Context"), contextLabel); + } + + AI.evasionIgnoreMyTargetTargetingMe = GUI.Toggle(ToggleButtonRect(line, contentWidth), AI.evasionIgnoreMyTargetTargetingMe, StringUtils.Localize("#LOC_BDArmory_AI_EvasionIgnoreMyTargetTargetingMe"), AI.evasionIgnoreMyTargetTargetingMe ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + line += 1.25f; + + #region Craft Avoidance + line += 0.5f; + GUI.Label(SettinglabelRect(line++), StringUtils.Localize("#LOC_BDArmory_AIWindow_Avoidance"), BoldLabel); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.collisionAvoidanceThreshold, nameof(AI.collisionAvoidanceThreshold), "CollisionAvoidanceThreshold", $"{AI.collisionAvoidanceThreshold:0}m"); + line = ContentEntry(ContentType.FloatSlider, line, contentWidth, ref AI.vesselCollisionAvoidanceLookAheadPeriod, nameof(AI.vesselCollisionAvoidanceLookAheadPeriod), "CollisionAvoidanceLookAheadPeriod", $"{AI.vesselCollisionAvoidanceLookAheadPeriod:0.0}s"); + #endregion + + GUI.EndGroup(); + sectionHeights[Section.Evasion] = Mathf.Lerp(sectionHeight, line, 0.15f); + line += 0.1f; + contentHeight += line * entryHeight; + } + + GUI.EndGroup(); + GUI.EndScrollView(); + } + + if (infoLinkEnabled) + { + windowColumns = 3; + + GUI.Label(new Rect(contentMargin + ColumnWidth * 2, contentTop, ColumnWidth - contentMargin, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" + BeginArea(new Rect(contentMargin + ColumnWidth * 2, contentTop + entryHeight * 1.5f, ColumnWidth - contentMargin, WindowHeight - entryHeight * 1.5f - 2 * contentTop)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - contentMargin), Height(WindowHeight - entryHeight * 1.5f - 2 * contentTop))) + { + scrollInfoVector = scrollViewScope.scrollPosition; + + if (showSection[Section.PID]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Orbital_PID"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Combat]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Orbital_Combat"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Speed]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Orbital_Speeds"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Control]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Orbital_Control"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + if (showSection[Section.Evasion]) { GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_infolink_Orbital_Evasion"), infoLinkStyle, Width(ColumnWidth - contentMargin * 4 - 20)); } + } + EndArea(); + } + } + break; + } + } + WindowWidth = Mathf.Lerp(WindowWidth, windowColumns * ColumnWidth, 0.15f); + if (minHeight == 0 && AISelectionComboBox != null) minHeight = 2 * _windowMargin + _buttonSize + AISelectionComboBox.Height; + WindowHeight = Mathf.Max(WindowHeight, minHeight); + + #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)) + { + if (Event.current.button == 1) + { + resizingWindow = false; + autoResizingWindow = true; + } + else + { + resizingWindow = true; + autoResizingWindow = false; + } + } + + if (Event.current.type == EventType.Repaint) + { + if (resizingWindow) + { + WindowHeight += Mouse.delta.y / BDArmorySettings.UI_SCALE_ACTUAL; + WindowHeight = Mathf.Max(WindowHeight, minHeight); + if (BDArmorySettings.DEBUG_OTHER) GUI.Label(new Rect(WindowWidth / 2, WindowHeight - 26, WindowWidth / 2 - 26, 26), $"Resizing: {Mathf.Round(WindowHeight * BDArmorySettings.UI_SCALE_ACTUAL)}", Label); + } + else if (autoResizingWindow) + { + WindowHeight = Mathf.Clamp((_windowMargin + _buttonSize) * 2 + contentHeight + 1, minHeight, (Screen.height - BDArmorySetup.WindowRectAI.yMin) / BDArmorySettings.UI_SCALE_ACTUAL); + if (BDArmorySetup.WindowRectAI.height > WindowHeight) BDArmorySetup.WindowRectAI.height = WindowHeight; // Avoid sticking to bottom of screen during RepositionWindow. + } + } + #endregion + + var previousWindowHeight = BDArmorySetup.WindowRectAI.height; + BDArmorySetup.WindowRectAI.height = WindowHeight; + BDArmorySetup.WindowRectAI.width = WindowWidth; + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectAI, previousWindowHeight); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectAI, _guiCheckIndex); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectAI); + } + #endregion GUI + + internal void OnDestroy() + { + GameEvents.onVesselChange.Remove(OnVesselChange); + GameEvents.onVesselPartCountChanged.Remove(OnVesselModified); + GameEvents.onPartDestroyed.Remove(OnPartDestroyed); + GameEvents.onEditorLoad.Remove(OnEditorLoad); + GameEvents.onEditorPartPlaced.Remove(OnEditorPartPlacedEvent); + GameEvents.onEditorPartDeleted.Remove(OnEditorPartDeletedEvent); + if (HighLogic.LoadedSceneIsEditor) GUIUtils.PreventClickThrough(BDArmorySetup.WindowRectAI, "AIGUI lock", true); + } + } +} diff --git a/BDArmory/UI/BDArmorySetup.cs b/BDArmory/UI/BDArmorySetup.cs index 9297a4a88..2d1787906 100644 --- a/BDArmory/UI/BDArmorySetup.cs +++ b/BDArmory/UI/BDArmorySetup.cs @@ -1,22 +1,35 @@ -using System; -using System.Collections; using System.Collections.Generic; +using System.Collections; using System.Globalization; +using System.IO.Compression; +using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System; +using UnityEngine; +using KSP.Localization; +using KSP.UI.Screens; + +using BDArmory.Armor; using BDArmory.Bullets; +using BDArmory.Competition.RemoteOrchestration; using BDArmory.Competition; using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; 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 BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons; namespace BDArmory.UI { @@ -26,19 +39,31 @@ public class BDArmorySetup : MonoBehaviour public static bool SMART_GUARDS = true; public static bool showTargets = true; - //=======Window position settings Git Issue #13 + //=======Window position settings [BDAWindowSettingsField] public static Rect WindowRectToolbar; [BDAWindowSettingsField] public static Rect WindowRectGps; [BDAWindowSettingsField] public static Rect WindowRectSettings; [BDAWindowSettingsField] public static Rect WindowRectRadar; [BDAWindowSettingsField] public static Rect WindowRectRwr; [BDAWindowSettingsField] public static Rect WindowRectVesselSwitcher; + [BDAWindowSettingsField] static Rect _WindowRectVesselSwitcherUIHidden; + [BDAWindowSettingsField] static Rect _WindowRectVesselSwitcherUIVisible; [BDAWindowSettingsField] public static Rect WindowRectWingCommander = new Rect(45, 75, 240, 800); [BDAWindowSettingsField] public static Rect WindowRectTargetingCam; [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 WindowRectWayPointSpawner; + [BDAWindowSettingsField] public static Rect WindowRectVesselMover; + [BDAWindowSettingsField] public static Rect WindowRectVesselMoverVesselSelection = new Rect(Screen.width / 2 - 300, Screen.height / 2 - 400, 660, 800); + + [BDAWindowSettingsField] public static Rect WindowRectAI; + [BDAWindowSettingsField] public static Rect WindowRectScores = new Rect(0, 0, 500, 50); + [BDAWindowSettingsField] static Rect _WindowRectScoresUIHidden; + [BDAWindowSettingsField] static Rect _WindowRectScoresUIVisible; + //reflection field lists static FieldInfo[] iFs; @@ -74,8 +99,12 @@ static FieldInfo[] inputFields //particle optimization public static int numberOfParticleEmitters = 0; public static BDArmorySetup Instance; + static GameScenes InScene = GameScenes.CREDITS; // The scene the instance was instantiated in (for duplicate detection). public static bool GAME_UI_ENABLED = true; - public string Version { get; private set; } = "Unknown"; + public static string Version { get; private set; } = "Unknown"; + + //toolbar button + public static bool toolbarButtonAdded = false; //settings gui public static bool windowSettingsEnabled; @@ -83,20 +112,36 @@ static FieldInfo[] inputFields //editor alignment public static bool showWeaponAlignment; + public static bool showCASESimulation; + + //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; public static bool windowBDAToolBarEnabled; - float toolWindowWidth = 300; + float toolWindowWidth = 400; float toolWindowHeight = 100; + float columnWidth = 400; bool showWeaponList; bool showGuardMenu; bool showModules; + bool showPriorities; + bool showTargetOptions; + bool showEngageList; int numberOfModules; bool showWindowGPS; + bool infoLinkEnabled; + bool NumFieldsEnabled; + int numberOfButtons = 6; // 6 without evolution, will adjust automatically. + private Vector2 scrollInfoVector; + public Dictionary textNumFields; //gps window public bool showingWindowGPS @@ -104,31 +149,54 @@ public bool showingWindowGPS get { return showWindowGPS; } } - bool maySavethisInstance = false; + bool saveWindowPosition = false; float gpsEntryCount; float gpsEntryHeight = 24; float gpsBorder = 5; bool editingGPSName; int editingGPSNameIndex; bool hasEnteredGPSName; - string newGPSName = String.Empty; + string newGPSName = string.Empty; - public MissileFire ActiveWeaponManager; + // Note: Use OnGUIWM instead for stuff in OnGUI. + public MissileFire ActiveWeaponManager + { + get + { + var activeVessel = HighLogic.LoadedSceneIsFlight ? FlightGlobals.ActiveVessel : null; + if (activeVessel == null || !activeVessel.loaded || activeVessel.packed) _activeWeaponManager = null; + else if (_activeWeaponManager == null || !_activeWeaponManager.IsPrimaryWM || _activeWeaponManager.vessel != activeVessel) + { + _activeWeaponManager = (activeVessel != null && activeVessel.loaded) ? activeVessel.ActiveController().WM : null; + if (_activeWeaponManager != null && _activeWeaponManager.vessel != activeVessel) _activeWeaponManager = null; + if (_activeWeaponManager != null) ConfigTextFields(_activeWeaponManager); + } + return _activeWeaponManager; + } + } + MissileFire _activeWeaponManager; + public MissileFire OnGUIWM; // Separate instance to make sure we only update the active WM once for OnGUI rendering. This is updated in LateUpdate. public bool missileWarning; public float missileWarningTime = 0; - //load range stuff - VesselRanges combatVesselRanges = new VesselRanges(); - float physRangeTimer; - public static List Flares = new List(); + public static List Decoys = new List(); + + public List mutators = new List(); + bool[] mutators_selected; + + List dependencyWarnings = new List(); + double dependencyLastCheckTime = 0; //gui styles + GUIStyle settingsTitleStyle; GUIStyle centerLabel; GUIStyle centerLabelRed; GUIStyle centerLabelOrange; GUIStyle centerLabelBlue; GUIStyle leftLabel; + GUIStyle leftLabelBold; + GUIStyle infoLinkStyle; GUIStyle leftLabelRed; GUIStyle rightLabelRed; GUIStyle leftLabelGray; @@ -142,16 +210,38 @@ public bool showingWindowGPS GUIStyle waterMarkStyle; GUIStyle redErrorStyle; GUIStyle redErrorShadowStyle; + GUIStyle textFieldStyle; + bool stylesConfigured = false; public SortedList Teams = new SortedList { { "Neutral", new BDTeam("Neutral", neutral: true) } }; - + static float _SystemMaxMemory = 0; + public static float SystemMaxMemory + { + get + { + if (_SystemMaxMemory == 0) + { + _SystemMaxMemory = SystemInfo.systemMemorySize / 1024; // System Memory in GB. + if (BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD > _SystemMaxMemory + 1) BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD = _SystemMaxMemory + 1; + } + return _SystemMaxMemory; + } + } + string CheatCodeGUI = ""; + string HoSString = ""; + public string HoSTag = ""; + bool enteredHoS = false; + (float, float)[] hosDmgCache; + static GameParameters.AdvancedParams advancedParams; //competition mode - string compDistGui = "1000"; + public string compDistGui; + string compIntraTeamSeparationBase; + string compIntraTeamSeparationPerMember; #region Textures @@ -159,6 +249,7 @@ public bool showingWindowGPS bool drawCursor; Texture2D cursorTexture = GameDatabase.Instance.GetTexture(textureDir + "aimer", false); + bool temporarilyShowMouse = false; private Texture2D dti; @@ -200,6 +291,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 @@ -245,6 +349,16 @@ public Texture2D greenSpikedPointCircleTexture } } + private Texture2D gC; + + public Texture2D greenCross + { + get + { + return gC ? gC : gC = GameDatabase.Instance.GetTexture(textureDir + "greenCross", false); + } + } + private Texture2D wSqr; public Texture2D whiteSquareTexture @@ -292,51 +406,167 @@ public Texture2D settingsIconTexture get { return si ? si : si = GameDatabase.Instance.GetTexture(textureDir + "settingsIcon", false); } } + + private Texture2D FAimg; + + public Texture2D FiringAngleImage + { + get { return FAimg ? FAimg : FAimg = GameDatabase.Instance.GetTexture(textureDir + "FiringAnglePic", false); } + } + #endregion Textures public static bool GameIsPaused { - get { return PauseMenu.isOpen || Time.timeScale == 0; } + get { return HighLogic.LoadedSceneIsFlight && (PauseMenu.isOpen || Time.timeScale == 0); } } - void Start() + void Awake() { + if (Instance != null) + { + if (InScene == HighLogic.LoadedScene) + { + // In some scenes (such as the editors) addons get instantiated multiple times if using KSPAddon.Startup.EveryScene. + // This avoids the duplicate instance from messing with our syncing to KSP's settings. + Destroy(this); + return; + } + Destroy(Instance); + } Instance = this; + InScene = HighLogic.LoadedScene; + if (!(HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor)) + { + windowSettingsEnabled = false; // Close the settings on other scenes (it's been saved when the other scene was destroyed). + } - //wmgr toolbar - if (HighLogic.LoadedSceneIsFlight) - maySavethisInstance = true; //otherwise later we should NOT save the current window positions! - - // Create settings file if not present. - if (ConfigNode.Load(BDArmorySettings.settingsConfigURL) == null) + // Create settings file if not present or migrate the old one to the PluginsData folder for compatibility with ModuleManager. + var fileNode = ConfigNode.Load(BDArmorySettings.settingsConfigURL); + if (fileNode == null) { - var node = new ConfigNode(); - node.AddNode("BDASettings"); - node.Save(BDArmorySettings.settingsConfigURL); + fileNode = ConfigNode.Load(BDArmorySettings.oldSettingsConfigURL); // Try the old location. + if (fileNode == null) + { + fileNode = new ConfigNode(); + fileNode.AddNode("BDASettings"); + } + if (!Directory.GetParent(BDArmorySettings.settingsConfigURL).Exists) + { Directory.GetParent(BDArmorySettings.settingsConfigURL).Create(); } + var success = fileNode.Save(BDArmorySettings.settingsConfigURL); + if (success && File.Exists(BDArmorySettings.oldSettingsConfigURL)) // Remove the old settings if it exists and the new settings were saved. + { File.Delete(BDArmorySettings.oldSettingsConfigURL); } } // window position settings - WindowRectToolbar = new Rect(Screen.width - toolWindowWidth - 40, 150, toolWindowWidth, toolWindowHeight); - // Default, if not in file. + WindowRectToolbar = new Rect(Screen.width - toolWindowWidth - 40, 150, toolWindowWidth, toolWindowHeight); // Default, if not in file. WindowRectGps = new Rect(0, 0, WindowRectToolbar.width - 10, 0); SetupSettingsSize(); BDAWindowSettingsField.Load(); CheckIfWindowsSettingsAreWithinScreen(); + toolWindowHeight = WindowRectToolbar.height; + + // Configure UI visibility and window rects + GAME_UI_ENABLED = true; + if (_WindowRectScoresUIVisible != default) WindowRectScores = _WindowRectScoresUIVisible; + if (_WindowRectVesselSwitcherUIVisible != default) WindowRectVesselSwitcher = _WindowRectVesselSwitcherUIVisible; WindowRectGps.width = WindowRectToolbar.width - 10; - //settings + // 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([','])[0] == "BDArmory") + Version = assembly.GetName().Version.ToString(); + + // Load settings LoadConfig(); - physRangeTimer = Time.time; - GAME_UI_ENABLED = true; + // Check for Apple Processor + AppleSilicon = CultureInfo.InvariantCulture.CompareInfo.IndexOf(SystemInfo.processorType, "Apple", CompareOptions.IgnoreCase) >= 0; + + // Ensure AutoSpawn folder exists. + var autoSpawnFolder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn")); + if (!Directory.Exists(autoSpawnFolder)) + { Directory.CreateDirectory(autoSpawnFolder); } + // Ensure GameData/Custom/Flags folder exists. + var customFlagsFolder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "Custom", "Flags")); + if (!Directory.Exists(customFlagsFolder)) + { Directory.CreateDirectory(customFlagsFolder); } + } + + 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; + } + 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(BDGuiSkin.button); + SelectedButtonStyle = new GUIStyle(BDGuiSkin.button); + (SelectedButtonStyle.active, SelectedButtonStyle.normal) = (SelectedButtonStyle.normal, SelectedButtonStyle.active); + SelectedButtonStyle.hover = SelectedButtonStyle.normal; + + ModuleManagerLoaded = ModuleManager.CheckForModuleManager(); + PhysicsRangeExtenderLoaded = PhysicsRangeExtender.CheckForPhysicsRangeExtender(); + + if (HighLogic.LoadedSceneIsFlight) + { + SaveVolumeSettings(); + + GameEvents.onHideUI.Add(HideGameUI); + GameEvents.onShowUI.Add(ShowGameUI); + GameEvents.onVesselGoOffRails.Add(OnVesselGoOffRails); + GameEvents.OnGameSettingsApplied.Add(SaveVolumeSettings); + GameEvents.onVesselChange.Add(VesselChange); + } + GameEvents.onGameSceneSwitchRequested.Add(OnGameSceneSwitchRequested); + GameEvents.onGameStateSave.Add(OnGameStateSave); + GameEvents.onGameStateSaved.Add(OnGameStateSaved); + + 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) RWPSettings.SyncWithGameSettings(); // Re-sync BDA settings to game settings in Start since they get overwritten during scene initialisation. + if (!HighLogic.LoadedSceneIsFlight) OtherUtils.SetTimeOverride(false); // Make sure time override is disabled when switching to any scene other than flight. + + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) + { StartCoroutine(ToolbarButtonRoutine()); } + + for (int i = 0; i < MutatorInfo.mutators.Count; i++) + { + mutators.Add(MutatorInfo.mutators[i].name); + } + UpdateSelectedMutators(); + } + + void ConfigureStyles() + { centerLabel = new GUIStyle(); centerLabel.alignment = TextAnchor.UpperCenter; centerLabel.normal.textColor = Color.white; + settingsTitleStyle = new GUIStyle(centerLabel); + settingsTitleStyle.alignment = TextAnchor.MiddleCenter; + settingsTitleStyle.fontSize = 16; + settingsTitleStyle.fontStyle = FontStyle.Bold; + centerLabelRed = new GUIStyle(); centerLabelRed.alignment = TextAnchor.UpperCenter; centerLabelRed.normal.textColor = Color.red; @@ -353,6 +583,15 @@ void Start() leftLabel.alignment = TextAnchor.UpperLeft; leftLabel.normal.textColor = Color.white; + leftLabelBold = new GUIStyle(); + leftLabelBold.alignment = TextAnchor.UpperLeft; + leftLabelBold.normal.textColor = Color.white; + leftLabelBold.fontStyle = FontStyle.Bold; + + infoLinkStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); + infoLinkStyle.alignment = TextAnchor.UpperLeft; + infoLinkStyle.normal.textColor = Color.white; + middleLeftLabel = new GUIStyle(leftLabel); middleLeftLabel.alignment = TextAnchor.MiddleLeft; @@ -396,81 +635,97 @@ void Start() redErrorStyle = new GUIStyle(BDGuiSkin.label); redErrorStyle.normal.textColor = Color.red; redErrorStyle.fontStyle = FontStyle.Bold; - redErrorStyle.fontSize = 22; + redErrorStyle.fontSize = 24; redErrorStyle.alignment = TextAnchor.UpperCenter; 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": - PhysicsRangeExtenderLoaded = true; - break; + textFieldStyle = new GUIStyle(GUI.skin.textField); + textFieldStyle.alignment = TextAnchor.MiddleRight; - case "BDArmory": - Version = a.Current.GetName().Version.ToString(); - break; - } - } + stylesConfigured = true; + } - if (HighLogic.LoadedSceneIsFlight) + /// + /// 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)) { - SaveVolumeSettings(); - - GameEvents.onHideUI.Add(HideGameUI); - GameEvents.onShowUI.Add(ShowGameUI); - GameEvents.onVesselGoOffRails.Add(OnVesselGoOffRails); - GameEvents.OnGameSettingsApplied.Add(SaveVolumeSettings); - - GameEvents.onVesselChange.Add(VesselChange); + guiColor.a = (enable ? BDArmorySettings.GUI_OPACITY : 1f); + GUI.backgroundColor = guiColor; } + } - BulletInfo.Load(); - RocketInfo.Load(); - - // Spawn fields - spawnFields = new Dictionary { - { "lat", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, -90, 90) }, - { "lon", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, -180, 180) }, - { "alt", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 0) }, - }; - compDistGui = BDArmorySettings.COMPETITION_DISTANCE.ToString(); + IEnumerator ToolbarButtonRoutine() + { + if (toolbarButtonAdded) yield break; + yield return new WaitUntil(() => ApplicationLauncher.Ready); + if (toolbarButtonAdded) yield break; + Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon", false); + ApplicationLauncher.Instance.AddModApplication( + ToggleToolbarButton, + ToggleToolbarButton, + () => { }, + () => { }, + () => { }, + () => { }, + ApplicationLauncher.AppScenes.FLIGHT | ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB, + buttonTexture + ); + toolbarButtonAdded = true; + } + /// + /// Toggle the BDAToolbar or BDA settings window depending on the scene. + /// + void ToggleToolbarButton() + { + if (HighLogic.LoadedSceneIsFlight) { windowBDAToolBarEnabled = !windowBDAToolBarEnabled; } + else { ToggleWindowSettings(); } } private void CheckIfWindowsSettingsAreWithinScreen() { - 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); + GUIUtils.UseMouseEventInRect(WindowRectSettings); + GUIUtils.RepositionWindow(ref WindowRectToolbar); + GUIUtils.RepositionWindow(ref WindowRectSettings); + GUIUtils.RepositionWindow(ref WindowRectRwr); + GUIUtils.RepositionWindow(ref WindowRectVesselSwitcher); + GUIUtils.RepositionWindow(ref _WindowRectVesselSwitcherUIHidden); + GUIUtils.RepositionWindow(ref _WindowRectVesselSwitcherUIVisible); + GUIUtils.RepositionWindow(ref WindowRectWingCommander); + GUIUtils.RepositionWindow(ref WindowRectTargetingCam); + GUIUtils.RepositionWindow(ref WindowRectAI); + GUIUtils.RepositionWindow(ref WindowRectScores); + GUIUtils.RepositionWindow(ref _WindowRectScoresUIHidden); + GUIUtils.RepositionWindow(ref _WindowRectScoresUIVisible); + GUIUtils.RepositionWindow(ref WindowRectEvolution); } void Update() { + if (!scalingUI) BDArmorySettings.PREVIOUS_UI_SCALE = BDArmorySettings.UI_SCALE_ACTUAL; + if (HighLogic.LoadedSceneIsFlight) { - if (missileWarning && Time.time - missileWarningTime > 1.5f) - { - missileWarning = false; - } - - if (Input.GetKeyDown(KeyCode.KeypadMultiply)) - { - windowBDAToolBarEnabled = !windowBDAToolBarEnabled; - } + if (missileWarning && Time.time - missileWarningTime > 1.5f) missileWarning = false; + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.GUI_WM_TOGGLE)) windowBDAToolBarEnabled = !windowBDAToolBarEnabled; + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TIME_SCALING)) OtherUtils.SetTimeOverride(!BDArmorySettings.TIME_OVERRIDE); +#if DEBUG + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.DEBUG_CLEAR_DEV_CONSOLE)) Debug.ClearDeveloperConsole(); +#endif + if (temporarilyShowMouse != (temporarilyShowMouse = BDInputUtils.GetKey(BDInputSettingsFields.TEMPORARILY_SHOW_MOUSE))) UpdateCursorState(); } else if (HighLogic.LoadedSceneIsEditor) { @@ -478,6 +733,10 @@ void Update() { showWeaponAlignment = !showWeaponAlignment; } + if (Input.GetKeyDown(KeyCode.F3)) + { + showCASESimulation = !showCASESimulation; + } } if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) @@ -507,17 +766,16 @@ void ToggleWindowSettings() } } - void LateUpdate() + public void UpdateCursorState() { - if (HighLogic.LoadedSceneIsFlight) + if (temporarilyShowMouse) { - //UpdateCursorState(); + drawCursor = false; + Cursor.visible = true; + return; } - } - - public void UpdateCursorState() - { - if (ActiveWeaponManager == null) + var weaponManager = ActiveWeaponManager; + if (weaponManager == null) { drawCursor = false; //Screen.showCursor = true; @@ -532,27 +790,35 @@ public void UpdateCursorState() return; } - drawCursor = false; - if (!MapView.MapIsEnabled && !Misc.Misc.CheckMouseIsOnGui() && !PauseMenu.isOpen) + if (HighLogic.LoadedSceneIsFlight) { - if (ActiveWeaponManager.selectedWeapon != null && ActiveWeaponManager.weaponIndex > 0 && - !ActiveWeaponManager.guardMode) + drawCursor = false; + if (!MapView.MapIsEnabled && !GUIUtils.CheckMouseIsOnGui() && !PauseMenu.isOpen) { - if (ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || - ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || - ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) + if (weaponManager.selectedWeapon != null && weaponManager.weaponIndex > 0 && + !weaponManager.guardMode) { - ModuleWeapon mw = - ActiveWeaponManager.selectedWeapon.GetPart().FindModuleImplementing(); - if (mw.weaponState == ModuleWeapon.WeaponStates.Enabled && mw.maxPitch > 1 && !mw.slaved && - !mw.aiControlled) + if (weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || + weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || + weaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) { - //Screen.showCursor = false; - Cursor.visible = false; - drawCursor = true; - return; + ModuleWeapon mw = weaponManager.selectedWeapon.GetWeaponModule(); + if (mw != null && mw.weaponState == ModuleWeapon.WeaponStates.Enabled && mw.maxPitch > 1 && !mw.slaved && !mw.GPSTarget && !mw.aiControlled) + { + //Screen.showCursor = false; + Cursor.visible = false; + drawCursor = true; + return; + } } } + + if (MouseAimFlight.IsMouseAimActive) + { + Cursor.visible = false; + drawCursor = false; + return; + } } } @@ -562,38 +828,71 @@ public void UpdateCursorState() void VesselChange(Vessel v) { - if (v.isActiveVessel) + if (v != null && v.isActiveVessel) { - GetWeaponManager(); Instance.UpdateCursorState(); } } - void GetWeaponManager() + public void ConfigTextFields(MissileFire weaponManager) { - using (List.Enumerator mf = FlightGlobals.ActiveVessel.FindPartModulesImplementing().GetEnumerator()) - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - ActiveWeaponManager = mf.Current; - return; - } - ActiveWeaponManager = null; - return; + textNumFields = new Dictionary { + { "rippleRPM", gameObject.AddComponent().Initialise(0, weaponManager.rippleRPM, 0, 1600) }, + { "targetScanInterval", gameObject.AddComponent().Initialise(0, weaponManager.targetScanInterval, 0.5f, 60f) }, + { "fireBurstLength", gameObject.AddComponent().Initialise(0, weaponManager.fireBurstLength, 0, 10) }, + { "AutoFireCosAngleAdjustment", gameObject.AddComponent().Initialise(0, weaponManager.AutoFireCosAngleAdjustment, 0, 4) }, + { "guardAngle", gameObject.AddComponent().Initialise(0, weaponManager.guardAngle, 10, 360) }, + { "guardRange", gameObject.AddComponent().Initialise(0, weaponManager.guardRange, 100, BDArmorySettings.MAX_GUARD_VISUAL_RANGE) }, + { "gunRange", gameObject.AddComponent().Initialise(0, weaponManager.gunRange, 0, weaponManager.maxGunRange) }, + { "multiTargetNum", gameObject.AddComponent().Initialise(0, weaponManager.multiTargetNum, 1, 10) }, + { "multiMissileTgtNum", gameObject.AddComponent().Initialise(0, weaponManager.multiMissileTgtNum, 1, 10) }, + { "maxMissilesOnTarget", gameObject.AddComponent().Initialise(0, weaponManager.maxMissilesOnTarget, 1, MissileFire.maxAllowableMissilesOnTarget) }, + + { "targetBias", gameObject.AddComponent().Initialise(0, weaponManager.targetBias, -10, 10) }, + { "targetWeightRange", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightRange, -10, 10) }, + { "targetWeightAirPreference", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightAirPreference, -10, 10) }, + { "targetWeightATA", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightATA, -10, 10) }, + { "targetWeightAoD", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightAoD, -10, 10) }, + { "targetWeightAccel", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightAccel,-10, 10) }, + { "targetWeightClosureTime", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightClosureTime, -10, 10) }, + { "targetWeightWeaponNumber", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightWeaponNumber, -10, 10) }, + { "targetWeightMass", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightMass,-10, 10) }, + { "targetWeightDamage", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightDamage,-10, 10) }, + { "targetWeightFriendliesEngaging", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightFriendliesEngaging, -10, 10) }, + { "targetWeightThreat", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightThreat, -10, 10) }, + { "targetWeightProtectTeammate", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightProtectTeammate, -10, 10) }, + { "targetWeightProtectVIP", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightProtectVIP, -10, 10) }, + { "targetWeightAttackVIP", gameObject.AddComponent().Initialise(0, weaponManager.targetWeightAttackVIP, -10, 10) }, + }; } + static bool firstLoad = true; public static void LoadConfig() { try { - Debug.Log("[BDArmory]=== Loading settings.cfg ==="); + Debug.Log("[BDArmory.BDArmorySetup]=== Loading settings.cfg ==="); - BDAPersistantSettingsField.Load(); + if (firstLoad) + { + BDAPersistentSettingsField.Upgrade(); + firstLoad = false; + } + BDAPersistentSettingsField.Load(); BDInputSettingsFields.LoadSettings(); + TournamentScores.LoadWeights(); + ContinuousSpawning.LoadWeights(); + SanitiseSettings(); + RWPSettings.SyncWithGameSettings(toKSP: false); // Sync KSP's advanced settings to BDA's backing values before we load any overrides. + RWPSettings.Load(); + CompSettings.Load(); + VesselSpawnerField.Load(); + BDArmorySettings.ready = true; + if (BDAEditorArmorWindow.Instance) BDAEditorArmorWindow.Instance.SetupLegalityValues(); } - catch (NullReferenceException) + catch (NullReferenceException e) { - Debug.Log("[BDArmory]=== Failed to load settings config ==="); + Debug.LogError("[BDArmory.BDArmorySetup]=== Failed to load settings config ===: " + e.Message + "\n" + e.StackTrace); } } @@ -601,31 +900,72 @@ public static void SaveConfig() { try { - Debug.Log("[BDArmory] == Saving settings.cfg == "); + Debug.Log("[BDArmory.BDArmorySetup] == Saving settings.cfg == "); - BDAPersistantSettingsField.Save(); + if (BDArmorySettings.RUNWAY_PROJECT) + { + RWPSettings.StoreSettings(true); // Temporarily store the current RWP settings. + RWPSettings.RestoreSettings(); // Revert RWP specific settings so we save the underlying ones. + } + RWPSettings.Save(); // Save the RWP settings to file. + BDAPersistentSettingsField.Save(BDArmorySettings.settingsConfigURL); + if (BDArmorySettings.RUNWAY_PROJECT) + { + RWPSettings.RestoreSettings(true); // Restore the current RWP settings. + } BDInputSettingsFields.SaveSettings(); + TournamentScores.SaveWeights(); + ContinuousSpawning.SaveWeights(); if (OnSavedSettings != null) { OnSavedSettings(); } } - catch (NullReferenceException) + catch (NullReferenceException e) { - Debug.Log("[BDArmory]: === Failed to save settings.cfg ===="); + Debug.LogError("[BDArmory.BDArmorySetup]: === Failed to save settings.cfg ====: " + e.Message + "\n" + e.StackTrace); } } + static void SanitiseSettings() + { + BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y = Mathf.Min(BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y, 1e5f); // Anything over this pretty much breaks KSP. + BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x = Mathf.Clamp(BDArmorySettings.PROC_ARMOR_ALT_LIMITS.x, BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y * 1e-8f, BDArmorySettings.PROC_ARMOR_ALT_LIMITS.y); // More than 8 orders of magnitude breaks the mesh collider engine. + BDArmorySettings.PREVIOUS_UI_SCALE = BDArmorySettings.UI_SCALE; + } + + /// + /// Update which mutators are selected in the UI. + /// Call this if the mutators are modified somewhere other than by toggling them in the UI. + /// + public void UpdateSelectedMutators() + { + mutators_selected = new bool[mutators.Count]; + for (int i = 0; i < mutators_selected.Length; ++i) + { + mutators_selected[i] = BDArmorySettings.MUTATOR_LIST.Contains(mutators[i]); + } + } #region GUI + void LateUpdate() + { + OnGUIWM = ActiveWeaponManager; + } void OnGUI() { if (!GAME_UI_ENABLED) return; + if (!stylesConfigured) ConfigureStyles(); if (windowSettingsEnabled) { - WindowRectSettings = GUI.Window(129419, WindowRectSettings, WindowSettings, GUIContent.none); + var guiMatrix = GUI.matrix; // Store and restore the GUI.matrix so we can apply a different scaling for the WM window. + if (scalingUI && Mouse.Left.GetButtonUp()) scalingUI = false; // Don't rescale the settings window until the mouse is released otherwise it messes with the slider. + if (!scalingUI) oldUIScale = BDArmorySettings.UI_SCALE_ACTUAL; + if (oldUIScale != 1) GUIUtility.ScaleAroundPivot(oldUIScale * Vector2.one, WindowRectSettings.position); + WindowRectSettings = GUI.Window(129419, WindowRectSettings, WindowSettings, GUIContent.none, settingsTitleStyle); + GUI.matrix = guiMatrix; } if (drawCursor) @@ -641,63 +981,110 @@ void OnGUI() } if (!windowBDAToolBarEnabled || !HighLogic.LoadedSceneIsFlight) return; - WindowRectToolbar = GUI.Window(321, WindowRectToolbar, WindowBDAToolbar, Localizer.Format("#LOC_BDArmory_WMWindow_title") + " ", BDGuiSkin.window);//"BDA Weapon Manager" - BDGUIUtils.UseMouseEventInRect(WindowRectToolbar); - if (showWindowGPS && ActiveWeaponManager) + SetGUIOpacity(); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, WindowRectToolbar.position); + WindowRectToolbar = GUI.Window(321, WindowRectToolbar, WindowBDAToolbar, "", BDGuiSkin.window);//"BDA Weapon Manager" + SetGUIOpacity(false); + GUIUtils.UseMouseEventInRect(WindowRectToolbar); + if (showWindowGPS && OnGUIWM) { //gpsWindowRect = GUI.Window(424333, gpsWindowRect, GPSWindow, "", GUI.skin.box); - BDGUIUtils.UseMouseEventInRect(WindowRectGps); - List.Enumerator coord = - BDATargetManager.GPSTargetList(ActiveWeaponManager.Team).GetEnumerator(); - while (coord.MoveNext()) - { - BDGUIUtils.DrawTextureOnWorldPos(coord.Current.worldPos, Instance.greenDotTexture, new Vector2(8, 8), 0); - } - coord.Dispose(); + GUIUtils.UseMouseEventInRect(WindowRectGps); + using (var coord = BDATargetManager.GPSTargetList(OnGUIWM.Team).GetEnumerator()) + while (coord.MoveNext()) + { + 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. + { + CheckDependencies(); + } + if (dependencyWarnings.Count() > 0) + { + GUI.Label(new Rect(Screen.width / 2 - 300 + 2, Screen.height / 6 + 2, 600, 100), string.Join("\n", dependencyWarnings), redErrorShadowStyle); + GUI.Label(new Rect(Screen.width / 2 - 300, Screen.height / 6, 600, 100), string.Join("\n", dependencyWarnings), redErrorStyle); } + } - // big error messages for missing dependencies - if (ModuleManagerLoaded && PhysicsRangeExtenderLoaded) return; - string message = (ModuleManagerLoaded ? "Physics Range Extender" : "Module Manager") + " is missing. BDA will not work properly."; - GUI.Label(new Rect(0 + 2, Screen.height / 6 + 2, Screen.width, 100), - message, redErrorShadowStyle); - GUI.Label(new Rect(0, Screen.height / 6, Screen.width, 100), - message, redErrorStyle); + /// + /// 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 + ) + && !PhysicsRangeExtender.IsPREEnabled) 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 showVesselSwitcherGUI = false; - public bool showVesselSpawnerGUI = false; + public bool hasWPCourseSpawner = false; + public bool hasVesselMover = false; + public bool hasEvolution = false; + public static bool showVesselSwitcherGUI = false; + public static bool showVesselSpawnerGUI = false; + public static bool showWPBuilderGUI = false; + public static bool showVesselMoverGUI = false; + public bool showEvolutionGUI = false; float rippleHeight; float weaponsHeight; + float priorityheight; float guardHeight; + float TargetingHeight; + 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); + + (float, float)[] cacheGuardRange, cacheGunRange; void WindowBDAToolbar(int windowID) { float line = 0; - float leftIndent = 10; - float contentWidth = (toolWindowWidth) - (2 * leftIndent); - float contentTop = 10; - float entryHeight = 20; - float _buttonSize = 26; - float _windowMargin = 4; + float contentWidth = columnWidth - 2 * leftIndent; + float windowColumns = 1; + int buttonNumber = 0; - GUI.DragWindow(new Rect(_windowMargin + _buttonSize, 0, toolWindowWidth - 2 * _windowMargin - 4 * _buttonSize, _windowMargin + _buttonSize)); + GUI.DragWindow(new Rect(_windowMargin + _buttonSize, 0, columnWidth - 2 * _windowMargin - numberOfButtons * _buttonSize, _windowMargin + _buttonSize)); line += 1.25f; line += 0.25f; + //title + 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(toolWindowWidth - _windowMargin - 3 * _buttonSize - 57, 23, 57, 10), Version, waterMarkStyle); + GUI.Label(new Rect(columnWidth - _windowMargin - (numberOfButtons - 1) * _buttonSize - 100, 23, 57, 10), Version, waterMarkStyle); //SETTINGS BUTTON if (!BDKeyBinder.current && - GUI.Button(new Rect(toolWindowWidth - _windowMargin - _buttonSize, _windowMargin, _buttonSize, _buttonSize), settingsIconTexture, BDGuiSkin.button)) + GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), settingsIconTexture, BDGuiSkin.button)) { ToggleWindowSettings(); } @@ -706,9 +1093,9 @@ void WindowBDAToolbar(int windowID) if (hasVesselSwitcher) { GUIStyle vsStyle = showVesselSwitcherGUI ? BDGuiSkin.box : BDGuiSkin.button; - if (GUI.Button(new Rect(toolWindowWidth - _windowMargin - 2 * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "VS", vsStyle)) + if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "VS", vsStyle)) { - showVesselSwitcherGUI = !showVesselSwitcherGUI; + LoadedVesselSwitcher.Instance.SetVisible(!showVesselSwitcherGUI); } } @@ -716,81 +1103,157 @@ void WindowBDAToolbar(int windowID) if (hasVesselSpawner) { GUIStyle vsStyle = showVesselSpawnerGUI ? BDGuiSkin.box : BDGuiSkin.button; - if (GUI.Button(new Rect(toolWindowWidth - _windowMargin - 3 * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "Sp", vsStyle)) + if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "Sp", vsStyle)) { - showVesselSpawnerGUI = !showVesselSpawnerGUI; + VesselSpawnerWindow.Instance.SetVisible(!showVesselSpawnerGUI); if (!showVesselSpawnerGUI) SaveConfig(); } } - if (ActiveWeaponManager != null) + // 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)) + { + EvolutionWindow.Instance.SetVisible(!showEvolutionGUI); + } + } + + //infolink + GUIStyle iStyle = infoLinkEnabled ? BDGuiSkin.box : BDGuiSkin.button; + if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "i", iStyle)) + { + infoLinkEnabled = !infoLinkEnabled; + } + + //numeric fields + GUIStyle nStyle = NumFieldsEnabled ? BDGuiSkin.box : BDGuiSkin.button; + if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "#", nStyle)) + { + NumFieldsEnabled = !NumFieldsEnabled; + if (!NumFieldsEnabled) + { + // Try to parse all the fields immediately so that they're up to date. + foreach (var field in textNumFields.Keys) + { textNumFields[field].tryParseValueNow(); } + if (OnGUIWM != null) + { + foreach (var field in textNumFields.Keys) + { + try + { + var fieldInfo = typeof(MissileFire).GetField(field); + if (fieldInfo != null) + { fieldInfo.SetValue(OnGUIWM, Convert.ChangeType(textNumFields[field].currentValue, fieldInfo.FieldType)); } + else // Check if it's a property instead of a field. + { + var propInfo = typeof(MissileFire).GetProperty(field); + propInfo.SetValue(OnGUIWM, Convert.ChangeType(textNumFields[field].currentValue, propInfo.PropertyType)); + } + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDArmorySetup]: Failed to set current value of {field}: " + e.Message); } + } + } + // Then make any special conversions here. + } + else // Set the input fields to their current values. + { + // Make any special conversions first. + // Then set each of the field values to the current slider value. + if (OnGUIWM != null) + { + foreach (var field in textNumFields.Keys) + { + try + { + var fieldInfo = typeof(MissileFire).GetField(field); + if (fieldInfo != null) + { textNumFields[field].SetCurrentValue(Convert.ToDouble(fieldInfo.GetValue(OnGUIWM))); } + else // Check if it's a property instead of a field. + { + var propInfo = typeof(MissileFire).GetProperty(field); + textNumFields[field].SetCurrentValue(Convert.ToDouble(propInfo.GetValue(OnGUIWM))); + } + } + catch (Exception e) { Debug.LogError($"[BDArmory.BDArmorySetup]: Failed to set current value of {field}: " + e.Message); } + } + } + } + } + + if (OnGUIWM != 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 (OnGUIWM.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 " - if (ActiveWeaponManager.isArmed) + string armedText = StringUtils.Localize("#LOC_BDArmory_WMWindow_ArmedText");//"Trigger is " + if (OnGUIWM.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)) { - ActiveWeaponManager.ToggleArm(); + OnGUIWM.ToggleArm(); } } GUIStyle teamButtonStyle = BDGuiSkin.box; - string teamText = $"{Localizer.Format("#LOC_BDArmory_WMWindow_TeamText")}: {ActiveWeaponManager.Team.Name}";//Team - + string teamText = StringUtils.Localize("#LOC_BDArmory_WMWindow_TeamText") + $": {OnGUIWM.Team.Name + (OnGUIWM.Team.Neutral ? (OnGUIWM.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) { - BDTeamSelector.Instance.Open(ActiveWeaponManager, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); + BDTeamSelector.Instance.Open(OnGUIWM, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); } else { - ActiveWeaponManager.NextTeam(); + OnGUIWM.NextTeam(); } } 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 weaponName = OnGUIWM.selectedWeaponString; + 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; //if weapon can ripple, show option and slider. - if (ActiveWeaponManager.hasLoadedRippleData && ActiveWeaponManager.canRipple) + if (OnGUIWM.hasLoadedRippleData && OnGUIWM.canRipple) { - if (ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Gun - || ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket - || ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) //remove rocket ripple slider - moved to editor + if (OnGUIWM.selectedWeapon != null && OnGUIWM.weaponIndex > 0 && + (OnGUIWM.selectedWeapon.GetWeaponClass() == WeaponClasses.Gun + || OnGUIWM.selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket + || OnGUIWM.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" - GUIStyle rippleStyle = ActiveWeaponManager.rippleFire + string rippleText = OnGUIWM.rippleFire + ? StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText1", OnGUIWM.gunRippleRpm.ToString("0"))//"Barrage: " + + " RPM" + : StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText2");//"Salvo" + GUIStyle rippleStyle = OnGUIWM.rippleFire ? BDGuiSkin.box : BDGuiSkin.button; if ( @@ -798,17 +1261,22 @@ void WindowBDAToolbar(int windowID) new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth / 2, entryHeight * 1.25f), rippleText, rippleStyle)) { - ActiveWeaponManager.ToggleRippleFire(); + OnGUIWM.ToggleRippleFire(); + } + if (OnGUIWM.rippleFire) + { + GUI.Label(new Rect(leftIndent + contentWidth / 2 + _windowMargin, contentTop + line * entryHeight, contentWidth / 4 - _windowMargin, entryHeight * 1.25f), $"{StringUtils.Localize("#LOC_BDArmory_WMWindow_barrageStagger")}: {(OnGUIWM.barrageStagger > 0 ? OnGUIWM.barrageStagger : 1):G1}"); + OnGUIWM.barrageStagger = BDAMath.RoundToUnit(GUI.HorizontalSlider(new Rect(leftIndent + 3 * contentWidth / 4, contentTop + (line + 0.25f) * entryHeight, contentWidth / 4, entryHeight), OnGUIWM.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" - GUIStyle rippleStyle = ActiveWeaponManager.rippleFire + string rippleText = OnGUIWM.rippleFire + ? StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText3", OnGUIWM.rippleRPM.ToString("0"))//"Ripple: " + + " RPM" + : StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText4");//"Ripple: OFF" + GUIStyle rippleStyle = OnGUIWM.rippleFire ? BDGuiSkin.box : BDGuiSkin.button; if ( @@ -816,14 +1284,22 @@ void WindowBDAToolbar(int windowID) new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth / 2, entryHeight * 1.25f), rippleText, rippleStyle)) { - ActiveWeaponManager.ToggleRippleFire(); + OnGUIWM.ToggleRippleFire(); } - if (ActiveWeaponManager.rippleFire) + if (OnGUIWM.rippleFire) { - Rect sliderRect = new Rect(leftIndent + (contentWidth / 2) + 2, - contentTop + (line * entryHeight) + 6.5f, (contentWidth / 2) - 2, 12); - ActiveWeaponManager.rippleRPM = GUI.HorizontalSlider(sliderRect, - ActiveWeaponManager.rippleRPM, 100, 1600, rippleSliderStyle, rippleThumbStyle); + if (!NumFieldsEnabled) + { + OnGUIWM.rippleRPM = GUI.HorizontalSlider(new Rect(leftIndent + (contentWidth / 2) + 2, contentTop + (line * entryHeight) + 6.5f, (contentWidth / 2) - 2, 12), + OnGUIWM.rippleRPM, 100, 1600, rippleSliderStyle, rippleThumbStyle); + } + else + { + var field = textNumFields["rippleRPM"]; + field.tryParseValue(GUI.TextField(new Rect(leftIndent + (contentWidth / 2) + 2, contentTop + (line * entryHeight) + 6.5f, (contentWidth / 2) - 2, entryHeight), + field.possibleValue, 4, field.style)); + OnGUIWM.rippleRPM = (float)field.currentValue; + } } rippleHeight = Mathf.Lerp(rippleHeight, 1.25f, 0.15f); } @@ -832,24 +1308,27 @@ void WindowBDAToolbar(int windowID) { rippleHeight = Mathf.Lerp(rippleHeight, 0, 0.15f); } - //line += 1.25f; line += rippleHeight; line += 0.1f; if (!toolMinimized) { showWeaponList = - GUI.Toggle(new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth / 3, entryHeight), - showWeaponList, Localizer.Format("#LOC_BDArmory_WMWindow_ListWeapons"), showWeaponList ? BDGuiSkin.box : BDGuiSkin.button);//"Weapons" + GUI.Toggle(new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth / 4, entryHeight), + showWeaponList, StringUtils.Localize("#LOC_BDArmory_WMWindow_ListWeapons"), showWeaponList ? BDGuiSkin.box : BDGuiSkin.button);//"Weapons" showGuardMenu = GUI.Toggle( - new Rect(leftIndent + (contentWidth / 3), contentTop + (line * entryHeight), contentWidth / 3, - entryHeight), showGuardMenu, Localizer.Format("#LOC_BDArmory_WMWindow_GuardMenu"),//"Guard Menu" + new Rect(leftIndent + (contentWidth / 4), contentTop + (line * entryHeight), contentWidth / 4, + 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, StringUtils.Localize("#LOC_BDArmory_WMWindow_TargetPriority"),//"Tgt priority" + showPriorities ? BDGuiSkin.box : BDGuiSkin.button); showModules = GUI.Toggle( - new Rect(leftIndent + (2 * contentWidth / 3), contentTop + (line * entryHeight), contentWidth / 3, - entryHeight), showModules, Localizer.Format("#LOC_BDArmory_WMWindow_ModulesToggle"),//"Modules" + new Rect(leftIndent + (3 * contentWidth / 4), contentTop + (line * entryHeight), contentWidth / 4, + entryHeight), showModules, StringUtils.Localize("#LOC_BDArmory_WMWindow_ModulesToggle"),//"Modules" showModules ? BDGuiSkin.box : BDGuiSkin.button); line++; } @@ -858,15 +1337,15 @@ void WindowBDAToolbar(int windowID) if (showWeaponList && !toolMinimized) { line += 0.25f; - Rect weaponListGroupRect = new Rect(5, contentTop + (line * entryHeight), toolWindowWidth - 10, - ((float)ActiveWeaponManager.weaponArray.Length + 0.1f) * entryHeight); + Rect weaponListGroupRect = new Rect(5, contentTop + (line * entryHeight), columnWidth - 10, weaponsHeight * entryHeight); GUI.BeginGroup(weaponListGroupRect, GUIContent.none, BDGuiSkin.box); //darker box weaponLines += 0.1f; - for (int i = 0; i < ActiveWeaponManager.weaponArray.Length; i++) + + for (int i = 0; i < OnGUIWM.weaponArray.Length; i++) { GUIStyle wpnListStyle; GUIStyle tgtStyle; - if (i == ActiveWeaponManager.weaponIndex) + if (i == OnGUIWM.weaponIndex) { wpnListStyle = middleLeftLabelOrange; tgtStyle = targetModeStyleSelected; @@ -878,34 +1357,34 @@ void WindowBDAToolbar(int windowID) } string label; string subLabel; - if (ActiveWeaponManager.weaponArray[i] != null) + if (OnGUIWM.weaponArray[i] != null) { - label = ActiveWeaponManager.weaponArray[i].GetShortName(); - subLabel = ActiveWeaponManager.weaponArray[i].GetSubLabel(); + label = OnGUIWM.weaponArray[i].GetShortName(); + subLabel = OnGUIWM.weaponArray[i].GetSubLabel(); } else { - label = Localizer.Format("#LOC_BDArmory_WMWindow_NoneWeapon");//"None" - subLabel = String.Empty; + 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); if (GUI.Button(weaponButtonRect, label, wpnListStyle)) { - ActiveWeaponManager.CycleWeapon(i); + OnGUIWM.CycleWeapon(i); } - if (i < ActiveWeaponManager.weaponArray.Length - 1) + if (i < OnGUIWM.weaponArray.Length - 1) { - BDGUIUtils.DrawRectangle( + GUIUtils.DrawRectangle( new Rect(weaponButtonRect.x, weaponButtonRect.y + weaponButtonRect.height, weaponButtonRect.width, 1), Color.white); } weaponLines++; } + weaponLines += 0.1f; GUI.EndGroup(); } @@ -916,234 +1395,647 @@ void WindowBDAToolbar(int windowID) if (showGuardMenu && !toolMinimized) { line += 0.25f; - GUI.BeginGroup( - new Rect(5, contentTop + (line * entryHeight), toolWindowWidth - 10, 8.45f * 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)) - { - ActiveWeaponManager.ToggleGuardMode(); - } - guardLines += 1.25f; - - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_FiringInterval"), leftLabel);//"Firing Interval" - ActiveWeaponManager.targetScanInterval = - GUI.HorizontalSlider( - new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.targetScanInterval, 1, 60); - ActiveWeaponManager.targetScanInterval = Mathf.Round(ActiveWeaponManager.targetScanInterval); - 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); - ActiveWeaponManager.fireBurstLength = - GUI.HorizontalSlider( - new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.fireBurstLength, 0, 60); - ActiveWeaponManager.fireBurstLength = Mathf.Round(ActiveWeaponManager.fireBurstLength * 2) / 2; - 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); - ActiveWeaponManager.AutoFireCosAngleAdjustment = - GUI.HorizontalSlider( - new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.AutoFireCosAngleAdjustment, 0, 2); - ActiveWeaponManager.AutoFireCosAngleAdjustment = Mathf.Round(ActiveWeaponManager.AutoFireCosAngleAdjustment * 20) / 20; - 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" - leftLabel); - float guardAngle = ActiveWeaponManager.guardAngle; - guardAngle = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - guardAngle, 10, 360); - guardAngle = guardAngle / 10; - guardAngle = Mathf.Round(guardAngle); - ActiveWeaponManager.guardAngle = guardAngle * 10; - 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" - 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; - 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" - float gRange = ActiveWeaponManager.gunRange; - gRange = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - gRange, 0, BDArmorySettings.MAX_BULLET_RANGE); - gRange /= 100f; - gRange = Mathf.Round(gRange); - gRange *= 100f; - ActiveWeaponManager.gunRange = gRange; - 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_MissilesTgt"), leftLabel);//"Missiles/Tgt" - 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; - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.maxMissilesOnTarget.ToString(), leftLabel); - guardLines++; - - string targetType = Localizer.Format("#LOC_BDArmory_WMWindow_TargetType");//"Target Type: " - if (ActiveWeaponManager.targetMissiles) - { - targetType += Localizer.Format("#LOC_BDArmory_WMWindow_TargetType_Missiles");//"Missiles" + string guardButtonLabel = StringUtils.Localize("#LOC_BDArmory_WMWindow_GuardMode", OnGUIWM.guardMode ? StringUtils.Localize("#LOC_BDArmory_Generic_On") : StringUtils.Localize("#LOC_BDArmory_Generic_Off"));//"Guard Mode " + "ON""Off" + if (GUI.Button(ButtonRect(guardLines), guardButtonLabel, OnGUIWM.guardMode ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.ToggleGuardMode(); + } + guardLines += 0.25f; + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_FiringInterval"), leftLabel);//"Firing Interval" + if (!NumFieldsEnabled) + { + OnGUIWM.targetScanInterval = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.targetScanInterval, 0.5f, 60f), 0.5f); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.targetScanInterval.ToString(), leftLabel); } else { - targetType += Localizer.Format("#LOC_BDArmory_WMWindow_TargetType_All");//"All Targets" + var field = textNumFields["targetScanInterval"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetScanInterval = (float)field.currentValue; } - if (GUI.Button(new Rect(leftIndent, (guardLines * entryHeight), contentWidth, entryHeight), targetType, - BDGuiSkin.button)) + string burstLabel = StringUtils.Localize("#LOC_BDArmory_WMWindow_BurstLength");//"Burst Length" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), burstLabel, leftLabel); + if (!NumFieldsEnabled) { - ActiveWeaponManager.ToggleTargetType(); + OnGUIWM.fireBurstLength = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.fireBurstLength, 0, 10), 0.05f); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.fireBurstLength.ToString(), leftLabel); + } + else + { + var field = textNumFields["fireBurstLength"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.fireBurstLength = (float)field.currentValue; } - guardLines++; - GUI.EndGroup(); - line += 0.1f; - } - guardHeight = Mathf.Lerp(guardHeight, guardLines, 0.15f); - line += guardHeight; - - float moduleLines = 0; - if (showModules && !toolMinimized) - { - line += 0.25f; - GUI.BeginGroup( - new Rect(5, contentTop + (line * entryHeight), toolWindowWidth - 10, numberOfModules * entryHeight), - GUIContent.none, BDGuiSkin.box); - numberOfModules = 0; - moduleLines += 0.1f; - //RWR - if (ActiveWeaponManager.rwr) + // extension for feature_engagementenvelope: set the firing accuracy tolarance + var oldAutoFireCosAngleAdjustment = OnGUIWM.AutoFireCosAngleAdjustment; + string accuracyLabel = StringUtils.Localize("#LOC_BDArmory_WMWindow_FiringTolerance");//"Firing Angle" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), accuracyLabel, leftLabel); + if (!NumFieldsEnabled) { - 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); - if (GUI.Button(rwrRect, label, isEnabled ? centerLabelOrange : centerLabel)) + OnGUIWM.AutoFireCosAngleAdjustment = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.AutoFireCosAngleAdjustment, 0, 4), 0.05f); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.AutoFireCosAngleAdjustment.ToString(), leftLabel); + } + else + { + var field = textNumFields["AutoFireCosAngleAdjustment"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.AutoFireCosAngleAdjustment = (float)field.currentValue; + } + if (OnGUIWM.AutoFireCosAngleAdjustment != oldAutoFireCosAngleAdjustment) + OnGUIWM.OnAFCAAUpdated(null, null); + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_FieldofView"),//"Field of View" + leftLabel); + if (!NumFieldsEnabled) + { + OnGUIWM.guardAngle = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.guardAngle, 10, 360), 0.1f); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.guardAngle.ToString(), leftLabel); + } + else + { + var field = textNumFields["guardAngle"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.guardAngle = (float)field.currentValue; + } + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_VisualRange"), leftLabel);//"Visual Range" + if (!NumFieldsEnabled) + { + OnGUIWM.guardRange = GUIUtils.HorizontalSemiLogSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.guardRange, 100, BDArmorySettings.MAX_GUARD_VISUAL_RANGE, 1, false, false, ref cacheGuardRange); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.guardRange < 1000 ? $"{OnGUIWM.guardRange:G4}m" : $"{OnGUIWM.guardRange / 1000:G4}km", leftLabel); + } + else + { + var field = textNumFields["guardRange"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 8, field.style)); + OnGUIWM.guardRange = (float)field.currentValue; + } + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_GunsRange"), leftLabel);//"Guns Range" + if (!NumFieldsEnabled) + { + OnGUIWM.gunRange = GUIUtils.HorizontalPowerSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.gunRange, 0, OnGUIWM.maxGunRange, 2, 2, ref cacheGunRange); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.gunRange < 1000 ? $"{OnGUIWM.gunRange:G4}m" : $"{OnGUIWM.gunRange / 1000:G4}km", leftLabel); + } + else + { + var field = textNumFields["gunRange"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 8, field.style)); + OnGUIWM.gunRange = (float)field.currentValue; + } + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_MultiTargetNum"), leftLabel);//"Max Turret targets " + if (!NumFieldsEnabled) + { + OnGUIWM.multiTargetNum = Mathf.Round(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.multiTargetNum, 1, 10)); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.multiTargetNum.ToString(), leftLabel); + } + else + { + var field = textNumFields["multiTargetNum"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 2, field.style)); + OnGUIWM.multiTargetNum = (float)field.currentValue; + } + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_MultiMissileNum"), leftLabel);//"Max Turret targets " + if (!NumFieldsEnabled) + { + OnGUIWM.multiMissileTgtNum = Mathf.Round(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.multiMissileTgtNum, 1, 10)); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.multiMissileTgtNum.ToString(), leftLabel); + } + else + { + var field = textNumFields["multiMissileTgtNum"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 2, field.style)); + OnGUIWM.multiMissileTgtNum = (float)field.currentValue; + } + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_MissilesTgt"), leftLabel);//"Missiles/Tgt" + if (!NumFieldsEnabled) + { + OnGUIWM.maxMissilesOnTarget = Mathf.Round(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), OnGUIWM.maxMissilesOnTarget, 1, MissileFire.maxAllowableMissilesOnTarget)); + GUI.Label(RightLabelRect(guardLines), OnGUIWM.maxMissilesOnTarget.ToString(), leftLabel); + } + else + { + var field = textNumFields["maxMissilesOnTarget"]; + field.tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), field.possibleValue, 2, field.style)); + OnGUIWM.maxMissilesOnTarget = (float)field.currentValue; + } + + 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) + { + 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 = StringUtils.Localize("#LOC_BDArmory_TargetCOM", (OnGUIWM.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, OnGUIWM.targetCoM ? BDGuiSkin.box : BDGuiSkin.button)) { - if (isEnabled) + OnGUIWM.targetCoM = !OnGUIWM.targetCoM; + OnGUIWM.StartGuardTurretFiring(); //reset weapon targeting assignments + if (OnGUIWM.targetCoM) { - //ActiveWeaponManager.rwr.DisableRWR(); - ActiveWeaponManager.rwr.displayRWR = false; + OnGUIWM.targetCommand = false; + OnGUIWM.targetEngine = false; + OnGUIWM.targetWeapon = false; + OnGUIWM.targetMass = false; + OnGUIWM.targetRandom = false; } - else + if (!OnGUIWM.targetCoM && (!OnGUIWM.targetWeapon && !OnGUIWM.targetEngine && !OnGUIWM.targetCommand && !OnGUIWM.targetMass && !OnGUIWM.targetRandom)) { - //ActiveWeaponManager.rwr.EnableRWR(); - ActiveWeaponManager.rwr.displayRWR = true; + OnGUIWM.targetRandom = true; } } - moduleLines++; + TargetLines += 1.1f; + string Commandlabel = StringUtils.Localize("#LOC_BDArmory_Command", (OnGUIWM.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, OnGUIWM.targetCommand ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.targetCommand = !OnGUIWM.targetCommand; + OnGUIWM.StartGuardTurretFiring(); + if (OnGUIWM.targetCommand) + { + OnGUIWM.targetCoM = false; + } + if (!OnGUIWM.targetCoM && (!OnGUIWM.targetWeapon && !OnGUIWM.targetEngine && !OnGUIWM.targetCommand && !OnGUIWM.targetMass && !OnGUIWM.targetRandom)) + { + OnGUIWM.targetCoM = true; + } + } + string Engineslabel = StringUtils.Localize("#LOC_BDArmory_Engines", (OnGUIWM.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, OnGUIWM.targetEngine ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.targetEngine = !OnGUIWM.targetEngine; + OnGUIWM.StartGuardTurretFiring(); + if (OnGUIWM.targetEngine) + { + OnGUIWM.targetCoM = false; + } + if (!OnGUIWM.targetCoM && (!OnGUIWM.targetWeapon && !OnGUIWM.targetEngine && !OnGUIWM.targetCommand && !OnGUIWM.targetMass && !OnGUIWM.targetRandom)) + { + OnGUIWM.targetCoM = true; + } + } + TargetLines += 1.1f; + string Weaponslabel = StringUtils.Localize("#LOC_BDArmory_Weapons", (OnGUIWM.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, OnGUIWM.targetWeapon ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.targetWeapon = !OnGUIWM.targetWeapon; + OnGUIWM.StartGuardTurretFiring(); + if (OnGUIWM.targetWeapon) + { + OnGUIWM.targetCoM = false; + } + if (!OnGUIWM.targetCoM && (!OnGUIWM.targetWeapon && !OnGUIWM.targetEngine && !OnGUIWM.targetCommand && !OnGUIWM.targetMass && !OnGUIWM.targetRandom)) + { + OnGUIWM.targetCoM = true; + } + } + string Masslabel = StringUtils.Localize("#LOC_BDArmory_Mass", (OnGUIWM.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, OnGUIWM.targetMass ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.targetMass = !OnGUIWM.targetMass; + OnGUIWM.StartGuardTurretFiring(); + if (OnGUIWM.targetMass) + { + OnGUIWM.targetCoM = false; + } + if (!OnGUIWM.targetCoM && (!OnGUIWM.targetWeapon && !OnGUIWM.targetEngine && !OnGUIWM.targetCommand && !OnGUIWM.targetMass && !OnGUIWM.targetRandom)) + { + OnGUIWM.targetCoM = true; + } + } + TargetLines += 1.1f; + string Randomlabel = StringUtils.Localize("#LOC_BDArmory_Random", (OnGUIWM.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, OnGUIWM.targetRandom ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.targetRandom = !OnGUIWM.targetRandom; + OnGUIWM.StartGuardTurretFiring(); + if (OnGUIWM.targetRandom) + { + OnGUIWM.targetCoM = false; + } + if (!OnGUIWM.targetCoM && (!OnGUIWM.targetWeapon && !OnGUIWM.targetEngine && !OnGUIWM.targetCommand && !OnGUIWM.targetMass && !OnGUIWM.targetRandom)) + { + OnGUIWM.targetCoM = true; + } + } + TargetLines += 1.1f; + OnGUIWM.targetingString = (OnGUIWM.targetCoM ? StringUtils.Localize("#LOC_BDArmory_TargetCOM") + "; " : "") + + (OnGUIWM.targetMass ? StringUtils.Localize("#LOC_BDArmory_Mass") + "; " : "") + + (OnGUIWM.targetCommand ? StringUtils.Localize("#LOC_BDArmory_Command") + "; " : "") + + (OnGUIWM.targetEngine ? StringUtils.Localize("#LOC_BDArmory_Engines") + "; " : "") + + (OnGUIWM.targetWeapon ? StringUtils.Localize("#LOC_BDArmory_Weapons") + "; " : "") + + (OnGUIWM.targetWeapon ? StringUtils.Localize("#LOC_BDArmory_Random") + "; " : ""); + GUI.EndGroup(); } + TargetingHeight = Mathf.Lerp(TargetingHeight, TargetLines, 0.15f); + guardLines += TargetingHeight; - //TGP - List.Enumerator mtc = ActiveWeaponManager.targetingPods.GetEnumerator(); - while (mtc.MoveNext()) + 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) + { + 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 = StringUtils.Localize("#LOC_BDArmory_EngageAir", (OnGUIWM.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, OnGUIWM.engageAir ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.ToggleEngageAir(); + } + string Missilelabel = StringUtils.Localize("#LOC_BDArmory_EngageMissile", (OnGUIWM.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, OnGUIWM.engageMissile ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.ToggleEngageMissile(); + } + EngageLines += 1.1f; + string Srflabel = StringUtils.Localize("#LOC_BDArmory_EngageSurface", (OnGUIWM.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, OnGUIWM.engageSrf ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.ToggleEngageSrf(); + } + + string SLWlabel = StringUtils.Localize("#LOC_BDArmory_EngageSLW", (OnGUIWM.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, OnGUIWM.engageSLW ? BDGuiSkin.box : BDGuiSkin.button)) + { + OnGUIWM.ToggleEngageSLW(); + } + EngageLines += 1.1f; + GUI.EndGroup(); + } + EngageHeight = Mathf.Lerp(EngageHeight, EngageLines, 0.15f); + guardLines += EngageHeight; + GUI.EndGroup(); + ++guardLines; + } + guardHeight = Mathf.Lerp(guardHeight, guardLines, 0.15f); + line += guardHeight; + + float priorityLines = 0; + if (showPriorities && !toolMinimized) + { + line += 0.25f; + GUI.BeginGroup(new Rect(5, contentTop + line * entryHeight, columnWidth - 10, priorityheight * entryHeight), GUIContent.none, BDGuiSkin.box); + priorityLines += 0.1f; + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetBias"), leftLabel);//"current target bias" + if (!NumFieldsEnabled) + { + OnGUIWM.targetBias = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetBias, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetBias.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetBias"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetBias = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetProximity"), leftLabel); //target proximity" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightRange = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightRange, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightRange.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightRange"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightRange = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetPreference"), leftLabel); //target Air preference" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightAirPreference = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightAirPreference, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightAirPreference.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightAirPreference"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightAirPreference = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAngletoTarget"), leftLabel); //target angle" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightATA = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightATA, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightATA.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightATA"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightATA = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAngleDist"), leftLabel); //Angle over Distance" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightAoD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightAoD, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightAoD.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightAoD"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightAoD = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAccel"), leftLabel); //target accel" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightAccel = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightAccel, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightAccel.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightAccel"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightAccel = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetClosingTime"), leftLabel); //target closing time" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightClosureTime = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightClosureTime, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightClosureTime.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightClosureTime"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightClosureTime = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetgunNumber"), leftLabel); //target weapon num." + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightWeaponNumber = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightWeaponNumber, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightWeaponNumber.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightWeaponNumber"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightWeaponNumber = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetMass"), leftLabel); //target mass" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightMass = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightMass, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightMass.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightMass"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightMass = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetDmg"), leftLabel); //target Damage" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightDamage = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightDamage, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightDamage.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightDamage"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightDamage = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAllies"), leftLabel); //target mass" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightFriendliesEngaging = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightFriendliesEngaging, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightFriendliesEngaging.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightFriendliesEngaging"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightFriendliesEngaging = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetThreat"), leftLabel); //target proximity" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightThreat = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightThreat, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightThreat.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightThreat"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightThreat = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_defendTeammate"), leftLabel); //defend teammate" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightProtectTeammate = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightProtectTeammate, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightProtectTeammate.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightProtectTeammate"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightProtectTeammate = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_defendVIP"), leftLabel); //target proximity" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightProtectVIP = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightProtectVIP, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightProtectVIP.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightProtectVIP"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightProtectVIP = (float)field.currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetVIP"), leftLabel); //target proximity" + if (!NumFieldsEnabled) + { + OnGUIWM.targetWeightAttackVIP = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), OnGUIWM.targetWeightAttackVIP, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), OnGUIWM.targetWeightAttackVIP.ToString(), leftLabel); + } + else + { + var field = textNumFields["targetWeightAttackVIP"]; + field.tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), field.possibleValue, 4, field.style)); + OnGUIWM.targetWeightAttackVIP = (float)field.currentValue; + } + + priorityLines += 1.1f; + GUI.EndGroup(); + } + priorityheight = Mathf.Lerp(priorityheight, priorityLines, 0.15f); + line += priorityheight; + + float moduleLines = 0; + if (showModules && !toolMinimized) + { + line += 0.25f; + GUI.BeginGroup( + new Rect(5, contentTop + (line * entryHeight), columnWidth - 10, numberOfModules * entryHeight), + GUIContent.none, BDGuiSkin.box); + moduleLines += 0.1f; + + numberOfModules = 0; + + if (OnGUIWM.radars.Count > 0) { - 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) + string Radarlabel = $"{StringUtils.Localize("#LOC_BDArmory_DynamicRadar")}: {(!OnGUIWM.DynamicRadarOverride ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true"))}";//"Dynamic Radar vs ARMs: True, False + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), Radarlabel, OnGUIWM.DynamicRadarOverride ? BDGuiSkin.button : BDGuiSkin.box)) { - moduleStyle = centerLabelRed; - label = "[" + label + "]"; + OnGUIWM.DynamicRadarOverride = !OnGUIWM.DynamicRadarOverride; } - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), - label, moduleStyle)) + moduleLines += 1.1f; + } + + //RWR + if (OnGUIWM.rwr) + { + numberOfModules++; + bool isEnabled = OnGUIWM.rwr.displayRWR; + 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 (isActive) + if (isEnabled) { - mtc.Current.ToggleCamera(); + //OnGUIWM.rwr.DisableRWR(); + OnGUIWM.rwr.displayRWR = false; } else { - mtc.Current.EnableCamera(); + //OnGUIWM.rwr.EnableRWR(); + OnGUIWM.rwr.displayRWR = true; } } 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)) + //TGP + using (List.Enumerator mtc = OnGUIWM.targetingPods.GetEnumerator()) + while (mtc.MoveNext()) { - mr.Current.Toggle(); + 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), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) + { + if (isActive) + { + mtc.Current.ToggleCamera(); + } + else + { + mtc.Current.EnableCamera(); + } + } + moduleLines++; } - moduleLines++; - } - mr.Dispose(); + //RADAR + using (List.Enumerator mr = OnGUIWM.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), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) + { + mr.Current.Toggle(); + } + moduleLines++; + } + using (List.Enumerator mr = OnGUIWM.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++; + } //JAMMERS - List.Enumerator jammer = ActiveWeaponManager.jammers.GetEnumerator(); - while (jammer.MoveNext()) - { - if (jammer.Current == null) continue; - if (jammer.Current.alwaysOn) continue; + using (List.Enumerator jammer = OnGUIWM.jammers.GetEnumerator()) + while (jammer.MoveNext()) + { + if (jammer.Current == null) continue; + if (jammer.Current.isMissileECM) 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 = OnGUIWM.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()) + using (var module = OnGUIWM.wmModules.GetEnumerator()) while (module.MoveNext()) { if (module.Current == null) continue; @@ -1151,7 +2043,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(); @@ -1162,31 +2054,30 @@ 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; } moduleLines++; //wingCommander - if (ActiveWeaponManager.wingCommander) + if (OnGUIWM.wingCommander) { - GUIStyle wingComStyle = ActiveWeaponManager.wingCommander.showGUI + GUIStyle wingComStyle = OnGUIWM.wingCommander.showGUI ? 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(); + OnGUIWM.wingCommander.ToggleGUI(); } moduleLines++; } + moduleLines += 0.1f; GUI.EndGroup(); - - line += 0.1f; } modulesHeight = Mathf.Lerp(modulesHeight, moduleLines, 0.15f); line += modulesHeight; @@ -1195,28 +2086,96 @@ void WindowBDAToolbar(int windowID) if (showWindowGPS && !toolMinimized) { line += 0.25f; - GUI.BeginGroup(new Rect(5, contentTop + (line * entryHeight), toolWindowWidth, WindowRectGps.height)); + GUI.BeginGroup(new Rect(5, contentTop + (line * entryHeight), columnWidth, WindowRectGps.height)); WindowGPS(); GUI.EndGroup(); gpsLines = WindowRectGps.height / entryHeight; } gpsHeight = Mathf.Lerp(gpsHeight, gpsLines, 0.15f); line += gpsHeight; + + if (infoLinkEnabled && !toolMinimized) + { + windowColumns = 2; + + 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(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(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(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(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 + } + + } + GUILayout.EndArea(); + } } 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 + var previousWindowHeight = toolWindowHeight; + toolWindowWidth = Mathf.Lerp(toolWindowWidth, columnWidth * windowColumns, 0.15f); toolWindowHeight = Mathf.Lerp(toolWindowHeight, contentTop + (line * entryHeight) + 5, 1); - var previousWindowHeight = WindowRectToolbar.height; WindowRectToolbar.height = toolWindowHeight; - 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); + WindowRectToolbar.width = toolWindowWidth; + numberOfButtons = buttonNumber + 1; + GUIUtils.RepositionWindow(ref WindowRectToolbar, previousWindowHeight); } +#if DEBUG + float quitTimer = 0; +#endif bool validGPSName = true; @@ -1228,7 +2187,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")}: {OnGUIWM.designatedGPSInfo.name}";//GPS Target GUI.Label(new Rect(0, 0, listRect.width, gpsEntryHeight), targetLabel, kspTitleLabel); // Expand/Collapse Target Toggle button @@ -1236,577 +2195,2082 @@ public void WindowGPS() showTargets = !showTargets; gpsEntryCount += 0.85f; - if (ActiveWeaponManager.designatedGPSCoords != Vector3d.zero) + if (OnGUIWM.designatedGPSCoords != Vector3d.zero) { GUI.Label(new Rect(0, gpsEntryCount * gpsEntryHeight, listRect.width - gpsEntryHeight, gpsEntryHeight), - Misc.Misc.FormattedGeoPos(ActiveWeaponManager.designatedGPSCoords, true), BDGuiSkin.box); + BodyUtils.FormattedGeoPos(OnGUIWM.designatedGPSCoords, true), BDGuiSkin.box); if ( GUI.Button( new Rect(listRect.width - gpsEntryHeight, gpsEntryCount * gpsEntryHeight, gpsEntryHeight, gpsEntryHeight), "X", BDGuiSkin.button)) { - ActiveWeaponManager.designatedGPSInfo = new GPSTargetInfo(); + OnGUIWM.designatedGPSInfo = new GPSTargetInfo(); } } 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; int indexToRemove = -1; int index = 0; - BDTeam myTeam = ActiveWeaponManager.Team; + BDTeam myTeam = OnGUIWM.Team; if (showTargets) { - List.Enumerator coordinate = BDATargetManager.GPSTargetList(myTeam).GetEnumerator(); - while (coordinate.MoveNext()) + using (var coordinate = BDATargetManager.GPSTargetList(myTeam).GetEnumerator()) + while (coordinate.MoveNext()) + { + Color origWColor = GUI.color; + if (coordinate.Current.EqualsTarget(OnGUIWM.designatedGPSInfo)) + { + GUI.color = XKCDColors.LightOrange; + } + + string label = BodyUtils.FormattedGeoPosShort(coordinate.Current.gpsCoordinates, false); + float nameWidth = 100; + if (editingGPSName && index == editingGPSNameIndex) + { + if (validGPSName && Event.current.type == EventType.KeyDown && + Event.current.keyCode == KeyCode.Return) + { + editingGPSName = false; + hasEnteredGPSName = true; + } + else + { + 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, textFieldStyle); + GUI.color = origColor; + } + } + else + { + 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)) + { + OnGUIWM.designatedGPSInfo = coordinate.Current; + OnGUIWM.designatedGPSCoordsIndex = index; + editingGPSName = false; + } + + if ( + GUI.Button( + new Rect(listRect.width - gpsEntryHeight, gpsEntryCount * gpsEntryHeight, gpsEntryHeight, + gpsEntryHeight), "X", BDGuiSkin.button)) + { + indexToRemove = index; + } + + gpsEntryCount++; + index++; + GUI.color = origWColor; + } + } + + if (hasEnteredGPSName && editingGPSNameIndex < BDATargetManager.GPSTargetList(myTeam).Count) + { + hasEnteredGPSName = false; + GPSTargetInfo old = BDATargetManager.GPSTargetList(myTeam)[editingGPSNameIndex]; + if (OnGUIWM.designatedGPSInfo.EqualsTarget(old)) { - Color origWColor = GUI.color; - if (coordinate.Current.EqualsTarget(ActiveWeaponManager.designatedGPSInfo)) + OnGUIWM.designatedGPSInfo.name = newGPSName; + } + BDATargetManager.GPSTargetList(myTeam)[editingGPSNameIndex] = + new GPSTargetInfo(BDATargetManager.GPSTargetList(myTeam)[editingGPSNameIndex].gpsCoordinates, + newGPSName); + editingGPSNameIndex = 0; + BDATargetManager.Instance.SaveGPSTargets(); + } + + GUI.EndGroup(); + + if (indexToRemove >= 0) + { + BDATargetManager.GPSTargetList(myTeam).RemoveAt(indexToRemove); + BDATargetManager.Instance.SaveGPSTargets(); + } + + WindowRectGps.height = (2 * gpsBorder) + (gpsEntryCount * gpsEntryHeight); + } + + Rect SLineRect(float line, float indentLevel = 0, bool symmetric = false) + { + return new Rect(settingsMargin + indentLevel * settingsMargin, line * settingsLineHeight, settingsWidth - 2 * settingsMargin - (symmetric ? 2 : 1) * indentLevel * settingsMargin, settingsLineHeight); + } + + Rect SLeftRect(float line, float indentLevel = 0, bool symmetric = false) + { + return new Rect(settingsMargin + indentLevel * settingsMargin, line * settingsLineHeight, settingsWidth / 2 - settingsMargin - settingsMargin / 4 - (symmetric ? 2 : 1) * indentLevel * settingsMargin, settingsLineHeight); + } + + Rect SRightRect(float line, float indentLevel = 0, bool symmetric = false) + { + return new Rect(settingsWidth / 2 + settingsMargin / 4 + indentLevel * settingsMargin, line * settingsLineHeight, settingsWidth / 2 - settingsMargin - settingsMargin / 4 - (symmetric ? 2 : 1) * indentLevel * settingsMargin, settingsLineHeight); + } + + Rect SLeftSliderRect(float line, float indentLevel = 0) + { + return new Rect(settingsMargin + indentLevel * settingsMargin, (line + 0.1f) * settingsLineHeight, settingsWidth / 2 + settingsMargin / 2 - indentLevel * settingsMargin, settingsLineHeight); // Sliders are slightly out of alignment vertically. + } + + Rect SRightSliderRect(float line) + { + return new Rect(settingsMargin + settingsWidth / 2 + settingsMargin / 2, (line + 0.2f) * settingsLineHeight, settingsWidth / 2 - 7 / 2 * settingsMargin, settingsLineHeight); // Sliders are slightly out of alignment vertically. + } + + Rect SLeftButtonRect(float line) + { + return new Rect(settingsMargin, line * settingsLineHeight, (settingsWidth - 2 * settingsMargin) / 2 - settingsMargin / 4, settingsLineHeight); + } + + 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, int span = 1) + { + return new Rect(settingsMargin + pos * (settingsWidth - 2f * settingsMargin) / 3f, line * settingsLineHeight, span * (settingsWidth - 2f * settingsMargin) / 3f, settingsLineHeight); + } + + Rect SQuarterRect(float line, int pos, int span = 1) + { + 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) + { + return new Rect(settingsMargin + (pos % 8) * (settingsWidth - 2f * settingsMargin) / 8f, (line + (int)(pos / 8)) * settingsLineHeight, (settingsWidth - 2.5f * settingsMargin) / 8f, settingsLineHeight); + } + + List SRight2Rects(float line) + { + var rectGap = settingsMargin / 2; + var rectWidth = ((settingsWidth - 2 * settingsMargin) / 2 - 2 * rectGap) / 2; + var rects = new List(); + rects.Add(new Rect(settingsWidth / 2 + rectGap / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); + rects.Add(new Rect(settingsWidth / 2 + rectWidth + rectGap * 3 / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); + return rects; + } + + List SRight3Rects(float line) + { + var rectGap = settingsMargin / 3; + var rectWidth = ((settingsWidth - 2 * settingsMargin) / 2 - 3 * rectGap) / 3; + var rects = new List(); + rects.Add(new Rect(settingsWidth / 2 + rectGap / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); + rects.Add(new Rect(settingsWidth / 2 + rectWidth + rectGap * 3 / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); + rects.Add(new Rect(settingsWidth / 2 + 2 * rectWidth + rectGap * 5 / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); + return rects; + } + + float settingsWidth; + float settingsHeight; + float settingsLeft; + float settingsTop; + float settingsLineHeight; + float settingsMargin; + + private Vector2 scrollViewVector; + private bool selectMutators = false; + public List selectedMutators; + float mutatorHeight = 25; + bool editKeys; + bool scalingUI = false; + float oldUIScale = 1; +#if DEBUG + // int debug_numRaycasts = 4; +#endif + + void SetupSettingsSize() + { + settingsWidth = 420; + settingsHeight = 480; + settingsLeft = Screen.width / 2 - settingsWidth / 2; + settingsTop = 100; + settingsLineHeight = 22; + settingsMargin = 12; + WindowRectSettings = new Rect(settingsLeft, settingsTop, settingsWidth, settingsHeight); + } + + (float, float)[] asteroidFieldAltitude; + void WindowSettings(int windowID) + { + float line = 0.25f; // Top internal margin. + GUI.Box(new Rect(0, 0, settingsWidth, settingsHeight), GUIContent.none); + GUI.Label(new Rect(0, 2, settingsWidth, 22), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Title")} {Version}", settingsTitleStyle);//"BDArmory Settings" + if (GUI.Button(new Rect(settingsWidth - 24, 2, 22, 22), "X")) + { + windowSettingsEnabled = false; + } + GUI.DragWindow(new Rect(0, 0, settingsWidth, 25)); + if (editKeys) + { + InputSettings(); + return; + } + + 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_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_SETTINGS_TOGGLE = !BDArmorySettings.GRAPHICS_UI_SETTINGS_TOGGLE; + } + if (BDArmorySettings.GRAPHICS_UI_SETTINGS_TOGGLE) + { + line += 0.2f; + GUI.Label(SQuarterRect(++line, 0), $"{StringUtils.Localize("#LOC_BDArmory_Settings_UIScale")}: {BDArmorySettings.UI_SCALE_ACTUAL:0.00}x", leftLabel); // UI Scale + BDArmorySettings.UI_SCALE_FOLLOWS_STOCK = GUI.Toggle(SQuarterRect(line, 1), BDArmorySettings.UI_SCALE_FOLLOWS_STOCK, $"{StringUtils.Localize("#LOC_BDArmory_Settings_UIScaleFollowsStock")}"); + if (!BDArmorySettings.UI_SCALE_FOLLOWS_STOCK) + { + if (BDArmorySettings.UI_SCALE != (BDArmorySettings.UI_SCALE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.UI_SCALE, 0.5f, 2f), 0.05f))) + { + scalingUI = true; + BDACompetitionMode.Instance.UpdateGUIElements(); + } + } + + BDArmorySettings.DRAW_AIMERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DRAW_AIMERS, StringUtils.Localize("#LOC_BDArmory_Settings_DrawAimers"));//"Draw Aimers" + + if (!BDArmorySettings.ADVANCED_USER_SETTINGS) + { + 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; + BDArmorySettings.WATER_HIT_FX = BDArmorySettings.BULLET_HITS; + } + else + { + 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, StringUtils.Localize("#LOC_BDArmory_Settings_BulletHoleDecals"));//"Bullet Hole Decals" + 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, StringUtils.Localize("#LOC_BDArmory_Settings_EjectShells"));//"Eject Shells" + if (BDArmorySettings.EJECT_SHELLS) + { + 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, StringUtils.Localize("#LOC_BDArmory_Settings_AmmoGauges"));//"Ammo Gauges" + + if (BDArmorySettings.PERFORMANCE_OPTIONS != (BDArmorySettings.PERFORMANCE_OPTIONS = GUI.Toggle(SRightRect(line), BDArmorySettings.PERFORMANCE_OPTIONS, StringUtils.Localize("#LOC_BDArmory_Settings_PerfOptions"))))//"Enable FX" + { + // Configure FX pools + if (CMDropper.flarePool != null) CMDropper.ResetFlarePool(); + if (ExplosionFx.explosionFXPools != null) ExplosionFx.explosionFXPools.Clear(); + if (NukeFX.nukePool != null) NukeFX.nukePool.Clear(); + } + if (BDArmorySettings.ADVANCED_USER_SETTINGS && BDArmorySettings.PERFORMANCE_OPTIONS) + { + BDArmorySettings.GAPLESS_PARTICLE_EMITTERS = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.GAPLESS_PARTICLE_EMITTERS, StringUtils.Localize("#LOC_BDArmory_Settings_GaplessParticleEmitters"));//"Gapless Particle Emitters" + //BDArmorySettings.PERSISTENT_FX = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.PERSISTENT_FX, StringUtils.Localize("#LOC_BDArmory_Settings_PersistentFX"));//"Persistent FX" + if (BDArmorySettings.FLARE_SMOKE != (BDArmorySettings.FLARE_SMOKE = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.FLARE_SMOKE, StringUtils.Localize("#LOC_BDArmory_Settings_FlareSmoke"))))//"Flare Smoke" + { + if (CMDropper.flarePool != null) CMDropper.ResetFlarePool(); + } + BDArmorySettings.WATER_HIT_FX = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.WATER_HIT_FX, StringUtils.Localize("#LOC_BDArmory_Settings_WaterHitFX"));//"Water Hit FX" + //BDArmorySettings.LIGHTFX = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.LIGHTFX, StringUtils.Localize("#LOC_BDArmory_Settings_LightFX"));//Light FX" + //comment out below and uncomment above if !LIGHTFX = light range/intensity = o, but LightFX components still added. + if (BDArmorySettings.LIGHTFX != (BDArmorySettings.LIGHTFX = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.LIGHTFX, StringUtils.Localize("#LOC_BDArmory_Settings_LightFX"))))//"Light FX" { - GUI.color = XKCDColors.LightOrange; + if (ExplosionFx.explosionFXPools != null) + ExplosionFx.explosionFXPools.Clear(); + if (NukeFX.nukePool != null) + NukeFX.nukePool.Clear(); } + } + + 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(); } + } + 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 (HighLogic.LoadedSceneIsFlight) + { + 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, StringUtils.Localize("#LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI")); + } + 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, 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, StringUtils.Localize("#LOC_BDArmory_Settings_AutocategorizeParts"))))//"Autocategorize Parts" + { + KSP.UI.Screens.PartCategorizer.Instance.editorPartList.Refresh(); + } + } + + if (BDArmorySettings.ADVANCED_USER_SETTINGS) + { + { // 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); + } + + { // Numeric Input config + BDArmorySettings.NUMERIC_INPUT_SELF_UPDATE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.NUMERIC_INPUT_SELF_UPDATE, $"{StringUtils.Localize("#LOC_BDArmory_Settings_NumericInputSelfUpdate")}: {BDArmorySettings.NUMERIC_INPUT_DELAY:0.0}s"); // Numeric Input Self Update + BDArmorySettings.NUMERIC_INPUT_DELAY = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.NUMERIC_INPUT_DELAY, 0.1f, 2f), 0.1f); + } + + if (HighLogic.LoadedSceneIsEditor) // Craft-browser thumbnails + { + if (GUI.Button(SLeftRect(++line), StringUtils.Localize("#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnails"))) CraftBrowserMissingThumbnailGenerator.Instance.GenerateMissingThumbnails(EditorDriver.editorFacility); + CraftBrowserMissingThumbnailGenerator.recurse = GUI.Toggle(SRightRect(line), CraftBrowserMissingThumbnailGenerator.recurse, StringUtils.Localize("#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsRecurse")); + } + + 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 pilotAI = FlightGlobals.ActiveVessel.ActiveController().PilotAI; + if (pilotAI != null && pilotAI.pilotEnabled) pilotAI.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 ActiveController")) TestActiveController(); + // if (BDArmorySettings.DEBUG_OTHER && GUI.Button(SLineRect(++line), "Dump VesselModuleRegistry") && FlightGlobals.ActiveVessel != null) { VesselModuleRegistry.Instance.DumpRegistriesFor(FlightGlobals.ActiveVessel); } + // GUI.Label(SLeftSliderRect(++line), $"Initial correction: {(TestNumericalMethodsIC == 0 ? "None" : TestNumericalMethodsIC == 1 ? "All" : TestNumericalMethodsIC == 2 ? "Local" : "Gravity")}"); + // TestNumericalMethodsIC = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), TestNumericalMethodsIC, 0, 3)); + // if (GUI.Button(SLineRect(++line), $"Test Forward Euler vs Semi-Implicit Euler vs Leap-frog ({PROF_N * Time.fixedDeltaTime}s, {PROF_N / Math.Min(PROF_N / 2, PROF_n)} steps)")) StartCoroutine(TestNumericalMethods(PROF_N * Time.fixedDeltaTime, PROF_N / Math.Min(PROF_N / 2, PROF_n))); + // if (GUI.Button(SLineRect(++line), "Take Radar Snapshot")) + // { + // if (HighLogic.LoadedSceneIsFlight) + // { + // var v = FlightGlobals.ActiveVessel; + // var ti = RadarUtils.RenderVesselRadarSnapshot(v, v.transform); + // } + // else + // { + // Vessel v = new Vessel(); + // v.parts = EditorLogic.fetch.ship.Parts; + // v.vesselType = VesselType.Plane; + // var ti = RadarUtils.RenderVesselRadarSnapshot(v, EditorLogic.RootPart.transform); + // } + // } + // if (GUI.Button(SLineRect(++line), "Test Angle")) TestAngle(); + // if (GUI.Button(SLineRect(++line), "Test Abs")) TestAbs(); + // if (GUI.Button(SLineRect(++line), "Test \"up\"")) TestUp(); + // if (GUI.Button(SLineRect(++line), "Test inside vs on unit sphere")) TestInOnUnitSphere(); + // if (GUI.Button(SLineRect(++line), "Test Sqr vs x*x")) TestMaxRelSpeed(); + // if (GUI.Button(SLineRect(++line), "Test Sqr vs x*x")) TestSqrVsSqr(); + // if (GUI.Button(SLineRect(++line), "Test Order of Operations")) TestOrderOfOperations(); + // if (GUI.Button(SLineRect(++line), "Test GetMass vs Size performance")) TestMassVsSizePerformance(); + // if (GUI.Button(SLineRect(++line), "Test DotNorm performance")) TestDotNormPerformance(); + // if (GUI.Button(SLineRect(++line), "Vessel Naming")) + // { + // var v = FlightGlobals.ActiveVessel; + // Debug.Log($"DEBUG vesselName: {v.vesselName}, GetName: {v.GetName()}, GetDisplayName: {v.GetDisplayName()}"); + // } + // if (GUI.Button(SLineRect(++line), "Test name performance")) TestNamePerformance(); + // if (GUI.Button(SLineRect(++line), "Test rand performance")) TestRandPerformance(); + // 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($"DEBUG 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 {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); + // } + // if (GUI.Button(SLineRect(++line), $"Min Safe Altitudes")) + // { + // foreach (var body in FlightGlobals.Bodies) Debug.Log($"DEBUG Min Safe Altitude for {body.GetName()} is {body.MinSafeAltitude()}m"); + // } + // if (HighLogic.LoadedSceneIsEditor && GUI.Button(SLineRect(++line), "Test GetConnectedResourceTotals")) + // { + // EditorLogic.fetch.ship.UpdateResourceSets(); + // float dMass = 0; + // foreach (var res in EditorLogic.fetch.ship.parts.SelectMany(p => p.Resources, (p, r) => r.info).ToHashSet()) // Unique resource infos on the ship. + // { + // EditorLogic.fetch.ship.GetConnectedResourceTotals(res.id, true, out double fuelCurrent, out double fuelMax); + // dMass -= (float)fuelCurrent * res.density; + // Debug.Log($"DEBUG res {res.name}, ID: {res.id}, current: {fuelCurrent}, max: {fuelMax}, mass: {(float)fuelCurrent * res.density}"); + // } + // Debug.Log($"DEBUG dMass: {dMass}"); + // dMass = -EditorLogic.fetch.ship.parts.SelectMany(p => p.Resources, (p, r) => r.info).ToHashSet().Select(res => { EditorLogic.fetch.ship.GetConnectedResourceTotals(res.id, true, out double fuelCurrent, out double fuelMax); return (float)fuelCurrent * res.density; }).Sum(); + // Debug.Log($"DEBUG dMass: {dMass}"); + // dMass = 0; + // foreach (var part in EditorLogic.fetch.ship.parts) + // foreach (var res in part.Resources) + // { + // dMass -= (float)res.amount * res.info.density; + // } + // Debug.Log($"DEBUG dMass: {dMass}"); + // dMass = -EditorLogic.fetch.ship.parts.SelectMany(p => p.Resources, (p, r) => r).Select(res => (float)res.amount * res.info.density).Sum(); + // Debug.Log($"DEBUG dMass: {dMass}"); + // } + } +#endif + } + + line += 0.5f; + } + + 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; + } + if (BDArmorySettings.GAMEPLAY_SETTINGS_TOGGLE) + { + line += 0.2f; + + 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), $"{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) + KerbalSafetyManager.Instance.EnableKerbalSafety(); + else + KerbalSafetyManager.Instance.DisableKerbalSafety(); + } + if (BDArmorySettings.KERBAL_SAFETY != (int)KerbalSafetyLevel.Off) + { + string inventory; + switch (BDArmorySettings.KERBAL_SAFETY_INVENTORY) + { + case 1: + inventory = StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory_ResetDefault"); + break; + case 2: + inventory = StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory_ChuteOnly"); + break; + default: + inventory = StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory_NoChange"); + break; + } + 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)))) + { if (KerbalSafetyManager.Instance is not null) KerbalSafetyManager.Instance.ReconfigureInventories(); } + } + } + 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) + { + SpawnUtils.HackIntakesOnNewVessels(BDArmorySettings.HACK_INTAKES); + if (BDArmorySettings.HACK_INTAKES) // Add the hack to all in-game intakes. + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded) continue; + SpawnUtils.HackIntakes(vessel, true); + } + } + else // Reset all the in-game intakes back to their part-defined settings. + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded) continue; + SpawnUtils.HackIntakes(vessel, false); + } + } + } + } + + if (BDArmorySettings.ADVANCED_USER_SETTINGS) + { + 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.DISABLE_GUARDMODE_ON_SPAWN = GUI.Toggle(SRightRect(line), BDArmorySettings.DISABLE_GUARDMODE_ON_SPAWN, StringUtils.Localize("#LOC_BDArmory_Settings_DisableGuardModeOnSpawn")); // Disable Guard Mode on Spawn + BDArmorySettings.BULLET_WATER_DRAG = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_WATER_DRAG, StringUtils.Localize("#LOC_BDArmory_Settings_waterDrag"));// Underwater bullet drag + BDArmorySettings.RESET_HP = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_HP, StringUtils.Localize("#LOC_BDArmory_Settings_ResetHP")); + 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_ARMOUR = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_ARMOUR, StringUtils.Localize("#LOC_BDArmory_Settings_ResetArmor")); + 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.RESET_HULL = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_HULL, StringUtils.Localize("#LOC_BDArmory_Settings_ResetHull")); //Reset Hull + 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 + BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN, StringUtils.Localize("#LOC_BDArmory_Settings_AutoResumeContinuousSpawn")); // Auto-Resume Continuous Spawn + if (BDArmorySettings.AUTO_RESUME_TOURNAMENT || BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN) + { + BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT, StringUtils.Localize("#LOC_BDArmory_Settings_AutoQuitAtEndOfTournament")); // Auto Quit At End Of Tournament + } + if (BDArmorySettings.AUTO_RESUME_TOURNAMENT) + { + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AutoQuitMemoryUsage")}: ({(BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD > SystemMaxMemory ? StringUtils.Localize("#LOC_BDArmory_Generic_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), $"{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, $"{StringUtils.Localize("#LOC_BDArmory_Settings_TimeOverride")}: ({BDArmorySettings.TIME_SCALE:G2}x)"))) // Time override. + { + OtherUtils.SetTimeOverride(BDArmorySettings.TIME_OVERRIDE); + } + 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))) + { + if (BDArmorySettings.TIME_OVERRIDE) 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_RCS = GUI.Toggle(SLineRect(++line, 1), BDArmorySettings.ASPECTED_RCS, StringUtils.Localize("#LOC_BDArmory_Settings_AspectedRCS")); + if (BDArmorySettings.ASPECTED_RCS) + { + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AspectedRCSOverallRCSWeight")}: ({BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT})", leftLabel); + BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ASPECTED_RCS_OVERALL_RCS_WEIGHT, 0f, 1f), 0.05f); + } + + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_FlareFactor")}: ({BDArmorySettings.FLARE_FACTOR})", leftLabel); + 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)); + } + BDArmorySettings.IGNORE_TERRAIN_CHECK = GUI.Toggle(SLeftRect(++line), BDArmorySettings.IGNORE_TERRAIN_CHECK, StringUtils.Localize("#LOC_BDArmory_Settings_IGNORE_TERRAIN_CHECK")); // Ignore Terrain Check + BDArmorySettings.CHECK_WATER_TERRAIN = GUI.Toggle(SLeftRect(++line), BDArmorySettings.CHECK_WATER_TERRAIN, StringUtils.Localize("#LOC_BDArmory_Settings_CHECK_WATER_TERRAIN")); // Check Water + BDArmorySettings.RADAR_NOTCHING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RADAR_NOTCHING, StringUtils.Localize("#LOC_BDArmory_Settings_RADAR_NOTCHING")); // Radar Notching Toggle + if (BDArmorySettings.RADAR_NOTCHING) + { + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Notching_Factor")}: ({BDArmorySettings.RADAR_NOTCHING_FACTOR})", leftLabel); // Notch Effectiveness Multiplier + BDArmorySettings.RADAR_NOTCHING_FACTOR = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RADAR_NOTCHING_FACTOR, 0f, 1f), 0.05f); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Notching_SCR_Factor")}: ({BDArmorySettings.RADAR_NOTCHING_SCR_FACTOR})", leftLabel); // Notch SCR Multiplier, should be set to 0.01 as default though it's adjustable + BDArmorySettings.RADAR_NOTCHING_SCR_FACTOR = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RADAR_NOTCHING_SCR_FACTOR, 0f, 0.5f), 0.005f); + } + } + + line += 0.5f; + } + + 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; + } + if (BDArmorySettings.SLIDER_SETTINGS_TOGGLE) + { + line += 0.2f; + + float dmgMultiplier = BDArmorySettings.DMG_MULTIPLIER <= 100f ? BDArmorySettings.DMG_MULTIPLIER / 10f : BDArmorySettings.DMG_MULTIPLIER / 50f + 8f; + 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.ADVANCED_USER_SETTINGS) + { + 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), $"{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), $"{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), $"{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), $"{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), $"{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), $"{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), $"{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, 128f)); + + 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), $"{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), $"{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.ADVANCED_USER_SETTINGS) + { + 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), $"{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, 15f)); + + if (BDArmorySettings.ADVANCED_USER_SETTINGS) + { + 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); + } + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Max_Armor_Limit")}: {(BDArmorySettings.MAX_ARMOR_LIMIT >= 0 ? $"{BDArmorySettings.MAX_ARMOR_LIMIT:0}" : "Unclamped")}", leftLabel); // Armor Limit + if (BDArmorySettings.MAX_ARMOR_LIMIT != (BDArmorySettings.MAX_ARMOR_LIMIT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.MAX_ARMOR_LIMIT, -1, 100)))) + { + 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 ? 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; + } + if (BDArmorySettings.GAME_MODES_SETTINGS_TOGGLE) + { + line += 0.2f; + + if (BDArmorySettings.ADVANCED_USER_SETTINGS) + { + // Moving this stuff higher up as dragging the RWP slider changes the layout and can switch which slider is being dragged, causing unintended settings changes. + if (BDArmorySettings.RUNWAY_PROJECT != (BDArmorySettings.RUNWAY_PROJECT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RUNWAY_PROJECT, StringUtils.Localize("#LOC_BDArmory_Settings_RunwayProject"))))//Runway Project + { + RWPSettings.SetRWP(BDArmorySettings.RUNWAY_PROJECT, BDArmorySettings.RUNWAY_PROJECT_ROUND); + 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) + { + if (BDArmorySettings.RUNWAY_PROJECT && BDAEditorArmorWindow.Instance) BDAEditorArmorWindow.Instance.SetupLegalityValues(); + if (EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + } + if (CompSettings.CompOverridesEnabled) + { + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS != (BDArmorySettings.COMP_CONVENIENCE_CHECKS = GUI.Toggle(SRightRect(line), BDArmorySettings.COMP_CONVENIENCE_CHECKS, StringUtils.Localize("#LOC_BDArmory_Settings_CompChecks"))))//Runway Project + { + if (HighLogic.LoadedSceneIsEditor) + { + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS && BDAEditorArmorWindow.Instance) BDAEditorArmorWindow.Instance.SetupLegalityValues(); + if (EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + } + } + if (BDArmorySettings.RUNWAY_PROJECT) + { + 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 + if (BDArmorySettings.RUNWAY_PROJECT_ROUND != (BDArmorySettings.RUNWAY_PROJECT_ROUND = RWPSettings.RWPIndexToRound.GetValueOrDefault(Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), RWPSettings.RWPRoundToIndex.GetValueOrDefault(BDArmorySettings.RUNWAY_PROJECT_ROUND), 0, RWPSettings.RWPRoundToIndex.Count - 1))))) + // if (BDArmorySettings.RUNWAY_PROJECT_ROUND != (BDArmorySettings.RUNWAY_PROJECT_ROUND = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RUNWAY_PROJECT_ROUND, 10f, 70f)))) + RWPSettings.SetRWP(BDArmorySettings.RUNWAY_PROJECT, BDArmorySettings.RUNWAY_PROJECT_ROUND); + + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + 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), $"{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), $"{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), $"{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; + } + //TODO - convert these to gamemode types - e.g. Firerate Increase on Kill, Rapid Deployment, Spacemode, etc, and clear out all the empty round values + if (CheatCodeGUI != (CheatCodeGUI = GUI.TextField(SLeftRect(++line, 1, true), CheatCodeGUI, textFieldStyle))) //if we need super-secret stuff + { + switch (CheatCodeGUI) + { + case "ZombieMode": + { + BDArmorySettings.ZOMBIE_MODE = !BDArmorySettings.ZOMBIE_MODE; //sticking this here until we figure out a better home for it + CheatCodeGUI = ""; + break; + } + /* //Announcer + case "UTDeathMatch": + { + BDArmorySettings.GG_ANNOUNCER = !BDArmorySettings.GG_ANNOUNCER; + CheatCodeGUI = ""; + break; + } + */ + case "DiscoInferno": + { + BDArmorySettings.DISCO_MODE = !BDArmorySettings.DISCO_MODE; + CheatCodeGUI = ""; + break; + } + case "NoEngines": + { + BDArmorySettings.NO_ENGINES = !BDArmorySettings.NO_ENGINES; + CheatCodeGUI = ""; + break; + } + case "HallOfShame": + { + BDArmorySettings.ENABLE_HOS = !BDArmorySettings.ENABLE_HOS; + CheatCodeGUI = ""; + break; + } + case "altitudehack": //until we figure out where to put this + { + BDArmorySettings.ALTITUDE_HACKS = !BDArmorySettings.ALTITUDE_HACKS; + CheatCodeGUI = ""; + break; + } + } + } + //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), $"{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), $"{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, StringUtils.Localize("#LOC_BDArmory_Settings_BD_ZombieMode"));//"Allow battle Damage" + } + } + if (BDArmorySettings.ENABLE_HOS) + { + GUI.Label(SLeftRect(++line), StringUtils.Localize("--Hall Of Shame Enabled--"));//"Competition Distance" + HoSString = GUI.TextField(SLeftRect(++line, 1, true), HoSString, textFieldStyle); + if (!string.IsNullOrEmpty(HoSString)) + { + enteredHoS = GUI.Toggle(SRightRect(line), enteredHoS, StringUtils.Localize("Enter to Hall of Shame")); + { + if (enteredHoS) + { + if (HoSString == "Clear()") + { + BDArmorySettings.HALL_OF_SHAME_LIST.Clear(); + } + else + { + if (!BDArmorySettings.HALL_OF_SHAME_LIST.Contains(HoSString)) + { + BDArmorySettings.HALL_OF_SHAME_LIST.Add(HoSString); + } + else + { + BDArmorySettings.HALL_OF_SHAME_LIST.Remove(HoSString); + } + } + HoSString = ""; + enteredHoS = false; + } + } + } + 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), $"{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), $"{StringUtils.Localize("Frailty")}: ({(float)Math.Round(BDArmorySettings.HOS_DMG * 100, 2)}%) Dmg taken", leftLabel); + BDArmorySettings.HOS_DMG = GUIUtils.HorizontalSemiLogSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.HOS_DMG, 2), 0.1f, 10, 2f, false, true, ref hosDmgCache); + 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), 0, 200); + BDArmorySettings.HOS_SAS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.HOS_SAS, "Remove Reaction Wheels"); + BDArmorySettings.HOS_ASTEROID = GUI.Toggle(SLeftRect(++line), BDArmorySettings.HOS_ASTEROID, "Hunted by Asteroids"); + GUI.Label(SLeftRect(++line), StringUtils.Localize("--Shame badge--")); + HoSTag = GUI.TextField(SLeftRect(++line, 1, true), HoSTag, textFieldStyle); + BDArmorySettings.HOS_BADGE = HoSTag; + } + else + { + BDArmorySettings.HOS_FIRE = 0; + BDArmorySettings.HOS_MASS = 0; + BDArmorySettings.HOS_DMG = 100; + BDArmorySettings.HOS_THRUST = 100; + BDArmorySettings.HOS_SAS = false; + BDArmorySettings.HOS_ASTEROID = 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. + } + } + /* + if (BDArmorySettings.PS_CONVENIENCE_CHECKS) + { + if (CheatCodeGUI != (CheatCodeGUI = GUI.TextField(SRightRect(++line, 1, true), CheatCodeGUI, textFieldStyle))) + { + switch (CheatCodeGUI) + { + case "PSSettings": //until we figure out where to put this + { + PSSettings = !PSSettings; + CheatCodeGUI = ""; + break; + } + } + } + if (PSSettings) //rushjob hack. Look into implementing something akin to the RWPSettings/hooking into that, for a more modular approach? + { + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Extend Time Out")}: ({BDArmorySettings.PS_EXTEND_TIMEOUT})", leftLabel); + BDArmorySettings.PS_EXTEND_TIMEOUT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_EXTEND_TIMEOUT, 0, 60)); + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Max Extend Dist")}: ({BDArmorySettings.PS_EXTEND_DIST})", leftLabel); + BDArmorySettings.PS_EXTEND_DIST = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_EXTEND_DIST, 0, 30000), 100); + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Max View Range (1 seat)")}: ({BDArmorySettings.PS_MONOCOCKPIT_VIEWRANGE})", leftLabel); + BDArmorySettings.PS_MONOCOCKPIT_VIEWRANGE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_MONOCOCKPIT_VIEWRANGE, 0, 30000), 250); + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Max View Range (2 seat)")}: ({BDArmorySettings.PS_DUALCOCKPIT_VIEWRANGE})", leftLabel); + BDArmorySettings.PS_DUALCOCKPIT_VIEWRANGE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_DUALCOCKPIT_VIEWRANGE, 0, 30000), 250); + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Cockpit FOV (1 seat)")}: ({BDArmorySettings.PS_COCKPIT_FOV})", leftLabel); + BDArmorySettings.PS_COCKPIT_FOV = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_COCKPIT_FOV, 1, 360)); + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Avoid Threshold Min")}: ({BDArmorySettings.PS_AVOID_THRESH})", leftLabel); + BDArmorySettings.PS_AVOID_THRESH = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_AVOID_THRESH, 1, 30)); + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Avoid LookAhead Min")}: ({BDArmorySettings.PS_AVOID_LA})", leftLabel); + BDArmorySettings.PS_AVOID_LA = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_AVOID_LA, 0, 3) * 10f) / 10f; + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Avoid Strength Min")}: ({BDArmorySettings.PS_AVOID_STR})", leftLabel); + BDArmorySettings.PS_AVOID_STR = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_AVOID_STR, 0, 4) * 10f) / 10f; + + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("Idle Speed Min")}: ({BDArmorySettings.PS_IDLE_SPEED})", leftLabel); + BDArmorySettings.PS_IDLE_SPEED = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.PS_IDLE_SPEED, 100, 500), 10); + BDArmorySettings.PS_DISABLE_SAS = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.PS_DISABLE_SAS, StringUtils.Localize("Disable non-Cockpit SAS")); + line++; + //min/max Altitude setters? + } + } + */ + } + + if (BDArmorySettings.BATTLEDAMAGE != (BDArmorySettings.BATTLEDAMAGE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BATTLEDAMAGE, StringUtils.Localize("#LOC_BDArmory_Settings_BattleDamage")))) + { + BDArmorySettings.PAINTBALL_MODE = false; + } + BDArmorySettings.INFINITE_AMMO = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_AMMO, StringUtils.Localize("#LOC_BDArmory_Settings_InfiniteAmmo")); + if (BDArmorySettings.PAINTBALL_MODE != (BDArmorySettings.PAINTBALL_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.PAINTBALL_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_PaintballMode"))))//"Paintball Mode" + { + BulletHitFX.SetupShellPool(); + BDArmorySettings.BATTLEDAMAGE = false; + } + BDArmorySettings.INFINITE_ORDINANCE = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_ORDINANCE, StringUtils.Localize("#LOC_BDArmory_Settings_InfiniteMissiles")); + if (BDArmorySettings.PEACE_MODE != (BDArmorySettings.PEACE_MODE = GUI.Toggle(SLeftRect(++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; + } + BDArmorySettings.INFINITE_COUNTERMEASURES = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_COUNTERMEASURES, StringUtils.Localize("#LOC_BDArmory_Settings_InfiniteCountermeasures")); + BDArmorySettings.TAG_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TAG_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_TagMode"));//"Tag Mode" + 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; + } + 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) + { + BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD = 10; // For gravity hacks, we need a shorter grace period. + BDArmorySettings.COMPETITION_KILL_TIMER = 1; // and a shorter kill timer. + } + else + { + BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD = 60; // Reset grace period back to default of 60s. + BDArmorySettings.COMPETITION_KILL_TIMER = 15; // Reset kill timer period back to default of 15s. + PhysicsGlobals.GraviticForceMultiplier = 1; + VehiclePhysics.Gravity.Refresh(); + } + } + 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; + } + //Mutators + var oldMutators = BDArmorySettings.MUTATOR_MODE; + BDArmorySettings.MUTATOR_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.MUTATOR_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_Mutators")); + { + if (BDArmorySettings.MUTATOR_MODE) + { + if (!oldMutators) // Add missing modules when Space Hacks is toggled. + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel.ActiveController().WM != null && vessel.rootPart.FindModuleImplementing() == null) + { + vessel.rootPart.AddModule("BDAMutator"); + } + } + } + selectMutators = GUI.Toggle(SLeftRect(++line, 1f), selectMutators, StringUtils.Localize("#LOC_BDArmory_MutatorSelect")); + if (selectMutators) + { + ++line; + scrollViewVector = GUI.BeginScrollView(new Rect(settingsMargin + 1 * settingsMargin, line * settingsLineHeight, settingsWidth - 2 * settingsMargin - 1 * settingsMargin, settingsLineHeight * 6f), scrollViewVector, + new Rect(0, 0, settingsWidth - 2 * settingsMargin - 2 * settingsMargin, mutatorHeight)); + GUI.BeginGroup(new Rect(0, 0, settingsWidth - 2 * settingsMargin - 2 * settingsMargin, mutatorHeight), GUIContent.none); + int mutatorLine = 0; + for (int i = 0; i < mutators.Count; i++) + { + Rect buttonRect = new Rect(0, (i * 25), (settingsWidth - 4 * settingsMargin) / 2, 20); + if (mutators_selected[i] != (mutators_selected[i] = GUI.Toggle(buttonRect, mutators_selected[i], mutators[i]))) + { + if (mutators_selected[i]) + { + BDArmorySettings.MUTATOR_LIST.Add(mutators[i]); + } + else + { + BDArmorySettings.MUTATOR_LIST.Remove(mutators[i]); + } + } + mutatorLine++; + } - string label = Misc.Misc.FormattedGeoPosShort(coordinate.Current.gpsCoordinates, false); - float nameWidth = 100; - if (editingGPSName && index == editingGPSNameIndex) - { - if (validGPSName && Event.current.type == EventType.KeyDown && - Event.current.keyCode == KeyCode.Return) + mutatorHeight = Mathf.Lerp(mutatorHeight, (mutatorLine * 25), 1); + GUI.EndGroup(); + GUI.EndScrollView(); + line += 6.5f; + + 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)}"); + break; + default: + BDArmorySettings.MUTATOR_LIST.Clear(); + for (int i = 0; i < mutators_selected.Length; ++i) mutators_selected[i] = false; + Debug.Log("[BDArmory.BDArmorySetup]: Resetting Mutator list"); + break; + } + } + line += .2f; + } + 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 { - editingGPSName = false; - hasEnteredGPSName = true; + BDArmorySettings.MUTATOR_APPLY_KILL = false; } - else + 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 { - Color origColor = GUI.color; - if (newGPSName.Contains(";") || newGPSName.Contains(":") || newGPSName.Contains(",")) + BDArmorySettings.MUTATOR_APPLY_GUNGAME = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_APPLY_GUNGAME, StringUtils.Localize("#LOC_BDArmory_Settings_MutatorGungame")); + BDArmorySettings.MUTATOR_APPLY_GLOBAL = false; + BDArmorySettings.MUTATOR_APPLY_TIMER = false; + if (BDArmorySettings.MUTATOR_APPLY_GUNGAME) { - validGPSName = false; - GUI.color = Color.red; + BDArmorySettings.GG_PERSISTANT_PROGRESSION = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.GG_PERSISTANT_PROGRESSION, StringUtils.Localize("#LOC_BDArmory_settings_gungame_progression")); + BDArmorySettings.GG_CYCLE_LIST = GUI.Toggle(SRightRect(line, 1f), BDArmorySettings.GG_CYCLE_LIST, StringUtils.Localize("#LOC_BDArmory_settings_gungame_cycle")); } - else + if (GUI.Button(SLeftRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_reset")} {StringUtils.Localize("#LOC_BDArmory_Weapons")}")) SpawnUtilsInstance.Instance.gunGameProgress.Clear(); // Clear gun-game progress. + if (!MutatorInfo.gunGameConfigured) MutatorInfo.SetupGunGame(); + } + + if (BDArmorySettings.MUTATOR_LIST.Count > 1) + + { + 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 { - validGPSName = true; + BDArmorySettings.MUTATOR_APPLY_KILL = false; + //BDArmorySettings.MUTATOR_APPLY_GLOBAL = false; //global + timer causes a single globally appled mutator that shuffles, instead of chaos mode } + } + else + { + BDArmorySettings.MUTATOR_APPLY_TIMER = false; + } + if (!BDArmorySettings.MUTATOR_APPLY_TIMER && !BDArmorySettings.MUTATOR_APPLY_KILL) + { + BDArmorySettings.MUTATOR_APPLY_GLOBAL = true; + } - newGPSName = GUI.TextField( - new Rect(0, gpsEntryCount * gpsEntryHeight, nameWidth, gpsEntryHeight), newGPSName, 12); - GUI.color = origColor; + 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 > 0 ? BDArmorySettings.COMPETITION_DURATION : 15), 1); + 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) + { + BDArmorySettings.MUTATOR_APPLY_NUM = BDArmorySettings.MUTATOR_LIST.Count; + } + if (BDArmorySettings.MUTATOR_LIST.Count > 0 && BDArmorySettings.MUTATOR_APPLY_NUM < 1) + { + BDArmorySettings.MUTATOR_APPLY_NUM = 1; } + BDArmorySettings.MUTATOR_ICONS = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_ICONS, StringUtils.Localize("#LOC_BDArmory_Settings_MutatorIcons")); + } + else if (oldMutators && HighLogic.LoadedSceneIsFlight && !(BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 61)) + { + foreach (var vessel in FlightGlobals.VesselsLoaded) SpawnUtils.ApplyMutators(vessel, false); // Clear mutators on existing vessels when disabling this. + SpawnUtils.ApplyMutatorsOnNewVessels(false); // And prevent any new ones. + } + } + // Heartbleed + 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), $"{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), $"{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), $"{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)); + } + if (BDArmorySettings.G_LIMITS != (BDArmorySettings.G_LIMITS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.G_LIMITS, StringUtils.Localize("#LOC_BDArmory_Settings_GLimitsMode"))))//G-Force Limits + { + if (BDArmorySettings.G_LIMITS) // Sync the initial override values to the current ones. + { + RWPSettings.SyncWithGameSettings(toKSP: false); // Update the current backing values from the Game Difficulty. + RWPSettings.SyncWithGameSettings(); // Then override them. } else { - if (GUI.Button(new Rect(0, gpsEntryCount * gpsEntryHeight, nameWidth, gpsEntryHeight), - coordinate.Current.name, - BDGuiSkin.button)) - { - editingGPSName = true; - editingGPSNameIndex = index; - newGPSName = coordinate.Current.name; - } + RWPSettings.SyncWithGameSettings(); // Sync so that the Game Difficulty values get reverted to the backing values. } - - if ( - GUI.Button( - new Rect(nameWidth, gpsEntryCount * gpsEntryHeight, listRect.width - gpsEntryHeight - nameWidth, - gpsEntryHeight), label, BDGuiSkin.button)) + } + if (BDArmorySettings.G_LIMITS) + { + if (HighLogic.CurrentGame != null) { - ActiveWeaponManager.designatedGPSInfo = coordinate.Current; - editingGPSName = false; + // Sync with the Game Difficulty window if the checkbox was toggled there and update the backing values. (Note: this only happens if the menu is open.) + advancedParams = HighLogic.CurrentGame.Parameters.CustomParams(); // Grab the latest values, in case KSP has changed the instance. + if (BDArmorySettings.PART_GLIMIT != (BDArmorySettings.PART_GLIMIT = advancedParams.GPartLimits)) BDArmorySettings._PART_GLIMIT = BDArmorySettings.PART_GLIMIT; + if (BDArmorySettings.KERB_GLIMIT != (BDArmorySettings.KERB_GLIMIT = advancedParams.GKerbalLimits)) BDArmorySettings._KERB_GLIMIT = BDArmorySettings.KERB_GLIMIT; + if (BDArmorySettings.G_TOLERANCE != (BDArmorySettings.G_TOLERANCE = BDAMath.RoundToUnit(advancedParams.KerbalGToleranceMult * 20.5f, 0.5f))) BDArmorySettings._G_TOLERANCE = BDArmorySettings.G_TOLERANCE; } - if ( - GUI.Button( - new Rect(listRect.width - gpsEntryHeight, gpsEntryCount * gpsEntryHeight, gpsEntryHeight, - gpsEntryHeight), "X", BDGuiSkin.button)) + bool advParamsChanged = false; + if (BDArmorySettings.PART_GLIMIT != (BDArmorySettings.PART_GLIMIT = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.PART_GLIMIT, StringUtils.Localize("#autoLOC_140950"))))//Part G-Force Limits + advParamsChanged = true; + if (BDArmorySettings.KERB_GLIMIT != (BDArmorySettings.KERB_GLIMIT = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.KERB_GLIMIT, StringUtils.Localize("#autoLOC_140953"))))//Kerbal G-Force Limits + advParamsChanged = true; + if (BDArmorySettings.KERB_GLIMIT) { - indexToRemove = index; + //G-Limit is point at which Kerbs begin to fill the G-meter; e.g. a G_TOLERANCE of 8 would result in Kerbs indefinately tolerating a 8g turn, but a 9g sustained turn would *very* slowly fill the meter, a 13g sustained turn would KO them after 5-6 seconds, a 20g turn would KO them instantly, etc. + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#autoLOC_140956")}: ({BDArmorySettings.G_TOLERANCE:0.0}g)", leftLabel);//Kerbal G-Force Tolerance + if (BDArmorySettings.G_TOLERANCE != (BDArmorySettings.G_TOLERANCE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.G_TOLERANCE, 1f, 40f), 0.5f))) + advParamsChanged = true; + } + if (advParamsChanged) + { + RWPSettings.SyncWithGameSettings(); } - - gpsEntryCount++; - index++; - GUI.color = origWColor; } - coordinate.Dispose(); - } - - if (hasEnteredGPSName && editingGPSNameIndex < BDATargetManager.GPSTargetList(myTeam).Count) - { - hasEnteredGPSName = false; - GPSTargetInfo old = BDATargetManager.GPSTargetList(myTeam)[editingGPSNameIndex]; - if (ActiveWeaponManager.designatedGPSInfo.EqualsTarget(old)) + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AimingVisualMalus")}: ({BDArmorySettings.AIMING_VISUAL_MALUS:G2})", leftLabel); // Aiming Visual Malus + BDArmorySettings.AIMING_VISUAL_MALUS = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.AIMING_VISUAL_MALUS, 0f, 1f), 0.01f); + // 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) { - ActiveWeaponManager.designatedGPSInfo.name = newGPSName; + 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), $"{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), $"{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; } - BDATargetManager.GPSTargetList(myTeam)[editingGPSNameIndex] = - new GPSTargetInfo(BDATargetManager.GPSTargetList(myTeam)[editingGPSNameIndex].gpsCoordinates, - newGPSName); - editingGPSNameIndex = 0; - BDATargetManager.Instance.SaveGPSTargets(); - } - - GUI.EndGroup(); - - if (indexToRemove >= 0) - { - BDATargetManager.GPSTargetList(myTeam).RemoveAt(indexToRemove); - BDATargetManager.Instance.SaveGPSTargets(); - } - - WindowRectGps.height = (2 * gpsBorder) + (gpsEntryCount * gpsEntryHeight); - } - - Rect SLineRect(float line) - { - return new Rect(settingsMargin, line * settingsLineHeight, settingsWidth - 2 * settingsMargin, settingsLineHeight); - } - - Rect SLeftRect(float line) - { - return new Rect(settingsMargin, line * settingsLineHeight, settingsWidth / 2 - settingsMargin - settingsMargin / 4, settingsLineHeight); - } - - Rect SRightRect(float line) - { - return new Rect(settingsWidth / 2 + settingsMargin / 4, line * settingsLineHeight, settingsWidth / 2 - settingsMargin - settingsMargin / 4, settingsLineHeight); - } - - Rect SLeftSliderRect(float line) - { - return new Rect(settingsMargin, line * settingsLineHeight, settingsWidth / 2 + settingsMargin / 2, settingsLineHeight); - } - - Rect SRightSliderRect(float line) - { - return new Rect(settingsMargin + settingsWidth / 2 + settingsMargin / 2, line * settingsLineHeight, settingsWidth / 2 - 7 / 2 * settingsMargin, settingsLineHeight); - } - - Rect SLeftButtonRect(float line) - { - return new Rect(settingsMargin, line * settingsLineHeight, (settingsWidth - 2 * settingsMargin) / 2 - settingsMargin / 4, settingsLineHeight); - } - - Rect SRightButtonRect(float line) - { - return new Rect(settingsWidth / 2 + settingsMargin / 4, line * settingsLineHeight, (settingsWidth - 2 * settingsMargin) / 2 - settingsMargin / 4, settingsLineHeight); - } - - Rect SQuarterRect(float line, int pos) - { - return new Rect(settingsMargin + (pos % 4) * (settingsWidth - 2f * settingsMargin) / 4f, (line + (int)(pos / 4)) * settingsLineHeight, (settingsWidth - 2.5f * settingsMargin) / 4f, settingsLineHeight); - } - - List SRight2Rects(float line) - { - var rectGap = settingsMargin / 2; - var rectWidth = ((settingsWidth - 2 * settingsMargin) / 2 - 2 * rectGap) / 2; - var rects = new List(); - rects.Add(new Rect(settingsWidth / 2 + rectGap / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); - rects.Add(new Rect(settingsWidth / 2 + rectWidth + rectGap * 3 / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); - return rects; - } - - List SRight3Rects(float line) - { - var rectGap = settingsMargin / 3; - var rectWidth = ((settingsWidth - 2 * settingsMargin) / 2 - 3 * rectGap) / 3; - var rects = new List(); - rects.Add(new Rect(settingsWidth / 2 + rectGap / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); - rects.Add(new Rect(settingsWidth / 2 + rectWidth + rectGap * 3 / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); - rects.Add(new Rect(settingsWidth / 2 + 2 * rectWidth + rectGap * 5 / 2, line * settingsLineHeight, rectWidth, settingsLineHeight)); - return rects; - } - - float settingsWidth; - float settingsHeight; - float settingsLeft; - float settingsTop; - float settingsLineHeight; - float settingsMargin; - - bool editKeys; - - void SetupSettingsSize() - { - settingsWidth = 420; - settingsHeight = 480; - settingsLeft = Screen.width / 2 - settingsWidth / 2; - settingsTop = 100; - settingsLineHeight = 22; - settingsMargin = 12; - WindowRectSettings = new Rect(settingsLeft, settingsTop, settingsWidth, settingsHeight); - } - - private class SpawnField : MonoBehaviour - { - public SpawnField Initialise(double l, double v, double minV = double.MinValue, double maxV = double.MaxValue) { lastUpdated = l; currentValue = v; minValue = minV; maxValue = maxV; return this; } - public double lastUpdated; - public string possibleValue = string.Empty; - private double _value; - public double currentValue { get { return _value; } set { _value = value; possibleValue = _value.ToString("G6"); } } - private double minValue; - private double maxValue; - private bool coroutineRunning = false; - private Coroutine coroutine; - - public void tryParseValue(string v) - { - if (v != possibleValue) + 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) { - lastUpdated = Time.time; - possibleValue = v; - if (!coroutineRunning) + if (HighLogic.LoadedSceneIsFlight) { - coroutine = StartCoroutine(UpdateValueCoroutine()); + 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); } - } - - private IEnumerator UpdateValueCoroutine() - { - coroutineRunning = true; - while (Time.time - lastUpdated < 0.5) - yield return new WaitForFixedUpdate(); - double newValue; - if (double.TryParse(possibleValue, out newValue)) + else { - currentValue = Math.Min(Math.Max(newValue, minValue), maxValue); - lastUpdated = Time.time; + BDArmorySettings.SF_FRICTION = false; + BDArmorySettings.SF_GRAVITY = false; + BDArmorySettings.SF_REPULSOR = false; } - possibleValue = currentValue.ToString("G6"); - coroutineRunning = false; - yield return new WaitForFixedUpdate(); - } - } - Dictionary spawnFields; - - 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" - if (GUI.Button(new Rect(settingsWidth - 18, 2, 16, 16), "X")) - { - windowSettingsEnabled = false; - } - GUI.DragWindow(new Rect(0, 0, settingsWidth, 25)); - if (editKeys) - { - InputSettings(); - return; - } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.GENERAL_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_GeneralSettingsToggle")))//Show/hide general settings. - { - BDArmorySettings.GENERAL_SETTINGS_TOGGLE = !BDArmorySettings.GENERAL_SETTINGS_TOGGLE; - } - if (BDArmorySettings.GENERAL_SETTINGS_TOGGLE) - { - BDArmorySettings.INSTAKILL = GUI.Toggle(SLeftRect(++line), BDArmorySettings.INSTAKILL, Localizer.Format("#LOC_BDArmory_Settings_Instakill"));//"Instakill" - BDArmorySettings.INFINITE_AMMO = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_AMMO, Localizer.Format("#LOC_BDArmory_Settings_InfiniteAmmo"));//"Infinite Ammo" - BDArmorySettings.BULLET_HITS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_HITS, Localizer.Format("#LOC_BDArmory_Settings_BulletHits"));//"Bullet Hits" - BDArmorySettings.EJECT_SHELLS = GUI.Toggle(SRightRect(line), BDArmorySettings.EJECT_SHELLS, Localizer.Format("#LOC_BDArmory_Settings_EjectShells"));//"Eject Shells" - BDArmorySettings.AIM_ASSIST = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AIM_ASSIST, Localizer.Format("#LOC_BDArmory_Settings_AimAssist"));//"Aim Assist" - BDArmorySettings.DRAW_AIMERS = GUI.Toggle(SRightRect(line), BDArmorySettings.DRAW_AIMERS, Localizer.Format("#LOC_BDArmory_Settings_DrawAimers"));//"Draw Aimers" - BDArmorySettings.DRAW_DEBUG_LINES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DRAW_DEBUG_LINES, Localizer.Format("#LOC_BDArmory_Settings_DebugLines"));//"Debug Lines" - BDArmorySettings.DRAW_DEBUG_LABELS = GUI.Toggle(SRightRect(line), BDArmorySettings.DRAW_DEBUG_LABELS, Localizer.Format("#LOC_BDArmory_Settings_DebugLabels"));//"Debug Labels" - BDArmorySettings.REMOTE_SHOOTING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.REMOTE_SHOOTING, Localizer.Format("#LOC_BDArmory_Settings_RemoteFiring"));//"Remote Firing" - BDArmorySettings.BOMB_CLEARANCE_CHECK = GUI.Toggle(SRightRect(line), BDArmorySettings.BOMB_CLEARANCE_CHECK, Localizer.Format("#LOC_BDArmory_Settings_ClearanceCheck"));//"Clearance Check" - BDArmorySettings.SHOW_AMMO_GAUGES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SHOW_AMMO_GAUGES, Localizer.Format("#LOC_BDArmory_Settings_AmmoGauges"));//"Ammo Gauges" - BDArmorySettings.SHELL_COLLISIONS = GUI.Toggle(SRightRect(line), BDArmorySettings.SHELL_COLLISIONS, Localizer.Format("#LOC_BDArmory_Settings_ShellCollisions"));//"Shell Collisions" - BDArmorySettings.BULLET_DECALS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_DECALS, Localizer.Format("#LOC_BDArmory_Settings_BulletHoleDecals"));//"Bullet Hole Decals" - BDArmorySettings.DISABLE_RAMMING = GUI.Toggle(SRightRect(line), BDArmorySettings.DISABLE_RAMMING, Localizer.Format("#LOC_BDArmory_Settings_DisableRamming"));// Disable Ramming - BDArmorySettings.DEFAULT_FFA_TARGETING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DEFAULT_FFA_TARGETING, Localizer.Format("#LOC_BDArmory_Settings_DefaultFFATargeting"));// Free-for-all combat style - BDArmorySettings.DEBUG_RAMMING_LOGGING = GUI.Toggle(SRightRect(line), BDArmorySettings.DEBUG_RAMMING_LOGGING, Localizer.Format("#LOC_BDArmory_Settings_DebugRammingLogging"));// Disable Ramming - BDArmorySettings.PERFORMANCE_LOGGING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.PERFORMANCE_LOGGING, Localizer.Format("#LOC_BDArmory_Settings_PerformanceLogging"));//"Performance Logging" - BDArmorySettings.STRICT_WINDOW_BOUNDARIES = GUI.Toggle(SRightRect(line), BDArmorySettings.STRICT_WINDOW_BOUNDARIES, Localizer.Format("#LOC_BDArmory_Settings_StrictWindowBoundaries"));//"Strict Window Boundaries" - if (BDArmorySettings.TAG_MODE != (BDArmorySettings.TAG_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TAG_MODE, Localizer.Format("#LOC_BDArmory_Settings_TagMode"))))//"Tag Mode" + // Asteroids + if (BDArmorySettings.ASTEROID_FIELD != (BDArmorySettings.ASTEROID_FIELD = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_FIELD, StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidField")))) // Asteroid Field { - if (BDACompetitionMode.Instance != null) BDACompetitionMode.Instance.lastTagUpdateTime = Planetarium.GetUniversalTime(); + if (!BDArmorySettings.ASTEROID_FIELD) AsteroidField.Instance.Reset(true); } - 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.RUNWAY_PROJECT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RUNWAY_PROJECT, Localizer.Format("#LOC_BDArmory_Settings_RunwayProject"));//Runway Project - BDArmorySettings.DISABLE_KILL_TIMER = GUI.Toggle(SRightRect(line), BDArmorySettings.DISABLE_KILL_TIMER, Localizer.Format("#LOC_BDArmory_Settings_DisableKillTimer"));//"Disable Kill Timer" - if (BDArmorySettings.GRAVITY_HACKS != (BDArmorySettings.GRAVITY_HACKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.GRAVITY_HACKS, Localizer.Format("#LOC_BDArmory_Settings_GravityHacks"))))//"Gravity hacks" + if (BDArmorySettings.ASTEROID_FIELD) { - if (BDArmorySettings.GRAVITY_HACKS) + if (GUI.Button(SRightButtonRect(line), "Spawn Field Now"))//"Spawn Field Now")) { - BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD = 10; // For gravity hacks, we need a shorter grace period. - BDArmorySettings.COMPETITION_KILL_TIMER = 1; // and a shorter kill timer. + if (Event.current.button == 1) + AsteroidField.Instance.Reset(); + else if (Event.current.button == 2) // Middle click + // AsteroidUtils.CheckOrbit(); + AsteroidField.Instance.CheckPooledAsteroids(); + else + AsteroidField.Instance.SpawnField(BDArmorySettings.ASTEROID_FIELD_NUMBER, BDArmorySettings.ASTEROID_FIELD_ALTITUDE, BDArmorySettings.ASTEROID_FIELD_RADIUS, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS); } - else + line += 0.25f; + 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 < 1000f ? $"{BDArmorySettings.ASTEROID_FIELD_ALTITUDE:G3}m" : $"{BDArmorySettings.ASTEROID_FIELD_ALTITUDE / 1000f:G5}km"; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldAltitude")}: ({altitudeString})", leftLabel); + BDArmorySettings.ASTEROID_FIELD_ALTITUDE = GUIUtils.HorizontalSemiLogSlider(SRightSliderRect(line), BDArmorySettings.ASTEROID_FIELD_ALTITUDE, 100f, Mathf.Max(100000f, BDArmorySettings.VESSEL_SPAWN_ALTITUDE), 1.5f, false, false, ref asteroidFieldAltitude); // Asteroid Field Altitude + 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 ? $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction")}: ({BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH:G2})" : StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction")))) // Anomalous Attraction { - BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD = 60; // Reset grace period back to default of 60s. - BDArmorySettings.COMPETITION_KILL_TIMER = 15; // Reset kill timer period back to default of 15s. - PhysicsGlobals.GraviticForceMultiplier = 1; - VehiclePhysics.Gravity.Refresh(); + if (!BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION && AsteroidField.Instance != null) + { AsteroidField.Instance.anomalousAttraction = Vector3d.zero; } } + if (BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION) + { + 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, StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRain")))) // Asteroid Rain + { + if (!BDArmorySettings.ASTEROID_RAIN) AsteroidRain.Instance.Reset(); } - BDArmorySettings.BATTLEDAMAGE = GUI.Toggle(SRightRect(line), BDArmorySettings.BATTLEDAMAGE, Localizer.Format("#LOC_BDArmory_Settings_BattleDamage")); - BDArmorySettings.EXTRA_DAMAGE_SLIDERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.EXTRA_DAMAGE_SLIDERS, Localizer.Format("#LOC_BDArmory_Settings_ExtraDamageSliders")); - BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING, Localizer.Format("#LOC_BDArmory_Settings_AutoEnableVesselSwitching")); - 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")); - if (HighLogic.LoadedSceneIsEditor) + if (BDArmorySettings.ASTEROID_RAIN) { - if (BDArmorySettings.SHOW_CATEGORIES != (BDArmorySettings.SHOW_CATEGORIES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SHOW_CATEGORIES, Localizer.Format("#LOC_BDArmory_Settings_ShowEditorSubcategories"))))//"Show Editor Subcategories" + if (GUI.Button(SRightButtonRect(line), "Spawn Rain Now")) { - KSP.UI.Screens.PartCategorizer.Instance.editorPartList.Refresh(); + if (Event.current.button == 1) + AsteroidRain.Instance.Reset(); + else if (Event.current.button == 2) + AsteroidRain.Instance.CheckPooledAsteroids(); + else + AsteroidRain.Instance.SpawnRain(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS); } - if (BDArmorySettings.AUTOCATEGORIZE_PARTS != (BDArmorySettings.AUTOCATEGORIZE_PARTS = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTOCATEGORIZE_PARTS, Localizer.Format("#LOC_BDArmory_Settings_AutocategorizeParts"))))//"Autocategorize Parts" + 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) { - KSP.UI.Screens.PartCategorizer.Instance.editorPartList.Refresh(); + 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), $"{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), $"{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), $"{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; } - ++line; + BDArmorySettings.WAYPOINTS_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_WaypointsMode")); + line += 0.5f; } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SLIDER_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_SliderSettingsToggle")))//Show/hide slider settings. - { - BDArmorySettings.SLIDER_SETTINGS_TOGGLE = !BDArmorySettings.SLIDER_SETTINGS_TOGGLE; - } - if (BDArmorySettings.SLIDER_SETTINGS_TOGGLE) + if (BDArmorySettings.BATTLEDAMAGE) { - 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 - dmgMultiplier = (int)GUI.HorizontalSlider(SRightSliderRect(line), dmgMultiplier, 1f, 28f); - BDArmorySettings.DMG_MULTIPLIER = dmgMultiplier < 11 ? (int)(dmgMultiplier * 10f) : (int)(50f * (dmgMultiplier - 8f)); - if (BDArmorySettings.EXTRA_DAMAGE_SLIDERS) + 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. { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_BallisticDamageMultiplier")}: ({BDArmorySettings.BALLISTIC_DMG_FACTOR})", leftLabel); - BDArmorySettings.BALLISTIC_DMG_FACTOR = (int)(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BALLISTIC_DMG_FACTOR * 20f, 0f, 60f)) / 20f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_ExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW})", leftLabel); - BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW = (int)(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW * 20f, 0f, 30f)) / 20f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_MISSILE})", leftLabel); - BDArmorySettings.EXP_DMG_MOD_MISSILE = (int)(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_MISSILE * 4f, 0f, 40f)) / 4f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_ImplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_IMP_MOD})", leftLabel); - BDArmorySettings.EXP_IMP_MOD = (int)(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_IMP_MOD * 20, 0f, 20f)) / 20f; + BDArmorySettings.BATTLEDAMAGE_TOGGLE = !BDArmorySettings.BATTLEDAMAGE_TOGGLE; } + if (BDArmorySettings.BATTLEDAMAGE_TOGGLE) + { + line += 0.2f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_MaxBulletHoles")}: ({BDArmorySettings.MAX_NUM_BULLET_DECALS})", leftLabel); // Max Bullet Holes - if (BDArmorySettings.MAX_NUM_BULLET_DECALS != (BDArmorySettings.MAX_NUM_BULLET_DECALS = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.MAX_NUM_BULLET_DECALS, 1f, 999))) - BulletHitFX.AdjustDecalPoolSizes(BDArmorySettings.MAX_NUM_BULLET_DECALS); - - 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. - BDArmorySettings.TERRAIN_ALERT_FREQUENCY = (int)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 - BDArmorySettings.CAMERA_SWITCH_FREQUENCY = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.CAMERA_SWITCH_FREQUENCY, 1f, 10f); - - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_DebrisCleanUpDelay")}: ({BDArmorySettings.DEBRIS_CLEANUP_DELAY}s)", leftLabel); // Debris Clean-up delay - BDArmorySettings.DEBRIS_CLEANUP_DELAY = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.DEBRIS_CLEANUP_DELAY, 1f, 60f); + 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)); - 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 - BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY, 1f, 61f); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Proc_Pen")}: ({BDArmorySettings.BD_DAMAGE_PENETRATION})", leftLabel); //Proc Chance Penetration + BDArmorySettings.BD_DAMAGE_PENETRATION = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BD_DAMAGE_PENETRATION, 0f, 1f), 0.01f); - 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); - BDArmorySettings.COMPETITION_DURATION = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_DURATION, 0f, 15f); + 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), $"{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), $"{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), $"{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, 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" + } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionInitialGracePeriod")}: ({BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD}s)", leftLabel); - BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD, 0f, 60f); + 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), $"{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, StringUtils.Localize("#LOC_BDArmory_Settings_BD_CtrlSrf"));//"Ctrl Surface Damage" + } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionFinalGracePeriod")}: ({(BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD > 60 ? "Inf" : BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD + "s")})", leftLabel); - BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD, 0f, 61f); + 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, StringUtils.Localize("#LOC_BDArmory_Settings_BD_PilotKill"));//"Crew Fatalities" + } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionKillTimer")}: ({BDArmorySettings.COMPETITION_KILL_TIMER}s, {(BDArmorySettings.DISABLE_KILL_TIMER ? "off" : "on")})", leftLabel); // FIXME the toggle and this slider could be merged - BDArmorySettings.COMPETITION_KILL_TIMER = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_KILL_TIMER, 1f, 60f); + 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), $"{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), $"{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, 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" - if (BDArmorySettings.RUNWAY_PROJECT) - { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod")}: ({BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD}s)", leftLabel); - BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD / 10f, 0, 18) * 10f; + 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, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Volatile_Ammo"));//"Ammo Bins Explode When Destroyed" + } - 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.killerGMenabled ? "on" : "off")})", leftLabel); - BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY = (int)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). - // BDACompetitionMode.Instance.killerGMenabled = !(BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60); + 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, 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, 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 + } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionKillerGMMaxAltitude")}: ({(BDArmorySettings.COMPETITION_KILLER_GM_MAX_ALTITUDE > 100 ? "Never" : BDArmorySettings.COMPETITION_KILLER_GM_MAX_ALTITUDE + "km")})", leftLabel); - BDArmorySettings.COMPETITION_KILLER_GM_MAX_ALTITUDE = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_KILLER_GM_MAX_ALTITUDE, 1f, 101f); + line += 0.5f; } - - ++line; } - 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; } if (BDArmorySettings.RADAR_SETTINGS_TOGGLE) { - GUI.Label(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_RWRWindowScale") + ": " + (BDArmorySettings.RWR_WINDOW_SCALE * 100).ToString("0") + "%", leftLabel); // RWR Window Scale + line += 0.2f; + + 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(SRightRect(line), rwrScale, BDArmorySettings.RWR_WINDOW_SCALE_MIN, BDArmorySettings.RWR_WINDOW_SCALE_MAX) * 100.0f) * 0.01f; + 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)) { ResizeRwrWindow(rwrScale); } - GUI.Label(SLeftRect(++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(SRightRect(line), radarScale, BDArmorySettings.RADAR_WINDOW_SCALE_MIN, BDArmorySettings.RADAR_WINDOW_SCALE_MAX) * 100.0f) * 0.01f; + 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)) { ResizeRadarWindow(radarScale); } - GUI.Label(SLeftRect(++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(SRightRect(line), targetScale, BDArmorySettings.TARGET_WINDOW_SCALE_MIN, BDArmorySettings.TARGET_WINDOW_SCALE_MAX) * 100.0f) * 0.01f; + 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)) { ResizeTargetWindow(targetScale); } - ++line; + 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; } if (BDArmorySettings.OTHER_SETTINGS_TOGGLE) { - GUI.Label(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TriggerHold") + ": " + BDArmorySettings.TRIGGER_HOLD_TIME.ToString("0.00") + "s", leftLabel);//Trigger Hold - BDArmorySettings.TRIGGER_HOLD_TIME = GUI.HorizontalSlider(SRightRect(line), BDArmorySettings.TRIGGER_HOLD_TIME, 0.02f, 1f); + line += 0.2f; + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TriggerHold")}: {BDArmorySettings.TRIGGER_HOLD_TIME:0.00} s", leftLabel);//Trigger Hold + BDArmorySettings.TRIGGER_HOLD_TIME = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TRIGGER_HOLD_TIME, 0.02f, 1f), 0.02f); - GUI.Label(SLeftRect(++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(SRightRect(line), uiVol, 0f, 1f); + uiVol = GUI.HorizontalSlider(SRightSliderRect(line), uiVol, 0f, 1f); if (uiVol != BDArmorySettings.BDARMORY_UI_VOLUME && OnVolumeChange != null) { OnVolumeChange(); } BDArmorySettings.BDARMORY_UI_VOLUME = uiVol; - GUI.Label(SLeftRect(++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(SRightRect(line), weaponVol, 0f, 1f); + weaponVol = GUI.HorizontalSlider(SRightSliderRect(line), weaponVol, 0f, 2f); if (uiVol != BDArmorySettings.BDARMORY_WEAPONS_VOLUME && OnVolumeChange != null) { OnVolumeChange(); } BDArmorySettings.BDARMORY_WEAPONS_VOLUME = weaponVol; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) + { + BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS = GUI.Toggle(SLineThirdRect(++line, 0, 2), 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 ? StringUtils.Localize("#LOC_BDArmory_Settings_TraceVesselsManualStop") : StringUtils.Localize("#LOC_BDArmory_Settings_TraceVesselsManualStart"))) + { + if (LoadedVesselSwitcher.Instance.vesselTraceEnabled) + { LoadedVesselSwitcher.Instance.StopVesselTracing(); } + else + { LoadedVesselSwitcher.Instance.StartVesselTracing(); } + } + } + BDArmorySettings.AUTO_LOG_TIME_SYNC = GUI.Toggle(SLineThirdRect(++line, 0, 2), BDArmorySettings.AUTO_LOG_TIME_SYNC, StringUtils.Localize("#LOC_BDArmory_Settings_AutoLogTimeSync")); + if (GUI.Button(SLineThirdRect(line, 2), StringUtils.Localize(logTimeSyncEnabled ? "#LOC_BDArmory_Settings_LogTimeSyncStop" : "#LOC_BDArmory_Settings_LogTimeSyncStart"))) SetTimeSyncLogging(!logTimeSyncEnabled); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_LogTimeSyncInterval")}: {BDArmorySettings.LOG_TIME_SYNC_INTERVAL:G2}s", leftLabel); + BDArmorySettings.LOG_TIME_SYNC_INTERVAL = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.LOG_TIME_SYNC_INTERVAL, Time.fixedDeltaTime, 1f), Time.fixedDeltaTime); + } + + line += 0.5f; + } + + 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; + } + if (BDArmorySettings.COMPETITION_SETTINGS_TOGGLE) + { + line += 0.2f; + + BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START, StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart")); + + 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), $"{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), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay")}: ({(BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY > 60 ? StringUtils.Localize("#LOC_BDArmory_Generic_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), $"{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.ADVANCED_USER_SETTINGS) + { + 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), $"{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 + string startNowAfter; + if (BDArmorySettings.COMPETITION_START_NOW_AFTER > 10) + { + startNowAfter = StringUtils.Localize("#LOC_BDArmory_Generic_Off"); + } + else if (BDArmorySettings.COMPETITION_START_NOW_AFTER > 5) + { + startNowAfter = $"{BDArmorySettings.COMPETITION_START_NOW_AFTER - 5}mins"; + } + else + { + startNowAfter = $"{BDArmorySettings.COMPETITION_START_NOW_AFTER * 10}s"; + } + 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), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionKillTimer")}: (" + (BDArmorySettings.COMPETITION_KILL_TIMER > 0 ? (BDArmorySettings.COMPETITION_KILL_TIMER + "s") : StringUtils.Localize("#LOC_BDArmory_Generic_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), 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, textFieldStyle); + 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, textFieldStyle); + 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 (float.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 (float.TryParse(compIntraTeamSeparationPerMember, out float cIntraPerMember) && BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_PER_MEMBER != cIntraPerMember) + { + 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, textFieldStyle); + if (float.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 ? 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; + } + if (BDArmorySettings.GM_SETTINGS_TOGGLE) { - if (GUI.Button(SLeftRect(++line), "Run DEBUG checks"))// Run DEBUG checks + line += 0.2f; + + { // Killer GM Max Altitude + string killerGMMaxAltitudeText; + if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH > 54f) killerGMMaxAltitudeText = "Never"; + 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), $"{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 + string killerGMMinAltitudeText; + if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -38f) killerGMMinAltitudeText = "Never"; // Never + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -28f) killerGMMinAltitudeText = Mathf.RoundToInt(BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW + 28f) + "km"; // -10km — -1km @ 1km + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -19f) killerGMMinAltitudeText = Mathf.RoundToInt((BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW + 19f) * 100f) + "m"; // -900m — -100m @ 100m + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 0f) killerGMMinAltitudeText = Mathf.RoundToInt(BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW * 5f) + "m"; // -95m — -5m @ 5m + 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), $"{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) { - BDACompetitionMode.Instance.RunDebugChecks(); + 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), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionKillerGMFrequency")}: ({(BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? StringUtils.Localize("#LOC_BDArmory_Generic_Off") : BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY + "s")}, {(BDACompetitionMode.Instance != null && BDACompetitionMode.Instance.killerGMenabled ? StringUtils.Localize("#LOC_BDArmory_Generic_On") : StringUtils.Localize("#LOC_BDArmory_Generic_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). } + // Craft autokill criteria + BDArmorySettings.COMPETITION_GM_KILL_WEAPON = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_GM_KILL_WEAPON, StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionGMWeaponKill")); + BDArmorySettings.COMPETITION_GM_KILL_ENGINE = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_GM_KILL_ENGINE, StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionGMEngineKill")); + BDArmorySettings.COMPETITION_GM_KILL_DISABLED = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_GM_KILL_DISABLED, StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionGMDisableKill")); + string GMKillHP; + if (BDArmorySettings.COMPETITION_GM_KILL_HP <= 0f) GMKillHP = StringUtils.Localize("#LOC_BDArmory_Generic_Off"); + else GMKillHP = $"<{Mathf.RoundToInt((BDArmorySettings.COMPETITION_GM_KILL_HP))}%"; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionGMHPKill")}: {GMKillHP}", leftLabel); + BDArmorySettings.COMPETITION_GM_KILL_HP = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_GM_KILL_HP, 0, 99)); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionGMKillDelay")}: {(BDArmorySettings.COMPETITION_GM_KILL_TIME > -1 ? (BDArmorySettings.COMPETITION_GM_KILL_TIME + "s") : StringUtils.Localize("#LOC_BDArmory_Generic_Off"))}", leftLabel); + BDArmorySettings.COMPETITION_GM_KILL_TIME = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_GM_KILL_TIME, -1, 60)); + + 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" : StringUtils.Localize("#LOC_BDArmory_Generic_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; } - } - //competition mode - if (HighLogic.LoadedSceneIsFlight) - { - ++line; - if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.REMOTE_LOGGING_VISIBLE) + if (BDArmorySettings.REMOTE_LOGGING_VISIBLE) { - bool remoteLoggingEnabled = BDArmorySettings.REMOTE_LOGGING_ENABLED; - BDArmorySettings.REMOTE_LOGGING_ENABLED = GUI.Toggle(SLeftRect(++line), remoteLoggingEnabled, Localizer.Format("#LOC_BDArmory_Settings_RemoteLogging"));//"Remote Logging" - if (remoteLoggingEnabled) + 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), BDArmorySettings.COMPETITION_HASH); + 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, textFieldStyle); + 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)); } } else + { BDArmorySettings.REMOTE_LOGGING_ENABLED = false; + } + + line += 0.5f; + } + + if (HighLogic.LoadedSceneIsFlight && BDACompetitionMode.Instance != null) + { + line += 0.5f; - bool origPm = BDArmorySettings.PEACE_MODE; - BDArmorySettings.PEACE_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.PEACE_MODE, Localizer.Format("#LOC_BDArmory_Settings_PeaceMode"));//"Peace Mode" - if (BDArmorySettings.PEACE_MODE && !origPm) + GUI.Label(SLineRect(++line), $"=== {StringUtils.Localize("#LOC_BDArmory_Settings_DogfightCompetition")} ===", centerLabel);//Dogfight Competition + if (BDACompetitionMode.Instance.competitionIsActive) { - BDATargetManager.ClearDatabase(); - if (OnPeaceEnabled != null) + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_StopCompetition"))) // Stop competition. { - OnPeaceEnabled(); + BDACompetitionMode.Instance.StopCompetition(); } } - - ++line; - GUI.Label(SLineRect(++line), "= " + Localizer.Format("#LOC_BDArmory_Settings_DogfightCompetition") + " =", centerLabel);//Dogfight Competition - if (!BDACompetitionMode.Instance.competitionStarting) + else if (BDACompetitionMode.Instance.competitionStarting) { - GUI.Label(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_CompetitionDistance"));//"Competition Distance" - float cDist; - compDistGui = GUI.TextField(SRightRect(line), compDistGui); - if (Single.TryParse(compDistGui, out cDist)) + 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" { - BDArmorySettings.COMPETITION_DISTANCE = (int)cDist; + BDACompetitionMode.Instance.StopCompetition(); } - - if (GUI.Button(SLeftButtonRect(++line), "Reset Scores")) // resets competition scores + if (GUI.Button(SRightButtonRect(line), StringUtils.Localize("#LOC_BDArmory_Settings_StartCompetitionNow"))) // Start competition NOW button. { - BDACompetitionMode.Instance.ResetCompetitionScores(); + BDACompetitionMode.Instance.StartCompetitionNow(); + if (BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START) CloseSettingsWindow(); } - - if (GUI.Button(SRightButtonRect(line), Localizer.Format("#LOC_BDArmory_Settings_StartCompetition")))//"Start Competition" + } + else + { + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) { - - BDArmorySettings.COMPETITION_DISTANCE = Mathf.Max(BDArmorySettings.COMPETITION_DISTANCE, 0); - compDistGui = BDArmorySettings.COMPETITION_DISTANCE.ToString(); - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - SaveConfig(); - windowSettingsEnabled = false; + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_RemoteSync"))) // Run Via Remote Orchestration + { + string vesselPath = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn")); + if (!Directory.Exists(vesselPath)) Directory.CreateDirectory(vesselPath); + BDAScoreService.Instance.Configure(vesselPath, BDArmorySettings.COMPETITION_HASH); + if (BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START) CloseSettingsWindow(); + } } - if (BDArmorySettings.RUNWAY_PROJECT) + else { - if (GUI.Button(SLeftButtonRect(++line), "Rapid Deploy")) + string startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_StartCompetition"); + if (BDArmorySettings.RUNWAY_PROJECT) { - BDACompetitionMode.Instance.StartRapidDeployment(0); - SaveConfig(); - windowSettingsEnabled = false; + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + case 33: + startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_StartRapidDeployment"); + break; + case 44: + startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_LowGravDeployment"); + break; + case 53: // FIXME temporary index, to be assigned later + startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_StartOrbitalDeployment"); + break; + } } - if (BDArmorySettings.REMOTE_LOGGING_ENABLED && GUI.Button(SRightRect(line), "Sync Remote")) + if (GUI.Button(SLineRect(++line), startCompetitionText))//"Start Competition" { - string vesselPath = Environment.CurrentDirectory + $"/AutoSpawn"; - if (!System.IO.Directory.Exists(vesselPath)) + + BDArmorySettings.COMPETITION_DISTANCE = Mathf.Max(BDArmorySettings.COMPETITION_DISTANCE, 0); + compDistGui = BDArmorySettings.COMPETITION_DISTANCE.ToString(); + if (BDArmorySettings.RUNWAY_PROJECT) { - System.IO.Directory.CreateDirectory(vesselPath); + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + case 33: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 44: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 53: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 67: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + case 77: + BDACompetitionMode.Instance.StartRapidDeployment(0); + break; + default: + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + break; + } } - BDAScoreService.Instance.Configure(vesselPath, BDArmorySettings.COMPETITION_HASH); - SaveConfig(); - windowSettingsEnabled = false; + else + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + if (BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START) CloseSettingsWindow(); } } } - else - { - 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" - { - BDACompetitionMode.Instance.StopCompetition(); - } - if (GUI.Button(SRightButtonRect(line), Localizer.Format("#LOC_BDArmory_Settings_StartCompetitionNow"))) // Start competition NOW button. - { - BDACompetitionMode.Instance.StartCompetitionNow(); - SaveConfig(); - windowSettingsEnabled = false; - } - } } ++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; - if (!BDKeyBinder.current && GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Generic_SaveandClose")))//"Save and Close" + line += 0.5f; + if (!BDKeyBinder.current) { - SaveConfig(); - windowSettingsEnabled = false; + if (GUI.Button(SQuarterRect(++line, 0), StringUtils.Localize("#LOC_BDArmory_Generic_Reload"))) // Reload + { + LoadConfig(); + } + if (GUI.Button(SQuarterRect(line, 1, 3), StringUtils.Localize("#LOC_BDArmory_Generic_SaveandClose")))//"Save and Close" + { + SaveConfig(); + windowSettingsEnabled = false; + } } line += 1.5f; // Bottom internal margin settingsHeight = (line * settingsLineHeight); WindowRectSettings.height = settingsHeight; - BDGUIUtils.RepositionWindow(ref WindowRectSettings); - BDGUIUtils.UseMouseEventInRect(WindowRectSettings); + if (!scalingUI) GUIUtils.RepositionWindow(ref WindowRectSettings); + GUIUtils.UseMouseEventInRect(WindowRectSettings); + } + + void CloseSettingsWindow() + { + SaveConfig(); + windowSettingsEnabled = false; } internal static void ResizeRwrWindow(float rwrScale) @@ -1853,47 +4317,61 @@ 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 + 9) * settingsLineHeight : settingsHeight); + Rect scrollerRect = new Rect(0, 0, settingsWidth - GUI.skin.verticalScrollbar.fixedWidth - 1, inputFields != null ? (inputFields.Length + 2 * 9) * settingsLineHeight : settingsHeight); _displayViewerPosition = GUI.BeginScrollView(viewRect, _displayViewerPosition, scrollerRect, false, true); - GUI.Label(SLineRect(line), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_Weapons") + " -", centerLabel);//Weapons - line++; +#if DEBUG + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_Settings_DebugSettingsToggle")} -", centerLabel); //Debugging + InputSettingsList("DEBUG_", ref inputID, ref line); + ++line; +#endif + + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_GUI")} -", centerLabel); //GUI + InputSettingsList("GUI_", ref inputID, ref line); + ++line; + + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_Weapons")} -", centerLabel);//Weapons InputSettingsList("WEAP_", ref inputID, ref line); - line++; + ++line; - GUI.Label(SLineRect(line), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_TargetingPod") + " -", centerLabel);//Targeting Pod - line++; + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_TargetingPod")} -", centerLabel);//Targeting Pod InputSettingsList("TGP_", ref inputID, ref line); - line++; + ++line; - GUI.Label(SLineRect(line), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_Radar") + " -", centerLabel);//Radar - line++; + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_Radar")} -", centerLabel);//Radar InputSettingsList("RADAR_", ref inputID, ref line); - line++; + ++line; - GUI.Label(SLineRect(line), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_VesselSwitcher") + " -", centerLabel);//Vessel Switcher - line++; + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_VesselSwitcher")} -", centerLabel);//Vessel Switcher InputSettingsList("VS_", ref inputID, ref line); - line++; + ++line; - GUI.Label(SLineRect(line), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_Tournament") + " -", centerLabel);//Tournament - line++; + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_Tournament")} -", centerLabel);//Tournament InputSettingsList("TOURNAMENT_", ref inputID, ref line); + ++line; + + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_TimeScaling")} -", centerLabel);//Time Scaling + InputSettingsList("TIME_", ref inputID, ref line); + ++line; + + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_TemporarilyShowMouse")} -", centerLabel);//Temporarily Show Mouse + InputSettingsList("SHOW_MOUSE", ref inputID, ref line); + ++line; GUI.EndScrollView(); line = settingsHeight / settingsLineHeight; 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) @@ -1914,7 +4392,7 @@ void InputSettingsList(string prefix, ref int id, ref float line) void InputSettingsLine(string fieldName, int id, ref float line) { GUI.Box(SLineRect(line), GUIContent.none); - string label = String.Empty; + string label = string.Empty; if (BDKeyBinder.IsRecordingID(id)) { string recordedInput; @@ -1925,7 +4403,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 { @@ -1934,32 +4412,31 @@ void InputSettingsLine(string fieldName, int id, ref float line) { inputInfo = (BDInputInfo)typeof(BDInputSettingsFields).GetField(fieldName).GetValue(null); } - catch (NullReferenceException) + catch (NullReferenceException e) { - Debug.Log("[BDArmory]: Reflection failed to find input info of field: " + fieldName); + Debug.LogWarning("[BDArmory.BDArmorySetup]: Reflection failed to find input info of field: " + fieldName + ": " + e.Message); editKeys = false; return; } 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) @@ -1974,34 +4451,73 @@ Rect SClearKeyRect(float line) void HideGameUI() { + if (GAME_UI_ENABLED) + { + // Switch visible/hidden window rects + _WindowRectScoresUIVisible = WindowRectScores; + if (_WindowRectScoresUIHidden != default) WindowRectScores = _WindowRectScoresUIHidden; + if (ScoreWindow.Instance.autoResizingWindow) WindowRectScores.height = _WindowRectScoresUIVisible.height; + _WindowRectVesselSwitcherUIVisible = WindowRectVesselSwitcher; + if (_WindowRectVesselSwitcherUIHidden != default) WindowRectVesselSwitcher = _WindowRectVesselSwitcherUIHidden; + } + GAME_UI_ENABLED = false; + BDACompetitionMode.Instance.UpdateGUIElements(); + UpdateCursorState(); } void ShowGameUI() { + if (!GAME_UI_ENABLED) + { + // Switch visible/hidden window rects + _WindowRectScoresUIHidden = WindowRectScores; + if (_WindowRectScoresUIVisible != default) WindowRectScores = _WindowRectScoresUIVisible; + if (ScoreWindow.Instance.autoResizingWindow) WindowRectScores.height = _WindowRectScoresUIHidden.height; + _WindowRectVesselSwitcherUIHidden = WindowRectVesselSwitcher; + if (_WindowRectVesselSwitcherUIVisible != default) WindowRectVesselSwitcher = _WindowRectVesselSwitcherUIVisible; + } + GAME_UI_ENABLED = true; + BDACompetitionMode.Instance.UpdateGUIElements(); + UpdateCursorState(); } internal void OnDestroy() { - if (maySavethisInstance) + if (saveWindowPosition) { + if (GAME_UI_ENABLED) + { + _WindowRectScoresUIVisible = WindowRectScores; + _WindowRectVesselSwitcherUIVisible = WindowRectVesselSwitcher; + } + else + { + _WindowRectScoresUIHidden = WindowRectScores; + _WindowRectVesselSwitcherUIHidden = WindowRectVesselSwitcher; + } BDAWindowSettingsField.Save(); - SaveConfig(); } + if (windowSettingsEnabled || showVesselSpawnerGUI) + SaveConfig(); + SetTimeSyncLogging(false); GameEvents.onHideUI.Remove(HideGameUI); GameEvents.onShowUI.Remove(ShowGameUI); GameEvents.onVesselGoOffRails.Remove(OnVesselGoOffRails); GameEvents.OnGameSettingsApplied.Remove(SaveVolumeSettings); GameEvents.onVesselChange.Remove(VesselChange); + GameEvents.onGameSceneSwitchRequested.Remove(OnGameSceneSwitchRequested); + GameEvents.onGameStateSave.Remove(OnGameStateSave); + GameEvents.onGameStateSaved.Remove(OnGameStateSaved); } void OnVesselGoOffRails(Vessel v) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { - Debug.Log("[BDArmory]: Loaded vessel: " + v.vesselName + ", Velocity: " + v.Velocity() + ", packed: " + v.packed); + Debug.Log("[BDArmory.BDArmorySetup]: Loaded vessel: " + v.vesselName + ", Velocity: " + v.Velocity() + ", packed: " + v.packed); //v.SetWorldVelocity(Vector3d.zero); } } @@ -2012,5 +4528,762 @@ public void SaveVolumeSettings() SeismicChargeFX.originalMusicVolume = GameSettings.MUSIC_VOLUME; SeismicChargeFX.originalAmbienceVolume = GameSettings.AMBIENCE_VOLUME; } + + #region TimeSyncLogging + public bool logTimeSyncEnabled = false; + readonly List<(float, float)> timeSyncLog = []; // List of time-sync timestamps. + /// + /// Enable time-sync logging. + /// Game timestamps are made during the normal LateUpdate timing stage. + /// + /// Enable or disable time-sync logging. + /// A tag to use when writing the file when disabling time-sync logging. + public void SetTimeSyncLogging(bool enable, string tag = null) + { + if (enable) + { + if (!logTimeSyncEnabled) TimingManager.LateUpdateAdd(TimingManager.TimingStage.Normal, LogTimeSync); + } + else + { + TimingManager.LateUpdateRemove(TimingManager.TimingStage.Normal, LogTimeSync); + DumpTimeSyncLog(tag); + timeSyncLog.Clear(); + } + logTimeSyncEnabled = enable; + } + /// + /// Register a time-sync timestamp if the time-sync interval has passed (approx). + /// + void LogTimeSync() + { + if (Time.time - timeSyncLog.LastOrDefault().Item1 < BDArmorySettings.LOG_TIME_SYNC_INTERVAL - Time.fixedDeltaTime / 2f) return; // Round to the nearest physics frame. + timeSyncLog.Add((Time.time, Time.realtimeSinceStartup)); + } + /// + /// Dump the time-sync timestamps to a compressed log. + /// + /// The name of the file to dump to (without extension). + void DumpTimeSyncLog(string tag) + { + if (timeSyncLog.Count == 0) return; + var folder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs", "TimeSync")); + if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); + using FileStream fileStream = File.Create(Path.Combine(folder, string.IsNullOrEmpty(tag) ? "time-sync.csv.gz" : $"{tag}.csv.gz")); + using GZipStream gzStream = new(fileStream, CompressionMode.Compress); + var startTime = timeSyncLog.FirstOrDefault(); + var tsLogBytes = Encoding.ASCII.GetBytes("Game-Time,Real-Time\n" + string.Join("\n", timeSyncLog.Select(ts => $"{ts.Item1 - startTime.Item1:F2},{ts.Item2 - startTime.Item2:F2}"))); + gzStream.Write(tsLogBytes, 0, tsLogBytes.Length); + } + #endregion +#if DEBUG + // static int PROF_N_pow = 3, PROF_n_pow = 4; + static int PROF_N = 1000, PROF_n = 10000; + IEnumerator TestVesselPositionTiming() + { + var wait = new WaitForFixedUpdate(); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.ObscenelyEarly, ObscenelyEarly); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Early, Early); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Precalc, Precalc); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Earlyish, Earlyish); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Normal, Normal); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.FashionablyLate, FashionablyLate); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.FlightIntegrator, FlightIntegrator); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Late, Late); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.BetterLateThanNever, BetterLateThanNever); + yield return wait; Debug.Log($"DEBUG {Time.time} WaitForFixedUpdate"); + yield return wait; Debug.Log($"DEBUG {Time.time} WaitForFixedUpdate"); + yield return wait; Debug.Log($"DEBUG {Time.time} WaitForFixedUpdate"); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.ObscenelyEarly, ObscenelyEarly); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Early, Early); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Precalc, Precalc); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Earlyish, Earlyish); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Normal, Normal); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.FashionablyLate, FashionablyLate); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.FlightIntegrator, FlightIntegrator); + 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.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Early() { Debug.Log($"DEBUG {Time.time} Early, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Precalc() { Debug.Log($"DEBUG {Time.time} Precalc, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Earlyish() { Debug.Log($"DEBUG {Time.time} Earlyish, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Normal() { Debug.Log($"DEBUG {Time.time} Normal, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void FashionablyLate() { Debug.Log($"DEBUG {Time.time} FashionablyLate, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void FlightIntegrator() { Debug.Log($"DEBUG {Time.time} FlightIntegrator, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Late() { Debug.Log($"DEBUG {Time.time} Late, active vessel position: {FlightGlobals.ActiveVessel.CoM.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void BetterLateThanNever() { Debug.Log($"DEBUG {Time.time} BetterLateThanNever, active vessel position: {FlightGlobals.ActiveVessel.CoM.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"); + } + + int TestNumericalMethodsIC = 0; + IEnumerator TestNumericalMethods(float duration, int steps) + { + var vessel = FlightGlobals.ActiveVessel; + var wait = new WaitForFixedUpdate(); + yield return wait; // Wait for the next fixedUpdate to synchronise with the physics. + if (vessel == null) yield break; + var dt = duration / steps; + if (dt < Time.fixedDeltaTime) + { + dt = Time.fixedDeltaTime; + steps = (int)(duration / dt); + } + Vector3 x = vessel.transform.position, x0 = x, x1 = x, x2 = x; + Vector3 a0 = vessel.acceleration - FlightGlobals.getGeeForceAtPosition(x0), a1 = a0, a2 = a0; // Separate acceleration into local and gravitational. + // Note: acceleration is an averaged (or derived) value, we should use acceleration_immediate instead, but most of the rest of the code uses acceleration. + Vector3 v = vessel.rb_velocity + BDKrakensbane.FrameVelocityV3f; + switch (TestNumericalMethodsIC) + { + case 0: // No correction + break; + case 1: + v += 0.5f * Time.fixedDeltaTime * vessel.acceleration; // Unity integration correction for all acceleration + break; + case 2: + v += 0.5f * Time.fixedDeltaTime * a0; // Unity integration correction for local acceleration only — this seems to give the best results for leap-frog + break; + case 3: + v += 0.5f * Time.fixedDeltaTime * FlightGlobals.getGeeForceAtPosition(x0); // Unity integration correction for gravity only + break; + } + Vector3 v0 = v, v1 = v, v2 = v; + Debug.Log($"DEBUG {Time.time} IC: {TestNumericalMethodsIC}, Initial acceleration: {a0}m/s^2 constant local + {(Vector3)FlightGlobals.getGeeForceAtPosition(x0)}m/s^2 gravity"); + for (int i = 0; i < steps; ++i) + { + // Forward Euler (1st order, not symplectic) + var g = FlightGlobals.getGeeForceAtPosition(x0); // Get the gravity at the current time + x0 += dt * v0; // Update position based on velocity at the current time + v0 += dt * (a0 + g); // Update the velocity based on the potential at the current time + + // Semi-implicit Euler (1st order, symplectic) + v1 += dt * (a1 + FlightGlobals.getGeeForceAtPosition(x1)); // Update velocity based on the potential at the current time + x1 += dt * v1; // Update position based on velocity at the future time + + // Leap-frog (2nd order, symplectic) + // Note: combining the second half-update to v2 with the first one of the next step into a single evaluation is where the name of the method comes from. + v2 += 0.5f * dt * (a2 + FlightGlobals.getGeeForceAtPosition(x2)); // Update velocity half a step based on the potential at the current time + x2 += dt * v2; // Update the position based on the velocity half-way between the current and future times + v2 += 0.5f * dt * (a2 + FlightGlobals.getGeeForceAtPosition(x2)); // Update velocity another half a step based on the potential at the future time + } + + var tic = Time.time; + while (Time.time - tic < duration - Time.fixedDeltaTime / 2) + { + yield return wait; + if (vessel == null) + { + Debug.Log($"DEBUG Active vessel disappeared, aborting."); + yield break; + } + + if (BDKrakensbane.IsActive) // Correct for Krakensbane + { + x0 -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + x1 -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + x2 -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + } + x = vessel.transform.position; + Debug.Log($"DEBUG {Time.time}: After {duration}s ({steps}*{dt}={steps * dt}), Actual x = {x}, Forward Euler predicts x = {x0} (Δ = {(x - x0).magnitude}), Semi-implicit Euler predicts x = {x1} (Δ = {(x - x1).magnitude}), Leap-frog predicts x = {x2} (Δ = {(x - x2).magnitude})"); + } + + public static void TestActiveController() + { + Vessel vessel = FlightGlobals.ActiveVessel; + 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"); + ActiveController activeController = null; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { activeController = vessel.ActiveController(); } }; + Debug.Log($"DEBUG vessel.ActiveController() took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {activeController} with hash {(uint)activeController.GetHashCode()}"); + MissileFire wm = null; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { wm = vessel.ActiveController().WM; } }; + Debug.Log($"DEBUG vessel.ActiveController().WM took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {wm} with hash {(uint)wm.GetHashCode()}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { wm = VesselModuleRegistry.GetMissileFire(vessel); } }; + Debug.Log($"DEBUG VesselModuleRegistry.GetMissileFire(vessel) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {wm} with hash {(uint)wm.GetHashCode()}"); + bool same = false; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { same = wm != null && wm.IsPrimaryWM && wm.vessel == vessel; } }; + Debug.Log($"DEBUG wm != null && wm.IsPrimaryWM && wm.vessel == vessel took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {same}"); + BDModulePilotAI pilotAI = null; + BDModuleSurfaceAI surfaceAI = null; + BDModuleVTOLAI vtolAI = null; + BDModuleOrbitalAI orbitalAI = null; + IBDAIControl AI = vessel.ActiveController().AI; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => + { + for (int i = 0; i < PROF_n; ++i) + { + pilotAI = null; surfaceAI = null; vtolAI = null; orbitalAI = null; + if (AI != null && AI.pilotEnabled) + { + switch (AI.aiType) + { + case AIType.PilotAI: pilotAI = AI as BDModulePilotAI; break; + case AIType.SurfaceAI: surfaceAI = AI as BDModuleSurfaceAI; break; + case AIType.VTOLAI: vtolAI = AI as BDModuleVTOLAI; break; + case AIType.OrbitalAI: orbitalAI = AI as BDModuleOrbitalAI; break; + } + } + } + }; + Debug.Log($"DEBUG Multiple AI type selection took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {pilotAI}, {surfaceAI}, {vtolAI}, {orbitalAI}"); + } + + public static void TestAbs() + { + Vessel vessel = FlightGlobals.ActiveVessel; + Vector3 pos = vessel.CoM; + 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"); + float x = 1.234f, y = -1.234f, zx = 0, zy = 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] float Abs(float x) { return x < 0 ? -x : x; } + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { zx = Mathf.Abs(x); zy = Mathf.Abs(y); } }; + Debug.Log($"DEBUG Mathf.Abs took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {zx}, {zy}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { zx = Math.Abs(x); zy = Math.Abs(y); } }; + Debug.Log($"DEBUG Math.Abs(x) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {zx}, {zy}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { zx = (float)Math.Abs((double)x); zy = (float)Math.Abs((double)y); } }; + Debug.Log($"DEBUG (float)Math.Abs((double)x) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {zx}, {zy}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { zx = Abs(x); zy = Abs(y); } }; + Debug.Log($"DEBUG Abs(x) {{ return x < 0 ? -x : x; }} took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {zx}, {zy}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { zx = x < 0 ? -x : x; zy = y < 0 ? -y : y; } }; + Debug.Log($"DEBUG inline x<0?-x:x took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {zx}, {zy}"); + } + + // public static void TestAngle() + // { + // Vector3 v = UnityEngine.Random.onUnitSphere, v2=Vector3.zero; + // var ax = Vector3.Cross(Vector3.up, v); + // foreach (var angle in new List { 0, 1e-8f, 1e-7f, 1e-6f, 1e-5f, 1e-4f, 1e-3f, 1e-2f, 2e-2f, 3e-2f, 4e-2f, 5e-2f, 1e-1f }) + // { + // v2 = Quaternion.AngleAxis(angle, ax) * v; + // Debug.Log($"DEBUG angle from v={v} to v rotated by {angle} is {Vector3.Angle(v, v2)} vs {VectorUtils.Angle(v, v2)} vs {(float)Vector3d.Angle(v, v2)}"); + // } + // 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"); + // float a = 0; + // var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { a = Vector3.Angle(v,v2); } }; + // Debug.Log($"DEBUG Vector3.Angle took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {a}"); + // a = 0; + // func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { a = VectorUtils.Angle(v,v2); } }; + // Debug.Log($"DEBUG VectorUtils.Angle took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {a}"); + // a = 0; + // func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { a = (float)Vector3d.Angle(v,v2); } }; + // Debug.Log($"DEBUG (float)Vector3d.Angle took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {a}"); + // } + + public static void TestUp() + { + Vessel vessel = FlightGlobals.ActiveVessel; + Vector3 pos = vessel.CoM; + 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"); + Vector3 up = default; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { up = vessel.upAxis; } }; + Debug.Log($"DEBUG vessel.upAxis took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {(Vector3d)up}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { up = VectorUtils.GetUpDirection(pos); } }; + Debug.Log($"DEBUG GetUpDirection took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {(Vector3d)up}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { up = vessel.up; } }; + Debug.Log($"DEBUG vessel.up took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {(Vector3d)up}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { up = vessel.transform.up; } }; + Debug.Log($"DEBUG vessel.transform.up took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {(Vector3d)up}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { up = -FlightGlobals.getGeeForceAtPosition(pos).normalized; } }; + Debug.Log($"DEBUG -getGeeForceAtPosition.normalized took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {(Vector3d)up}"); + } + + public static void TestInOnUnitSphere() + { + 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"); + Vector3 point = default; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { point = UnityEngine.Random.onUnitSphere; } }; + Debug.Log($"DEBUG onUnitSphere took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {point}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { point = UnityEngine.Random.insideUnitSphere; } }; + Debug.Log($"DEBUG insideUnitSphere took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {point}"); + } + + public static void TestMaxRelSpeed() + { + 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"); + var currentPosition = FlightGlobals.ActiveVessel.transform.position; + var currentVelocity = FlightGlobals.ActiveVessel.rb_velocity + BDKrakensbane.FrameVelocityV3f; + float maxRelSpeed = 0; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { maxRelSpeed = BDAMath.Sqrt((float)FlightGlobals.Vessels.Where(v => v != null && v.loaded).Max(v => (v.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity).sqrMagnitude)); } }; + Debug.Log($"DEBUG Without Dot took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {maxRelSpeed}"); + maxRelSpeed = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { maxRelSpeed = BDAMath.Sqrt((float)FlightGlobals.Vessels.Where(v => v != null && v.loaded && Vector3.Dot(v.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity, v.transform.position - currentPosition) < 0).Select(v => (v.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity).sqrMagnitude).DefaultIfEmpty(0).Max()); } }; + Debug.Log($"DEBUG With Dot took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {maxRelSpeed}"); + maxRelSpeed = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => + { + for (int i = 0; i < PROF_n; ++i) + { + float maxRelSpeedSqr = 0, relVelSqr; + Vector3 relVel; + using (var v = FlightGlobals.Vessels.GetEnumerator()) + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded) continue; + relVel = v.Current.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity; + // if (Vector3.Dot(relVel, v.Current.transform.position - currentPosition) >= 0) continue; + relVelSqr = relVel.sqrMagnitude; + if (relVelSqr > maxRelSpeedSqr) maxRelSpeedSqr = relVelSqr; + } + maxRelSpeed = BDAMath.Sqrt(maxRelSpeedSqr); + } + }; + Debug.Log($"DEBUG Explicit without Dot took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {maxRelSpeed}"); + maxRelSpeed = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => + { + for (int i = 0; i < PROF_n; ++i) + { + float maxRelSpeedSqr = 0, relVelSqr; + Vector3 relVel; + using (var v = FlightGlobals.Vessels.GetEnumerator()) + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded) continue; + relVel = v.Current.rb_velocity + BDKrakensbane.FrameVelocityV3f - currentVelocity; + if (Vector3.Dot(relVel, v.Current.transform.position - currentPosition) >= 0) continue; + relVelSqr = relVel.sqrMagnitude; + if (relVelSqr > maxRelSpeedSqr) maxRelSpeedSqr = relVelSqr; + } + maxRelSpeed = BDAMath.Sqrt(maxRelSpeedSqr); + } + }; + Debug.Log($"DEBUG Explicit with Dot took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {maxRelSpeed}"); + } + + public static void TestSqrVsSqr() + { + 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"); + float sqr = 0, value = 3.14159f; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { sqr = value.Sqr(); } }; + Debug.Log($"DEBUG value.Sqr() took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {sqr}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { sqr = value * value; } }; + Debug.Log($"DEBUG value * value took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {sqr}"); + Vector3 a = UnityEngine.Random.insideUnitSphere, b = UnityEngine.Random.insideUnitSphere; + float distance = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { distance = Vector3.Distance(a, b); } }; + Debug.Log($"DEBUG Vector3.Distance took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {distance}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { distance = (a - b).magnitude; } }; + Debug.Log($"DEBUG magnitude took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {distance}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { distance = (a - b).sqrMagnitude; } }; + Debug.Log($"DEBUG sqrMagnitude took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {distance}"); + bool lessThan = false; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { lessThan = Vector3.Distance(a, b) < 0.5f; } }; + Debug.Log($"DEBUG Vector3.Distance < v took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {distance}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { lessThan = (a - b).sqrMagnitude < 0.5f * 0.5f; } }; + Debug.Log($"DEBUG sqrMagnitude < v*v took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {distance}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { lessThan = (a - b).sqrMagnitude < 0.5f.Sqr(); } }; + Debug.Log($"DEBUG sqrMagnitude < v.Sqr() took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {distance}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { lessThan = a.CloserToThan(b, 0.5f); } }; + Debug.Log($"DEBUG a.CloserToThan(b,f) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {lessThan}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { lessThan = a.FurtherFromThan(b, 0.5f); } }; + Debug.Log($"DEBUG a.FurtherFromThan(b,f) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {lessThan}"); + } + + public static void TestOrderOfOperations() + { + 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"); + float x = UnityEngine.Random.Range(-1f, 1f); + Vector3 X = UnityEngine.Random.insideUnitSphere; + Vector3 result = default; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = x * X; } }; + Debug.Log($"DEBUG x*X took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = X * x; } }; + Debug.Log($"DEBUG X*x took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = x * x * X; } }; + Debug.Log($"DEBUG x*x*X took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = X * x * x; } }; + Debug.Log($"DEBUG X*x*x took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = x * X * x; } }; + Debug.Log($"DEBUG x*X*x took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = x * x * x * X; } }; + Debug.Log($"DEBUG x*x*x*X took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = X * x * x * x; } }; + Debug.Log($"DEBUG X*x*x*x took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs"); + } + + public static void TestMassVsSizePerformance() + { + 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"); + var vessel = FlightGlobals.ActiveVessel; + float mass = 0, size = 0; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { mass = vessel.GetTotalMass(); } }; + Debug.Log($"DEBUG GetTotalMass took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {mass}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { size = vessel.vesselSize.sqrMagnitude; } }; + Debug.Log($"DEBUG Size took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {size}"); + } + + public static void TestDotNormPerformance() + { + 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"); + var v1 = UnityEngine.Random.insideUnitSphere; + var v2 = UnityEngine.Random.insideUnitSphere; + float dot = 0; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { dot = Vector3.Dot(v1.normalized, v2.normalized); } }; + Debug.Log($"DEBUG Dot(v1.norm, v2.norm) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {dot}"); + dot = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { dot = v1.DotNormalized(v2); } }; + Debug.Log($"DEBUG v1.DotNorm(v2) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {dot}"); + } + + public static void TestNamePerformance() + { + 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"); + var v = FlightGlobals.ActiveVessel; + string vesselName, getName, getDisplayName; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { vesselName = v.vesselName; } }; + Debug.Log($"DEBUG vesselName took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {v.vesselName}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { getName = v.GetName(); } }; + Debug.Log($"DEBUG GetName took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {v.GetName()}"); + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { getDisplayName = v.GetDisplayName(); } }; + Debug.Log($"DEBUG GetDisplayName took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {v.GetDisplayName()}"); + } + + public static void TestRandPerformance() + { + 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"); + Vector3 result = default; + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = VectorUtils.GaussianVector3(); } }; + Debug.Log($"DEBUG VectorUtils.GaussianVector3() 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 = UnityEngine.Random.insideUnitSphere; } }; + Debug.Log($"DEBUG UnityEngine.Random.insideUnitSphere 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 = new Vector3(UnityEngine.Random.value * 2f - 1f, UnityEngine.Random.value * 2f - 1f, UnityEngine.Random.value * 2f - 1f); } }; + Debug.Log($"DEBUG new Vector3(UnityEngine.Random.value * 2f - 1f, UnityEngine.Random.value * 2f - 1f, UnityEngine.Random.value * 2f - 1f) took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {(Vector3d)result}"); + float value = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { value = UnityEngine.Random.value; } }; + Debug.Log($"DEBUG UnityEngine.Random.value took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {value}"); + value = 0; + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { value += UnityEngine.Random.insideUnitSphere.magnitude; } }; + Debug.Log($"DEBUG UnityEngine.Random.insideUnitSphere.magnitude took {ProfileFunc(func, PROF_N) / PROF_n:G3}μs to give {value / PROF_N / PROF_n}"); + } + + 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 + + /// + /// On leaving flight mode. Perform some clean-up of things that might still be active. + /// Not everything gets cleaned up by default for some reason. + /// + void OnGameSceneSwitchRequested(GameEvents.FromToAction fromTo) + { + if (windowSettingsEnabled) ToggleWindowSettings(); // Close the settings window so that the following settings changes don't propagate back into the settings window. + if (BDArmorySettings.G_LIMITS && (fromTo.from == GameScenes.EDITOR || fromTo.from == GameScenes.FLIGHT)) + { + RWPSettings.SyncWithGameSettings(restoreOverrides: true); + } + if (fromTo.from == GameScenes.FLIGHT && fromTo.to != GameScenes.FLIGHT) + { + DisableAllFXAndProjectiles(); + } + } + void OnGameStateSave(ConfigNode node) + { + if (BDArmorySettings.G_LIMITS) + { + RWPSettings.SyncWithGameSettings(restoreOverrides: true); + } + } + void OnGameStateSaved(Game game) + { + if (BDArmorySettings.G_LIMITS) + { + RWPSettings.SyncWithGameSettings(); + } + } + public static void DisableAllFXAndProjectiles() + { + SpawnUtils.DisableAllBulletsAndRockets(); + ExplosionFx.DisableAllExplosionFX(); + NukeFX.DisableAllExplosionFX(); + FXEmitter.DisableAllFX(); + CMDropper.DisableAllCMs(); + BulletHitFX.DisableAllFX(); + // FIXME Add in any other things that may otherwise continue doing stuff on scene changes, e.g., various FX. + } } } diff --git a/BDArmory/UI/BDColorPicker.cs b/BDArmory/UI/BDColorPicker.cs new file mode 100644 index 000000000..75391372d --- /dev/null +++ b/BDArmory/UI/BDColorPicker.cs @@ -0,0 +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), 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 52a8588ae..147050f2d 100644 --- a/BDArmory/UI/BDGUIComboBox.cs +++ b/BDArmory/UI/BDGUIComboBox.cs @@ -4,109 +4,125 @@ namespace BDArmory.UI { public class BDGUIComboBox { - private static bool forceToUnShow = false; - private static int useControlID = -1; - private bool isClickedComboButton = false; - private int selectedItemIndex = -1; - - private Rect rect; - private Rect buttonRect; - private GUIContent buttonContent; - private GUIContent[] listContent; - private GUIStyle listStyle; - private Vector2 scrollViewVector; - private float comboxbox_height; - - public BDGUIComboBox(Rect rect, Rect buttonRect, GUIContent buttonContent, GUIContent[] listContent, float combo_height, GUIStyle listStyle) + public bool IsOpen => isOpen; + public float Height => scrollViewRect.height; + + Rect buttonRect; + Rect listRect; + GUIContent buttonContent; + GUIContent[] listContent; + float maxHeight; + GUIStyle listStyle; + int columns; + bool persistant; + + 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. + /// Does the box remain open after clicking a selection + public BDGUIComboBox(Rect buttonRect, Rect listRect, GUIContent buttonContent, GUIContent[] listContent, float maxHeight, GUIStyle listStyle, int columns = 2, bool persistant = false) { - 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; + this.persistant = persistant; + UpdateContent(listContent); } + /// + /// Display the button and combo-box. + /// + /// The selected item's index. public int Show() { - if (forceToUnShow) - { - forceToUnShow = false; - isClickedComboButton = false; - } - - bool done = false; - int controlID = GUIUtility.GetControlID(FocusType.Passive); - - switch (Event.current.GetTypeForControl(controlID)) - { - case EventType.MouseUp: - { - if (isClickedComboButton) - { - done = true; - } - } - break; - } - - if (selectedItemIndex > -1) - buttonContent.text = listContent[selectedItemIndex].text; - - if (GUI.Button(buttonRect, buttonContent, BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(buttonRect, buttonContent, BDArmorySetup.BDGuiSkin.button)) // Button { - if (useControlID == -1) - { - useControlID = controlID; - isClickedComboButton = false; - } - - if (useControlID != controlID) - { - forceToUnShow = true; - useControlID = controlID; - } - isClickedComboButton = true; + isClickedComboButton = !isClickedComboButton; } + isOpen = isClickedComboButton; // Flag indicating if the selection grid open this frame. - if (isClickedComboButton) + if (isClickedComboButton) // Selection grid { - 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); - - int newSelectedItemIndex = GUI.SelectionGrid(listRect, selectedItemIndex, listContent, 2, listStyle); - if (newSelectedItemIndex != selectedItemIndex) + 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. { - selectedItemIndex = newSelectedItemIndex; - done = true; + if (selectedItemIndex > -1) buttonContent.text = listContent[selectedItemIndex].text; + if (!persistant) isClickedComboButton = false; } - GUI.EndScrollView(); } - if (done) - isClickedComboButton = false; + return selectedItemIndex; + } + /// + /// Externally set the selected item index. + /// + /// + public int SetSelectedItemIndex(int index) + { + selectedItemIndex = index; return selectedItemIndex; } - public int SelectedItemIndex + /// + /// Update internal rects when the button rect has moved. + /// + /// + public void UpdateRect(Rect updatedButtonRect) { - get - { - return selectedItemIndex; - } - set - { - selectedItemIndex = value; - } + if (updatedButtonRect == buttonRect) return; + listRect.x += updatedButtonRect.x - buttonRect.x; + listRect.y += updatedButtonRect.y - buttonRect.y; + buttonRect = updatedButtonRect; + UpdateScrollViewRect(); + } + + /// + /// Update the content of the combobox and recalculate sizes. + /// + /// + public void UpdateContent(GUIContent[] content) + { + 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 fb4886ba4..000000000 --- a/BDArmory/UI/BDGUIUtils.cs +++ /dev/null @@ -1,200 +0,0 @@ -using UnityEngine; -using BDArmory.Core; -using Random = UnityEngine.Random; - -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 (Misc.Misc.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/BDInputUtils.cs b/BDArmory/UI/BDInputUtils.cs deleted file mode 100644 index c49e5143b..000000000 --- a/BDArmory/UI/BDInputUtils.cs +++ /dev/null @@ -1,161 +0,0 @@ -using UnityEngine; - -namespace BDArmory.UI -{ - public class BDInputUtils - { - public static string GetInputString() - { - //keyCodes - string[] names = System.Enum.GetNames(typeof(KeyCode)); - int numberOfKeycodes = names.Length; - - for (int i = 0; i < numberOfKeycodes; i++) - { - string output = names[i]; - - if (output.Contains("Keypad")) - { - output = "[" + output.Substring(6).ToLower() + "]"; - } - else if (output.Contains("Alpha")) - { - output = output.Substring(5); - } - else //lower case key - { - output = output.ToLower(); - } - - //modifiers - if (output.Contains("control")) - { - output = output.Split('c')[0] + " ctrl"; - } - else if (output.Contains("alt")) - { - output = output.Split('a')[0] + " alt"; - } - else if (output.Contains("shift")) - { - output = output.Split('s')[0] + " shift"; - } - else if (output.Contains("command")) - { - output = output.Split('c')[0] + " cmd"; - } - - //special keys - else if (output == "backslash") - { - output = @"\"; - } - else if (output == "backquote") - { - output = "`"; - } - else if (output == "[period]") - { - output = "[.]"; - } - else if (output == "[plus]") - { - output = "[+]"; - } - else if (output == "[multiply]") - { - output = "[*]"; - } - else if (output == "[divide]") - { - output = "[/]"; - } - else if (output == "[minus]") - { - output = "[-]"; - } - else if (output == "[enter]") - { - output = "enter"; - } - else if (output.Contains("page")) - { - output = output.Insert(4, " "); - } - else if (output.Contains("arrow")) - { - output = output.Split('a')[0]; - } - else if (output == "capslock") - { - output = "caps lock"; - } - else if (output == "minus") - { - output = "-"; - } - - //test if input is valid - try - { - if (Input.GetKey(output)) - { - return output; - } - } - catch (System.Exception) - { - } - } - - //mouse - for (int m = 0; m < 6; m++) - { - string inputString = "mouse " + m; - try - { - if (Input.GetKey(inputString)) - { - return inputString; - } - } - catch (UnityException) - { - Debug.Log("Invalid mouse: " + inputString); - } - } - - //joysticks - for (int j = 1; j < 12; j++) - { - for (int b = 0; b < 20; b++) - { - string inputString = "joystick " + j + " button " + b; - try - { - if (Input.GetKey(inputString)) - { - return inputString; - } - } - catch (UnityException) - { - return string.Empty; - } - } - } - - return string.Empty; - } - - public static bool GetKey(BDInputInfo input) - { - return input.inputString != string.Empty && Input.GetKey(input.inputString); - } - - public static bool GetKeyDown(BDInputInfo input) - { - return input.inputString != string.Empty && Input.GetKeyDown(input.inputString); - } - } -} diff --git a/BDArmory/UI/BDStagingAreaGauge.cs b/BDArmory/UI/BDStagingAreaGauge.cs index 692f47d28..6f830b34c 100644 --- a/BDArmory/UI/BDStagingAreaGauge.cs +++ b/BDArmory/UI/BDStagingAreaGauge.cs @@ -1,22 +1,21 @@ -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) private ProtoStageIconInfo heatGauge; private ProtoStageIconInfo emptyGauge; private ProtoStageIconInfo ammoGauge; + private ProtoStageIconInfo cmGauge; private ProtoStageIconInfo reloadBar; void Start() @@ -34,7 +33,7 @@ void OnDestroy() private void ReloadIconOnVesselSwitch(Vessel data0, Vessel data1) { - if (part?.vessel != null && part.vessel.isActiveVessel) + if (part != null && part.vessel != null && part.vessel.isActiveVessel) { ForceRedraw(); } @@ -64,7 +63,7 @@ public void UpdateAmmoMeter(float ammoLevel) } if (emptyGauge == null) { - emptyGauge = InitEmptyGauge(); + emptyGauge = InitEmptyGauge(StringUtils.Localize("#LOC_BDArmory_ProtoStageIconInfo_AmmoOut")); emptyGauge?.SetValue(1, 0, 1); } } @@ -75,6 +74,41 @@ public void UpdateAmmoMeter(float ammoLevel) } } + public void UpdateCMMeter(float cmLevel, CounterMeasure.CMDropper.CountermeasureTypes type) + { + if (BDArmorySettings.SHOW_AMMO_GAUGES && !BDArmorySettings.INFINITE_COUNTERMEASURES) + { + if (cmLevel > 0) + { + if (emptyGauge != null) + { + ForceRedraw(); + } + if (cmGauge == null) + { + cmGauge = InitCMGauge(AmmoName, type); + } + cmGauge?.SetValue(cmLevel, 0, 1); + } + else + { + if (cmGauge != null) + { + ForceRedraw(); + } + if (emptyGauge == null) + { + emptyGauge = InitEmptyGauge(StringUtils.Localize("#LOC_BDArmory_ProtoStageIconInfo_CMsOut")); + emptyGauge?.SetValue(1, 0, 1); + } + } + } + else if (cmGauge != null || emptyGauge != null) + { + ForceRedraw(); + } + } + /// 0 is no heat, 1 is max heat public void UpdateHeatMeter(float heatLevel) { @@ -101,31 +135,24 @@ 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); - } } } private void ForceRedraw() { part.stackIcon.ClearInfoBoxes(); - //null everything so other gauges will perperly re-initialize post ClearinfoBoxes() + //null everything so other gauges will properly re-initialize post ClearinfoBoxes() ammoGauge = null; heatGauge = null; reloadBar = null; emptyGauge = null; + cmGauge = null; } private void EnsureStagingIcon() @@ -149,7 +176,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,13 +193,50 @@ 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); } return v; } + private ProtoStageIconInfo InitCMGauge(string ammoName, CounterMeasure.CMDropper.CountermeasureTypes type) + { + EnsureStagingIcon(); + ProtoStageIconInfo a = part.stackIcon.DisplayInfo(); + // fix nullref if no stackicon exists + if (a != null) + { + a.SetMsgBgColor(XKCDColors.OffWhite); + a.SetMessage($"{ammoName}"); + a.SetProgressBarBgColor(XKCDColors.DarkGrey); + switch (type) + { + case CounterMeasure.CMDropper.CountermeasureTypes.Flare: + a.SetProgressBarColor(XKCDColors.Brick); + a.SetMsgTextColor(XKCDColors.Brick); + break; + case CounterMeasure.CMDropper.CountermeasureTypes.Chaff: + a.SetProgressBarColor(XKCDColors.Silver); + a.SetMsgTextColor(XKCDColors.Silver); + break; + case CounterMeasure.CMDropper.CountermeasureTypes.Smoke: + a.SetProgressBarColor(XKCDColors.Brown); + a.SetMsgTextColor(XKCDColors.Brown); + break; + case CounterMeasure.CMDropper.CountermeasureTypes.Bubbles: + a.SetProgressBarColor(XKCDColors.TealGreen); + a.SetMsgTextColor(XKCDColors.TealGreen); + break; + case CounterMeasure.CMDropper.CountermeasureTypes.Decoy: + a.SetProgressBarColor(XKCDColors.DarkBlue); + a.SetMsgTextColor(XKCDColors.DarkBlue); + break; + + } + } + return a; + } private ProtoStageIconInfo InitAmmoGauge(string ammoName) //thanks DYJ { EnsureStagingIcon(); @@ -189,8 +253,7 @@ private ProtoStageIconInfo InitAmmoGauge(string ammoName) //thanks DYJ } return a; } - - private ProtoStageIconInfo InitEmptyGauge() //could remove emptygauge, mainly a QoL thing, removal might increase performance slightly + private ProtoStageIconInfo InitEmptyGauge(string message) //could remove emptygauge, mainly a QoL thing, removal might increase performance slightly { EnsureStagingIcon(); ProtoStageIconInfo g = part.stackIcon.DisplayInfo(); @@ -199,7 +262,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(message); g.SetProgressBarBgColor(XKCDColors.Yellow); g.SetProgressBarColor(XKCDColors.Black); } diff --git a/BDArmory/UI/BDTISetup.cs b/BDArmory/UI/BDTISetup.cs new file mode 100644 index 000000000..2726f6be3 --- /dev/null +++ b/BDArmory/UI/BDTISetup.cs @@ -0,0 +1,461 @@ +using KSP.Localization; +using KSP.UI.Screens; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.ModIntegration; +using BDArmory.Extensions; + +/* +* *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? +*/ +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + class BDTISetup : MonoBehaviour + { + private ApplicationLauncherButton toolbarButton = null; + public static Rect WindowRectGUI; + + private string windowTitle = StringUtils.Localize("#LOC_BDArmory_Icons_title"); + public static BDTISetup Instance = null; + public static GUIStyle TILabel; + private bool showTeamIconGUI = false; + float toolWindowWidth = 250; + float toolWindowHeight = 150; + Rect IconOptionsGroup; + Rect TeamColorsGroup; + public string selectedTeam; + public bool UpdateTeamColor = false; + private float updateList = 0; + private bool maySavethisInstance = false; + + // Opacity Settings + internal const float textOpacity = 2f; + internal const float iconOpacity = 1f; + + public SortedList> weaponManagers = []; + + public static string textureDir = "BDArmory/Textures/"; + + //legacy version check + bool LegacyTILoaded = false; + bool showPSA = false; + + GUIStyle title; + private Texture2D dit; + public Texture2D TextureIconDebris + { + get { return dit ? dit : dit = GameDatabase.Instance.GetTexture(textureDir + "Icons/debrisIcon", false); } + } + private Texture2D mit; + public Texture2D TextureIconMissile + { + get { return mit ? mit : mit = GameDatabase.Instance.GetTexture(textureDir + "Icons/missileIcon", false); } + } + private Texture2D rit; + public Texture2D TextureIconRocket + { + get { return rit ? rit : rit = GameDatabase.Instance.GetTexture(textureDir + "Icons/rocketIcon", false); } + } + private Texture2D ti7; + public Texture2D TextureIconGeneric + { + get { return ti7 ? ti7 : ti7 = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Generic", false); } + } + private Texture2D ti1A; + public Texture2D TextureIconShip + { + get { return ti1A ? ti1A : ti1A = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Ship", false); } + } + private Texture2D ti2A; + public Texture2D TextureIconPlane + { + get { return ti2A ? ti2A : ti2A = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Plane", false); } + } + private Texture2D ti3A; + public Texture2D TextureIconRover + { + get { return ti3A ? ti3A : ti3A = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Rover", false); } + } + private Texture2D ti4A; + public Texture2D TextureIconBase + { + get { return ti4A ? ti4A : ti4A = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Base", false); } + } + private Texture2D ti5A; + public Texture2D TextureIconProbe + { + get { return ti5A ? ti5A : ti5A = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Probe", false); } + } + private Texture2D ti6A; + public Texture2D TextureIconSub + { + get { return ti6A ? ti6A : ti6A = GameDatabase.Instance.GetTexture(textureDir + "Icons/Icon_Sub", false); } + } + + private Texture2D MTAcc; + public Texture2D MutatorIconAcc + { + get { return MTAcc ? MTAcc : MTAcc = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconAccuracy", false); } + } + private Texture2D MTAtk; + public Texture2D MutatorIconAtk + { + get { return MTAtk ? MTAtk : MTAtk = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconAttack", false); } + } + private Texture2D MTAtk2; + public Texture2D MutatorIconAtk2 + { + get { return MTAtk2 ? MTAtk2 : MTAtk2 = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconAttack2", false); } + } + private Texture2D MTBal; + public Texture2D MutatorIconBullet + { + get { return MTBal ? MTBal : MTBal = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconBallistic", false); } + } + private Texture2D MTDef; + public Texture2D MutatorIconDefense + { + get { return MTDef ? MTDef : MTDef = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconDefense", false); } + } + private Texture2D MTLsr; + public Texture2D MutatorIconLaser + { + get { return MTLsr ? MTLsr : MTLsr = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconLaser", false); } + } + private Texture2D MTmass; + public Texture2D MutatorIconMass + { + get { return MTmass ? MTmass : MTmass = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconMass", false); } + } + private Texture2D MTHP; + public Texture2D MutatorIconRegen + { + get { return MTHP ? MTHP : MTHP = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconRegen", false); } + } + private Texture2D MTRkt; + public Texture2D MutatorIconRocket + { + get { return MTRkt ? MTRkt : MTRkt = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconRocket", false); } + } + private Texture2D MTdoom; + public Texture2D MutatorIconDoom + { + get { return MTdoom ? MTdoom : MTdoom = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconSkull", false); } + } + private Texture2D MTSpd; + public Texture2D MutatorIconSpeed + { + get { return MTSpd ? MTSpd : MTSpd = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconSpeed", false); } + } + private Texture2D MTTgt; + public Texture2D MutatorIconTarget + { + get { return MTTgt ? MTTgt : MTTgt = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconTarget", false); } + } + private Texture2D MTVmp; + public Texture2D MutatorIconVampire + { + get { return MTVmp ? MTVmp : MTVmp = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconVampire", false); } + } + private Texture2D MTRnd; + public Texture2D MutatorIconNull + { + get { return MTRnd ? MTRnd : MTRnd = GameDatabase.Instance.GetTexture(textureDir + "Mutators/IconUnknown", false); } + } + + Rect SRect(float line, float margin = 20) + { + return new Rect(15, line * 25, toolWindowWidth - margin, 20); + } + + void Start() + { + Instance = this; + if (HighLogic.LoadedSceneIsFlight) + maySavethisInstance = true; + if (ConfigNode.Load(BDTISettings.settingsConfigURL) == null) + { + var node = new ConfigNode(); + node.AddNode("IconSettings"); + node.Save(BDTISettings.settingsConfigURL); + } + + AddToolbarButton(); + LoadConfig(); + UpdateList(); + + LegacyTILoaded = LegacyTeamIcons.CheckForLegacyTeamIcons(); + if (HighLogic.LoadedSceneIsFlight && LegacyTILoaded) + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_legacyinstall"), 20.0f, ScreenMessageStyle.UPPER_CENTER); + TILabel = new GUIStyle + { + font = BDArmorySetup.BDGuiSkin.window.font, + fontSize = BDArmorySetup.BDGuiSkin.window.fontSize, + fontStyle = BDArmorySetup.BDGuiSkin.window.fontStyle + }; + IconOptionsGroup = new Rect(10, 55, toolWindowWidth - 20, 290); + TeamColorsGroup = new Rect(10, IconOptionsGroup.height, toolWindowWidth - 20, 25); + WindowRectGUI = new Rect(Screen.width - BDArmorySettings.UI_SCALE_ACTUAL * (toolWindowWidth + 40), 150, toolWindowWidth, toolWindowHeight); + } + + private void MissileFireOnToggleTeam(MissileFire wm, BDTeam team) + { + if (BDTISettings.TEAMICONS) + { + UpdateList(); + } + } + private void VesselEventUpdate(Vessel v) + { + if (BDTISettings.TEAMICONS) + { + UpdateList(true); + } + } + private void Update() + { + if (BDTISettings.TEAMICONS) + { + updateList -= Time.deltaTime; + if (updateList < 0) + { + UpdateList(); + updateList = 1f; // check team lists less often than every frame + } + } + } + public Dictionary ColorAssignments = new Dictionary(); + private void UpdateList(bool fromModifiedEvent = false) + { + weaponManagers.Clear(); + + using (List.Enumerator 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; + if (fromModifiedEvent) VesselModuleRegistry.OnVesselModified(v.Current, true); + var wm = v.Current.ActiveController().WM; + if (wm != null) + { + if (!ColorAssignments.ContainsKey(wm.teamString)) + { + float rnd = UnityEngine.Random.Range(0f, 100f); + ColorAssignments.Add(wm.Team.Name, Color.HSVToRGB((rnd / 100f), 1f, 1f)); + } + if (weaponManagers.TryGetValue(wm.Team.Name, out var teamManagers)) + teamManagers.Add(wm); + else + weaponManagers.Add(wm.Team.Name, [wm]); + } + } + } + public 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) + { + ApplicationLauncher.Instance.RemoveModApplication(toolbarButton); + toolbarButton = null; + } + if (maySavethisInstance) + { + SaveConfig(); + } + } + + void AddToolbarButton() + { + if (!HighLogic.LoadedSceneIsFlight) return; + StartCoroutine(ToolbarButtonRoutine()); + } + IEnumerator ToolbarButtonRoutine() + { + if (toolbarButton) // Just update the callbacks for the current instance. + { + toolbarButton.onTrue = ShowToolbarGUI; + toolbarButton.onFalse = HideToolbarGUI; + yield break; + } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. + Texture buttonTexture = GameDatabase.Instance.GetTexture("BDArmory/Textures/Icons/icon", false); + toolbarButton = ApplicationLauncher.Instance.AddModApplication(ShowToolbarGUI, HideToolbarGUI, null, null, null, null, ApplicationLauncher.AppScenes.FLIGHT, buttonTexture); + } + + public void ShowToolbarGUI() + { + if (LegacyTILoaded) + { + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_legacyinstall"), 5.0f, ScreenMessageStyle.UPPER_CENTER); + } + else + { + showTeamIconGUI = true; + showPSA = false; + LoadConfig(); + } + } + + public void HideToolbarGUI() + { + showTeamIconGUI = false; + SaveConfig(); + } + + public static void LoadConfig() + { + try + { + Debug.Log("[BDTeamIcons]=== Loading settings.cfg ==="); + + SettingsDataField.Load(); + if (BDTISettings.MAX_DISTANCE_THRESHOLD < 1 || BDTISettings.MAX_DISTANCE_THRESHOLD > BDArmorySettings.MAX_GUARD_VISUAL_RANGE) BDTISettings.MAX_DISTANCE_THRESHOLD = BDArmorySettings.MAX_GUARD_VISUAL_RANGE; + } + catch (NullReferenceException) + { + Debug.Log("[BDTeamIcons]=== Failed to load settings config ==="); + } + } + + public static void SaveConfig() + { + try + { + Debug.Log("[BDTeamIcons] == Saving settings.cfg == "); + SettingsDataField.Save(); + } + catch (NullReferenceException) + { + Debug.Log("[BDTeamIcons]: === Failed to save settings.cfg ===="); + } + } + + void OnGUI() + { + if (LegacyTILoaded) return; + if (!BDArmorySetup.GAME_UI_ENABLED) return; + if (title == null) + { + title = new GUIStyle(GUI.skin.label) + { + fontSize = 30, + alignment = TextAnchor.MiddleLeft, + wordWrap = false + }; + } + + if (showTeamIconGUI) + { + if (HighLogic.LoadedSceneIsFlight) + { + maySavethisInstance = true; + } + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, WindowRectGUI.position); + WindowRectGUI = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), WindowRectGUI, TeamIconGUI, windowTitle, BDArmorySetup.BDGuiSkin.window); + } + if (HighLogic.LoadedSceneIsFlight && BDTISettings.TEAMICONS) + { + if (GameSettings.FLT_VESSEL_LABELS && !showPSA) + { + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_PSA"), 20.0f, ScreenMessageStyle.UPPER_CENTER); + showPSA = true; + } + } + } + public bool showTeamIconSelect = false; + public bool showColorSelect = false; + + (float, float)[] cacheMaxDistanceThreshold; + void TeamIconGUI(int windowID) + { + float line = 0; + GUI.DragWindow(new Rect(0, 0, WindowRectGUI.width, 25)); + 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(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(SRect(line++), BDTISettings.TEAMNAMES, StringUtils.Localize("#LOC_BDArmory_Icon_teams"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.VESSELNAMES = GUI.Toggle(SRect(line++), BDTISettings.VESSELNAMES, StringUtils.Localize("#LOC_BDArmory_Icon_names"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.SCORE = GUI.Toggle(SRect(line++), BDTISettings.SCORE, StringUtils.Localize("#LOC_BDArmory_Icon_score"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.HEALTHBAR = GUI.Toggle(SRect(line++), BDTISettings.HEALTHBAR, StringUtils.Localize("#LOC_BDArmory_Icon_healthbars"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.SHOW_SELF = GUI.Toggle(SRect(line++), BDTISettings.SHOW_SELF, StringUtils.Localize("#LOC_BDArmory_Icon_show_self"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.MISSILES = GUI.Toggle(SRect(line++), BDTISettings.MISSILES, StringUtils.Localize("#LOC_BDArmory_Icon_missiles"), BDArmorySetup.BDGuiSkin.toggle); + if (BDTISettings.MISSILES) BDTISettings.MISSILE_TEXT = GUI.Toggle(SRect(line++), BDTISettings.MISSILE_TEXT, StringUtils.Localize("#LOC_BDArmory_Icon_missile_text"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.DEBRIS = GUI.Toggle(SRect(line++), BDTISettings.DEBRIS, StringUtils.Localize("#LOC_BDArmory_Icon_debris"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.PERSISTANT = GUI.Toggle(SRect(line++), BDTISettings.PERSISTANT, StringUtils.Localize("#LOC_BDArmory_Icon_persist"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.THREATICON = GUI.Toggle(SRect(line++), BDTISettings.THREATICON, StringUtils.Localize("#LOC_BDArmory_Icon_threats"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.POINTERS = GUI.Toggle(SRect(line++), BDTISettings.POINTERS, StringUtils.Localize("#LOC_BDArmory_Icon_pointers"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.TELEMETRY = GUI.Toggle(SRect(line++), BDTISettings.TELEMETRY, StringUtils.Localize("#LOC_BDArmory_Icon_telemetry"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.STORE_TEAM_COLORS = GUI.Toggle(SRect(line++), BDTISettings.STORE_TEAM_COLORS, StringUtils.Localize("#LOC_BDArmory_Icon_StoreTeamColors"), BDArmorySetup.BDGuiSkin.toggle); + line += 0.25f; + GUI.Label(SRect(line++), StringUtils.Localize("#LOC_BDArmory_Icon_scale") + " " + (BDTISettings.ICONSCALE * 100f).ToString("0") + "%"); + BDTISettings.ICONSCALE = GUI.HorizontalSlider(SRect(line++, 40), BDTISettings.ICONSCALE, 0.25f, 2f); + line -= 0.15f; + GUI.Label(SRect(line++), $"{StringUtils.Localize("#LOC_BDArmory_Icon_distance_threshold")} {BDTISettings.DISTANCE_THRESHOLD:0}m"); + BDTISettings.DISTANCE_THRESHOLD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRect(line++, 40), BDTISettings.DISTANCE_THRESHOLD, 10f, 250f), 10f); + GUI.Label(SRect(line++), $"{StringUtils.Localize("#LOC_BDArmory_Icon_opacity")} {BDTISettings.OPACITY * 100f:0}%"); + BDTISettings.OPACITY = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRect(line++, 40), BDTISettings.OPACITY, 0f, 1f), 0.01f); + GUI.Label(SRect(line++), $"{StringUtils.Localize("#LOC_BDArmory_Icon_max_distance_threshold")} {(BDTISettings.MAX_DISTANCE_THRESHOLD < BDArmorySettings.MAX_GUARD_VISUAL_RANGE ? $"{BDTISettings.MAX_DISTANCE_THRESHOLD / 1000f:0}km" : "Unlimited")}"); + BDTISettings.MAX_DISTANCE_THRESHOLD = GUIUtils.HorizontalSemiLogSlider(SRect(line++, 40), BDTISettings.MAX_DISTANCE_THRESHOLD / 1000f, 1f, BDArmorySettings.MAX_GUARD_VISUAL_RANGE / 1000f, 1, false, false, ref cacheMaxDistanceThreshold) * 1000f; + GUI.EndGroup(); + IconOptionsGroup.height = 25f * line; + + TeamColorsGroup.y = IconOptionsGroup.y + IconOptionsGroup.height; + GUI.BeginGroup(TeamColorsGroup, GUIContent.none, BDArmorySetup.BDGuiSkin.box); + line = 0; + using (var teamManagers = weaponManagers.GetEnumerator()) + while (teamManagers.MoveNext()) + { + line++; + Rect buttonRect = new Rect(30, -20 + (line * 25), 190, 20); + GUIStyle vButtonStyle = showColorSelect ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; + if (GUI.Button(buttonRect, $"{teamManagers.Current.Key}", vButtonStyle)) + { + showColorSelect = !showColorSelect; + selectedTeam = teamManagers.Current.Key; + } + if (ColorAssignments.ContainsKey(teamManagers.Current.Key)) + { + title.normal.textColor = ColorAssignments[teamManagers.Current.Key]; + } + 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); + } + toolWindowHeight = Mathf.Lerp(toolWindowHeight, 50 + (BDTISettings.TEAMICONS ? IconOptionsGroup.height + TeamColorsGroup.height : 0) + 15, 0.35f); + WindowRectGUI.height = toolWindowHeight; + } + } +} diff --git a/BDArmory/UI/BDTargetSelector.cs b/BDArmory/UI/BDTargetSelector.cs new file mode 100644 index 000000000..e6508def2 --- /dev/null +++ b/BDArmory/UI/BDTargetSelector.cs @@ -0,0 +1,221 @@ +using System.Collections; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; + +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] + public class BDTargetSelector : MonoBehaviour + { + public static BDTargetSelector Instance; + + const float width = 250; + const float margin = 5; + const float buttonHeight = 20; + const float buttonGap = 2; + + private static int guiCheckIndex = -1; + private bool ready = false; + private bool open = false; + private Rect window; + private float height; + + private Vector2 windowLocation; + private MissileFire targetWeaponManager; + + public void Open(MissileFire weaponManager, Vector2 position) + { + 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), StringUtils.Localize("#LOC_BDArmory_Selecttargeting"), labelStyle); + if (GUI.Button(new Rect(width - 18, 2, 16, 16), "X")) + { + 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, StringUtils.Localize("#LOC_BDArmory_TargetCOM"), CoMStyle)) + { + targetWeaponManager.targetCoM = !targetWeaponManager.targetCoM; + if (targetWeaponManager.targetCoM) + { + 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 && !targetWeaponManager.targetRandom)) + { + targetWeaponManager.targetRandom = true; + } + } + height += buttonHeight; + + height += buttonGap; + Rect MassRect = new Rect(margin, height, width - 2 * margin, buttonHeight); + GUIStyle MassStyle = targetWeaponManager.targetMass ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; + + 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 && !targetWeaponManager.targetRandom)) + { + targetWeaponManager.targetCoM = true; + } + } + height += buttonHeight; + + height += buttonGap; + Rect CommandRect = new Rect(margin, height, width - 2 * margin, buttonHeight); + GUIStyle CommandStyle = targetWeaponManager.targetCommand ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; + + 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 && !targetWeaponManager.targetRandom)) + { + targetWeaponManager.targetCoM = true; + } + } + height += buttonHeight; + + height += buttonGap; + Rect EngineRect = new Rect(margin, height, width - 2 * margin, buttonHeight); + GUIStyle EngineStyle = targetWeaponManager.targetEngine ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; + + 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 && !targetWeaponManager.targetRandom)) + { + targetWeaponManager.targetCoM = true; + } + } + height += buttonHeight; + + height += buttonGap; + Rect weaponRect = new Rect(margin, height, width - 2 * margin, buttonHeight); + GUIStyle WepStyle = targetWeaponManager.targetWeapon ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; + + 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 && !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; + } + } + height += buttonHeight; + + height += margin; + 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() + { + if (!BDArmorySetup.GAME_UI_ENABLED) return; + if (ready) + { + if (!open) return; + + var clientRect = new Rect( + Mathf.Min(windowLocation.x, Screen.width - width), + Mathf.Min(windowLocation.y, Screen.height - height), + width, + height); + BDArmorySetup.SetGUIOpacity(); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, clientRect.position); + window = GUI.Window(10591029, clientRect, TargetingSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(window, guiCheckIndex); + } + } + + private void Awake() + { + if (Instance) + Destroy(Instance); + Instance = this; + } + + private void Start() + { + StartCoroutine(WaitForBdaSettings()); + } + + private void OnDestroy() + { + ready = false; + } + + private IEnumerator WaitForBdaSettings() + { + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); + + ready = true; + if (guiCheckIndex < 0) guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + } + } +} diff --git a/BDArmory/UI/BDTeamSelector.cs b/BDArmory/UI/BDTeamSelector.cs index 1c5f064b3..0549250d7 100644 --- a/BDArmory/UI/BDTeamSelector.cs +++ b/BDArmory/UI/BDTeamSelector.cs @@ -1,9 +1,10 @@ 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.Settings; +using BDArmory.Utils; namespace BDArmory.UI { @@ -16,16 +17,18 @@ public class BDTeamSelector : MonoBehaviour const float margin = 5; const float buttonHeight = 20; const float buttonGap = 2; - const float newTeanButtonWidth = 40; + const float newTeamButtonWidth = 40; const float scrollWidth = 20; - private int guiCheckIndex; + private static int guiCheckIndex = -1; private bool ready = false; private bool open = false; private Rect window; private float height; private bool scrollable; private Vector2 scrollPosition = Vector2.zero; + Rect alliesRect; + GUIStyle alliesStyle; private Vector2 windowLocation; private MissileFire targetWeaponManager; @@ -33,26 +36,49 @@ public class BDTeamSelector : MonoBehaviour public void Open(MissileFire weaponManager, Vector2 position) { - open = true; targetWeaponManager = weaponManager; newTeamName = string.Empty; windowLocation = position; + SetVisible(true); + } + + public void SetVisible(bool visible) + { + open = visible; + GUIUtils.SetGUIRectVisible(guiCheckIndex, visible); + if (visible) + { + window = new Rect( + Mathf.Min(windowLocation.x, Screen.width - (scrollable ? width + scrollWidth : width)), + Mathf.Min(windowLocation.y, Screen.height - height), + width, + scrollable ? Screen.height / 2 + buttonHeight + buttonGap + 2 * margin : height + ); + alliesStyle ??= new GUIStyle(BDArmorySetup.BDGuiSkin.label) { wordWrap = true, alignment = TextAnchor.UpperLeft, fixedWidth = width }; + } } private void TeamSelectorWindow(int id) { height = margin; + GUI.Label(new Rect(margin, height, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_SelectTeam"), BDArmorySetup.BDGuiSkin.label); + GUI.DragWindow(new Rect(margin, margin, width - 2 * margin - buttonHeight, buttonHeight)); + if (GUI.Button(new Rect(width - 18, 2, 18, 18), "X")) + { + SetVisible(false); + } + height += buttonHeight; // Team input field - newTeamName = GUI.TextField(new Rect(margin, margin, width - buttonGap - 2 * margin - newTeanButtonWidth, buttonHeight), newTeamName, 30); + newTeamName = GUI.TextField(new Rect(margin, height, width - buttonGap - 2 * margin - newTeamButtonWidth, buttonHeight), newTeamName, 30); // 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" + Rect newTeamButtonRect = new Rect(width - margin - newTeamButtonWidth, height, newTeamButtonWidth, buttonHeight); + 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; + newTeamName = string.Empty; } } @@ -74,18 +100,48 @@ private void TeamSelectorWindow(int id) if (teams.Current == null || !teams.Current.Name.ToLowerInvariant().StartsWith(newTeamName.ToLowerInvariant().Trim())) continue; height += buttonGap; - Rect buttonRect = new Rect(margin, height, width - 2 * margin, buttonHeight); + Rect buttonRect = new Rect(margin, height, width - buttonHeight - 4 * margin, buttonHeight); GUIStyle buttonStyle = (teams.Current == targetWeaponManager.Team) ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - if (GUI.Button(buttonRect, teams.Current.Name, buttonStyle)) + if (GUI.Button(buttonRect, teams.Current.Name + (teams.Current.Neutral ? (teams.Current.Name != "Neutral" ? "(Neutral)" : "") : ""), buttonStyle)) { - targetWeaponManager.SetTeam(teams.Current); - open = false; + switch (Event.current.button) + { + case 1: // right click + if (teams.Current.Name != "Neutral" && teams.Current.Name != "A" && teams.Current.Name != "B") + teams.Current.Neutral = !teams.Current.Neutral; + break; + default: + targetWeaponManager.SetTeam(teams.Current); + if (targetWeaponManager.Team.Allies.Contains(teams.Current.Name)) + targetWeaponManager.Team.Allies.Remove(teams.Current.Name); + break; + } + } + if (teams.Current.Name != "Neutral" && teams.Current.Name != "A" && teams.Current.Name != "B" && !teams.Current.Neutral && teams.Current != targetWeaponManager.Team) + { + if (GUI.Button(new Rect(width - buttonHeight, height, buttonHeight - 2 * margin, buttonHeight), "[A]", (targetWeaponManager.Team.Allies.Contains(teams.Current.Name)) ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + switch (Event.current.button) + { + default: + targetWeaponManager.SetTeam(targetWeaponManager.Team); + if (targetWeaponManager.Team.Allies.Contains(teams.Current.Name)) + targetWeaponManager.Team.Allies.Remove(teams.Current.Name); + else targetWeaponManager.Team.Allies.Add(teams.Current.Name); + break; + } + } } - height += buttonHeight; } - + GUI.Label(new Rect(margin, height, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_Allies")); + height += buttonHeight + buttonGap; + string allies = string.Join("; ", targetWeaponManager.Team.Allies); + alliesRect = new Rect(margin, height, width - 2 * margin, buttonHeight) + { height = alliesStyle.CalcHeight(new GUIContent(allies), width) }; + GUI.Label(alliesRect, allies, alliesStyle); + height += alliesRect.height; if (scrollable) GUI.EndScrollView(); @@ -95,17 +151,18 @@ 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; + newTeamName = string.Empty; } else if (Event.current.keyCode == KeyCode.Escape) { - open = false; + SetVisible(false); } } height += margin; - BDGUIUtils.RepositionWindow(ref window); - BDGUIUtils.UseMouseEventInRect(window); + window.height = scrollable ? Screen.height / 2 + buttonHeight + buttonGap + 2 * margin : height; + GUIUtils.RepositionWindow(ref window); + GUIUtils.UseMouseEventInRect(window); } protected virtual void OnGUI() @@ -116,7 +173,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) @@ -126,12 +183,13 @@ protected virtual void OnGUI() Mathf.Min(windowLocation.y, Screen.height - height), width, scrollable ? Screen.height / 2 + buttonHeight + buttonGap + 2 * margin : height); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, clientRect.position); window = GUI.Window(10591029, clientRect, TeamSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); - Misc.Misc.UpdateGUIRect(window, guiCheckIndex); + GUIUtils.UpdateGUIRect(window, guiCheckIndex); } else { - Misc.Misc.UpdateGUIRect(new Rect(), guiCheckIndex); + GUIUtils.UpdateGUIRect(new Rect(), guiCheckIndex); } } } @@ -155,11 +213,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 = Misc.Misc.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..26d794b1e 100644 --- a/BDArmory/UI/KrakensbaneDebug.cs +++ b/BDArmory/UI/KrakensbaneDebug.cs @@ -1,10 +1,11 @@ #if DEBUG // This will only be live in debug builds -using System; using UnityEngine; -using BDArmory.Core; -using BDArmory.Misc; + +using BDArmory.Settings; +using BDArmory.Utils; +using System.Text; namespace BDArmory.UI { @@ -12,29 +13,38 @@ namespace BDArmory.UI public class KrakensbaneDebug : MonoBehaviour { float lastShift = 0; + Vector3 lastOffsetAmount = default; + StringBuilder debugString = new(); void FixedUpdate() { - if (!FloatingOrigin.Offset.IsZero()) + if (BDKrakensbane.IsActive) + { lastShift = Time.time; + lastOffsetAmount = BDKrakensbane.FloatingOriginOffset; + // Debug.Log($"DEBUG {Time.time} Krakensbane shifted by {BDKrakensbane.FloatingOriginOffset:G3} ({(Vector3)FloatingOrigin.Offset:G3}) | N-Kb {BDKrakensbane.FloatingOriginOffsetNonKrakensbane:G3} ({(Vector3)FloatingOrigin.OffsetNonKrakensbane:G3}) | V3f: {BDKrakensbane.FrameVelocityV3f:G3} ({Krakensbane.GetFrameVelocityV3f():G3})"); + } } void OnGUI() { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY) { - var frameVelocity = Krakensbane.GetFrameVelocityV3f(); + debugString.Clear(); + 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), - $"Frame velocity: {frameVelocity.magnitude} ({frameVelocity}){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}" - //+ $"Reference frame rotation speed: {rFrameRotation}{Environment.NewLine}" - //+ $"Reference frame angular speed: {rFrameRotation.magnitude / Mathf.PI * 180}{Environment.NewLine}" - //+ $"Ref frame is {(FlightGlobals.RefFrameIsRotating ? "" : "not ")}rotating{Environment.NewLine}" - ); + debugString.AppendLine($"Frame velocity: {frameVelocity.magnitude} ({frameVelocity})"); + debugString.AppendLine($"FO offset: {(Vector3)BDKrakensbane.FloatingOriginOffset:G3}"); + debugString.AppendLine($"N-Kb offset: {(Vector3)BDKrakensbane.FloatingOriginOffsetNonKrakensbane:G3}"); + debugString.AppendLine($"Last offset {Time.time - lastShift}s ago"); + debugString.AppendLine($"Last offset amount {lastOffsetAmount:G3}"); + debugString.AppendLine($"Local vessel speed: {FlightGlobals.ActiveVessel.rb_velocity.magnitude}, ({FlightGlobals.ActiveVessel.rb_velocity})"); + // debugString.AppendLine($"Reference frame speed: {rFrameVelocity}"); + // debugString.AppendLine($"Reference frame rotation speed: {rFrameRotation}"); + // debugString.AppendLine($"Reference frame angular speed: {rFrameRotation.magnitude / Mathf.PI * 180}"); + // debugString.AppendLine($"Ref frame is {(FlightGlobals.RefFrameIsRotating ? "" : "not ")}rotating"); + GUI.Label(new Rect(10, 150, 400, 400), debugString.ToString()); } } } diff --git a/BDArmory/UI/LoadedVesselSwitcher.cs b/BDArmory/UI/LoadedVesselSwitcher.cs index eed2728a5..9f6a10bfa 100644 --- a/BDArmory/UI/LoadedVesselSwitcher.cs +++ b/BDArmory/UI/LoadedVesselSwitcher.cs @@ -1,19 +1,18 @@ -using System.Collections; using System.Collections.Generic; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.Control; -using BDArmory.Core; -using UnityEngine; -using KSP.Localization; -using KSP.UI.Screens; -using BDArmory.FX; -using Expansions; -using System; -using VehiclePhysics; -using System.Net; +using System.Collections; using System.IO; using System.Linq; +using System.Text; +using System; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; namespace BDArmory.UI { @@ -23,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; @@ -34,21 +33,47 @@ 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; + public bool currentVesselDied = false; private double currentVesselDiedAt = 0; - private float updateTimer = 0; //gui params - private float _windowHeight; //auto adjusting + bool resizingWindow = false; + Vector2 windowSize = new(350, 32); // Height is auto-adjusting + float previousWindowHeight = 32; + private string camMode = "A"; + private int currentMode = 1; + private SortedList> weaponManagers = []; + private Dictionary cameraScores = []; + + private bool upToDateWMs = false; + public void UpdateWMs() { upToDateWMs = false; } // Update the WMs on the next frame. + public SortedList> WeaponManagers + { + get + { + if (!upToDateWMs) + UpdateList(); + return weaponManagers; + } + } - public SortedList> weaponManagers = new SortedList>(); - private Dictionary cameraScores = new Dictionary(); + private Dictionary> _vessels = []; + public Dictionary> Vessels + { + get + { + if (!upToDateWMs) UpdateList(); + return _vessels; + } + } // booleans to track state of buttons affecting everyone - private bool _freeForAll = false; + private bool _teamsAssigned = false; private bool _autoPilotEnabled = false; private bool _guardModeEnabled = false; + public bool vesselTraceEnabled = false; // Vessel spawning // private bool _vesselsSpawned = false; @@ -62,6 +87,8 @@ public class LoadedVesselSwitcher : MonoBehaviour private static GUIStyle ItVessel = new GUIStyle(BDArmorySetup.BDGuiSkin.button); private static GUIStyle ItVesselSelected = new GUIStyle(BDArmorySetup.BDGuiSkin.box); + public static GUISkin VSPUISkin = HighLogic.Skin; + private static System.Random rng; private void Awake() @@ -99,12 +126,9 @@ private void Start() StartCoroutine(WaitForBdaSettings()); - // TEST + // Set floating origin thresholds FloatingOrigin.fetch.threshold = 20000; //20km FloatingOrigin.fetch.thresholdSqr = 20000 * 20000; //20km - Debug.Log($"FLOATINGORIGIN: threshold is {FloatingOrigin.fetch.threshold}"); - - //BDArmorySetup.WindowRectVesselSwitcher = new Rect(10, Screen.height / 6f, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH, 10); } private void OnDestroy() @@ -119,58 +143,76 @@ private void OnDestroy() _ready = false; // TEST - Debug.Log($"FLOATINGORIGIN: threshold is {FloatingOrigin.fetch.threshold}"); + // Debug.Log($"[BDArmory.LoadedVesselSwitcher]: FLOATINGORIGIN: threshold is {FloatingOrigin.fetch.threshold}"); } 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 = Misc.Misc.RegisterGUIRect(new Rect()); + if (BDArmorySetup.WindowRectVesselSwitcher.size != default) windowSize = BDArmorySetup.WindowRectVesselSwitcher.size; + windowSize.x = Math.Max(windowSize.x, 350); + 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) { - 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(); + } + } + + void LateUpdate() + { + if (BDArmorySetup.showVesselSwitcherGUI) + GenerateVSEntries(); + } - // check for camera changes - if (_autoCameraSwitch) + void FixedUpdate() + { + if (!_ready) return; + + if (WeaponManagers.SelectMany(tm => tm.Value).Any(wm => wm == null)) upToDateWMs = false; + + if (vesselTraceEnabled) + { + 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) { - UpdateCamera(); + if (vessel == null) continue; + if (!vesselTraces.ContainsKey(vessel.vesselName)) vesselTraces[vessel.vesselName] = new List>(); + vesselTraces[vessel.vesselName].Add(new Tuple(Time.time, referenceRotationCorrection * (vessel.transform.position + floatingOriginCorrection), referenceRotationCorrection * vessel.transform.rotation)); } - - BDACompetitionMode.Instance.DoUpdate(); + if (survivingVessels.Count == 0) vesselTraceEnabled = false; } } @@ -186,23 +228,35 @@ 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()) { - if (v.Current == null || !v.Current.loaded || v.Current.packed) - continue; - using (var wms = v.Current.FindPartModulesImplementing().GetEnumerator()) - while (wms.MoveNext()) - if (wms.Current != null) - { - if (weaponManagers.TryGetValue(wms.Current.Team.Name, out var teamManagers)) - teamManagers.Add(wms.Current); - else - weaponManagers.Add(wms.Current.Team.Name, new List { wms.Current }); - break; - } + if (v.Current == null || !v.Current.loaded || v.Current.packed) continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(v.Current.vesselType)) continue; + var wm = v.Current.ActiveController().WM; + if (wm != null) + { + if (weaponManagers.TryGetValue(wm.Team.Name, out var teamManagers)) + teamManagers.Add(wm); + else + weaponManagers.Add(wm.Team.Name, [wm]); + } + } + + _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; } private void ToggleGuardModes() @@ -226,78 +280,165 @@ private void ToggleAutopilots() foreach (var weaponManager in autopilotsToToggle) { if (weaponManager == null) continue; - if (weaponManager.AI == null) continue; + var ai = weaponManager.AI; + if (ai == null) continue; if (_autoPilotEnabled) { - weaponManager.AI.ActivatePilot(); - BDArmory.Misc.Misc.fireNextNonEmptyStage(weaponManager.vessel); + ai.ActivatePilot(); + // Utils.fireNextNonEmptyStage(weaponManager.vessel); + // Trigger AG10 and then activate all engines if nothing was set on AG10. + weaponManager.vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); + if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(weaponManager.vessel) == 0) + { + if (SpawnUtils.CountActiveEngines(weaponManager.vessel) == 0) + SpawnUtils.ActivateAllEngines(weaponManager.vessel); + } } else { - weaponManager.AI.DeactivatePilot(); + ai.DeactivatePilot(); } } } private void OnGUI() { - if (_ready) + if (!(_ready && HighLogic.LoadedSceneIsFlight)) + return; + if (resizingWindow && Event.current.type == EventType.MouseUp) { resizingWindow = false; } + if (_showGui && (BDArmorySetup.GAME_UI_ENABLED || BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI)) { - if (_showGui && 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,//"BDA Vessel Switcher" - BDArmorySetup.BDGuiSkin.window); - Misc.Misc.UpdateGUIRect(BDArmorySetup.WindowRectVesselSwitcher, _guiCheckIndex); - } - else - { - Misc.Misc.UpdateGUIRect(new Rect(), _guiCheckIndex); - } + string windowTitle = StringUtils.Localize("#LOC_BDArmory_BDAVesselSwitcher_Title"); + if (BDArmorySettings.GRAVITY_HACKS) + windowTitle = windowTitle + " (" + BDACompetitionMode.gravityMultiplier.ToString("0.0") + "G)"; + + BDArmorySetup.SetGUIOpacity(); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectVesselSwitcher.position); + previousWindowHeight = BDArmorySetup.WindowRectVesselSwitcher.height; + BDArmorySetup.WindowRectVesselSwitcher = GUI.Window(10293444, BDArmorySetup.WindowRectVesselSwitcher, WindowVesselSwitcher, windowTitle, BDArmorySetup.BDGuiSkin.window); //"BDA Vessel Switcher" + ResizeWindow(); + BDArmorySetup.SetGUIOpacity(false); + } + else + { + GUIUtils.UpdateGUIRect(new Rect(), _guiCheckIndex); } } - private void SetNewHeight(float windowHeight) + public void SetVisible(bool visible) { - var previousWindowHeight = BDArmorySetup.WindowRectVesselSwitcher.height; - BDArmorySetup.WindowRectVesselSwitcher.height = windowHeight; - if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES && windowHeight < previousWindowHeight && 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); + BDArmorySetup.showVesselSwitcherGUI = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); + if (!visible && BDTeamSelector.Instance) BDTeamSelector.Instance.SetVisible(false); + } + + private void ResizeWindow() + { + if (resizingWindow) windowSize.x = Mathf.Clamp(windowSize.x, 350, Screen.width - BDArmorySetup.WindowRectVesselSwitcher.x); + BDArmorySetup.WindowRectVesselSwitcher.size = windowSize; + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselSwitcher, previousWindowHeight); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselSwitcher, _guiCheckIndex); + } + + 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 score.totalWPReached * TournamentScores.weights.GetValueOrDefault("Waypoint Count") + score.totalWPTime * TournamentScores.weights.GetValueOrDefault("Waypoint Time") + score.totalWPDeviation * TournamentScores.weights.GetValueOrDefault("Waypoint Deviation"); } private void WindowVesselSwitcher(int id) { - int numButtons = 9; - GUI.DragWindow(new Rect(3f * _buttonHeight + _margin, 0f, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - numButtons * _buttonHeight - 2f * _margin, _titleHeight)); + int leftButtonCount = 0, rightButtonCount = 0; - if (GUI.Button(new Rect(0f * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "><", BDArmorySetup.BDGuiSkin.button)) // Don't get so small that the buttons get hidden. + if (GUI.Button(new Rect(leftButtonCount++ * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "↕", BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { - BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH -= 50f; - if (BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 50f < 2f * _margin + numButtons * _buttonHeight) - BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH = 2f * _margin + numButtons * _buttonHeight; + BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING = !BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING; BDArmorySetup.SaveConfig(); } - if (GUI.Button(new Rect(1f * _buttonHeight + _margin, 4, _buttonHeight, _buttonHeight), "<>", BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(leftButtonCount++ * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "↔", BDArmorySettings.VESSEL_SWITCHER_WINDOW_ALIGNED ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + BDArmorySettings.VESSEL_SWITCHER_WINDOW_ALIGNED = !BDArmorySettings.VESSEL_SWITCHER_WINDOW_ALIGNED; + } + if (GUI.Button(new Rect(leftButtonCount++ * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "t", BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { - BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH += 50f; - if (BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH > Screen.width) // Don't go off the screen. - BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH = Screen.width; + BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE = !BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE; BDArmorySetup.SaveConfig(); } - if (GUI.Button(new Rect(2f * _buttonHeight + _margin, 4, _buttonHeight, _buttonHeight), "↕", BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(leftButtonCount++ * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "Sc", ScoreWindow.Instance.IsVisible ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { - BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING = !BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING; + ScoreWindow.Instance.SetVisible(!ScoreWindow.Instance.IsVisible); + } + if (GUI.Button(new Rect(leftButtonCount++ * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "UI", BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI = !BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI; BDArmorySetup.SaveConfig(); } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 6 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "M", BDACompetitionMode.Instance.killerGMenabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(windowSize.x - ++rightButtonCount * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), " X", BDArmorySetup.CloseButtonStyle)) + { + SetVisible(false); + return; + } + if (GUI.Button(new Rect(windowSize.x - ++rightButtonCount * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "T", _teamsAssigned ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + if (Event.current.button == 1) // Right click => original teams. + { + _teamsAssigned = true; + MassTeamSwitch(false, true); + } + else + { + // switch everyone onto different teams + _teamsAssigned = !_teamsAssigned; + MassTeamSwitch(_teamsAssigned); + } + } + if (GUI.Button(new Rect(windowSize.x - ++rightButtonCount * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "P", _autoPilotEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + // Toggle autopilots for everyone + ToggleAutopilots(); + } + if (GUI.Button(new Rect(windowSize.x - ++rightButtonCount * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "G", _guardModeEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + // switch everyon onto different teams + ToggleGuardModes(); + } + if (GUI.Button(new Rect(windowSize.x - ++rightButtonCount * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), camMode, _autoCameraSwitch ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + 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(windowSize.x - ++rightButtonCount * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "M", BDACompetitionMode.Instance.killerGMenabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { if (Event.current.button == 1) { @@ -311,401 +452,645 @@ 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)) + GUI.DragWindow(new Rect(leftButtonCount * _buttonHeight + _margin, 0f, windowSize.x - (leftButtonCount + rightButtonCount) * _buttonHeight - 3f * _margin, _titleHeight)); + GUI.Label(new Rect(windowSize.x - rightButtonCount * _buttonHeight - _margin - 70f, 4f, 70f, _titleHeight - 4f), BDArmorySetup.Version); + + float height = _titleHeight; + float vesselButtonWidth = windowSize.x - 2 * _margin - (!BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE || BDArmorySettings.TAG_MODE ? 6f : 5f) * _buttonHeight; + float teamMargin = (!BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE && weaponManagers.All(tm => tm.Value.Count() == 1)) ? 0 : _margin; + + // Show all the active vessels + foreach (var (teamName, entries) in VSEntryData) { - // set/disable automatic camera switching - _autoCameraSwitch = !_autoCameraSwitch; - Debug.Log("[BDArmory]: Setting AutoCameraSwitch"); + if (entries.Count == 0) continue; // Skip empty/dead teams + height += teamMargin; + if (BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE) // Show teams in sections. + { + if (BDTISetup.Instance.ColorAssignments.ContainsKey(teamName)) + { + BDTISetup.TILabel.normal.textColor = BDTISetup.Instance.ColorAssignments[teamName]; + } + GUI.Label(new Rect(_margin, height, windowSize.x - 2 * _margin, _buttonHeight), teamName, BDTISetup.TILabel); + height += _buttonHeight + _buttonGap; + } + foreach (var entry in entries) + { + if (entry == null) continue; + AddVesselSwitcherWindowEntry(teamName, entry, height, vesselButtonWidth); + height += _buttonHeight + _buttonGap; + } } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 4 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "G", _guardModeEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + height += _margin; + // add all the lost pilots at the bottom + if (!ContinuousSpawning.Instance.vesselsSpawningContinuously) // Don't show the dead vessels when continuously spawning. (Especially as command seats trigger all vessels as showing up as dead.) { - // switch everyon onto different teams - ToggleGuardModes(); + foreach (var player in BDACompetitionMode.Instance.Scores.deathOrder) + { + if (BDACompetitionMode.Instance.hasPinata && player == BDArmorySettings.PINATA_NAME) continue; // Ignore the piñata. + if (!deadVesselStrings.ContainsKey(player)) + { + 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, windowSize.x - 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.hasPinata && !BDACompetitionMode.Instance.pinataAlive) + { + if (!deadVesselStrings.ContainsKey(BDArmorySettings.PINATA_NAME)) + { + deadVesselString.Clear(); + deadVesselString.Append("Pinata Killers: "); + foreach (var player in BDACompetitionMode.Instance.Scores.Players) + { + 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()); + } + GUI.Label(new Rect(_margin, height, vesselButtonWidth, _buttonHeight), deadVesselStrings[BDArmorySettings.PINATA_NAME], BDArmorySetup.BDGuiSkin.label); + height += _buttonHeight + _buttonGap; } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 3 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "P", _autoPilotEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + height += _margin; + #region Resizing + windowSize.y = Mathf.Lerp(windowSize.y, Mathf.Max(height, _titleHeight + _buttonHeight), 0.15f); + var resizeRect = new Rect(windowSize.x - 16, windowSize.y - 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 (resizingWindow && Event.current.type == EventType.Repaint) windowSize.x += Mouse.delta.x / BDArmorySettings.UI_SCALE_ACTUAL; + #endregion + } + + StringBuilder VSEntryString = new StringBuilder(); + void AddVesselSwitcherWindowEntry(string team, VSVesselData vd, float height, float vesselButtonWidth) + { + // Team label + float _offset = 0; + if (!BDArmorySettings.VESSEL_SWITCHER_WINDOW_OLD_DISPLAY_STYLE || BDArmorySettings.TAG_MODE) { - // Toggle autopilots for everyone - ToggleAutopilots(); + if (BDTISetup.Instance.ColorAssignments.ContainsKey(team)) + { + BDTISetup.TILabel.normal.textColor = BDTISetup.Instance.ColorAssignments[team]; + } + GUI.Label(new Rect(_margin, height, _buttonHeight, _buttonHeight), $"{(team.Length > 2 ? team.Remove(2) : team)}", BDTISetup.TILabel); + _offset = _buttonHeight; } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "T", _freeForAll ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + // Vessel button + Rect buttonRect = new(_margin + _offset, height, vesselButtonWidth, _buttonHeight); + if (BDArmorySettings.VESSEL_SWITCHER_WINDOW_ALIGNED) { - // switch everyone onto different teams - _freeForAll = !_freeForAll; - MassTeamSwitch(_freeForAll); + vd.vesselButtonStyle.alignment = TextAnchor.MiddleLeft; + if (GUI.Button(buttonRect, $"{vd.vesselState}", vd.vesselButtonStyle)) + ForceSwitchVessel(vd.wm.vessel); + var rightSideRect = new Rect(buttonRect.x + buttonRect.width / 2, buttonRect.y, buttonRect.width / 2, buttonRect.height); + var rightSideStyle = new GUIStyle() { alignment = TextAnchor.MiddleLeft }; + GUI.Label(rightSideRect, $"{vd.status}", rightSideStyle); + rightSideStyle.alignment = TextAnchor.MiddleRight; + GUI.Label(rightSideRect, $"{vd.targetData?.targetName} ", rightSideStyle); + } + else + { + if (GUI.Button(buttonRect, $"{vd.vesselState} {vd.status} {vd.targetData?.targetName}", vd.vesselButtonStyle)) + ForceSwitchVessel(vd.wm.vessel); } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "X", BDArmorySetup.BDGuiSkin.button)) + // Current target / threat button + if (vd.targetData != null) { - BDArmorySetup.Instance.showVesselSwitcherGUI = false; - return; + Rect targetingButtonRect = new(_margin + vesselButtonWidth + _offset, height, _buttonHeight, _buttonHeight); + if (GUI.Button(targetingButtonRect, vd.targetData.isThreat ? "><" : "[]", vd.targetData.targetStyle)) + ForceSwitchVessel(vd.targetData.vessel); } - float height = _titleHeight; - float vesselButtonWidth = BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _margin - 6f * _buttonHeight; + // Guard toggle + Rect guardButtonRect = new(_margin + vesselButtonWidth + _offset + _buttonHeight, height, _buttonHeight, _buttonHeight); + if (GUI.Button(guardButtonRect, "G", vd.guardStyle)) + { + vd.wm.ToggleGuardMode(); + } - // Show all the active vessels + // AI toggle + if (vd.ai != null) + { + Rect aiButtonRect = new(_margin + vesselButtonWidth + _offset + 2 * _buttonHeight, height, _buttonHeight, _buttonHeight); + if (GUI.Button(aiButtonRect, "P", vd.aiStyle)) + { + vd.ai.TogglePilot(); + if (Event.current.button == 1 && !vd.ai.pilotEnabled) // Right click, trigger AG10 / activate engines + { + // Trigger AG10 and then activate all engines if nothing was set on AG10. + vd.wm.vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); + if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vd.wm.vessel) == 0) + { + if (SpawnUtils.CountActiveEngines(vd.wm.vessel) == 0) + SpawnUtils.ActivateAllEngines(vd.wm.vessel); + } + } + } + } + + // Team toggle + Rect teamButtonRect = new(_margin + vesselButtonWidth + _offset + 3 * _buttonHeight, height, _buttonHeight, _buttonHeight); + if (GUI.Button(teamButtonRect, "T", BDArmorySetup.BDGuiSkin.button)) + { + if (Event.current.button == 1) + { + BDTeamSelector.Instance.Open(vd.wm, new Vector2(Input.mousePosition.x + 2 * _buttonHeight + _margin, Screen.height - Input.mousePosition.y)); + } + else if (Event.current.button == 2) + { + //wm.SetTeam(BDTeam.Get("Neutral")); + //if (wm.Team.Name != "Neutral" && wm.Team.Name != "A" && wm.Team.Name != "B") wm.Team.Neutral = !wm.Team.Neutral; + vd.wm.NextTeam(true); + } + else + { + vd.wm.NextTeam(); + } + } + + // BIG RED BUTTON + Rect killButtonRect = new(_margin + vesselButtonWidth + _offset + 4 * _buttonHeight, height, _buttonHeight, _buttonHeight); + if (vd.wm.vessel != null && GUI.Button(killButtonRect, "X", vd.xStyle)) + { + // must use right button + if (Event.current.button == 1) + { + if (BDACompetitionMode.Instance.Scores.ScoreData.TryGetValue(vd.vesselName, out var scoreData)) + { + if (scoreData.lastPersonWhoDamagedMe == "") + { + scoreData.lastPersonWhoDamagedMe = "BIG RED BUTTON"; // only do this if it's not already damaged + } + BDACompetitionMode.Instance.Scores.RegisterDeath(vd.vesselName, GMKillReason.BigRedButton); // Indicate that it was us who killed it. + BDACompetitionMode.Instance.competitionStatus.Add($"{vd.vesselName} {(BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vd.vesselName) ? " (HoS)" : "")} was killed by the BIG RED BUTTON."); + } + VesselUtils.ForceDeadVessel(vd.wm.vessel); + } + } + } + + class VSVesselData + { + public string vesselName; + public string vesselState; + public string status; + public MissileFire wm; + public IBDAIControl ai; + public VSTargetData targetData; + public GUIStyle vesselButtonStyle; + public GUIStyle guardStyle; + public GUIStyle aiStyle; + public GUIStyle xStyle; + } + class VSTargetData + { + public string targetName; + public Vessel vessel; + public bool isThreat; + public float distSqr; + public GUIStyle targetStyle = BDArmorySetup.BDGuiSkin.button; + } + readonly List<(string, List)> VSEntryData = []; + /// + /// Generate the data to be used to populate the VS entries. + /// + void GenerateVSEntries() + { + #region Active Vessels + VSEntryData.Clear(); if (BDArmorySettings.VESSEL_SWITCHER_WINDOW_SORTING) { if (BDArmorySettings.TAG_MODE) { // Sort vessels based on total tag time or tag scores. - var orderedWMs = weaponManagers.SelectMany(tm => tm.Value, (tm, weaponManager) => new Tuple(tm.Key, weaponManager)).ToList(); // Use a local copy. - if (VesselSpawner.Instance.vesselsSpawningContinuously && orderedWMs.All(mf => mf != null && BDACompetitionMode.Instance.Scores.ContainsKey(mf.Item2.vessel.vesselName) && VesselSpawner.Instance.continuousSpawningScores.ContainsKey(mf.Item2.vessel.vesselName))) - orderedWMs.Sort((mf1, mf2) => ((VesselSpawner.Instance.continuousSpawningScores[mf2.Item2.vessel.vesselName].cumulativeTagTime + BDACompetitionMode.Instance.Scores[mf2.Item2.vessel.vesselName].tagTotalTime).CompareTo(VesselSpawner.Instance.continuousSpawningScores[mf1.Item2.vessel.vesselName].cumulativeTagTime + BDACompetitionMode.Instance.Scores[mf1.Item2.vessel.vesselName].tagTotalTime))); - else if (orderedWMs.All(mf => mf != null && BDACompetitionMode.Instance.Scores.ContainsKey(mf.Item2.vessel.vesselName))) - orderedWMs.Sort((mf1, mf2) => (BDACompetitionMode.Instance.Scores[mf2.Item2.vessel.vesselName].tagScore.CompareTo(BDACompetitionMode.Instance.Scores[mf1.Item2.vessel.vesselName].tagScore))); + var orderedWMs = weaponManagers.SelectMany(tm => tm.Value, (tm, weaponManager) => new Tuple(tm.Key, weaponManager)).Where(t => t.Item2 != null && t.Item2.vessel != null).ToList(); + if (ContinuousSpawning.Instance.vesselsSpawningContinuously && orderedWMs.All(mf => BDACompetitionMode.Instance.Scores.ScoreData.ContainsKey(mf.Item2.vessel.vesselName) && ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(mf.Item2.vessel.vesselName))) + orderedWMs.Sort((mf1, mf2) => (ContinuousSpawning.Instance.continuousSpawningScores[mf2.Item2.vessel.vesselName].cumulativeTagTime + BDACompetitionMode.Instance.Scores.ScoreData[mf2.Item2.vessel.vesselName].tagTotalTime).CompareTo(ContinuousSpawning.Instance.continuousSpawningScores[mf1.Item2.vessel.vesselName].cumulativeTagTime + BDACompetitionMode.Instance.Scores.ScoreData[mf1.Item2.vessel.vesselName].tagTotalTime)); + else if (orderedWMs.All(mf => BDACompetitionMode.Instance.Scores.ScoreData.ContainsKey(mf.Item2.vessel.vesselName))) + orderedWMs.Sort((mf1, mf2) => BDACompetitionMode.Instance.Scores.ScoreData[mf2.Item2.vessel.vesselName].tagScore.CompareTo(BDACompetitionMode.Instance.Scores.ScoreData[mf1.Item2.vessel.vesselName].tagScore)); foreach (var weaponManagerPair in orderedWMs) { - if (weaponManagerPair.Item2 == null) continue; try { - AddVesselSwitcherWindowEntry(weaponManagerPair.Item2, weaponManagerPair.Item1, height, vesselButtonWidth); + VSEntryData.Add((weaponManagerPair.Item1, [GenerateVSEntry(weaponManagerPair.Item2)])); } catch (Exception e) { - Debug.LogError("DEBUG AddVesselSwitcherWindowEntry threw an exception trying to add " + weaponManagerPair.Item2.vessel.vesselName + " on team " + weaponManagerPair.Item1 + " to the list: " + e.Message); + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: GenerateVSEntry threw an exception trying to add {weaponManagerPair.Item2.vessel.vesselName} on team {weaponManagerPair.Item1} to the list: {e.Message}"); + } + } + } + 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(); + orderedWMs.Sort((mf1, mf2) => WaypointRank(mf2.Item2.vessel.vesselName).CompareTo(WaypointRank(mf1.Item2.vessel.vesselName))); + foreach (var weaponManagerPair in orderedWMs) + { + try + { + VSEntryData.Add((weaponManagerPair.Item1, [GenerateVSEntry(weaponManagerPair.Item2)])); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: GenerateVSEntry threw an exception trying to add {weaponManagerPair.Item2.vessel.vesselName} on team {weaponManagerPair.Item1} to the list: {e.Message}"); } - height += _buttonHeight + _buttonGap; } } else // Sorting of teams by hit counts. { - var orderedTeamManagers = weaponManagers.Select(tm => new Tuple>(tm.Key, tm.Value)).ToList(); - if (VesselSpawner.Instance.vesselsSpawningContinuously) + var orderedTeamManagers = weaponManagers.Select(tm => new Tuple>(tm.Key, [.. tm.Value.Where(wm => wm != null && wm.vessel != null && !string.IsNullOrEmpty(wm.vessel.vesselName))])).ToList(); + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) { foreach (var teamManager in orderedTeamManagers) - teamManager.Item2.Sort((wm1, wm2) => ((VesselSpawner.Instance.continuousSpawningScores.ContainsKey(wm2.vessel.vesselName) ? VesselSpawner.Instance.continuousSpawningScores[wm2.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.ContainsKey(wm2.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm2.vessel.vesselName].Score : 0)).CompareTo((VesselSpawner.Instance.continuousSpawningScores.ContainsKey(wm1.vessel.vesselName) ? VesselSpawner.Instance.continuousSpawningScores[wm1.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.ContainsKey(wm1.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm1.vessel.vesselName].Score : 0))); // Sort within each team by cumulative hits. - orderedTeamManagers.Sort((tm1, tm2) => (tm2.Item2.Sum(wm => (VesselSpawner.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName) ? VesselSpawner.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.ContainsKey(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm.vessel.GetName()].Score : 0)).CompareTo(tm1.Item2.Sum(wm => (VesselSpawner.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName) ? VesselSpawner.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.ContainsKey(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm.vessel.GetName()].Score : 0))))); // Sort teams by total cumulative hits. + 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.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.ContainsKey(wm2.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm2.vessel.vesselName].Score : 0).CompareTo(BDACompetitionMode.Instance.Scores.ContainsKey(wm1.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm1.vessel.vesselName].Score : 0)); // Sort within each team by hits. - orderedTeamManagers.Sort((tm1, tm2) => (tm2.Item2.Sum(wm => BDACompetitionMode.Instance.Scores.ContainsKey(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm.vessel.GetName()].Score : 0).CompareTo(tm1.Item2.Sum(wm => BDACompetitionMode.Instance.Scores.ContainsKey(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores[wm.vessel.GetName()].Score : 0)))); // Sort teams by total hits. + 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.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) { - height += _margin; - foreach (var weaponManager in teamManager.Item2) + var (teamName, teamMembers) = teamManager; + if (teamMembers.Count == 0) continue; + if (teamMembers.First().Team.Neutral && teamName != "Neutral") teamName += " (Neutral)"; + List vsEntries = []; + foreach (var weaponManager in teamMembers) { - if (weaponManager == null) continue; try { - AddVesselSwitcherWindowEntry(weaponManager, teamManager.Item1, height, vesselButtonWidth); + vsEntries.Add(GenerateVSEntry(weaponManager)); } catch (Exception e) { - Debug.LogError("DEBUG AddVesselSwitcherWindowEntry threw an exception trying to add " + weaponManager.vessel.vesselName + " on team " + teamManager.Item1 + " to the list: " + e.Message); + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: GenerateVSEntry threw an exception trying to add {weaponManager.vessel.vesselName} on team {teamName} to the list: {e.Message}"); } - height += _buttonHeight + _buttonGap; } + VSEntryData.Add((teamName, vsEntries)); } } } else // Regular sorting. - foreach (var teamManagers in weaponManagers.ToList()) // Use a copy as something seems to be modifying the list occassionally. + { + foreach (var teamManager in weaponManagers) { - height += _margin; - foreach (var weaponManager in teamManagers.Value) + var teamMembers = teamManager.Value.Where(wm => wm != null && wm.vessel != null && !string.IsNullOrEmpty(wm.vessel.vesselName)).ToList(); + if (teamMembers.Count == 0) continue; + var teamName = teamManager.Key; + if (teamMembers.First().Team.Neutral && teamName != "Neutral") teamName += " (Neutral)"; + List vsEntries = []; + foreach (var weaponManager in teamMembers) { - if (weaponManager == null) continue; try { - AddVesselSwitcherWindowEntry(weaponManager, teamManagers.Key, height, vesselButtonWidth); + vsEntries.Add(GenerateVSEntry(weaponManager)); } catch (Exception e) { - Debug.LogError("DEBUG AddVesselSwitcherWindowEntry threw an exception trying to add " + weaponManager.vessel.vesselName + " on team " + teamManagers.Key + " to the list: " + e.Message); + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: GenerateVSEntry threw an exception trying to add {weaponManager.vessel.vesselName} on team {teamName} to the list: {e.Message}"); } - height += _buttonHeight + _buttonGap; } + VSEntryData.Add((teamName, vsEntries)); } + } + #endregion - height += _margin; - // add all the lost pilots at the bottom - if (!VesselSpawner.Instance.vesselsSpawningContinuously) // Don't show the dead vessels when continuously spawning. (Especially as command seats trigger all vessels as showing up as dead.) - foreach (var key in BDACompetitionMode.Instance.DeathOrder.Keys) + #region Dead Vessel Strings + if (!ContinuousSpawning.Instance.vesselsSpawningContinuously) // Don't show the dead vessels when continuously spawning. (Especially as command seats trigger all vessels as showing up as dead.) + { + foreach (var player in BDACompetitionMode.Instance.Scores.deathOrder) { - string statusString = ""; - if (BDACompetitionMode.Instance.Scores.ContainsKey(key)) + if (BDACompetitionMode.Instance.hasPinata && player == BDArmorySettings.PINATA_NAME) continue; // Ignore the piñata. + if (!deadVesselStrings.ContainsKey(player)) { + deadVesselString.Clear(); + var playerScoreData = BDACompetitionMode.Instance.Scores.ScoreData[player]; // DEAD :: vesselName([, ][, ])[ KILLED|RAMMED BY ], where is the number of hits made is the number of parts destroyed. - statusString += "DEAD " + BDACompetitionMode.Instance.DeathOrder[key].Item1 + ":" + BDACompetitionMode.Instance.DeathOrder[key].Item2.ToString("0.0") + " : " + key + " (" + BDACompetitionMode.Instance.Scores[key].Score.ToString(); - if (BDACompetitionMode.Instance.Scores[key].totalDamagedPartsDueToMissiles > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores[key].totalDamagedPartsDueToMissiles; - if (BDACompetitionMode.Instance.Scores[key].totalDamagedPartsDueToRamming > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores[key].totalDamagedPartsDueToRamming; - if (VesselSpawner.Instance.vesselsSpawningContinuously && BDACompetitionMode.Instance.Scores[key].tagTotalTime > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores[key].tagTotalTime.ToString("0.0"); - else if (BDACompetitionMode.Instance.Scores[key].tagScore > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores[key].tagScore.ToString("0.0"); - switch (BDACompetitionMode.Instance.Scores[key].LastDamageWasFrom()) + deadVesselString.Append($"DEAD {playerScoreData.deathOrder}:{playerScoreData.deathTime:0.0} : {player} ({playerScoreData.hits} hits"); + if (playerScoreData.totalDamagedPartsDueToRockets > 0) + deadVesselString.Append($", {playerScoreData.totalDamagedPartsDueToRockets} rkt"); + if (playerScoreData.totalDamagedPartsDueToMissiles > 0) + deadVesselString.Append($", {playerScoreData.totalDamagedPartsDueToMissiles} mis"); + if (playerScoreData.totalDamagedPartsDueToRamming > 0) + deadVesselString.Append($", {playerScoreData.totalDamagedPartsDueToRamming} ram"); + if (ContinuousSpawning.Instance.vesselsSpawningContinuously && playerScoreData.tagTotalTime > 0) + deadVesselString.Append($", {playerScoreData.tagTotalTime:0.0} tag"); + else if (playerScoreData.tagScore > 0) + deadVesselString.Append($", {playerScoreData.tagScore:0.0} tag"); + switch (playerScoreData.lastDamageWasFrom) { - case DamageFrom.Bullet: - statusString += ") KILLED BY " + BDACompetitionMode.Instance.Scores[key].LastPersonWhoDamagedMe() + (BDACompetitionMode.Instance.Scores[key].cleanDeath ? " (Head-shot!)" : ", et al."); + case DamageFrom.Guns: + deadVesselString.Append($") KILLED BY {playerScoreData.lastPersonWhoDamagedMe}"); break; - case DamageFrom.Missile: - statusString += ") EXPLODED BY " + BDACompetitionMode.Instance.Scores[key].LastPersonWhoDamagedMe() + (BDACompetitionMode.Instance.Scores[key].cleanDeath ? " (Head-shot!)" : ", et al."); + case DamageFrom.Rockets: + deadVesselString.Append($") FRAGGED BY {playerScoreData.lastPersonWhoDamagedMe}"); break; - case DamageFrom.Ram: - statusString += ") RAMMED BY " + BDACompetitionMode.Instance.Scores[key].LastPersonWhoDamagedMe() + (BDACompetitionMode.Instance.Scores[key].cleanDeath ? " (Head-shot!)" : ", et al."); + case DamageFrom.Missiles: + deadVesselString.Append($") EXPLODED BY {playerScoreData.lastPersonWhoDamagedMe}"); break; - default: - statusString += ")"; + case DamageFrom.Ramming: + deadVesselString.Append($") RAMMED BY {playerScoreData.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($") {playerScoreData.gmKillReason}"); + break; + default: // Note: All the cases ought to be covered above. + deadVesselString.Append(")"); break; } - GUI.Label(new Rect(_margin, height, vesselButtonWidth, _buttonHeight), statusString, BDArmorySetup.BDGuiSkin.label); - height += _buttonHeight + _buttonGap; + switch (playerScoreData.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()); } } + } // Piñata killers. - if (!BDACompetitionMode.Instance.pinataAlive) + if (BDACompetitionMode.Instance.hasPinata && !BDACompetitionMode.Instance.pinataAlive) { - string postString = ""; - foreach (var killer in BDACompetitionMode.Instance.Scores.Keys) + if (!deadVesselStrings.ContainsKey(BDArmorySettings.PINATA_NAME)) { - if (BDACompetitionMode.Instance.Scores[killer].PinataHits > 0) + deadVesselString.Clear(); + deadVesselString.Append("Pinata Killers: "); + foreach (var player in BDACompetitionMode.Instance.Scores.Players) { - postString += " " + killer; + if (BDACompetitionMode.Instance.Scores.ScoreData[player].PinataHits > 0) //not reporting any players? + { + deadVesselString.Append($" {player};"); + //BDACompetitionMode.Instance.Scores.ScoreData[BDArmorySettings.PINATA_NAME].lastPersonWhoDamagedMe + } } - } - if (postString != "") - { - GUI.Label(new Rect(_margin, height, vesselButtonWidth, _buttonHeight), "Pinata Killers: " + postString, BDArmorySetup.BDGuiSkin.label); - height += _buttonHeight + _buttonGap; + deadVesselStrings.Add(BDArmorySettings.PINATA_NAME, deadVesselString.ToString()); } } - - height += _margin; - _windowHeight = height; + #endregion } - void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, float vesselButtonWidth) + /// + /// Generate state, status, target strings for the vessel switcher entry. + /// + /// + /// + VSVesselData GenerateVSEntry(MissileFire wm) { - GUI.Label(new Rect(_margin, height, _buttonHeight, _buttonHeight), $"{team}", BDArmorySetup.BDGuiSkin.label); - Rect buttonRect = new Rect(_margin + _buttonHeight, 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(); - BDArmory.Control.ScoringData scoreData = null; - string status = UpdateVesselStatus(wm, vButtonStyle); - int currentScore = 0; - int currentRamScore = 0; - int currentMissileScore = 0; - double currentTagTime = 0; - double currentTagScore = 0; - int currentTimesIt = 0; - - if (BDACompetitionMode.Instance.Scores.ContainsKey(vesselName)) - { - scoreData = BDACompetitionMode.Instance.Scores[vesselName]; - currentScore = scoreData.Score; - currentRamScore = scoreData.totalDamagedPartsDueToRamming; - currentMissileScore = scoreData.totalDamagedPartsDueToMissiles; - if (BDArmorySettings.TAG_MODE) - { - currentTagTime = scoreData.tagTotalTime; - currentTagScore = scoreData.tagScore; - currentTimesIt = scoreData.tagTimesIt; - } - } - if (VesselSpawner.Instance.vesselsSpawningContinuously) + if (wm == null) { - if (VesselSpawner.Instance.continuousSpawningScores.ContainsKey(vesselName)) - { - currentScore += VesselSpawner.Instance.continuousSpawningScores[vesselName].cumulativeHits; - currentRamScore += VesselSpawner.Instance.continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToRamming; - currentMissileScore += VesselSpawner.Instance.continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToMissiles; - } - if (BDArmorySettings.TAG_MODE && VesselSpawner.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName)) - currentTagTime += VesselSpawner.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeTagTime; + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: Trying to generate VS entry for null WM!"); + return null; } - - // current target - string targetName = ""; - Vessel targetVessel = wm.vessel; - bool incomingThreat = false; - if (wm.incomingThreatVessel != null) + var vd = new VSVesselData() { - incomingThreat = true; - targetName = "<<<" + wm.incomingThreatVessel.GetName(); - targetVessel = wm.incomingThreatVessel; - } - else if (wm.currentTarget) + wm = wm, + ai = wm.AI, + vesselName = wm.vessel.vesselName, + vesselButtonStyle = new GUIStyle(wm.team == "IT" ? (wm.vessel.isActiveVessel ? ItVesselSelected : ItVessel) : wm.vessel.isActiveVessel ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button), + guardStyle = wm.guardMode ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button + }; + + VSEntryString.Clear(); + if (wm.vessel.isActiveVessel) VSEntryString.Append(" "); // The box style is poorly aligned. + if (ContinuousSpawning.Instance.vesselsSpawningContinuously && BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0 && ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(vd.vesselName)) { - targetName = ">>>" + wm.currentTarget.Vessel.GetName(); - targetVessel = wm.currentTarget.Vessel; + VSEntryString.Append($"(Lives:{BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL - (ContinuousSpawning.Instance.continuousSpawningScores[vd.vesselName].spawnCount - 1)}) "); } + VSEntryString.Append(vd.vesselName); + if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vd.vesselName)) VSEntryString.Append(" (HoS)"); + VSEntryString.Append(UpdateVesselStatus(vd.ai, vd.vesselButtonStyle)); // status + { // Scoring + int currentScore = 0; + int currentRocketScore = 0; + int currentRamScore = 0; + int currentMissileScore = 0; + double currentTagTime = 0; + double currentTagScore = 0; + // int currentTimesIt = 0; + + if (BDACompetitionMode.Instance.Scores.ScoreData.TryGetValue(vd.vesselName, out ScoringData scoreData)) + { + currentScore = scoreData.hits; + currentRocketScore = scoreData.totalDamagedPartsDueToRockets; + currentRamScore = scoreData.totalDamagedPartsDueToRamming; + currentMissileScore = scoreData.totalDamagedPartsDueToMissiles; + if (BDArmorySettings.TAG_MODE) + { + currentTagTime = scoreData.tagTotalTime; + currentTagScore = scoreData.tagScore; + // currentTimesIt = scoreData.tagTimesIt; + } + } + if (ContinuousSpawning.Instance.vesselsSpawningContinuously && ContinuousSpawning.Instance.continuousSpawningScores.TryGetValue(vd.vesselName, out var csData)) + { + currentScore += csData.cumulativeHits; + currentRocketScore += csData.cumulativeDamagedPartsDueToRockets; + currentRamScore += csData.cumulativeDamagedPartsDueToRamming; + currentMissileScore += csData.cumulativeDamagedPartsDueToMissiles; + if (BDArmorySettings.TAG_MODE) + currentTagTime += csData.cumulativeTagTime; + } - string postStatus = " (" + currentScore.ToString(); - if (currentMissileScore > 0) postStatus += ", " + currentMissileScore.ToString(); - if (currentRamScore > 0) postStatus += ", " + currentRamScore.ToString(); - if (BDArmorySettings.TAG_MODE) - postStatus += ", " + (VesselSpawner.Instance.vesselsSpawningContinuously ? currentTagTime.ToString("0.0") : currentTagScore.ToString("0.0")); - postStatus += ")"; - - if (wm.AI != null && wm.AI.currentStatus != null) - { - postStatus += " " + wm.AI.currentStatus; + if (BDArmorySettings.WAYPOINTS_MODE) + { + if (scoreData != null) // This probably won't work if running waypoints in continuous spawning mode, but that probably doesn't work anyway! + { + if (BDArmorySettings.WAYPOINT_GUARD_INDEX >= 0 && currentScore > 0) VSEntryString.Append($" ({currentScore} hits)"); + VSEntryString.Append($" ({scoreData.totalWPReached:0}, {scoreData.totalWPTime:0.00}s, {scoreData.totalWPDeviation:0.00}m), "); + } + } + else + { + 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(")"); + } } - float targetDistance = 5000; - if (wm.currentTarget != null) + if (BDACompetitionMode.Instance.KillTimer.ContainsKey(vd.vesselName)) { - targetDistance = Vector3.Distance(wm.vessel.GetWorldPos3D(), wm.currentTarget.position); + VSEntryString.Append($" x{BDACompetitionMode.Instance.KillTimer[vd.vesselName]}x"); } + vd.vesselState = VSEntryString.ToString(); - //postStatus += " :" + Convert.ToInt32(wm.vessel.srfSpeed).ToString(); - // display killerGM stats - //if ((BDACompetitionMode.Instance.killerGMenabled) && BDACompetitionMode.Instance.FireCount.ContainsKey(vesselName)) - //{ - // postStatus += " " + (BDACompetitionMode.Instance.FireCount[vesselName] + BDACompetitionMode.Instance.FireCount2[vesselName]).ToString() + ":" + Convert.ToInt32(BDACompetitionMode.Instance.AverageSpeed[vesselName] / BDACompetitionMode.Instance.averageCount).ToString(); - //} - - if (BDACompetitionMode.Instance.KillTimer.ContainsKey(vesselName)) + if (vd.ai != null) { - postStatus += " x" + BDACompetitionMode.Instance.KillTimer[vesselName].ToString() + "x"; + vd.status = vd.ai.currentStatus; + vd.aiStyle = new GUIStyle(vd.ai.pilotEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + if (vd.wm.underFire) + { + var distSqr = (vd.wm.vessel.CoM - vd.wm.incomingThreatPosition).sqrMagnitude; + vd.aiStyle.normal.textColor = distSqr < 25e4f ? Color.red : distSqr < 1e6f ? Color.yellow : Color.blue; // <500m, <1km, >=1km + } } - if (targetName != "") + if (wm.incomingThreatVessel != null) // Incoming threats { - postStatus += " " + targetName; + vd.targetData = new VSTargetData() + { + targetName = $"<<<{wm.incomingThreatVessel.vesselName}", + vessel = wm.incomingThreatVessel, + isThreat = true, + distSqr = (wm.incomingThreatVessel.CoM - wm.vessel.CoM).sqrMagnitude, + }; } - - /*if (cameraScores.ContainsKey(vesselName)) + else if (wm.currentTarget != null) { - int sc = (int)(cameraScores[vesselName]); - postStatus += " [" + sc.ToString() + "]"; + vd.targetData = new VSTargetData() + { + targetName = $">>>{wm.currentTarget.Vessel.vesselName}", + vessel = wm.currentTarget.Vessel, + isThreat = false, + distSqr = (wm.currentTarget.Vessel.CoM - wm.vessel.CoM).sqrMagnitude + }; } - */ - - if (GUI.Button(buttonRect, status + vesselName + postStatus, vButtonStyle)) - ForceSwitchVessel(wm.vessel); - - // selects current target - if (targetName != "") + if (vd.targetData != null) { - Rect targettingButtonRect = new Rect(_margin + vesselButtonWidth + _buttonHeight, height, _buttonHeight, _buttonHeight); - GUIStyle targButton = BDArmorySetup.BDGuiSkin.button; - if (wm.currentGun != null && wm.currentGun.recentlyFiring) + if (vd.targetData.isThreat || vd.wm.currentGun != null && vd.wm.currentGun.recentlyFiring) { - if (targetDistance < 500) - { - targButton = redLight; - } - else if (targetDistance < 1000) - { - targButton = yellowLight; - } - else - { - targButton = blueLight; - } + vd.targetData.targetStyle = vd.targetData.distSqr < 25e4f ? redLight : vd.targetData.distSqr < 1e6f ? yellowLight : blueLight; // <500m, <1km, >=1km } - if (GUI.Button(targettingButtonRect, incomingThreat ? "><" : "[]", targButton)) - ForceSwitchVessel(targetVessel); } - //guard toggle - GUIStyle guardStyle = wm.guardMode ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - Rect guardButtonRect = new Rect(_margin + vesselButtonWidth + 2 * _buttonHeight, height, _buttonHeight, _buttonHeight); - if (GUI.Button(guardButtonRect, "G", guardStyle)) - wm.ToggleGuardMode(); - - //AI toggle - if (wm.AI != null) + vd.xStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); + if (BDACompetitionMode.Instance.Scores.Players.Contains(vd.vesselName)) { - GUIStyle aiStyle = new GUIStyle(wm.AI.pilotEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); - if (wm.underFire) + var scoreData = BDACompetitionMode.Instance.Scores.ScoreData[vd.vesselName]; + if (scoreData != null) { - var distance = Vector3.Distance(wm.vessel.GetWorldPos3D(), wm.incomingThreatPosition); - if (distance < 500) + var currentParts = wm.vessel.parts.Count; + if (currentParts < scoreData.previousPartCount) { - aiStyle.normal.textColor = Color.red; + vd.xStyle.normal.textColor = Color.red; } - else if (distance < 1000) - { - aiStyle.normal.textColor = Color.yellow; - } - else + else if (Planetarium.GetUniversalTime() - scoreData.lastDamageTime < 4) { - aiStyle.normal.textColor = Color.blue; + vd.xStyle.normal.textColor = Color.yellow; } } - Rect aiButtonRect = new Rect(_margin + vesselButtonWidth + 3 * _buttonHeight, height, _buttonHeight, - _buttonHeight); - if (GUI.Button(aiButtonRect, "P", aiStyle)) - wm.AI.TogglePilot(); } - //team toggle - Rect teamButtonRect = new Rect(_margin + vesselButtonWidth + 4 * _buttonHeight, height, - _buttonHeight, _buttonHeight); - if (GUI.Button(teamButtonRect, "T", BDArmorySetup.BDGuiSkin.button)) - { - if (Event.current.button == 1) - { - BDTeamSelector.Instance.Open(wm, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); - } - else - { - wm.NextTeam(); - } - } + return vd; + } - // boom - Rect killButtonRect = new Rect(_margin + vesselButtonWidth + 5 * _buttonHeight, height, _buttonHeight, _buttonHeight); - GUIStyle xStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); - var currentParts = wm.vessel.parts.Count; - if (scoreData != null) + StringBuilder vesselStatusString = new StringBuilder(); + private string UpdateVesselStatus(IBDAIControl ai, GUIStyle vButtonStyle) + { + vesselStatusString.Clear(); + if (ai != null) { - if (currentParts < scoreData.previousPartCount) + var surfaceAI = ai as BDModuleSurfaceAI; + if (surfaceAI == null && ai.vessel.LandedOrSplashed) { - xStyle.normal.textColor = Color.red; + vesselStatusString.Append(" "); + if (ai.vessel.Landed) + vesselStatusString.Append(StringUtils.Localize("#LOC_BDArmory_VesselStatus_Landed")); // "(Landed)" + else if (ai.vessel.IsUnderwater()) + vesselStatusString.Append(StringUtils.Localize("#LOC_BDArmory_VesselStatus_Underwater")); // "(Underwater)" + else + vesselStatusString.Append(StringUtils.Localize("#LOC_BDArmory_VesselStatus_Splashed")); // "(Splashed)" + vButtonStyle.fontStyle = FontStyle.Italic; } - else if (Planetarium.GetUniversalTime() - scoreData.lastHitTime < 4 || Planetarium.GetUniversalTime() - scoreData.lastRammedTime < 4) + else if (surfaceAI != null && surfaceAI.currentStatusMode == BDModuleSurfaceAI.StatusMode.Panic) // Surface AIs have their own panic modes. { - xStyle.normal.textColor = Color.yellow; + vButtonStyle.fontStyle = FontStyle.Italic; } - } - if (wm.vessel != null && GUI.Button(killButtonRect, "X", xStyle)) - { - // must use right button - if (Event.current.button == 1) + else { - if (scoreData != null) - { - if (scoreData.LastPersonWhoDamagedMe() == "") - { - scoreData.lastPersonWhoHitMe = "BIG RED BUTTON"; // only do this if it's not already damaged - } - scoreData.gmKillReason = GMKillReason.BigRedButton; // Indicate that it was us who killed it and remove any "clean" kills. - if (BDACompetitionMode.Instance.whoCleanShotWho.ContainsKey(vesselName)) BDACompetitionMode.Instance.whoCleanShotWho.Remove(vesselName); - if (BDACompetitionMode.Instance.whoCleanRammedWho.ContainsKey(vesselName)) BDACompetitionMode.Instance.whoCleanRammedWho.Remove(vesselName); - if (BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles.ContainsKey(vesselName)) BDACompetitionMode.Instance.whoCleanShotWhoWithMissiles.Remove(vesselName); - } - Misc.Misc.ForceDeadVessel(wm.vessel); + vButtonStyle.fontStyle = FontStyle.Normal; } } - } - - private string UpdateVesselStatus(MissileFire wm, GUIStyle vButtonStyle) - { - string status = ""; - if (wm.vessel.LandedOrSplashed) - { - if (wm.vessel.Landed) - status = Localizer.Format("#LOC_BDArmory_VesselStatus_Landed");//"(Landed)" - else - status = Localizer.Format("#LOC_BDArmory_VesselStatus_Splashed");//"(Splashed)" - vButtonStyle.fontStyle = FontStyle.Italic; - } else { vButtonStyle.fontStyle = FontStyle.Normal; } - return status; + return vesselStatusString.ToString(); } private void SwitchToNextVessel() @@ -734,21 +1119,49 @@ private void SwitchToNextVessel() /* If groups or specific are specified, then they take preference. * groups is a list of ints of the number of vessels to assign to each team. - * specific is a dictionary of craft names and teams. + * specific is a list of lists of craft names. * If the sum of groups is less than the number of vessels, then the extras get assigned to their own team. * If specific does not contain all the vessel names, then the unmentioned vessels get assigned to team 'A'. */ - public void MassTeamSwitch(bool separateTeams = false, List groups = null, Dictionary specific = null) + public void MassTeamSwitch(bool separateTeams = false, bool originalTeams = false, List groups = null, List> specific = null) { + if (originalTeams) + { + 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 (SpawnUtils.originalTeams.ContainsKey(weaponManager.vessel.vesselName)) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.LoadedVesselSwitcher]: assigning " + weaponManager.vessel.GetName() + " to team " + SpawnUtils.originalTeams[weaponManager.vessel.vesselName]); + weaponManager.SetTeam(BDTeam.Get(SpawnUtils.originalTeams[weaponManager.vessel.vesselName])); + if (BDACompetitionMode.Instance.Scores != null && BDACompetitionMode.Instance.Scores.Players.Contains(weaponManager.vessel.vesselName)) + BDACompetitionMode.Instance.Scores.ScoreData[weaponManager.vessel.vesselName].team = weaponManager.Team.Name; + } + } + return; + } char T = 'A'; if (specific != null) { - foreach (var weaponManager in weaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).ToList()) + var weaponManagersByName = weaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).ToDictionary(wm => wm.vessel.vesselName); + foreach (var craftList in specific) { - if (specific.ContainsKey(weaponManager.vessel.vesselName)) - weaponManager.SetTeam(BDTeam.Get(specific[weaponManager.vessel.vesselName].ToString())); // Assign the vessel to the specfied team. - else - weaponManager.SetTeam(BDTeam.Get('A'.ToString())); // Otherwise, assign them to team 'A'. + foreach (var craftName in craftList) + { + if (weaponManagersByName.ContainsKey(craftName)) + weaponManagersByName[craftName].SetTeam(BDTeam.Get(T.ToString())); + else + Debug.Log("[BDArmory.LoadedVesselSwitcher]: Specified vessel (" + craftName + ") not found amongst active vessels."); + weaponManagersByName.Remove(craftName); // Remove the vessel from our dictionary once it's assigned. + } + ++T; + } + foreach (var craftName in weaponManagersByName.Keys) + { + Debug.Log("[BDArmory.LoadedVesselSwitcher]: Vessel " + craftName + " was not specified to be part of a team, but is active. Assigning to team " + T.ToString() + "."); + weaponManagersByName[craftName].SetTeam(BDTeam.Get(T.ToString())); // Assign anyone who wasn't specified to a separate team. + weaponManagersByName[craftName].Team.Neutral = false; + if (BDACompetitionMode.Instance.Scores != null && BDACompetitionMode.Instance.Scores.Players.Contains(craftName)) + BDACompetitionMode.Instance.Scores.ScoreData[craftName].team = weaponManagersByName[craftName].Team.Name; } return; } @@ -765,6 +1178,9 @@ public void MassTeamSwitch(bool separateTeams = false, List groups = null, ++T; } weaponManager.SetTeam(BDTeam.Get(T.ToString())); // Otherwise, assign them to team T. + weaponManager.Team.Neutral = false; + if (BDACompetitionMode.Instance.Scores != null && BDACompetitionMode.Instance.Scores.Players.Contains(weaponManager.vessel.vesselName)) + BDACompetitionMode.Instance.Scores.ScoreData[weaponManager.vessel.vesselName].team = weaponManager.Team.Name; ++count; } return; @@ -772,8 +1188,11 @@ public void MassTeamSwitch(bool separateTeams = false, List groups = null, // 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. { - Debug.Log("[BDArmory]: assigning " + weaponManager.vessel.GetDisplayName() + " to team " + T.ToString()); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.LoadedVesselSwitcher]: assigning " + weaponManager.vessel.GetName() + " to team " + T.ToString()); weaponManager.SetTeam(BDTeam.Get(T.ToString())); + weaponManager.Team.Neutral = false; + if (BDACompetitionMode.Instance.Scores != null && BDACompetitionMode.Instance.Scores.Players.Contains(weaponManager.vessel.vesselName)) + BDACompetitionMode.Instance.Scores.ScoreData[weaponManager.vessel.vesselName].team = weaponManager.Team.Name; if (separateTeams) T++; } } @@ -805,26 +1224,38 @@ void CurrentVesselWillDestroy(Vessel v) if (_autoCameraSwitch && lastActiveVessel == v) { currentVesselDied = true; - currentVesselDiedAt = Planetarium.GetUniversalTime(); + if (v.IsMissile()) + { + currentVesselDiedAt = Time.time - (BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD == 0 ? BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f : BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD) / 2f; // Wait half the death cam period on missile death. + // FIXME If the missile is a clustermissile, we should immediately switch to one of the sub-missiles. + } + else + { + currentVesselDiedAt = Time.time; + } } } private void UpdateCamera() { - var now = Planetarium.GetUniversalTime(); + var now = Time.time; double timeSinceLastCheck = now - lastCameraCheck; if (currentVesselDied) { - if (now - currentVesselDiedAt < BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2) // Prevent camera changes for a bit. + if (now - currentVesselDiedAt < (BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD == 0 ? BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f : BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD)) // Prevent camera changes for a bit. return; else { currentVesselDied = false; lastCameraSwitch = 0; + lastActiveVessel = null; + timeSinceLastCheck = minCameraCheckInterval + 1f; } } - if (timeSinceLastCheck > 0.25) + if (ModIntegration.MouseAimFlight.IsMouseAimActive) return; // Don't switch while MouseAimFlight is active. + + if (timeSinceLastCheck > minCameraCheckInterval) { lastCameraCheck = now; @@ -837,41 +1268,132 @@ 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); + // Don't switch away from an active missile until it misses or is off-target, or if it is within 1 km of its target position + bool stayOnMissile = mb != null && + !mb.HasMissed && + Vector3.Dot((mb.TargetPosition - mb.vessel.transform.position).normalized, mb.vessel.transform.up) < 0.5f && + (mb.vessel.transform.position - mb.TargetPosition).sqrMagnitude < 1e6; + if (stayOnMissile) return; + lastCameraCheck -= TimeWarp.deltaTime; // Speed up moving away from less relevant missiles. + } 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. + { + foreach (MissileBase missile in BDATargetManager.FiredMissiles.Cast()) + { + 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.GetForwardTransform()) < 0.5f * targetDistance) continue; // Ignore off-target missiles. + if (missile.targetVessel != null && missile.targetVessel.Vessel.IsMissile()) continue; // Ignore missiles targeting missiles. + if (Vector3.Dot(missile.TargetVelocity - missile.vessel.Velocity(), missile.GetForwardTransform()) > -1f) continue; // Ignore missiles that aren't gaining on their targets. + float missileScore = targetDistance < 1e3f ? 0.1f : 0.1f + (targetDistance - 1e3f) * (targetDistance - 1e3f) * 5e-8f; // Prioritise missiles that are within 1km from their targets and de-prioritise those more than 5km away. + 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; - using (var wms = v.Current.FindPartModulesImplementing().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; + if (string.IsNullOrEmpty(vesselName)) continue; // avoid lingering on dying things + bool recentlyLostParts = false; bool recentlyDamaged = false; bool recentlyLanded = false; // check for damage & landed status - if (BDACompetitionMode.Instance.Scores.ContainsKey(vesselName)) + if (BDACompetitionMode.Instance.Scores.Players.Contains(vesselName)) { - var currentParts = v.Current.parts.Count; - var vdat = BDACompetitionMode.Instance.Scores[vesselName]; + 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. - { + recentlyLostParts = true; + if (now - vdat.lastDamageTime < 1d) // Took damage within the last 1s. recentlyDamaged = true; - } if (vdat.landedState) { @@ -883,87 +1405,165 @@ private void UpdateCamera() } } vesselScore = Math.Abs(vesselScore); + float HP = 0; + float WreckFactor = 0; + var ai = wm.Current.AI; + BDModulePilotAI PAI = null; + BDModuleOrbitalAI OAI = null; + if (ai != null && ai.pilotEnabled) switch (ai.aiType) + { + case AIType.PilotAI: PAI = ai as BDModulePilotAI; break; + case AIType.OrbitalAI: OAI = ai as BDModuleOrbitalAI; break; + } - if (!recentlyLanded && v.Current.verticalSpeed < -15) // Vessels gently floating to the ground aren't interesting + // If we're running a waypoints competition (without combat), only focus on vessels still running waypoints. + if (BDACompetitionMode.Instance.competitionType == CompetitionType.WAYPOINTS) { - crashTime = (float)(-Math.Abs(v.Current.radarAltitude) / v.Current.verticalSpeed); + if (PAI == null || (BDArmorySettings.WAYPOINT_GUARD_INDEX < 0 && !PAI.IsRunningWaypoints)) continue; + vesselScore *= 2f - Mathf.Clamp01((float)wm.Current.vessel.speed / PAI.maxSpeed); // For waypoints races, craft going near their max speed are more interesting. + vesselScore *= Mathf.Max(0.5f, 1f - 15.8f / BDAMath.Sqrt(PAI.waypointRange)); // Favour craft that are approaching a gate (capped at 1km). } - if (wms.Current.currentTarget != null) + + HP = wm.Current.currentHP / wm.Current.totalHP; + if (HP < 1) + { + WreckFactor += 1f - HP * HP; //the less plane remaining, the greater the chance it's a wreck + } + 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 (PAI == null || wm.Current.vessel.radarAltitude < PAI.defaultAltitude) //craft is uncontrollably diving, not returning from high alt to cruising alt + { + WreckFactor += 0.5f; + } + } + if (VesselModuleRegistry.GetModuleCount(wm.Current.vessel) > 0) + { + int engineOut = 0; + foreach (var engine in VesselModuleRegistry.GetModules(wm.Current.vessel)) + { + if (engine == null || engine.flameout || engine.finalThrust <= 0) + engineOut++; + } + WreckFactor += (engineOut / VesselModuleRegistry.GetModuleCount(wm.Current.vessel)) / 2; + } + else + { + WreckFactor += 0.5f; //could be a glider, could be missing engines + } + if (WreckFactor > 1f) // 'wrecked' requires some combination of diving, no engines, and missing parts + { + WreckFactor *= 2; + vesselScore *= WreckFactor; //disincentivise switching to wrecks + } + if (!recentlyLanded && wm.Current.vessel.verticalSpeed < -15) // Vessels gently floating to the ground aren't interesting { - targetDistance = Vector3.Distance(wms.Current.vessel.GetWorldPos3D(), wms.Current.currentTarget.position); + crashTime = (float)(-Math.Abs(wm.Current.vessel.radarAltitude) / wm.Current.vessel.verticalSpeed); } - vesselScore *= 0.031623f * Mathf.Sqrt(targetDistance); // Equal to 1 at 1000m - if (crashTime < 30) + if (crashTime < 30 && HP > 0.5f) { vesselScore *= crashTime / 30; } - if (wms.Current.currentGun != null) + if (wm.Current.currentTarget != null) { - if (wms.Current.currentGun.recentlyFiring) + targetDistance = Vector3.Distance(wm.Current.vessel.GetWorldPos3D(), wm.Current.currentTarget.position); + if (!wm.Current.HasWeaponsAndAmmo()) // no remaining weapons { - // shooting at things is more interesting - vesselScore *= 0.25f; + if (!BDArmorySettings.DISABLE_RAMMING && PAI != null && PAI.allowRamming) //ramming's fun to watch + { + vesselScore *= (0.031623f * BDAMath.Sqrt(targetDistance) / 2); + } + else + { + vesselScore *= 3; //ramming disabled. Boring! + } } + //else got weapons and engaging } - if (wms.Current.guardFiringMissile) + if (OAI) // Maneuvering is interesting, other statuses are not { - // firing a missile at things is more interesting - vesselScore *= 0.2f; + if (OAI.currentStatusMode == BDModuleOrbitalAI.StatusMode.Maneuvering) + vesselScore *= 0.5f; + else if (OAI.currentStatusMode == BDModuleOrbitalAI.StatusMode.CorrectingOrbit) + vesselScore *= 1.5f; + else if (OAI.currentStatusMode == BDModuleOrbitalAI.StatusMode.Idle) + vesselScore *= 2f; + else if (OAI.currentStatusMode == BDModuleOrbitalAI.StatusMode.Stranded) + vesselScore *= 3f; + // else -- Firing, Evading covered by weapon manager checks } + 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.incomingMissileDistance / (float)wms.Current.incomingMissileVessel.srfSpeed; + 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 (recentlyLostParts) + vesselScore *= 0.3f; // Because losing parts is very interesting. if (recentlyDamaged) + vesselScore *= 0.2f; // It's even more interesting if we're currently taking damage because we might explode. + if (wm.Current.vessel.LandedOrSplashed) + { + if (wm.Current.vessel.srfSpeed > 2) //margin for physics jitter + { + vesselScore *= Mathf.Min(((80 / (float)wm.Current.vessel.srfSpeed) / 2), 4); //srf Ai driven stuff thats still mobile + } + else + { + 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 (unless we're currently taking damage) + if (wm.Current.vessel.isActiveVessel) { - vesselScore *= 0.3f; // because taking hits is very interesting; - } - if (!recentlyLanded && wms.Current.vessel.LandedOrSplashed) - { - vesselScore *= 3; // 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) - { - vesselScore = (float)(vesselScore * timeSinceChange / 8.0); + vesselScore *= (float)(recentlyDamaged ? Math.Min(timeSinceChange / 8.0, 1.0) : (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; } + } } - if (!foundActiveVessel) + lastActiveVessel = FlightGlobals.ActiveVessel; + if (!foundActiveVessel) // if the active vessel dies it'll use a default score for a few seconds { var score = 100 * timeSinceChange; if (score < bestScore) @@ -971,16 +1571,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 (bestVessel != null && bestVessel.loaded && !bestVessel.packed && !bestVessel.isActiveVessel) { - Debug.Log("[BDArmory]: Switching vessel to " + bestVessel.GetDisplayName()); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.LoadedVesselSwitcher]: Switching vessel to " + bestVessel.GetName()); 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) { @@ -992,9 +1603,33 @@ 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 (vessel != null && vessel.loaded && !vessel.packed) + { + var flightCam = FlightCamera.fetch; + if (flightCam != null && distance > 0) flightCam.SetDistance(distance); + } + } + + public void TriggerSwitchVessel(float delay = 0) + { + lastCameraSwitch = delay > 0 ? Time.time - (BDArmorySettings.CAMERA_SWITCH_FREQUENCY * timeScaleSqrt - delay) : 0f; + lastCameraCheck = 0f; + UpdateCamera(); } /// @@ -1009,5 +1644,58 @@ internal static Texture2D CreateColorPixel(Color32 Background) retTex.Apply(); return retTex; } + + #region Vessel Tracing + Vector3d floatingOriginCorrection = Vector3d.zero; + Quaternion referenceRotationCorrection = Quaternion.identity; + Dictionary>> vesselTraces = new Dictionary>>(); + + public void StartVesselTracing() + { + if (vesselTraceEnabled) return; + vesselTraceEnabled = true; + Debug.Log("[BDArmory.LoadedVesselSwitcher]: Starting vessel tracing."); + vesselTraces.Clear(); + + // Set the reference Up and Rotation based on the current FloatingOrigin. + var geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(Vector3.zero); + var altitude = FlightGlobals.getAltitudeAtPos(Vector3.zero); + var localUp = VectorUtils.GetUpDirection(Vector3.zero); + var q1 = Quaternion.FromToRotation(Vector3.up, localUp); + var q2 = Quaternion.AngleAxis(Vector3.SignedAngle(q1 * Vector3.forward, Vector3.up, localUp), localUp); + var referenceRotation = q2 * q1; // Plane tangential to the surface and aligned with north, + referenceRotationCorrection = Quaternion.Inverse(referenceRotation); + floatingOriginCorrection = altitude * localUp; + + // Record starting points + var survivingVessels = weaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).Select(wm => wm.vessel).ToList(); + foreach (var vessel in survivingVessels) + { + if (vessel == null) continue; + vesselTraces[vessel.vesselName] = new List>(); + vesselTraces[vessel.vesselName].Add(new Tuple(Time.time, new Vector3((float)geoCoords.x, (float)geoCoords.y, altitude), referenceRotation)); + } + } + public void StopVesselTracing() + { + if (!vesselTraceEnabled) return; + vesselTraceEnabled = false; + Debug.Log("[BDArmory.LoadedVesselSwitcher]: Stopping vessel tracing."); + var folder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs", "VesselTraces")); + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + foreach (var vesselName in vesselTraces.Keys) + { + var traceFile = Path.Combine(folder, vesselName + "-" + vesselTraces[vesselName][0].Item1.ToString("0.000") + ".json"); + Debug.Log("[BDArmory.LoadedVesselSwitcher]: Dumping trace for " + vesselName + " to " + traceFile); + List strings = new List(); + strings.Add("["); + strings.Add(string.Join(",\n", vesselTraces[vesselName].Select(entry => " { \"time\": " + entry.Item1.ToString("0.000") + ", \"position\": [" + entry.Item2.x.ToString("0.0") + ", " + entry.Item2.y.ToString("0.0") + ", " + entry.Item2.z.ToString("0.0") + "], \"rotation\": [" + entry.Item3.x.ToString("0.000") + ", " + entry.Item3.y.ToString("0.000") + ", " + entry.Item3.z.ToString("0.000") + ", " + entry.Item3.w.ToString("0.000") + "] }"))); + strings.Add("]"); + File.WriteAllLines(traceFile, strings); + } + vesselTraces.Clear(); + } + #endregion } } diff --git a/BDArmory/Competition/RemoteOrchestrationWindow.cs b/BDArmory/UI/RemoteOrchestrationWindow.cs similarity index 76% rename from BDArmory/Competition/RemoteOrchestrationWindow.cs rename to BDArmory/UI/RemoteOrchestrationWindow.cs index 66342be39..aeca5f802 100644 --- a/BDArmory/Competition/RemoteOrchestrationWindow.cs +++ b/BDArmory/UI/RemoteOrchestrationWindow.cs @@ -1,8 +1,10 @@ using System.Collections; using UnityEngine; -using BDArmory.Core; using KSP.Localization; -using BDArmory.Competition; + +using BDArmory.Competition.RemoteOrchestration; +using BDArmory.Settings; +using BDArmory.Utils; namespace BDArmory.UI { @@ -13,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; @@ -48,7 +50,7 @@ private void Update() private void OnGUI() { - if (!(showWindow && ready && BDArmorySetup.GAME_UI_ENABLED && BDArmorySettings.REMOTE_LOGGING_ENABLED)) + if (!(showWindow && ready && (BDArmorySetup.GAME_UI_ENABLED || (BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI && !BDArmorySetup.GAME_UI_ENABLED)) && BDArmorySettings.REMOTE_LOGGING_ENABLED)) return; SetNewHeight(_windowHeight); @@ -58,14 +60,17 @@ private void OnGUI() BDArmorySettings.REMOTE_ORCHESTRATION_WINDOW_WIDTH, _windowHeight ); + BDArmorySetup.SetGUIOpacity(); + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectRemoteOrchestration.position); 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 ); - Misc.Misc.UpdateGUIRect(BDArmorySetup.WindowRectRemoteOrchestration, _guiCheckIndex); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectRemoteOrchestration, _guiCheckIndex); } private void SetNewHeight(float windowHeight) @@ -86,19 +91,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 = Misc.Misc.RegisterGUIRect(new Rect()); + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); } private void WindowRemoteOrchestration(int id) @@ -106,7 +114,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; @@ -120,7 +128,7 @@ private void WindowRemoteOrchestration(int id) switch (service.status) { case BDAScoreService.StatusType.Waiting: - statusLine = status + " " + (30 + service.retryFindStartedAt - Planetarium.GetUniversalTime()).ToString("0") + "s"; + statusLine = status + " " + (service.TimeUntilNextHeat()).ToString("0") + "s"; break; default: statusLine = status; @@ -163,15 +171,15 @@ private void WindowRemoteOrchestration(int id) break; } } - if (nextButton && GUI.Button(new Rect(2 * width / 3, offset, width / 3 - _margin, _titleHeight), "Next", BDArmorySetup.BDGuiSkin.button)) - { - BDAScoreService.Instance.retryFindStartedAt = -1; - } + //if (nextButton && GUI.Button(new Rect(2 * width / 3, offset, width / 3 - _margin, _titleHeight), "Next", BDArmorySetup.BDGuiSkin.button)) + //{ + // BDAScoreService.Instance.waitStartedAt = -1; + //} offset += _titleHeight + _margin; _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/ScoreWindow.cs b/BDArmory/UI/ScoreWindow.cs new file mode 100644 index 000000000..bdc7debcc --- /dev/null +++ b/BDArmory/UI/ScoreWindow.cs @@ -0,0 +1,383 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; + +namespace BDArmory.UI +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class ScoreWindow : MonoBehaviour + { + #region Fields + public static ScoreWindow Instance; + public bool _ready = false; + public static string scoreWeightsURL = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/score_weights.cfg")); + + int _buttonSize = 24; + static int _guiCheckIndexScores = -1; + Vector2 windowSize = new Vector2(200, 100); + bool resizingWindow = false; + public bool autoResizingWindow = true; + Vector2 scoreScrollPos = default; + bool showTeamScores = false; + public enum Mode { Tournament, ContinuousSpawn } + static Mode mode = Mode.Tournament; + public static void SetMode(Mode scoreMode, Toggle teamScores = Toggle.NoChange) => Instance.SetMode_(scoreMode, teamScores); + void SetMode_(Mode scoreMode, Toggle teamScores) + { + mode = scoreMode; + showTeamScores = teamScores switch + { + Toggle.Toggle => !showTeamScores, + Toggle.On => true, + Toggle.Off => false, + _ => showTeamScores + }; + LoadWeights(); + ResetWindowSize(true); + } + #endregion + + #region Styles + bool stylesConfigured = false; + GUIStyle leftLabel; + GUIStyle rightLabel; + #endregion + + private void Awake() + { + if (Instance) + Destroy(this); + Instance = this; + } + + private void Start() + { + _ready = false; + StartCoroutine(WaitForBdaSettings()); + SetMode(Mode.Tournament); + showTeamScores = BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS != 0; + } + + private IEnumerator WaitForBdaSettings() + { + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); + + if (_guiCheckIndexScores < 0) _guiCheckIndexScores = GUIUtils.RegisterGUIRect(new Rect()); + if (_guiCheckIndexWeights < 0) _guiCheckIndexWeights = GUIUtils.RegisterGUIRect(new Rect()); + _ready = true; + AdjustWindowRect(BDArmorySetup.WindowRectScores.size, true); + } + + void ConfigureStyles() + { + stylesConfigured = true; + leftLabel = new GUIStyle + { + alignment = TextAnchor.MiddleLeft, + wordWrap = true, + fontSize = BDArmorySettings.SCORES_FONT_SIZE + }; + leftLabel.normal.textColor = Color.white; + rightLabel = new GUIStyle(leftLabel) + { + alignment = TextAnchor.MiddleRight, + wordWrap = false + }; + } + + void AdjustFontSize(bool up) + { + if (up) ++BDArmorySettings.SCORES_FONT_SIZE; + else --BDArmorySettings.SCORES_FONT_SIZE; + if (up) + { + leftLabel.fontSize = BDArmorySettings.SCORES_FONT_SIZE; + rightLabel.fontSize = BDArmorySettings.SCORES_FONT_SIZE; + } + else + { + leftLabel.fontSize = BDArmorySettings.SCORES_FONT_SIZE; + rightLabel.fontSize = BDArmorySettings.SCORES_FONT_SIZE; + } + ResetWindowSize(); + } + + private void OnGUI() + { + if (!(_ready && BDArmorySettings.SHOW_SCORE_WINDOW && (BDArmorySetup.GAME_UI_ENABLED || BDArmorySettings.SCORES_PERSIST_UI) && HighLogic.LoadedSceneIsFlight)) + return; + + if (!stylesConfigured) ConfigureStyles(); + + if (resizingWindow && Event.current.type == EventType.MouseUp) { resizingWindow = false; } + AdjustWindowRect(windowSize); + BDArmorySetup.SetGUIOpacity(); + var guiMatrix = GUI.matrix; + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectScores.position); + BDArmorySetup.WindowRectScores = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + BDArmorySetup.WindowRectScores, + WindowScores, + StringUtils.Localize("#LOC_BDArmory_BDAScores_Title"),//"BDA Scores" + BDArmorySetup.BDGuiSkin.window + ); + if (weightsVisible) + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) { GUI.matrix = guiMatrix; GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, weightsWindowRect.position); } + weightsWindowRect = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + weightsWindowRect, + WindowWeights, + StringUtils.Localize("#LOC_BDArmory_BDAScores_Weights"), // "Score Weights" + BDArmorySetup.BDGuiSkin.window + ); + } + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectScores, _guiCheckIndexScores); + } + + #region Scores + private void AdjustWindowRect(Vector2 size, bool force = false) + { + if (!autoResizingWindow && resizingWindow || force) + { + size.x = Mathf.Clamp(size.x, 150, Screen.width - BDArmorySetup.WindowRectScores.x); + size.y = Mathf.Clamp(size.y, 70, Screen.height - BDArmorySetup.WindowRectScores.y); // The ScrollView won't let us go smaller than this. + BDArmorySetup.WindowRectScores.size = size; + } + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectScores, windowSize.y); + windowSize = BDArmorySetup.WindowRectScores.size; + } + + private void WindowScores(int id) + { + if (GUI.Button(new Rect(0, 0, _buttonSize, _buttonSize), "UI", BDArmorySettings.SCORES_PERSIST_UI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { BDArmorySettings.SCORES_PERSIST_UI = !BDArmorySettings.SCORES_PERSIST_UI; } + if (GUI.Button(new Rect(_buttonSize, 0, _buttonSize, _buttonSize), "-", BDArmorySetup.BDGuiSkin.button)) AdjustFontSize(false); + if (GUI.Button(new Rect(2 * _buttonSize, 0, _buttonSize, _buttonSize), "+", BDArmorySetup.BDGuiSkin.button)) AdjustFontSize(true); + GUI.DragWindow(new Rect(3 * _buttonSize, 0, windowSize.x - _buttonSize * 6, _buttonSize)); + if (GUI.Button(new Rect(windowSize.x - 3 * _buttonSize, 0, _buttonSize, _buttonSize), "T", showTeamScores ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle)) { showTeamScores = !showTeamScores; ResetWindowSize(); } + if (GUI.Button(new Rect(windowSize.x - 2 * _buttonSize, 0, _buttonSize, _buttonSize), "W", weightsVisible ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle)) SetWeightsVisible(!weightsVisible); + if (GUI.Button(new Rect(windowSize.x - _buttonSize, 0, _buttonSize, _buttonSize), " X", BDArmorySetup.CloseButtonStyle)) SetVisible(false); + + GUILayout.BeginVertical(GUI.skin.box, GUILayout.ExpandHeight(autoResizingWindow)); + switch (mode) + { + case Mode.Tournament: + { + var progress = BDATournament.Instance.GetTournamentProgress(); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_BDAScores_Round")} {progress.Item1} / {progress.Item2}, {StringUtils.Localize("#LOC_BDArmory_BDAScores_Heat")} {progress.Item3} / {progress.Item4}", leftLabel); + if (!autoResizingWindow) scoreScrollPos = GUILayout.BeginScrollView(scoreScrollPos); + int rank = 0; + using var scoreField = showTeamScores ? BDATournament.Instance.GetRankedTeamScores.GetEnumerator() : BDATournament.Instance.GetRankedScores.GetEnumerator(); + while (scoreField.MoveNext()) + { + GUILayout.BeginHorizontal(); + GUILayout.Label($"{++rank,3:D}", leftLabel, GUILayout.Width(BDArmorySettings.SCORES_FONT_SIZE * 2)); + GUILayout.Label(scoreField.Current.Key, leftLabel); + GUILayout.Label($"{scoreField.Current.Value,7:F3}", rightLabel); + GUILayout.EndHorizontal(); + } + if (!autoResizingWindow) GUILayout.EndScrollView(); + } + break; + case Mode.ContinuousSpawn: + { + // Show rank, vessel name, lives, score + GUILayout.BeginHorizontal(); + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_Settings_ContinuousSpawning"), leftLabel, GUILayout.ExpandWidth(true)); + if (BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0) + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_BDAScores_Lives"), rightLabel, GUILayout.Width(50)); + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_BDAScores_Score"), rightLabel, GUILayout.Width(70)); + GUILayout.EndHorizontal(); + if (!autoResizingWindow) scoreScrollPos = GUILayout.BeginScrollView(scoreScrollPos); + int rank = 0; + using var scoreField = ContinuousSpawning.Instance.Scores.GetEnumerator(); + while (scoreField.MoveNext()) + { + var (name, deaths, score) = scoreField.Current; + GUILayout.BeginHorizontal(); + GUILayout.Label($"{++rank,3:D}", leftLabel, GUILayout.Width(BDArmorySettings.SCORES_FONT_SIZE * 2)); + GUILayout.Label(name, leftLabel, GUILayout.ExpandWidth(true)); + if (BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0) + GUILayout.Label($"{BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL - deaths}", rightLabel, GUILayout.Width(50)); + GUILayout.Label($"{score,7:F2}", rightLabel, GUILayout.Width(70)); + GUILayout.EndHorizontal(); + } + if (!autoResizingWindow) GUILayout.EndScrollView(); + } + break; + } + GUILayout.EndVertical(); + + #region Resizing + var resizeRect = new Rect(windowSize.x - 16, windowSize.y - 16, 16, 16); + GUI.DrawTexture(resizeRect, GUIUtils.resizeTexture, ScaleMode.StretchToFill, true); + if (Event.current.type == EventType.MouseDown && resizeRect.Contains(Event.current.mousePosition)) + { + if (Event.current.button == 1) // Right click - reset to auto-resizing the height. + { + resizingWindow = false; + ResetWindowSize(true); + } + else + { + autoResizingWindow = false; + resizingWindow = true; + } + } + if (resizingWindow && Event.current.type == EventType.Repaint) + { windowSize += Mouse.delta / BDArmorySettings.UI_SCALE_ACTUAL; } + #endregion + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectScores); + } + + public void SetVisible(bool visible) + { + BDArmorySettings.SHOW_SCORE_WINDOW = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndexScores, visible); + } + public bool IsVisible => BDArmorySettings.SHOW_SCORE_WINDOW; + + /// + /// Reset the window size so that the height is tight. + /// + public void ResetWindowSize(bool force = false) + { + if (force) autoResizingWindow = true; + if (autoResizingWindow) + { + BDArmorySetup.WindowRectScores.height = 50; // Don't reset completely to 0 as that then covers half the title. + } + } + #endregion + + #region Weights + internal static int _guiCheckIndexWeights = -1; + bool weightsVisible = false; + Rect weightsWindowRect = new(0, 0, 300, 600); + Vector2 weightsScrollPos = default; + Dictionary weights; // Reference to the set of weights we're using (Tournament or ContinuousSpawning). + Dictionary scoreWeightFields; // The numeric input fields. + void LoadWeights() + { + if (scoreWeightFields != null) foreach (var value in scoreWeightFields.Values) Destroy(value); // Get rid of any old NumericInputField components. + switch (mode) + { + case Mode.Tournament: + TournamentScores.LoadWeights(); + weights = TournamentScores.weights; + weightsWindowRect.height = 600; + break; + case Mode.ContinuousSpawn: + ContinuousSpawning.LoadWeights(); + weights = ContinuousSpawning.weights; + weightsWindowRect.height = 450; + break; + default: + weights = null; + scoreWeightFields = null; + return; + } + scoreWeightFields = weights.ToDictionary(kvp => kvp.Key, kvp => gameObject.AddComponent().Initialise(0, kvp.Value)); + } + void SaveWeights() + { + switch (mode) + { + case Mode.Tournament: + TournamentScores.SaveWeights(); + break; + case Mode.ContinuousSpawn: + ContinuousSpawning.SaveWeights(); + break; + } + RecomputeScores(); + } + void ResetDefaultWeights() + { + switch (mode) + { + case Mode.Tournament: + TournamentScores.weights = new(TournamentScores.defaultWeights); + weights = TournamentScores.weights; + break; + case Mode.ContinuousSpawn: + ContinuousSpawning.weights = new(ContinuousSpawning.defaultWeights); + weights = ContinuousSpawning.weights; + break; + } + if (scoreWeightFields != null) foreach (var value in scoreWeightFields.Values) Destroy(value); // Get rid of any old NumericInputField components. + scoreWeightFields = weights.ToDictionary(kvp => kvp.Key, kvp => gameObject.AddComponent().Initialise(0, kvp.Value)); + SaveWeights(); + } + void RecomputeScores() + { + switch (mode) + { + case Mode.Tournament: + BDATournament.Instance.RecomputeScores(); + break; + case Mode.ContinuousSpawn: + ContinuousSpawning.Instance.RecomputeScores(); + break; + } + } + + void SetWeightsVisible(bool visible) + { + if (weights == null) return; + weightsVisible = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndexWeights, visible); + if (visible) + { + weightsWindowRect.y = BDArmorySetup.WindowRectScores.y; + if (BDArmorySetup.WindowRectScores.x + BDArmorySettings.UI_SCALE_ACTUAL * (windowSize.x + weightsWindowRect.width) <= Screen.width) + weightsWindowRect.x = BDArmorySetup.WindowRectScores.x + BDArmorySettings.UI_SCALE_ACTUAL * windowSize.x; + else + weightsWindowRect.x = BDArmorySetup.WindowRectScores.x - BDArmorySettings.UI_SCALE_ACTUAL * weightsWindowRect.width; + } + else + { + foreach (var weight in scoreWeightFields) + { + weight.Value.tryParseValueNow(); + weights[weight.Key] = (float)weight.Value.currentValue; + } + SaveWeights(); + } + } + void WindowWeights(int id) + { + GUI.DragWindow(new Rect(4 * _buttonSize, 0, weightsWindowRect.width - 5 * _buttonSize, _buttonSize)); + if (GUI.Button(new Rect(0, 0, 4 * _buttonSize, _buttonSize), "Defaults", BDArmorySetup.ButtonStyle)) ResetDefaultWeights(); + if (GUI.Button(new Rect(weightsWindowRect.width - _buttonSize, 0, _buttonSize, _buttonSize), " X", BDArmorySetup.CloseButtonStyle)) SetWeightsVisible(false); + GUILayout.BeginVertical(GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); + weightsScrollPos = GUILayout.BeginScrollView(weightsScrollPos, GUI.skin.box); + foreach (var weight in scoreWeightFields) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(weight.Key); + weight.Value.tryParseValue(GUILayout.TextField(weight.Value.possibleValue, 10, weight.Value.style, GUILayout.Width(80))); + if (weights[weight.Key] != (float)weight.Value.currentValue) + { + weights[weight.Key] = (float)weight.Value.currentValue; + RecomputeScores(); + } + GUILayout.EndHorizontal(); + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref weightsWindowRect); + GUIUtils.UpdateGUIRect(weightsWindowRect, _guiCheckIndexWeights); + GUIUtils.UseMouseEventInRect(weightsWindowRect); + } + #endregion + } +} diff --git a/BDArmory/UI/VesselSpawnerWindow.cs b/BDArmory/UI/VesselSpawnerWindow.cs index 83fb6fb08..01a083ee1 100644 --- a/BDArmory/UI/VesselSpawnerWindow.cs +++ b/BDArmory/UI/VesselSpawnerWindow.cs @@ -1,77 +1,27 @@ -using BDArmory.Control; -using BDArmory.Core; -using KSP.Localization; using System; using System.Collections; using System.Collections.Generic; +using System.IO; +using System.Linq; using UnityEngine; +using BDArmory.Competition.OrchestrationStrategies; +using BDArmory.Competition; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; +using BDArmory.Extensions; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.Flight, false)] public class VesselSpawnerWindow : MonoBehaviour { - private class SpawnField : MonoBehaviour - { - public SpawnField Initialise(double l, double v, double minV = double.MinValue, double maxV = double.MaxValue) { lastUpdated = l; currentValue = v; minValue = minV; maxValue = maxV; return this; } - public double lastUpdated; - public string possibleValue = string.Empty; - private double _value; - public double currentValue { get { return _value; } set { _value = value; possibleValue = _value.ToString("G6"); } } - private double minValue; - private double maxValue; - private bool coroutineRunning = false; - private Coroutine coroutine; - - public void tryParseValue(string v) - { - if (v != possibleValue) - { - lastUpdated = !string.IsNullOrEmpty(v) ? Time.time : Time.time + 0.5; // Give the empty string an extra 0.5s. - possibleValue = v; - if (!coroutineRunning) - { - coroutine = StartCoroutine(UpdateValueCoroutine()); - } - } - } - - IEnumerator UpdateValueCoroutine() - { - coroutineRunning = true; - while (Time.time - lastUpdated < 0.5) - yield return new WaitForFixedUpdate(); - tryParseCurrentValue(); - coroutineRunning = false; - yield return new WaitForFixedUpdate(); - } - - void tryParseCurrentValue() - { - double newValue; - if (double.TryParse(possibleValue, out newValue)) - { - currentValue = Math.Min(Math.Max(newValue, minValue), maxValue); - lastUpdated = Time.time; - } - possibleValue = currentValue.ToString("G6"); - } - - // Parse the current possible value immediately. - public void tryParseValueNow() - { - tryParseCurrentValue(); - if (coroutineRunning) - { - StopCoroutine(coroutine); - coroutineRunning = false; - } - } - } - #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; @@ -79,14 +29,27 @@ public void tryParseValueNow() private float _windowWidth; public bool _ready = false; private bool _vesselsSpawned = false; - Dictionary spawnFields; - - // FIXME RUNWAY_PROJECT Round 3 - // VesselSpawner.SpawnConfig targetSpawnConfig; - // static Dictionary targetSpawnFields; - // static float competitionStartDelay = 15; - // FIXME Round 4 - public bool round4running = false; + Dictionary spawnFields; + + private GUIContent[] planetGUI; + private GUIContent planetText; + private BDGUIComboBox planetBox; + private int previous_index = 1; + private bool planetslist = false; + int selected_index = 1; + int WaygateCount = -1; + public int gateModelsCount => WaygateCount; + public float SelectedGate = -1; + public static string Gatepath; + public string SelectedModel; + public string[] gateFiles; + private bool waypointsRunning = false; + #endregion + #region GUI strings + string tournamentStyle = $"{(TournamentStyle)0}"; + string tournamentRoundType = $"{(TournamentRoundType)0}"; + int tournamentStyleMax = Enum.GetNames(typeof(TournamentStyle)).Length - 1; + int tournamentRoundTypeMax = Enum.GetNames(typeof(TournamentRoundType)).Length - 1; #endregion #region Styles @@ -125,9 +88,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 * (_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 + (pos % 4) * (_windowWidth - 2f * _margin) / 4f, (line + (int)(pos / 4)) * _lineHeight, (_windowWidth - 2.5f * _margin) / 4f, _lineHeight); + return new Rect(_margin, line * _lineHeight, width, _lineHeight); } List SRight2Rects(float line) @@ -152,8 +130,9 @@ List SRight3Rects(float line) } GUIStyle leftLabel; + GUIStyle listStyle; #endregion - + private string txtName = string.Empty; private void Awake() { if (Instance) @@ -169,57 +148,75 @@ 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 { - { "lat", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, -90, 90) }, - { "lon", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, -180, 180) }, - { "alt", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 0) }, + spawnFields = new Dictionary { + { "lat", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, -90, 90) }, + { "lon", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, -180, 180) }, + { "alt", gameObject.AddComponent().Initialise(0, BDArmorySettings.VESSEL_SPAWN_ALTITUDE) }, }; - // if (BDArmorySettings.RUNWAY_PROJECT) - // { - // targetSpawnConfig = new VesselSpawner.SpawnConfig( - // BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, - // BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - // 5000, - // 1000, - // true, - // BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, - // true, - // true, - // "Targets" - // ); - - // targetSpawnFields = new Dictionary { - // { "lat", gameObject.AddComponent().Initialise(0, targetSpawnConfig.latitude + 1, -90, 90) }, - // { "lon", gameObject.AddComponent().Initialise(0, targetSpawnConfig.longitude, -180, 180) }, - // { "alt", gameObject.AddComponent().Initialise(0, targetSpawnConfig.altitude, 0) }, - // }; - // } + selected_index = FlightGlobals.currentMainBody != null ? FlightGlobals.currentMainBody.flightGlobalsIndex : 1; + + tournamentStyle = $"{(TournamentStyle)BDArmorySettings.TOURNAMENT_STYLE}"; + tournamentRoundType = $"{(TournamentRoundType)BDArmorySettings.TOURNAMENT_ROUND_TYPE}"; + + try + { + Gatepath = Path.GetDirectoryName(Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", WaypointFollowingStrategy.ModelPath))); + gateFiles = Directory.GetFiles(Gatepath, "*.mu").Select(f => Path.GetFileName(f)).ToArray(); + Array.Sort(gateFiles, StringComparer.Ordinal); // Sort them alphabetically, uppercase first. + WaygateCount = gateFiles.Count() - 1; + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.VesselSpawnerWindow]: Failed to locate waypoint marker models: {e.Message}"); + } + if (WaygateCount >= 0) + { + if (SelectedGate >= 0) SelectedModel = Path.GetFileNameWithoutExtension(gateFiles[(int)Mathf.Clamp(SelectedGate, 0, WaygateCount)]); + } + else Debug.LogWarning($"[BDArmory.VesselSpawnerWindow]: No waypoint gate models found in {Gatepath}!"); + if (BDArmorySettings.WAYPOINT_COURSE_INDEX >= WaypointCourses.CourseLocations.Count) BDArmorySettings.WAYPOINT_COURSE_INDEX = 0; // Sanitise the index in case the course list has changed. } private IEnumerator WaitForBdaSettings() { - while (BDArmorySetup.Instance == null) - yield return null; + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); BDArmorySetup.Instance.hasVesselSpawner = true; - _guiCheckIndex = Misc.Misc.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() + { + planetGUI = new GUIContent[FlightGlobals.Bodies.Count]; + for (int i = 0; i < FlightGlobals.Bodies.Count; i++) + { + GUIContent gui = new GUIContent(FlightGlobals.Bodies[i].name); + planetGUI[i] = gui; + } + + planetText = new GUIContent(); + 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; - SetNewHeight(_windowHeight); BDArmorySetup.WindowRectVesselSpawner = new Rect( BDArmorySetup.WindowRectVesselSpawner.x, @@ -227,20 +224,44 @@ private void OnGUI() _windowWidth, _windowHeight ); + BDArmorySetup.SetGUIOpacity(); + var guiMatrix = GUI.matrix; + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectVesselSpawner.position); 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 ); - Misc.Misc.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 + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) { GUI.matrix = guiMatrix; GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, observerWindowRect.position); } + observerWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), observerWindowRect, ObserverWindow, StringUtils.Localize("#LOC_BDArmory_ObserverSelection_Title"), BDArmorySetup.BDGuiSkin.window); + } + } } void HotKeys() { if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TOURNAMENT_SETUP)) - BDATournament.Instance.SetupTournament(BDArmorySettings.TOURNAMENT_FILES_LOCATION, BDArmorySettings.TOURNAMENT_ROUNDS, BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT); + BDATournament.Instance.SetupTournament( + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION, + BDArmorySettings.TOURNAMENT_ROUNDS, + BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, + BDArmorySettings.TOURNAMENT_NPCS_PER_HEAT, + BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, + BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + (TournamentStyle)BDArmorySettings.TOURNAMENT_STYLE, + (TournamentRoundType)BDArmorySettings.TOURNAMENT_ROUND_TYPE + ); if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TOURNAMENT_RUN)) BDATournament.Instance.RunTournament(); } @@ -250,71 +271,80 @@ void ParseAllSpawnFieldsNow() spawnFields["lat"].tryParseValueNow(); spawnFields["lon"].tryParseValueNow(); spawnFields["alt"].tryParseValueNow(); - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x = Math.Min(Math.Max(spawnFields["lat"].currentValue, -90), 90); - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y = Math.Min(Math.Max(spawnFields["lon"].currentValue, -180), 180); - BDArmorySettings.VESSEL_SPAWN_ALTITUDE = Math.Max(0, (float)spawnFields["alt"].currentValue); + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x = spawnFields["lat"].currentValue; + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y = spawnFields["lon"].currentValue; + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX = FlightGlobals.currentMainBody != null ? FlightGlobals.currentMainBody.flightGlobalsIndex : 1; //selected_index? + BDArmorySettings.VESSEL_SPAWN_ALTITUDE = (float)spawnFields["alt"].currentValue; + } + + /// + /// Update the value of the spawn fields from the current settings values. + /// + public void RefreshSpawnFieldsFromSettings() + { + spawnFields["lat"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x); + spawnFields["lon"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y); + spawnFields["alt"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_ALTITUDE); } private void SetNewHeight(float windowHeight) { var previousWindowHeight = BDArmorySetup.WindowRectVesselSpawner.height; BDArmorySetup.WindowRectVesselSpawner.height = windowHeight; - if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES && windowHeight < previousWindowHeight && 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, previousWindowHeight); } + (float, float)[] cacheVesselSpawnDistance; 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; } if (BDArmorySettings.SHOW_SPAWN_OPTIONS) { - 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 - 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); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnDistance")}: ({(BDArmorySettings.VESSEL_SPAWN_DISTANCE < 1000 ? $"{BDArmorySettings.VESSEL_SPAWN_DISTANCE:G4}m" : $"{BDArmorySettings.VESSEL_SPAWN_DISTANCE / 1000:G4}km")})", leftLabel);//Spawn Distance + BDArmorySettings.VESSEL_SPAWN_DISTANCE = GUIUtils.HorizontalSemiLogSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_DISTANCE, 10, 200000, 1.5f, false, false, ref cacheVesselSpawnDistance); } 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), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnRefHeading")}: ({BDArmorySettings.VESSEL_SPAWN_REF_HEADING:000}°)", leftLabel); // Reference Heading + BDArmorySettings.VESSEL_SPAWN_REF_HEADING = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_REF_HEADING, 0, 359)); + + 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), $"{Localizer.Format("#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 = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS, 0f, 20f); + 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) - BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL, 0f, 20f); + 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 = ((int)(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 ((int)BDArmorySettings.OUT_OF_AMMO_KILL_TIME) + switch (Mathf.RoundToInt(BDArmorySettings.OUT_OF_AMMO_KILL_TIME)) { case 0: outOfAmmoKillTime = 1f; @@ -345,7 +375,7 @@ private void WindowVesselSpawner(int id) break; } outOfAmmoKillTime = GUI.HorizontalSlider(SRightSliderRect(line), outOfAmmoKillTime, 1f, 9f); - switch ((int)outOfAmmoKillTime) + switch (Mathf.RoundToInt(outOfAmmoKillTime)) { case 1: BDArmorySettings.OUT_OF_AMMO_KILL_TIME = 0f; // 0s @@ -376,234 +406,813 @@ private void WindowVesselSpawner(int id) break; } - 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_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. + string fillSeats = ""; + switch (BDArmorySettings.VESSEL_SPAWN_FILL_SEATS) + { + case 0: + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_Minimal"); + break; + case 1: + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_Default"); // Full cockpits or the first combat seat if no cockpits are found. + break; + case 2: + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints"); + break; + case 3: + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_Cabins"); + break; + } + 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 = StringUtils.Localize("#LOC_BDArmory_Settings_Teams_FFA"); + break; + case 1: // 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 = $"{StringUtils.Localize("#LOC_BDArmory_Settings_Teams_SplitEvenly")} {BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS:0}"; + break; + } + 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), $"{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, 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_INITIAL_VELOCITY = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_INITIAL_VELOCITY, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnInitialVelocity")); // Planes spawn at their idle speed. + ++line; // Placeholder for a removed entry. + BDArmorySettings.VESSEL_SPAWN_CS_FOLLOWS_CENTROID = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_CS_FOLLOWS_CENTROID, StringUtils.Localize("#LOC_BDArmory_Settings_CSFollowsCentroid")); //CS spawn-point follows centroid. - 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_WarpHere"), BDArmorySetup.BDGuiSkin.button)) + { + SpawnUtils.ShowSpawnPoint(selected_index, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE); + } + if (GUI.Button(SRightButtonRect(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), 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; - if (Physics.Raycast(ray, out hit, 10000, 1 << 15)) + if (Physics.Raycast(ray, out hit, 10000, (int)LayerMasks.Scenery)) { BDArmorySettings.VESSEL_SPAWN_GEOCOORDS = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(hit.point); - spawnFields["lat"].currentValue = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x; - spawnFields["lon"].currentValue = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y; + spawnFields["lat"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x); + spawnFields["lon"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y); } } rects = SRight3Rects(line); - spawnFields["lat"].tryParseValue(GUI.TextField(rects[0], spawnFields["lat"].possibleValue, 8)); - spawnFields["lon"].tryParseValue(GUI.TextField(rects[1], spawnFields["lon"].possibleValue, 8)); - spawnFields["alt"].tryParseValue(GUI.TextField(rects[2], spawnFields["alt"].possibleValue, 8)); - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x = Math.Min(Math.Max(spawnFields["lat"].currentValue, -90), 90); - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y = Math.Min(Math.Max(spawnFields["lon"].currentValue, -180), 180); - BDArmorySettings.VESSEL_SPAWN_ALTITUDE = Math.Max(0, (float)spawnFields["alt"].currentValue); + spawnFields["lat"].tryParseValue(GUI.TextField(rects[0], spawnFields["lat"].possibleValue, 8, spawnFields["lat"].style)); + spawnFields["lon"].tryParseValue(GUI.TextField(rects[1], spawnFields["lon"].possibleValue, 8, spawnFields["lon"].style)); + spawnFields["alt"].tryParseValue(GUI.TextField(rects[2], spawnFields["alt"].possibleValue, 8, spawnFields["alt"].style)); + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x = spawnFields["lat"].currentValue; + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y = spawnFields["lon"].currentValue; + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX = FlightGlobals.currentMainBody != null ? FlightGlobals.currentMainBody.flightGlobalsIndex : 1; + BDArmorySettings.VESSEL_SPAWN_ALTITUDE = (float)spawnFields["alt"].currentValue; + + txtName = GUI.TextField(SRightButtonRect(++line), txtName); + 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(SThirdRect(++line, 0), StringUtils.Localize("#LOC_BDArmory_Settings_ClearDebrisNow"), BDArmorySetup.BDGuiSkin.button)) + { + // Clean up debris now + BDACompetitionMode.Instance.RemoveDebrisNow(); + } + 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, BDArmorySettings.UI_SCALE_ACTUAL * 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; } if (BDArmorySettings.SHOW_SPAWN_LOCATIONS) { line++; + /////////////////// + if (!planetslist) + { + FillPlanetList(); + planetBox = new BDGUIComboBox(SLeftButtonRect(line), SLineRect(line), planetText, planetGUI, _lineHeight * 6, listStyle, 3); + planetslist = true; + } + planetBox.UpdateRect(SLeftButtonRect(line)); + selected_index = planetBox.Show(); + if (planetBox.IsOpen) + { + line += planetBox.Height / _lineHeight; + } + if (selected_index != previous_index) + { + if (selected_index != -1) + { + //selectedWorld = FlightGlobals.Bodies[selected_index].name; + //Debug.Log("selected World Index: " + selected_index); + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX = selected_index; + } + previous_index = selected_index; + } + if (selected_index == -1) + { + selected_index = 1; + previous_index = 1; + } + //////////////////// + ++line; int i = 0; - foreach (var spawnLocation in VesselSpawner.spawnLocations) + foreach (var spawnLocation in SpawnLocations.spawnLocations) { + if (spawnLocation.worldIndex != selected_index) continue; if (GUI.Button(SQuarterRect(line, i++), spawnLocation.name, BDArmorySetup.BDGuiSkin.button)) { - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS = spawnLocation.location; - spawnFields["lat"].currentValue = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x; - spawnFields["lon"].currentValue = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y; - VesselSpawner.Instance.ShowSpawnPoint(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 20); - // if (BDArmorySettings.RUNWAY_PROJECT) // FIXME Round 3 - // { - // targetSpawnConfig.latitude = spawnLocation.location.x; - // targetSpawnConfig.longitude = spawnLocation.location.y; - // targetSpawnFields["lat"].currentValue = spawnLocation.location.x + 1; - // targetSpawnFields["lon"].currentValue = spawnLocation.location.y; - // } + switch (Event.current.button) + { + case 1: // right click + SpawnLocations.spawnLocations.Remove(spawnLocation); + break; + default: + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS = spawnLocation.location; + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX = spawnLocation.worldIndex; + spawnFields["lat"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x); + spawnFields["lon"].SetCurrentValue(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y); + SpawnUtils.ShowSpawnPoint(selected_index, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE); + break; + } } } - line += (i - 1) / 4; + line += (int)((i - 1) / 4); + line += 0.3f; } - // TODO Add a button for adding in spawn locations in the GUI. - 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 - { - BDArmorySettings.SHOW_TOURNAMENT_OPTIONS = !BDArmorySettings.SHOW_TOURNAMENT_OPTIONS; - } - if (BDArmorySettings.SHOW_TOURNAMENT_OPTIONS) + if (BDArmorySettings.WAYPOINTS_MODE) { - GUI.Label(SLeftRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentFilesLocation")} (AutoSpawn/): ", leftLabel); // Craft files location - BDArmorySettings.TOURNAMENT_FILES_LOCATION = GUI.TextField(SRightRect(line), BDArmorySettings.TOURNAMENT_FILES_LOCATION); + 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) + { + // Sanitise the waypoint course index in case the course list has changed. + BDArmorySettings.WAYPOINT_COURSE_INDEX = Mathf.Clamp(BDArmorySettings.WAYPOINT_COURSE_INDEX, 0, WaypointCourses.CourseLocations.Count - 1); - 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 = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS, 0f, 15f); + // 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; - var value = BDArmorySettings.TOURNAMENT_ROUNDS < 21 ? BDArmorySettings.TOURNAMENT_ROUNDS : (16 + BDArmorySettings.TOURNAMENT_ROUNDS / 5); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentRounds")}: ({BDArmorySettings.TOURNAMENT_ROUNDS})", leftLabel); // Rounds - value = (int)GUI.HorizontalSlider(SRightSliderRect(line), value, 1f, 36f); - BDArmorySettings.TOURNAMENT_ROUNDS = value < 21 ? value : (value - 16) * 5; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_ChooseCourse")}: ({waypointCourseName})", leftLabel); //Select COurse + BDArmorySettings.WAYPOINT_COURSE_INDEX = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINT_COURSE_INDEX, 0, WaypointCourses.CourseLocations.Count - 1)); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentVesselsPerHeat")}: ({(BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT > 1 ? BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT.ToString() : (BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT == 0 ? "Auto" : "Inf"))})", leftLabel); // Vessels Per Heat - BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT = (int)GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, 0f, 20f); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_Waypoint")} {StringUtils.Localize("#autoLOC_463493")}: {(BDArmorySettings.WAYPOINTS_ALTITUDE >= 0 ? $"({BDArmorySettings.WAYPOINTS_ALTITUDE:F0}m)" : StringUtils.Localize("#LOC_BDArmory_WP_CourseDefaults"))}", leftLabel); //Waypoint Altitude /use Course Settings + BDArmorySettings.WAYPOINTS_ALTITUDE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINTS_ALTITUDE, -50, 1000f), 50); - 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); - switch (BDATournament.Instance.tournamentStatus) - { - 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; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_MaxLaps")}: {BDArmorySettings.WAYPOINT_LOOP_INDEX:F0}", leftLabel); //max Laps + BDArmorySettings.WAYPOINT_LOOP_INDEX = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINT_LOOP_INDEX, 1, BDArmorySettings.WAYPOINT_MAX_LAPS)); - default: - if (GUI.Button(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TournamentSetup"), BDArmorySetup.BDGuiSkin.button)) // Setup tournament + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_GuardActivate")}: {(BDArmorySettings.WAYPOINT_GUARD_INDEX < 0 ? "Never" : $"{StringUtils.Localize("#LOC_BDArmory_WP_Waypoint")} {BDArmorySettings.WAYPOINT_GUARD_INDEX.ToString("F0")}")}", leftLabel); //Activate Guard After + 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), $"{StringUtils.Localize("#LOC_BDArmory_WP_Waypoint")} {StringUtils.Localize("#autoLOC_8200035")}: {(BDArmorySettings.WAYPOINTS_SCALE > 0 ? $"({BDArmorySettings.WAYPOINTS_SCALE:F0}m)" : StringUtils.Localize("#LOC_BDArmory_WP_CourseDefaults"))}", leftLabel); //Waypoint Radius; //use Course Settings + BDArmorySettings.WAYPOINTS_SCALE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINTS_SCALE, 0, 1000f), 50f); + + if (WaygateCount >= 0) { - ParseAllSpawnFieldsNow(); - BDATournament.Instance.SetupTournament(BDArmorySettings.TOURNAMENT_FILES_LOCATION, BDArmorySettings.TOURNAMENT_ROUNDS, BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT); - BDArmorySetup.SaveConfig(); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_WP_SelectModel")}: {SelectedModel}", leftLabel); //Waypoint Type + if (SelectedGate != (SelectedGate = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), SelectedGate, -1, WaygateCount), 1))) + { + if (SelectedGate >= 0) SelectedModel = Path.GetFileNameWithoutExtension(gateFiles[(int)Mathf.Clamp(SelectedGate, 0, WaygateCount)]); + else SelectedModel = string.Empty; + } } - - if (BDATournament.Instance.tournamentStatus != TournamentStatus.Completed) + } + if (BDArmorySetup.Instance.hasWPCourseSpawner) + { + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_BDAWaypointBuilder_Title"), BDArmorySetup.showWPBuilderGUI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide waypoints section { - if (GUI.Button(SRightRect(line), Localizer.Format("#LOC_BDArmory_Settings_TournamentRun"), BDArmorySetup.BDGuiSkin.button)) // Run tournament - BDATournament.Instance.RunTournament(); + CourseBuilderGUI.Instance.SetVisible(!BDArmorySetup.showWPBuilderGUI); + if (!BDArmorySetup.showWPBuilderGUI) + BDArmorySetup.SaveConfig(); } - break; + } + 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")); } } - /* - // Special settings for season 2 round 3 - if (BDArmorySettings.RUNWAY_PROJECT) + // Custom Spawn Template + if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 11) { - ++line; - if (GUI.Button(SLeftButtonRect(++line), Localizer.Format("Set target spawn here"), BDArmorySetup.BDGuiSkin.button)) //"Vessel Spawning Location" + 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 { - Ray ray = new Ray(FlightCamera.fetch.mainCamera.transform.position, FlightCamera.fetch.mainCamera.transform.forward); - RaycastHit hit; - if (Physics.Raycast(ray, out hit, 10000, 1 << 15)) + BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS = !BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS; + } + if (BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS) + { + line += 0.25f; + var spawnTemplate = CustomTemplateSpawning.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(BDArmorySettings.UI_SCALE_ACTUAL * Event.current.mousePosition + BDArmorySetup.WindowRectVesselSpawner.position); + } + if (GUI.Button(SQuarterRect(line, 3), 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(SQuarterRect(++line, 2), StringUtils.Localize("#LOC_BDArmory_Generic_New"), BDArmorySetup.BDGuiSkin.button)) // New generates a new template from the current vessels in the LoadedVesselSwitcher. { - var geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(hit.point); - targetSpawnFields["lat"].currentValue = geoCoords.x; - targetSpawnFields["lon"].currentValue = geoCoords.y; + spawnTemplate = CustomTemplateSpawning.Instance.NewTemplate(); + } + if (GUI.Button(SQuarterRect(line, 3), StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_SaveCraftToTemplate"), BDArmorySetup.BDGuiSkin.button)) // New generates a new template from the current vessels in the LoadedVesselSwitcher. + { + CustomTemplateSpawning.Instance.SaveCraftToTemplate(); + } + 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) + { + 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.ShipName(member.craftURL), BDArmorySetup.BDGuiSkin.button)) + { + if (Event.current.button == 1)//Right click + CustomTemplateSpawning.Instance.HideVesselSelection(member); + else + CustomTemplateSpawning.Instance.ShowVesselSelection(BDArmorySettings.UI_SCALE_ACTUAL * 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(BDArmorySettings.UI_SCALE_ACTUAL * Event.current.mousePosition + BDArmorySetup.WindowRectVesselSpawner.position, member); + } + } + ++teamName; + line += 0.25f; } } - rects = SRight3Rects(line); - targetSpawnFields["lat"].tryParseValue(GUI.TextField(rects[0], targetSpawnFields["lat"].possibleValue, 8)); - targetSpawnFields["lon"].tryParseValue(GUI.TextField(rects[1], targetSpawnFields["lon"].possibleValue, 8)); - targetSpawnFields["alt"].tryParseValue(GUI.TextField(rects[2], targetSpawnFields["alt"].possibleValue, 8)); - - targetSpawnConfig.latitude = Math.Min(Math.Max(targetSpawnFields["lat"].currentValue, -90), 90); - targetSpawnConfig.longitude = Math.Min(Math.Max(targetSpawnFields["lon"].currentValue, -180), 180); - targetSpawnConfig.altitude = Math.Max(0, (float)targetSpawnFields["alt"].currentValue); - targetSpawnConfig.absDistanceOrFactor = GUI.Toggle(SLeftRect(++line), targetSpawnConfig.absDistanceOrFactor, Localizer.Format("Target distance: abs vs factor")); // Toggle between distance factor and absolute distance. - if (targetSpawnConfig.absDistanceOrFactor) - { // Absolute distance - var value = targetSpawnConfig.distance < 100 ? targetSpawnConfig.distance / 10 : targetSpawnConfig.distance < 1000 ? 9 + targetSpawnConfig.distance / 100 : targetSpawnConfig.distance < 10000 ? 18 + targetSpawnConfig.distance / 1000 : 26 + targetSpawnConfig.distance / 5000; - var displayValue = targetSpawnConfig.distance < 1000 ? targetSpawnConfig.distance.ToString("0") + "m" : (targetSpawnConfig.distance / 1000).ToString("0") + "km"; - GUI.Label(SLeftSliderRect(++line), $"Target spawn distance: ({displayValue})", leftLabel);//Spawn Distance - value = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), value, 1f, 30f)); - targetSpawnConfig.distance = value < 10 ? 10 * value : value < 19 ? 100 * (value - 9) : value < 28 ? 1000 * (value - 18) : 5000 * (value - 26); + } + // Tournament options + { + 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 + { + BDArmorySettings.SHOW_TOURNAMENT_OPTIONS = !BDArmorySettings.SHOW_TOURNAMENT_OPTIONS; } - else - { // Distance factor - GUI.Label(SLeftSliderRect(++line), $"Target spawn distance factor: ({targetSpawnConfig.distance})", leftLabel);//Spawn Distance Factor - targetSpawnConfig.distance = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), targetSpawnConfig.distance / 10f, 1f, 10f) * 10f); + 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)); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds")}: ({ + BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS switch { + 0 => StringUtils.Localize("#LOC_BDArmory_Generic_Off"), + -5 => StringUtils.Localize("#LOC_BDArmory_Settings_TournamentTimeWarpDaylight"), + _ => $"{BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS}min" + } + })", leftLabel); // TimeWarp Between Rounds + BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS, -5f, 360f), 5); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentStyle")}: ({tournamentStyle})", leftLabel); // Tournament Style + if (BDArmorySettings.TOURNAMENT_STYLE != (BDArmorySettings.TOURNAMENT_STYLE = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_STYLE, 0f, tournamentStyleMax)))) + { tournamentStyle = $"{(TournamentStyle)BDArmorySettings.TOURNAMENT_STYLE}"; } + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentRoundType")}: ({tournamentRoundType})", leftLabel); // Tournament Round Type + if (BDArmorySettings.TOURNAMENT_ROUND_TYPE != (BDArmorySettings.TOURNAMENT_ROUND_TYPE = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_ROUND_TYPE, 0f, tournamentRoundTypeMax)))) + { tournamentRoundType = $"{(TournamentRoundType)BDArmorySettings.TOURNAMENT_ROUND_TYPE}"; } + + 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; + + 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)); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentNPCsPerHeat")}: ({BDArmorySettings.TOURNAMENT_NPCS_PER_HEAT})", leftLabel); // NPCs Per Heat + BDArmorySettings.TOURNAMENT_NPCS_PER_HEAT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_NPCS_PER_HEAT, 0f, 10f)); + } + 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)); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentVesselsPerTeam")}: ({(BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM > 0 ? BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM.ToString() : "auto")})", leftLabel); // Vessels Per Team + BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, 0f, 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 == 0 ? "auto" : 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_NPCS_PER_HEAT, + BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, + BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 11 ? 1 : BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 11 ? TournamentStyle.TemplateRNG : (TournamentStyle)BDArmorySettings.TOURNAMENT_STYLE, + (TournamentRoundType)BDArmorySettings.TOURNAMENT_ROUND_TYPE + ); + 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; + } } - // Countdown - GUI.Label(SLeftSliderRect(++line), $"Countdown: ({competitionStartDelay}s)", leftLabel); // Countdown - competitionStartDelay = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), competitionStartDelay, 0f, 30f)); } - */ - ++line; - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_SingleSpawn"), _vesselsSpawned ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (BDArmorySettings.WAYPOINTS_MODE) { - BDATournament.Instance.StopTournament(); - ParseAllSpawnFieldsNow(); - if (!_vesselsSpawned && !VesselSpawner.Instance.vesselsSpawningContinuously && Event.current.button == 0) // Left click + if (!waypointsRunning) { - if (BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING) - VesselSpawner.Instance.SpawnAllVesselsOnceContinuously(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); // Spawn vessels. - else - VesselSpawner.Instance.SpawnAllVesselsOnce(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); // Spawn vessels. - _vesselsSpawned = true; + // Note: + // - left click is run with waypoint spawn point + // - right click is run with current spawn point + // - middle click is run current vessel through waypoints + if (GUI.Button(SLineRect(++line), "Run waypoints", BDArmorySetup.BDGuiSkin.button)) + { + if (BDArmorySetup.showWPBuilderGUI && !TournamentCoordinator.Instance.IsRunning) //delete loaded gates if builder is closed, but not if WP course is currently running + { + foreach (var gate in CourseBuilderGUI.Instance.loadedGates) + { + gate.disabled = true; + gate.gameObject.SetActive(false); + } + CourseBuilderGUI.Instance.loadedGates.Clear(); + } + BDATournament.Instance.StopTournament(); + if (TournamentCoordinator.Instance.IsRunning) // Stop either case. + { + TournamentCoordinator.Instance.Stop(); + TournamentCoordinator.Instance.StopForEach(); + } + + float spawnLatitude, spawnLongitude; + List course; + spawnLatitude = (float)WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.x; + spawnLongitude = (float)WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.y; + course = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].waypoints; + + SpawnUtils.ResetVesselNamingDeconfliction(); + if (Event.current.button == 2) // Middle click => Move the current craft to the spawn point and set it running waypoints. + { + SpawnUtils.ShowSpawnPoint( + WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].worldIndex, + spawnLatitude, + spawnLongitude, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE + ); + TournamentCoordinator.Instance.Configure(null, new WaypointFollowingStrategy(course), null); + TournamentCoordinator.Instance.Run(); + } + else if (!BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME) + { + TournamentCoordinator.Instance.Configure(new SpawnConfigStrategy( + 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, + killEverythingFirst: true, + assignTeams: BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + numberOfTeams: BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + teamCounts: null, + teamsSpecific: null, + folder: 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_REF_HEADING) + ), + new WaypointFollowingStrategy(course), + CircularSpawning.Instance + ); + + // Run the waypoint competition. + TournamentCoordinator.Instance.Run(); + } + else + { + var craftFiles = Directory.GetFiles(Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION)), "*.craft").ToList(); + var strategies = craftFiles.Select(craftFile => new SpawnConfigStrategy( + 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, + killEverythingFirst: true, + assignTeams: BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + numberOfTeams: 0, // This should always be 0 (FFA) to avoid the logic for spawning teams in one-at-a-time mode. + teamCounts: null, + teamsSpecific: null, + folder: null, + craftFiles: 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_REF_HEADING + ))).ToList(); + TournamentCoordinator.Instance.RunForEach(strategies, + new WaypointFollowingStrategy(course), + CircularSpawning.Instance + ); + } + waypointsRunning = true; + } } - else if (Event.current.button == 2) // Middle click, add a new spawn of vessels to the currently spawned vessels. + else { - VesselSpawner.Instance.SpawnAllVesselsOnce(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); // Spawn vessels, without killing off other vessels or changing camera positions. + if (GUI.Button(SLineRect(++line), "Stop waypoints", BDArmorySetup.BDGuiSkin.button)) + { + waypointsRunning = false; + BDATournament.Instance.StopTournament(); + if (TournamentCoordinator.Instance.IsRunning) // Stop either case. + { + TournamentCoordinator.Instance.Stop(); + TournamentCoordinator.Instance.StopForEach(); + } + } } } - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_ContinuousSpawning"), VesselSpawner.Instance.vesselsSpawningContinuously ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + else if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 11) // Custom Spawn Template { - BDATournament.Instance.StopTournament(); - ParseAllSpawnFieldsNow(); - if (!VesselSpawner.Instance.vesselsSpawningContinuously && !_vesselsSpawned && Event.current.button == 0) // Left click + if (BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting) { - VesselSpawner.Instance.SpawnVesselsContinuously(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); // Spawn vessels continuously at 1km above terrain. + 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 maybe start the competition. + SpawnUtils.ResetVesselNamingDeconfliction(); + CustomTemplateSpawning.Instance.SpawnCustomTemplate(CustomTemplateSpawning.customSpawnConfig); + } + } } } - /* - // Special buttons for special rounds. - if (BDArmorySettings.RUNWAY_PROJECT) + else { - if (GUI.Button(SLineRect(++line), Localizer.Format("Runway Project Season 2 Round 3"), _vesselsSpawned ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) // FIXME For round 3 only. + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_SingleSpawn"), _vesselsSpawned ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { BDATournament.Instance.StopTournament(); - if (!_vesselsSpawned && !VesselSpawner.Instance.vesselsSpawningContinuously && Event.current.button == 0) // Left click + ParseAllSpawnFieldsNow(); + if (!_vesselsSpawned && !ContinuousSpawning.Instance.vesselsSpawningContinuously && Event.current.button == 0) // Left click { - Debug.Log("[VesselSpawner]: Spawning 'Round 3' configuration."); + 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_REF_HEADING, + killEverythingFirst: true, + assignTeams: BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + numberOfTeams: BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + teamCounts: null, + teamsSpecific: null, + spawnFolder: BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ); // Spawn vessels. + } + else + { + SpawnUtils.ResetVesselNamingDeconfliction(); + 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_REF_HEADING, + killEverythingFirst: true, + assignTeams: BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + numberOfTeams: BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + teamCounts: null, + teamsSpecific: null, + spawnFolder: BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ); // Spawn vessels. + if (BDArmorySettings.VESSEL_SPAWN_START_COMPETITION_AUTOMATICALLY) + StartCoroutine(StartCompetitionOnceSpawned()); + } _vesselsSpawned = true; - VesselSpawner.Instance.TeamSpawn( - new List { - new VesselSpawner.SpawnConfig( - 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, - 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_REF_HEADING, + killEverythingFirst: false, + assignTeams: false, + numberOfTeams: 0, + teamCounts: null, + teamsSpecific: null, + spawnFolder: BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ); // Spawn vessels, without killing off other vessels or changing camera positions. + } + } + 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( + new CircularSpawnConfig( + new SpawnConfig( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, + killEverythingFirst: true, + assignTeams: BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + numberOfTeams: 1, + teamCounts: null, + teamsSpecific: null, + folder: BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION ), - targetSpawnConfig - }, - true, // Start the competition. - competitionStartDelay, // Wait for the target planes to get going first. - true // Enable startCompetitionNow so the competition starts as soon as the missiles have launched. - ); // FIXME, this is temporary + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING + ) + ); // Spawn vessels continuously at 1km above terrain. } } + 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."); + _vesselsSpawned = false; + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + Debug.Log("[BDArmory.VesselSpawnerWindow]: Resetting continuous spawning button."); + BDATournament.Instance.StopTournament(); + 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) { - if (GUI.Button(SLineRect(++line), "Runway Project Season 2 Round 4", !(round4running && BDATournament.Instance.tournamentStatus != TournamentStatus.Completed) ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box)) + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) { - round4running = true; - BDATournament.Instance.RunTournament(); + case 33: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + case 44: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + case 53: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + case 67: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + case 77: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; } } + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + } - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_CancelSpawning"), (_vesselsSpawned || VesselSpawner.Instance.vesselsSpawningContinuously) ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box)) + public void SetVisible(bool visible) + { + BDArmorySetup.showVesselSpawnerGUI = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); + if (!visible) ParseAllSpawnFieldsNow(); + } + + #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) { - if (_vesselsSpawned) - Debug.Log("[BDArmory]: Resetting spawning vessel button."); - _vesselsSpawned = false; - if (VesselSpawner.Instance.vesselsSpawningContinuously) - Debug.Log("[BDArmory]: Resetting continuous spawning button."); - BDATournament.Instance.StopTournament(); - VesselSpawner.Instance.CancelVesselSpawn(); - round4running = false; // FIXME Round 4 + observerWindowRect.position = position + new Vector2(50, -BDArmorySettings.UI_SCALE_ACTUAL * observerWindowRect.height / 2); // Centred and slightly offset to allow clicking the same spot. + RefreshObservers(); + bringObserverWindowToFront = true; } - - line += 1.25f; // Bottom internal margin - _windowHeight = (line * _lineHeight); + 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 (vessel.ActiveController().AI != null // Check for an AI. + && vessel.ActiveController().WM != null // 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 } -} +} \ No newline at end of file diff --git a/BDArmory/UI/_description b/BDArmory/UI/_description new file mode 100644 index 000000000..5170723b4 --- /dev/null +++ b/BDArmory/UI/_description @@ -0,0 +1 @@ +UI elements. \ No newline at end of file diff --git a/BDArmory/Control/AIUtils.cs b/BDArmory/Utils/AIUtils.cs similarity index 76% rename from BDArmory/Control/AIUtils.cs rename to BDArmory/Utils/AIUtils.cs index 601126bd4..24a3e1cb8 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,44 @@ public static class AIUtils /// vessel to be extrapolated /// after this time /// Vector3 extrapolated position - public static Vector3 PredictPosition(this Vessel v, float time) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 PredictPosition(this Vessel v, float time, bool immediate=true) { - Vector3 pos = v.CoM; - pos += v.Velocity() * time; - pos += 0.5f * v.acceleration * time * time; - return pos; + var vel = v.Velocity(); + var acc = immediate ? v.acceleration_immediate : v.acceleration; + var time2 = 0.5f * time * time; + return new Vector3( + (float)(v.CoM.x + time * vel.x + time2 * acc.x), + (float)(v.CoM.y + time * vel.y + time2 * acc.y), + (float)(v.CoM.z + time * vel.z + time2 * acc.z) + ); } + [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,28 +66,49 @@ 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 relPosition = targetPosition - vessel.CoM; Vector3 relVelocity = targetVelocity - vessel.Velocity(); Vector3 relAcceleration = targetAcceleration - vessel.acceleration; - 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); - if (A == 0) // Not actually a cubic. Relative acceleration is zero, so return the much simpler linear timeToCPA. + return TimeToCPA(relPosition, relVelocity, relAcceleration, maxTime, cpaType); + } + + /// + /// 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); + float c = Vector3.Dot(relVelocity, relVelocity); + if (a == 0 || a * maxTime < 1e-3f * c) // Not actually a cubic. Relative acceleration is zero or insignificant within the time limit, so return the much simpler linear timeToCPA. { - return Mathf.Clamp(-Vector3.Dot(relPosition, relVelocity) / relVelocity.sqrMagnitude, 0f, maxTime); + if (c > 0) + return Mathf.Clamp(-Vector3.Dot(relPosition, relVelocity) / relVelocity.sqrMagnitude, 0f, maxTime); + else + return 0; // The objects are static, so they're not going to get any closer. } + + float A = a / 2f; + float B = Vector3.Dot(relVelocity, relAcceleration) * 3f / 2f; + float C = c + Vector3.Dot(relPosition, relAcceleration); + float D = Vector3.Dot(relPosition, relVelocity); float D0 = B * B - 3f * A * C; - float D1 = 2 * B * B * B - 9f * A * B * C + 27f * A * A * D; + float D1 = 2f * 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) { // 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); @@ -80,37 +116,82 @@ public static float ClosestTimeToCPA(this Vessel vessel, Vector3 targetPosition, else if (E < 0) { // 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 + 2f * t * B + 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 + t * relVelocity + t * t / 2f * relAcceleration).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(D0) == 0f) { // 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 / D0; + float t1 = (4f * A * B * C - 9f * A * A * D - B * B * B) / A / D0; + 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 + t0 * relVelocity + t0 * t0 / 2f * relAcceleration).sqrMagnitude < (relPosition + t1 * relVelocity + t1 * t1 / 2f * relAcceleration).sqrMagnitude) ? t0 : t1; + break; + } + return Mathf.Clamp(time, 0, maxTime); } } } + public static float PredictClosestApproachSqrSeparation(this Vessel vessel, Vessel otherVessel, float maxTime) + { + var timeToCPA = vessel.TimeToCPA(otherVessel, maxTime); + if (timeToCPA > 0 && timeToCPA < maxTime) + return (vessel.PredictPosition(timeToCPA) - otherVessel.PredictPosition(timeToCPA)).sqrMagnitude; + else + return float.MaxValue; + } + /// /// Get the altitude of terrain below/above a point. /// @@ -140,11 +221,39 @@ public static Vector3 GetLocalFormationPosition(this IBDAIControl ai, int index) float lag = ai.commandLeader.lag; float right = rightSign * positionFactor * spread; - float back = positionFactor * lag * -1; + float back = -positionFactor * lag; 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 wm = v.Current.ActiveController().WM; + if (wm != 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 { @@ -152,6 +261,7 @@ public enum VehicleMovementType Land = 1, Water = 2, Amphibious = Land | Water, + Submarine = 4 | Water, } /// @@ -346,7 +456,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); @@ -460,7 +570,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); @@ -636,7 +746,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) @@ -646,7 +756,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 89% rename from BDArmory/Misc/BDAEditorTools.cs rename to BDArmory/Utils/BDAEditorTools.cs index 6fbed305d..0cd6a0c9c 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,12 +67,12 @@ public static List getRadars() return results; } - void dumpParts() + public static void dumpParts() { - String gunName = "bda_weapons_list.csv"; - String missileName = "bda_missile_list.csv"; - String radarName = "bda_radar_list.csv"; - String jammerName = "bda_jammer_list.csv"; + string gunName = "bda_weapons_list.csv"; + string missileName = "bda_missile_list.csv"; + string radarName = "bda_radar_list.csv"; + string jammerName = "bda_jammer_list.csv"; ModuleWeapon weapon = null; MissileLauncher missile = null; ModuleRadar radar = null; @@ -86,12 +95,12 @@ 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"); - Debug.Log("Dumping parts..."); + Debug.Log("[BDArmory.BDAEditorTools]: Dumping parts..."); // 3. iterate weapons and write out fields foreach (var item in PartLoader.LoadedPartsList) @@ -110,7 +119,7 @@ void dumpParts() fileguns.WriteLine( item.name + ";" + item.title + ";" + item.author + ";" + item.manufacturer + ";" + item.partPrefab.mass + ";" + item.cost + ";" + item.partPrefab.crashTolerance + ";" + item.partPrefab.maxTemp + ";" + weapon.roundsPerMinute + ";" + weapon.maxDeviation + ";" + weapon.maxEffectiveDistance + ";" + weapon.weaponType + ";" + weapon.bulletType + ";" + weapon.ammoName + ";" + weapon.bulletMass + ";" + weapon.bulletVelocity + ";" + - weapon.maxHeat + ";" + weapon.heatPerShot + ";" + weapon.heatLoss + ";" + weapon.tntMass + ";" + weapon.airDetonation + weapon.maxHeat + ";" + weapon.heatPerShot + ";" + weapon.heatLoss + ";" + weapon.tntMass ); } @@ -119,7 +128,7 @@ void dumpParts() filemissiles.WriteLine( item.name + ";" + item.title + ";" + item.author + ";" + item.manufacturer + ";" + item.partPrefab.mass + ";" + item.cost + ";" + item.partPrefab.crashTolerance + ";" + item.partPrefab.maxTemp + ";" + missile.thrust + ";" + missile.boostTime + ";" + missile.cruiseThrust + ";" + missile.cruiseTime + ";" + missile.maxTurnRateDPS + ";" + missile.blastPower + ";" + missile.blastHeat + ";" + missile.blastRadius + ";" + missile.guidanceActive + ";" + missile.homingType + ";" + missile.targetingType + ";" + missile.minLaunchSpeed + ";" + missile.minStaticLaunchRange + ";" + missile.maxStaticLaunchRange + ";" + missile.optimumAirspeed + ";" + - missile.terminalManeuvering + ";" + missile.terminalGuidanceType + ";" + missile.terminalGuidanceDistance + ";" + + missile.terminalGuidanceShouldActivate + ";" + missile.terminalGuidanceType + ";" + missile.terminalGuidanceDistance + ";" + missile.activeRadarRange + ";" + missile.radarLOAL + ";" + missile.maxOffBoresight + ";" + missile.lockedSensorFOV + ";" + missile.heatThreshold + ";" + @@ -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 @@ -154,7 +163,7 @@ void dumpParts() filemissiles.Close(); fileradars.Close(); filejammers.Close(); - Debug.Log("...dumping parts complete."); + Debug.Log("[BDArmory.BDAEditorTools]: ...dumping parts complete."); } } } diff --git a/BDArmory/Utils/BDAMath.cs b/BDArmory/Utils/BDAMath.cs new file mode 100644 index 000000000..0a1864db6 --- /dev/null +++ b/BDArmory/Utils/BDAMath.cs @@ -0,0 +1,114 @@ +using System; +using UnityEngine; +using System.Runtime.CompilerServices; + +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); + } + + [Obsolete("Use -VectorUtils.GetAngleOnPlane(fromDirection, toDirection, referenceRight) instead.")] + public static float SignedAngle(Vector3 fromDirection, Vector3 toDirection, Vector3 referenceRight) + { + float angle = VectorUtils.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; + } + + /// + /// Solve quadratic of a•t²+v•t=d for t, where acceleration (a) and distance (d) are assumed to be non-negative and v is the speed in the direction of the target. + /// + /// + /// + /// + /// + public static float SolveTime(float distance, float acceleration, float vel = 0) + { + if (acceleration == 0f) + { + if (vel == 0) + return float.MaxValue; + else + return Mathf.Abs(distance) / vel; + } + else + { + float a = 0.5f * acceleration; + float b = vel; + float c = -Mathf.Abs(distance); + + float x = (-b + BDAMath.Sqrt(b * b - 4 * a * c)) / (2 * a); + + return x; + } + } + + /// + /// A double version of Mathf.Clamp. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp(double value, double min, double max) + { + return value < min ? min : value > max ? max : value; + } + + } +} diff --git a/BDArmory/Misc/BDAModuleInfos.cs b/BDArmory/Utils/BDAModuleInfos.cs similarity index 79% rename from BDArmory/Misc/BDAModuleInfos.cs rename to BDArmory/Utils/BDAModuleInfos.cs index f6db31cd0..d608aeda2 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. @@ -16,6 +16,8 @@ internal class BDAModuleInfos : MonoBehaviour //{"WeaponModule", "Weapon"}, { "BDModuleSurfaceAI", "BDModule Surface AI"}, { "BDModulePilotAI", "BDModule Pilot AI"}, + { "BDModuleVTOLAI", "BDModule VTOL AI"}, + { "BDModuleOrbitalAI", "BDModule Orbital AI"}, }; public void Start() @@ -25,9 +27,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()) @@ -44,8 +44,8 @@ internal static IEnumerator ReloadModuleInfos() string info = partModules.Current.GetInfo(); for (int y = 0; y < loadedParts.Current.moduleInfos.Count; y++) { - Debug.Log($"moduleName: {loadedParts.Current.moduleInfos[y].moduleName}"); - Debug.Log($"KeyValue: {Modules[key]}"); + Debug.Log($"[BDArmory.BDAModuleInfos]: moduleName: {loadedParts.Current.moduleInfos[y].moduleName}"); + Debug.Log($"[BDArmory.BDAModuleInfos]: KeyValue: {Modules[key]}"); if (loadedParts.Current.moduleInfos[y].moduleName != Modules[key]) continue; loadedParts.Current.moduleInfos[y].info = info; break; diff --git a/BDArmory/Misc/BDAcTools.cs b/BDArmory/Utils/BDAcTools.cs similarity index 93% rename from BDArmory/Misc/BDAcTools.cs rename to BDArmory/Utils/BDAcTools.cs index 1e970e299..f1bf34a0c 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 { @@ -37,9 +37,6 @@ public static double Clamp(double value, double min, double max) return result; } - public static String AppPath = KSPUtil.ApplicationRootPath.Replace("\\", "/"); - public static String PlugInDataPath = AppPath + "GameData/BDAcCategorynFS/Plugin/"; - public static FloatCurve stringToFloatCurve(string curveString) { FloatCurve resultCurve = new FloatCurve(); @@ -78,7 +75,7 @@ public static List parseIntegers(string stringOfInts) } else { - Debug.Log("invalid integer: " + valueArray[i]); + Debug.Log("[BDArmory.BDAcTools]: invalid integer: " + valueArray[i]); } } return newIntList; @@ -97,7 +94,7 @@ public static List parseFloats(string stringOfFloats) } else { - Debug.Log("invalid float: " + array[i]); + Debug.Log("[BDArmory.BDAcTools]: invalid float: " + array[i]); } } return list; @@ -116,7 +113,7 @@ public static List ParseDoubles(string stringOfDoubles) } else { - Debug.Log("FSBDAcTools: invalid float: [len:" + array[i].Length + "] '" + array[i] + "']"); + Debug.Log("[BDArmory.BDAcTools]: invalid float: [len:" + array[i].Length + "] '" + array[i] + "']"); } } return list; @@ -197,7 +194,7 @@ public static UIPartActionWindow FindActionWindow(this Part part) goto foundField; } } - Debug.LogWarning("*PartUtils* Unable to find UIPartActionWindow list"); + Debug.LogWarning("[BDArmory.BDAcTools]: Unable to find UIPartActionWindow list"); return null; } foundField: 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/Utils/BDInputUtils.cs b/BDArmory/Utils/BDInputUtils.cs new file mode 100644 index 000000000..b58d16626 --- /dev/null +++ b/BDArmory/Utils/BDInputUtils.cs @@ -0,0 +1,299 @@ +using UnityEngine; +using System; +using System.Collections; + +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + public class BDInputUtils + { + public static string GetInputString() + { + //keyCodes + string[] names = System.Enum.GetNames(typeof(KeyCode)); + int numberOfKeycodes = names.Length; + + 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")) + { + output = "[" + output.Substring(6).ToLower() + "]"; + } + else if (output.Contains("Alpha")) + { + output = output.Substring(5); + } + else //lower case key + { + output = output.ToLower(); + } + + //modifiers + if (output.Contains("control")) + { + output = output.Split('c')[0] + " ctrl"; + } + else if (output.Contains("alt")) + { + output = output.Split('a')[0] + " alt"; + } + else if (output.Contains("shift")) + { + output = output.Split('s')[0] + " shift"; + } + else if (output.Contains("command")) + { + output = output.Split('c')[0] + " cmd"; + } + + //special keys + else if (output == "backslash") + { + output = @"\"; + } + else if (output == "backquote") + { + output = "`"; + } + else if (output == "[period]") + { + output = "[.]"; + } + else if (output == "[plus]") + { + output = "[+]"; + } + else if (output == "[multiply]") + { + output = "[*]"; + } + else if (output == "[divide]") + { + output = "[/]"; + } + else if (output == "[minus]") + { + output = "[-]"; + } + else if (output == "[enter]") + { + output = "enter"; + } + else if (output.Contains("page")) + { + output = output.Insert(4, " "); + } + else if (output.Contains("arrow")) + { + output = output.Split('a')[0]; + } + else if (output == "capslock") + { + output = "caps lock"; + } + else if (output == "minus") + { + output = "-"; + } + + //test if input is valid + try + { + if (Input.GetKey(output)) + { + return output; + } + } + catch (System.Exception e) + { + if (!e.Message.EndsWith("is unknown")) // Ignore unknown keys + Debug.LogWarning("[BDArmory.BDInputUtils]: Exception thrown in GetInputString: " + e.Message + "\n" + e.StackTrace); + } + } + + //mouse + for (int m = 0; m < 6; m++) + { + string inputString = "mouse " + m; + try + { + if (Input.GetKey(inputString)) + { + return inputString; + } + } + catch (UnityException e) + { + Debug.Log("[BDArmory.BDInputUtils]: Invalid mouse: " + inputString); + Debug.LogWarning("[BDArmory.BDInputUtils]: Exception thrown in GetInputString: " + e.Message + "\n" + e.StackTrace); + } + } + + //joysticks + for (int j = 1; j < 12; j++) + { + for (int b = 0; b < 20; b++) + { + string inputString = "joystick " + j + " button " + b; + try + { + if (Input.GetKey(inputString)) + { + return inputString; + } + } + catch (UnityException e) + { + Debug.LogWarning("[BDArmory.BDInputUtils]: Exception thrown in GetInputString: " + e.Message + "\n" + e.StackTrace); + return string.Empty; + } + } + } + + return string.Empty; + } + + public static bool GetKey(BDInputInfo input) + { + return !string.IsNullOrEmpty(input.inputString) && Input.GetKey(input.inputString); + } + + public static bool GetKeyDown(BDInputInfo input) + { + return !string.IsNullOrEmpty(input.inputString) && Input.GetKeyDown(input.inputString); + } + } + + /// + /// A class for more easily inputting numeric values in TextFields. + /// There's a configurable delay after the last keystroke before attempting to interpret the string as a double. Default: 0.5s. + /// Explicit cast to lower precision types may be needed when assigning the current value. + /// + public class NumericInputField : MonoBehaviour + { + public static GUIStyle InputFieldStyle; + public static GUIStyle InputFieldBadStyle; + static void ConfigureStyles() + { + InputFieldStyle = new GUIStyle(GUI.skin.textField) + { alignment = TextAnchor.MiddleRight }; + InputFieldBadStyle = new GUIStyle(InputFieldStyle); + InputFieldBadStyle.normal.textColor = Color.red; + InputFieldBadStyle.focused.textColor = Color.red; + } + + public NumericInputField Initialise(double lastUpdated, double currentValue, double minValue = double.MinValue, double maxValue = double.MaxValue, (float, float, bool, bool) meta = default) + { + this.lastUpdated = lastUpdated; this.currentValue = currentValue; this.minValue = minValue; this.maxValue = maxValue; + (rounding, sigFig, withZero, reducedPrecisionAtMin) = meta; + return this; + } + public double lastUpdated; + public string possibleValue = string.Empty; + private double _value; + public double currentValue // Note: setting the current value doesn't necessarily update the displayed string. Use SetCurrentValue to force updating the displayed value. + { + get { return _value; } + private set + { + _value = value; + if (string.IsNullOrEmpty(possibleValue)) + { + possibleValue = _value.ToString("G6"); + valid = true; + } + } + } + public double minValue; + public double maxValue; + private bool coroutineRunning = false; + private Coroutine coroutine; + public bool valid = true; + + #region Metadata (not used internally, but provided for reference for automation) + public float rounding; + public float sigFig; + public bool withZero; + public bool reducedPrecisionAtMin; + #endregion + + // Set the current value and force the display to update. + public void SetCurrentValue(double value) + { + possibleValue = null; // Clear the possibleValue first so that it gets updated. + currentValue = value; + lastUpdated = Time.time; + } + + public void tryParseValue(string v) + { + if (v != possibleValue) + { + lastUpdated = !string.IsNullOrEmpty(v) ? Time.time : Time.time + BDArmorySettings.NUMERIC_INPUT_DELAY; // Give the empty string an extra delay. + possibleValue = v; + if (!coroutineRunning) + { + coroutine = StartCoroutine(UpdateValueCoroutine()); + } + } + } + + IEnumerator UpdateValueCoroutine() + { + var wait = new WaitForFixedUpdate(); + coroutineRunning = true; + valid = true; // Flag the value as valid until we've tried parsing it. + while (Time.time - lastUpdated < BDArmorySettings.NUMERIC_INPUT_DELAY) + yield return wait; + tryParseCurrentValue(BDArmorySettings.NUMERIC_INPUT_SELF_UPDATE); + coroutineRunning = false; + yield return wait; + } + + void tryParseCurrentValue(bool updatePossible = false) + { + double newValue; + if (double.TryParse(possibleValue, out newValue)) + { + currentValue = Math.Min(Math.Max(newValue, minValue), Math.Max(maxValue, currentValue)); // Clamp the new value between the min and max, but not if it's been set higher with the unclamped tuning option. This still allows reducing the value while still above the clamp limit. + if (newValue != currentValue) // The value got clamped. + possibleValue = currentValue.ToString("G6"); + lastUpdated = Time.time; + valid = true; + } + else + { + valid = false; + } + if (updatePossible) + { + possibleValue = currentValue.ToString("G6"); + valid = true; + } + } + + // Parse the current possible value immediately. + public void tryParseValueNow() + { + tryParseCurrentValue(true); + if (coroutineRunning) + { + StopCoroutine(coroutine); + coroutineRunning = false; + } + } + + public GUIStyle style + { + get + { + if (InputFieldStyle == null || InputFieldBadStyle == null) ConfigureStyles(); + return valid ? InputFieldStyle : InputFieldBadStyle; + } + } + } +} diff --git a/BDArmory/UI/BDKeyBinder.cs b/BDArmory/Utils/BDKeyBinder.cs similarity index 93% rename from BDArmory/UI/BDKeyBinder.cs rename to BDArmory/Utils/BDKeyBinder.cs index 3f6eadf2a..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 { @@ -58,7 +58,7 @@ public static void BindKey(int id) { if (current != null) { - Debug.Log("[BDArmory]: Tried to bind key but key binder is in use."); + Debug.Log("[BDArmory.BDKeyBinder]: Tried to bind key but key binder is in use."); return; } diff --git a/BDArmory/Utils/BlastPhysicsUtils.cs b/BDArmory/Utils/BlastPhysicsUtils.cs new file mode 100644 index 000000000..b6cdf0599 --- /dev/null +++ b/BDArmory/Utils/BlastPhysicsUtils.cs @@ -0,0 +1,267 @@ +using System; +using UnityEngine; + +using BDArmory.Extensions; +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + public static class BlastPhysicsUtils + { + // This values represent percentage of the blast radius where we consider that the damage happens. + + // Methodology based on AASTP-1: MANUAL OF NATO SAFETY PRINCIPLES FOR THE STORAGE OF MILITARY AMMUNITION AND EXPLOSIVES + // Link: http://www.rasrinitiative.org/pdfs/AASTP-1-Ed1-Chge-3-Public-Release-110810.pdf + public static BlastInfo CalculatePartBlastEffects(Part part, float distanceToHit, double vesselMass, float explosiveMass, float range) + { + float clampedMinDistanceToHit = ClampRange(explosiveMass, distanceToHit); + + var minPressureDistance = distanceToHit + part.GetAverageBoundSize(); + + double minPressurePerMs = 0; + + float clampedMaxDistanceToHit = ClampRange(explosiveMass, minPressureDistance); + double maxScaledDistance = CalculateScaledDistance(explosiveMass, clampedMaxDistanceToHit); + double maxDistPositivePhase = CalculatePositivePhaseTime(maxScaledDistance, explosiveMass); + + if (minPressureDistance <= range) + { + minPressurePerMs = CalculateIncidentImpulse(maxScaledDistance, explosiveMass); + } + + double minScaledDistance = CalculateScaledDistance(explosiveMass, clampedMinDistanceToHit); + double maxPressurePerMs = CalculateIncidentImpulse(minScaledDistance, explosiveMass); + double minDistPositivePhase = CalculatePositivePhaseTime(minScaledDistance, explosiveMass); + + double totalDamage = (maxPressurePerMs + minPressurePerMs);// * 2 / 2 ; + + float effectivePartArea = CalculateEffectiveBlastAreaToPart(range, part); + + double maxforce = CalculateForce(maxPressurePerMs, effectivePartArea, minDistPositivePhase); + double minforce = CalculateForce(minPressurePerMs, effectivePartArea, maxDistPositivePhase); + + float positivePhase = (float)(minDistPositivePhase + maxDistPositivePhase) / 2f; + + double force = (maxforce + minforce) / 2f; + + float acceleration = vesselMass > 0 ? (float)(force / vesselMass) : 0; // If the vesselMass is 0, don't give infinite acceleration! + + // Calculation of damage + + float finalDamage = (float)totalDamage; + + if (BDArmorySettings.DEBUG_DAMAGE) + { + Debug.Log( + "[BDArmory.BlastPhysicsUtils]: Blast Debug data: {" + part.name + " on " + part.vessel.vesselName + "}, " + + " clampedMinDistanceToHit: {" + clampedMinDistanceToHit + "}," + + " minPressureDistance: {" + minPressureDistance + "}," + + " minScaledDistance: {" + minScaledDistance + "}," + + " maxScaledDistance: {" + maxScaledDistance + "}," + + " minPressurePerMs: {" + minPressurePerMs + "}," + + " maxPressurePerMs: {" + maxPressurePerMs + "}," + + " minDistPositivePhase: {" + minDistPositivePhase + "}," + + " maxDistPositivePhase: {" + maxDistPositivePhase + "}," + + " totalDamage: {" + totalDamage + "}," + + " finalDamage: {" + finalDamage + "},"); + } + + return new BlastInfo() { TotalPressure = maxPressurePerMs, EffectivePartArea = effectivePartArea, PositivePhaseDuration = positivePhase, VelocityChange = acceleration, Damage = finalDamage }; + } + + private static float CalculateEffectiveBlastAreaToPart(float range, Part part) + { + float circularArea = Mathf.PI * range * range; + + return Mathf.Clamp(circularArea, 0f, part.GetArea() * 0.40f); + } + + private static double CalculateScaledDistance(float explosiveCharge, float distanceToHit) + { + return (distanceToHit / Math.Pow(explosiveCharge, 1f / 3f)); + } + + private static float ClampRange(float explosiveCharge, float distanceToHit) + { + float cubeRootOfChargeWeight = (float)Math.Pow(explosiveCharge, 1f / 3f); + + return Mathf.Clamp(distanceToHit, 0.0674f * cubeRootOfChargeWeight, 40f * cubeRootOfChargeWeight); + } + + private static double CalculateIncidentImpulse(double scaledDistance, float explosiveCharge) + { + double t = Math.Log(scaledDistance) / Math.Log(10); + double cubeRootOfChargeWeight = Math.Pow(explosiveCharge, 0.3333333); + double ii = 0; + 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 * 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 * U2 + + 0.00595798753822 * U3 + + 0.014544526107 * U4 - + 0.00663289334734 * U5 - + 0.00284189327204 * U6 + + 0.0013644816227 * U7; + } + + ii = Math.Pow(10, ii); + ii = ii * cubeRootOfChargeWeight; + return ii; + } + + // Calculate positive phase time in ms from AASTP-1 + private static double CalculatePositivePhaseTime(double scaledDistance, float explosiveCharge) + { + scaledDistance = Math.Min(Math.Max(scaledDistance, 0.178), 40); // Formula only valid for scaled distances between 0.178 and 40 m + double t = Math.Log(scaledDistance) / Math.Log(10); + double cubeRootOfChargeWeight = Math.Pow(explosiveCharge, 0.3333333); + double ii = 0; + + 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 * 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 * 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 * U2 - + 0.00226884995013 * U3 - + 0.00295908591505 * U4 + + 0.00148029868929 * U5; + } + + ii = Math.Pow(10, ii); + ii = ii * cubeRootOfChargeWeight; + return ii; + } + + // Calculate duration of explosion event in seconds + public static float CalculateMaxTime(float tntMass) + { + float range = CalculateBlastRange(tntMass); + range = ClampRange(tntMass, range); + double scaledDistance = CalculateScaledDistance(tntMass, range); + + double t = Math.Log(scaledDistance) / Math.Log(10); + double cubeRootOfChargeWeight = Math.Pow(tntMass, 0.3333333); + 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 * 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; + return (float)ii; + } + + /// + /// Calculate newtons from the pressure in kPa and the surface on Square meters + /// + /// kPa + /// m2 + /// + private static double CalculateForce(double pressure, float surface, double timeInMs) + { + return pressure * 1000f * surface * (timeInMs / 1000f); + } + + /// + /// Method based on Hopkinson-Cranz Scaling Law + /// Z value of 14.8 + /// + /// tnt equivales mass in kg + /// explosive range in meters + public static float CalculateBlastRange(double tntMass) + { + return (float)(14.8f * Math.Pow(tntMass, 1 / 3f)); + } + + /// + /// Method based on Hopkinson-Cranz Scaling Law + /// Z value of 14.8 + /// + /// expected range in meters + /// explosive range in meters + public static float CalculateExplosiveMass(float range) + { + var scaledRange = range / 14.8f; + return (float)(scaledRange * scaledRange * scaledRange); + } + } + + public struct BlastInfo + { + public float VelocityChange { get; set; } + public float EffectivePartArea { get; set; } + public float Damage { get; set; } + public double TotalPressure { get; set; } + public double PositivePhaseDuration { get; set; } + } +} diff --git a/BDArmory/Utils/BodyUtils.cs b/BDArmory/Utils/BodyUtils.cs new file mode 100644 index 000000000..35cee1d50 --- /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 77% rename from BDArmory.Core/Utils/BulletPhysics.cs rename to BDArmory/Utils/BulletPhysics.cs index f8e7f3ed8..2fac54af5 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 { @@ -21,17 +21,17 @@ public static Vector3 CalculateDrag(Vector3 velocity, float bulletMass, float ca //float aDrag = (k * vSqr) / m; //Has to be in a direction opposite of the bullet's velocity vector - //Vector3 dragVec = aDrag * velocityVec.normalized * -1f; + //Vector3 dragVec = -aDrag * velocityVec.normalized; /////////////////////////////////////////////////////// - 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; float vSqr = velocity.sqrMagnitude; - float aDrag = (k * vSqr) / bulletMass; + float aDrag = k * vSqr / bulletMass; - Vector3 dragVec = aDrag * velocity.normalized * -1f; + Vector3 dragVec = -aDrag * velocity.normalized; return dragVec; } diff --git a/BDArmory/Utils/ConfigNodeUtils.cs b/BDArmory/Utils/ConfigNodeUtils.cs new file mode 100644 index 000000000..0109520f4 --- /dev/null +++ b/BDArmory/Utils/ConfigNodeUtils.cs @@ -0,0 +1,48 @@ +using UnityEngine; + +namespace BDArmory.Utils +{ + public class ConfigNodeUtils + { + static public string FindPartModuleConfigNodeValue(ConfigNode configNode, string moduleName, string fieldName, string moduleID = "", string moduleIDValue = "") + { + if (configNode == null) return null; + string retval = null; + // Search this node. + if (configNode.values != null) + { + /* + if (configNode.name == "MODULE" && configNode.HasValue("name") && configNode.GetValue("name") == moduleName) + if (configNode.HasValue(fieldName)) + return configNode.GetValue(fieldName); + */ + foreach (var module in configNode.GetNodes()) + { + if (module.name == "MODULE" && module.HasValue("name") && module.GetValue("name") == moduleName) + if (module.HasValue(fieldName)) + if (string.IsNullOrEmpty(moduleID) || module.GetValue(moduleID) == moduleIDValue) + return module.GetValue(fieldName); + } + } + // Search sub-nodes. + if (configNode.nodes != null) + { + for (int i = 0; i < configNode.nodes.Count; ++i) + if ((retval = FindPartModuleConfigNodeValue(configNode.nodes[i], moduleName, fieldName)) != null) + return retval; + } + return null; + } + + static public void PrintConfigNode(ConfigNode configNode, string indent = "") + { + Debug.Log("[BDArmory.ConfigNodeUtils]: " + indent + configNode.ToString() + ":: "); + for (int i = 0; i < configNode.values.Count; ++i) + Debug.Log("[BDArmory.ConfigNodeUtils]: " + indent + configNode.values[i].name + ": " + configNode.values[i].value); + foreach (var node in configNode.GetNodes()) + { + PrintConfigNode(node, indent + " "); + } + } + } +} \ No newline at end of file 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..048f8da0b --- /dev/null +++ b/BDArmory/Utils/FARUtils.cs @@ -0,0 +1,416 @@ +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 static float GetFARcurrWingMass(Part part) + { + if (!hasFARWing) return -1; + foreach (var module in part.Modules) + { + if (module.GetType() == FARWingModule) + { + var wingMass = (float)FARWingModule.GetField("curWingMass", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found wing Mass of {wingMass} for {part.name}."); + return wingMass; + } + if (module.GetType() == FARControllableSurfaceModule) + { + var wingMass = (float)FARControllableSurfaceModule.GetField("curWingMass", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found ctrl. srf. Mass multiplier of {wingMass} for {part.name}."); + return wingMass; + } + } + return -1; + } + public static double GetFARWingSweep(Part part) + { + if (!hasFARWing) return 0; + + foreach (var module in part.Modules) + { + if (module.GetType() == FARWingModule) + { + var sweep = (double)FARWingModule.GetField("MidChordSweep", BindingFlags.Public | BindingFlags.Instance).GetValue(module); //leading + trailing angle / 2 + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found mid chord sweep of {sweep} for {part.name}."); + return sweep; + } + if (module.GetType() == FARControllableSurfaceModule) + { + var sweep = (double)FARControllableSurfaceModule.GetField("MidChordSweep", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found ctrl. srf. mid chord sweep of {sweep} for {part.name}."); + return sweep; + } + } + return 0; + } + } + + 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 string PWAssyVersion = "unknown"; + 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; + PWAssyVersion = assy.assembly.GetName().Version.ToString(); + 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); + bool isAeroSrf = (bool)PWType.GetField("aeroIsLiftingSurface", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + //bool isAeroSrf = (float)PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).GetValue(module) > 0f; + float width = ((float)PWType.GetField("sharedBaseWidthRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseWidthTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)); + int edgeLeadingType = Mathf.RoundToInt((float)PWType.GetField("sharedEdgeTypeLeading", BindingFlags.Public | BindingFlags.Instance).GetValue(module)); + int edgeTrailingType = Mathf.RoundToInt((float)PWType.GetField("sharedEdgeTypeTrailing", BindingFlags.Public | BindingFlags.Instance).GetValue(module)); + float edgeWidth = ((!ctrlSrf && edgeLeadingType >= 2 ? + ((float)PWType.GetField("sharedEdgeWidthLeadingTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + + (float)PWType.GetField("sharedEdgeWidthLeadingRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) : 0) + + (ctrlSrf || edgeTrailingType >= 2 ? ((float)PWType.GetField("sharedEdgeWidthTrailingTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + + (float)PWType.GetField("sharedEdgeWidthTrailingRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) : 0)); + float thickness = 0.18f; //thickness (averge) of the wing + float adjustedThickness = 0.18f; //adjusted value for mass scaling + if (BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP) + { + thickness = ((float)PWType.GetField("sharedBaseThicknessRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseThicknessTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) / 2; + if (thickness > 0.18) + //adjustedThickness = (Mathf.Max(0.18f, (Mathf.Log(1.05f + thickness) * 0.78f))); //stock parts use exponential mass scalar, not linear - 25kg/s0, 125kg/s1, 500kg/s2, 4t/s3 for 1m long tanks + { + adjustedThickness = Mathf.Pow(thickness / 2.5f, 2.75f); //this follows stock mass scaling, up to about 3.75m parts + if (adjustedThickness >= 0.15f) adjustedThickness += 0.45f; + else adjustedThickness += (adjustedThickness * 1.66f) + 0.188f; + } + else + adjustedThickness = Mathf.Max(thickness, 0.05f); //2.5x2.5x0.938 tank: .5t; pWing: .36t - needs adjustedThickness of 1.35 instead of 0.96 + } //3.75x3.75x1.875 tank: 4t; pwing: 1.37t - needs adjustment of 3.6, not 1.19 + //float thickness = 0.36f; + + float liftCoeff = (length * ((width + edgeWidth) / 2)) / 3.515f; + float aeroVolume = (0.786f * length * ((width + edgeWidth) / 2) * Mathf.Clamp(adjustedThickness, 0, 0.275f)); //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}."); + + //if (PWAssyVersion != "0.44.0.0") //PWings now have edge colliders, unnecessary + if (!BDArmorySettings.PWING_EDGE_LIFT && !ctrlSrf) //if part !controlsurface, remove lift/mass from edges to bring inline with stock boards + { + aeroVolume = (0.786f * length * (width / 2) * Mathf.Clamp(adjustedThickness, 0, 0.275f)); //original .7 was based on errorneous 2x4 wingboard dimensions; stock reference wing area is 1.875x3.75m + liftCoeff = (length * (width / 2f)) / 3.515f; + } + if (!ctrlSrf && !WingctrlSrf) + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (float)Math.Round((liftCoeff / 10f) * (adjustedThickness * 5.56f), 3)); //Adjust PWing GUI mass readout + else + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (float)Math.Round((liftCoeff / 5f) * (adjustedThickness * 5.56f), 3)); //this modifies the IPartMassModifier, so the mass will also change along with the GUI + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.MAX_PWING_LIFT > 0) + { + liftCoeff = Mathf.Clamp((float)liftCoeff, 0, BDArmorySettings.MAX_PWING_LIFT + (HighLogic.LoadedSceneIsEditor ? 1e-4f : 0f)); //if Runway Project, check lift is within limit and clamp if not. Adding 1e-4f in editor scenes so it can get caught by the BDA Craft Utilities Tool looking for wings larger than the limit without displaying a larger number in the PAW + //f (!WingctrlSrf && !ctrlSrf) PWType.GetField("sharedBaseOffsetRoot", BindingFlags.Public | BindingFlags.Instance).SetValue(module, 0); //irrelevant in pWing 0.46.7 + if (BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP) + { + float chord = (width + ((BDArmorySettings.PWING_EDGE_LIFT || ctrlSrf) ? edgeWidth : 0)) / 2; + if (thickness > 0.24 && thickness / chord > 0.2f) + { + float TCRmod = Mathf.Clamp01(1 - ((thickness - 0.188f) / chord)); + liftCoeff *= TCRmod; //adjust lift coeff based on Thickness to Chord Ratio. Loggins lift nerf for circular/near-circular wing crossections + if (liftCoeff < 0) liftCoeff = 0; + if (TCRmod < 0.8f) //apply body lift reduction to pWings that are thicker than default and sufficiently thin (using pWings for enpennages/tailbooms/etc) standard thickness wings unaffected + { + if (HighLogic.LoadedSceneIsFlight) if (Mathf.Abs(Vector3.Dot(part.vessel.ReferenceTransform.up, part.transform.right)) > 0.85) liftCoeff /= 2; //something something a wing rotated 90deg would only be generating body lift due to airfoil now perpendicular to airflow. Loggins pWing body nerf + if (HighLogic.LoadedSceneIsEditor) if (Mathf.Abs(Vector3.Dot(EditorLogic.fetch.vesselRotation * Vector3d.up, part.transform.right)) > 0.85) liftCoeff /= 2; + } + } + } + } + PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).SetValue(module, isAeroSrf ? (float)Math.Round(liftCoeff, 2) : 0f); //adjust PWing GUI lift readout + if (!FerramAerospace.CheckForFAR()) part.Modules.GetModule().deflectionLiftCoeff = HighLogic.LoadedSceneIsEditor ? liftCoeff : (float)Math.Round(liftCoeff, 2); + if (part.name.Contains("B9.Aero.Wing.Procedural.Panel") || !isAeroSrf) //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 + edgeWidth) / 2)) / 3.515f) / 12.5f) * (Mathf.Max(0.3f, adjustedThickness * 5.6f))); //Struct panels lighter than wings, clamp mass for panels thinner than 0.05m + if (!FerramAerospace.CheckForFAR()) part.FindModuleImplementing().deflectionLiftCoeff = 0; + else + { + PWType.GetField("sharedArmorRatio", BindingFlags.Public | BindingFlags.Instance).SetValue(module, 100); + } + //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 + } + else + { + if (BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP && FerramAerospace.CheckForFAR()) //PWings disables massMod if FAR, so need to re-add the additional mass from thickness + { + float massToAdd = 0; + massToAdd = ((float)liftCoeff / ((!ctrlSrf && !WingctrlSrf) ? 10 : 5)) * (adjustedThickness * 2.8f) - + ((float)liftCoeff / ((!ctrlSrf && !WingctrlSrf) ? 10 : 5)) * (0.36f * 3); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: massToAdd {massToAdd} for {part.name}."); + + massToAdd += part.partInfo.partPrefab.mass; //this gets subtracted out in the WingProcedural GetModuleMass, so need to add it here to get proper mass addition + if (massToAdd > 0) + { + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, massToAdd); + PWType.GetField("sharedArmorRatio", BindingFlags.Public | BindingFlags.Instance).SetValue(module, 100); + } + } + } + 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; + } + if (FerramAerospace.CheckForFAR()) + { + 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.515f; + + 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) + { + bool ctrlSrf = (bool)PWType.GetField("isCtrlSrf", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + 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; + + int edgeLeadingType = Mathf.RoundToInt((float)PWType.GetField("sharedEdgeTypeLeading", BindingFlags.Public | BindingFlags.Instance).GetValue(module)); + int edgeTrailingType = Mathf.RoundToInt((float)PWType.GetField("sharedEdgeTypeTrailing", BindingFlags.Public | BindingFlags.Instance).GetValue(module)); + + if (BDArmorySettings.PWING_EDGE_LIFT) width += ((ctrlSrf || edgeLeadingType >= 2 ? (((float)PWType.GetField("sharedEdgeWidthLeadingTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + + (float)PWType.GetField("sharedEdgeWidthLeadingRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) / 2) : 0) + + (ctrlSrf || edgeTrailingType >= 2 ? (((float)PWType.GetField("sharedEdgeWidthTrailingTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + + (float)PWType.GetField("sharedEdgeWidthTrailingRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) / 2) : 0)); + + 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; + } + public static float getPwingThickness(Part part) + { + if (!hasPwingModule) return 20; + foreach (var module in part.Modules) + { + if (module.GetType() == PWType) + { + float thickness = ((float)PWType.GetField("sharedBaseThicknessRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseThicknessTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) / 2; + return Mathf.Max(thickness, 0.2f) * 100; + } + } + return 20; + } + } +} diff --git a/BDArmory/Utils/FireSpitterUtils.cs b/BDArmory/Utils/FireSpitterUtils.cs new file mode 100644 index 000000000..1071391ab --- /dev/null +++ b/BDArmory/Utils/FireSpitterUtils.cs @@ -0,0 +1,157 @@ +using System; +using System.Reflection; +using UnityEngine; + +using BDArmory.Settings; +using System.Collections.Generic; + +namespace BDArmory.Utils +{ + [KSPAddon(KSPAddon.Startup.MainMenu, true)] + public class FireSpitter : MonoBehaviour + { + public static FireSpitter Instance; + public static bool hasFireSpitter = false; + private static bool hasCheckedForFS = false; + public static bool hasFSEngine = false; + private static bool hasCheckedForFSEngine = false; + + public static Assembly FSAssembly; + public static Type FSEngineType; + + + void Awake() + { + if (Instance != null) return; // Don't replace existing instance. + Instance = new FireSpitter(); + } + + void Start() + { + CheckForFireSpitter(); + if (hasFireSpitter) CheckForFSEngine(); + } + + public static bool CheckForFireSpitter() + { + if (hasCheckedForFS) return hasFireSpitter; + hasCheckedForFS = true; + foreach (var assy in AssemblyLoader.loadedAssemblies) + { + if (assy.assembly.FullName.StartsWith("Firespitter")) + { + FSAssembly = assy.assembly; + hasFireSpitter = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found FireSpitter Assembly: {FSAssembly.FullName}"); + } + } + return hasFireSpitter; + } + + public static bool CheckForFSEngine() + { + if (!hasFireSpitter) return false; + if (hasCheckedForFSEngine) return hasFSEngine; + hasCheckedForFSEngine = true; + foreach (var type in FSAssembly.GetTypes()) + { + if (type.Name == "FSengine") + { + FSEngineType = type; + hasFSEngine = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found FSengine type."); + } + } + return hasFSEngine; + } + + public static void ActivateFSEngines(Vessel vessel, bool activate = true) + { + if (!hasFSEngine) return; + foreach (var part in vessel.Parts) + { + foreach (var module in part.Modules) + { + if (module.GetType() == FSEngineType || module.GetType().IsSubclassOf(FSEngineType)) + { + if (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.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found {module} on {vessel.vesselName}, attempting to call 'Shutdown'."); + FSEngineType.InvokeMember("Shutdown", BindingFlags.InvokeMethod, null, module, new object[] { }); + } + } + } + } + } + + public static int CountActiveEngines(Vessel vessel) + { + if (!hasFSEngine) return 0; + int activeEngines = 0; + foreach (var part in vessel.Parts) + { + foreach (var module in part.Modules) + { + if (module.GetType() == FSEngineType || module.GetType().IsSubclassOf(FSEngineType)) + { + if ((bool)FSEngineType.GetField("EngineIgnited", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) + ++activeEngines; + } + } + } + return activeEngines; + } + + public static List GetActiveEngines(Vessel vessel) + { + if (!hasFSEngine) return []; + List activeEngines = []; + foreach (var part in vessel.Parts) + { + foreach (var module in part.Modules) + { + if (module.GetType() == FSEngineType || module.GetType().IsSubclassOf(FSEngineType)) + { + if ((bool)FSEngineType.GetField("EngineIgnited", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) + activeEngines.Add(module); + } + } + } + return activeEngines; + } + + public static void SetActiveFSEngines(List modules, bool active) + { + foreach (var module in modules) + { + if (module.GetType() == FSEngineType || module.GetType().IsSubclassOf(FSEngineType)) + FSEngineType.InvokeMember(active ? "Activate" : "Shutdown", BindingFlags.InvokeMethod, null, module, new object[] { }); + } + } + + public static void CheckStatus(Vessel vessel) + { + if (!hasFSEngine) return; + foreach (var part in vessel.Parts) + { + foreach (var module in part.Modules) + { + if (module.GetType() == FSEngineType || module.GetType().IsSubclassOf(FSEngineType)) + { + FSEngineType.InvokeMember("updateStatus", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, module, new object[] { }); + Debug.Log($"DEBUG status of {module} on {vessel.vesselName}: {(string)FSEngineType.GetField("status", BindingFlags.Public | BindingFlags.Instance).GetValue(module)}"); + Debug.Log($"DEBUG thrust of {module} on {vessel.vesselName}: {(float)FSEngineType.GetField("thrustInfo", BindingFlags.Public | BindingFlags.Instance).GetValue(module)}"); + Debug.Log($"DEBUG RPM of {module} on {vessel.vesselName}: {(float)FSEngineType.GetField("RPM", BindingFlags.Public | BindingFlags.Instance).GetValue(module)}"); + Debug.Log($"DEBUG requestedThrottle of {module} on {vessel.vesselName}: {(float)FSEngineType.GetField("requestedThrottle", BindingFlags.Public | BindingFlags.Instance).GetValue(module)}"); + Debug.Log($"DEBUG finalThrust of {module} on {vessel.vesselName}: {(float)FSEngineType.GetField("finalThrust", BindingFlags.Public | BindingFlags.Instance).GetValue(module)}"); + } + } + } + } + } +} diff --git a/BDArmory/Utils/GUIUtils.cs b/BDArmory/Utils/GUIUtils.cs new file mode 100644 index 000000000..f9f2eb53c --- /dev/null +++ b/BDArmory/Utils/GUIUtils.cs @@ -0,0 +1,688 @@ +using System.Runtime.CompilerServices; +using System.Collections.Generic; +using System.Collections; +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; + var guiMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.identity; + 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); + GUI.matrix = guiMatrix; + } + + public static void DrawLabelOnWorldPos(Vector3 worldPos, string label, Vector2 size) + { + var cam = GetMainCamera(); + if (cam == null) return; + var guiMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.identity; + 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); + Rect iconRect = new Rect(xPos, yPos, size.x, size.y); + + GUI.Label(iconRect, label); + GUI.matrix = guiMatrix; + } + + 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; + + var guiMatrix = GUI.matrix; + 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 = guiMatrix; + } + + 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 (MouseIsInRect(rect) && ((Event.current.isMouse && Event.current.type == EventType.MouseDown) || Event.current.isScrollWheel)) // Don't consume MouseUp events as multiple windows should use these. + { + Event.current.Use(); + } + } + + /// + /// Lock the model if our own window is shown and has cursor focus to prevent click-through. + /// Code adapted from FAR Editor GUI + /// Only valid in an editor. + /// Use forceUnlock to unlock the lockID when hiding a window or when the behaviour is destroyed to avoid leaving orphaned locks. + /// + public static void PreventClickThrough(Rect rect, string lockID, bool forceUnlock = false) + { + EditorLogic EdLogInstance = EditorLogic.fetch; + if (!EdLogInstance) return; + if (forceUnlock) + { + EdLogInstance.Unlock(lockID); + return; + } + if (MouseIsInRect(rect)) + { + if (!CameraMouseLook.GetMouseLook()) + EdLogInstance.Lock(false, false, false, lockID); + else + EdLogInstance.Unlock(lockID); + } + else + { + EdLogInstance.Unlock(lockID); + } + } + + 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; + } + + /// + /// Reposition the window, taking care to keep the window on the screen and attached to screen edges (if strict boundaries). + /// + /// The window rect. + /// The previous height of the window, for auto-sizing windows. + internal static void RepositionWindow(ref Rect windowRect, float previousWindowHeight = 0) + { + var scaledWindowSize = BDArmorySettings.UI_SCALE_ACTUAL * windowRect.size; + if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES) + { + if ((windowRect.height < previousWindowHeight && Mathf.RoundToInt(windowRect.y + BDArmorySettings.UI_SCALE_ACTUAL * previousWindowHeight) >= Screen.height) || // Window shrunk while being at the bottom of screen. + (BDArmorySettings.PREVIOUS_UI_SCALE > BDArmorySettings.UI_SCALE_ACTUAL && Mathf.RoundToInt(windowRect.y + BDArmorySettings.PREVIOUS_UI_SCALE * windowRect.height) >= Screen.height)) + windowRect.y = Screen.height - scaledWindowSize.y; + if (BDArmorySettings.PREVIOUS_UI_SCALE > BDArmorySettings.UI_SCALE_ACTUAL && Mathf.RoundToInt(windowRect.x + BDArmorySettings.PREVIOUS_UI_SCALE * windowRect.width) >= Screen.width) // Window shrunk while being at the right of screen. + windowRect.x = Screen.width - scaledWindowSize.x; + + // This method uses Gui point system. + if (windowRect.x < 0) windowRect.x = 0; + if (windowRect.y < 0) windowRect.y = 0; + + if (windowRect.x + scaledWindowSize.x > Screen.width) // Don't go off the right of the screen. + windowRect.x = Screen.width - scaledWindowSize.x; + if (scaledWindowSize.y > Screen.height) // Don't go off the top of the screen. + windowRect.y = 0; + else if (windowRect.y + scaledWindowSize.y > Screen.height) // Don't go off the bottom of the screen. + windowRect.y = Screen.height - scaledWindowSize.y; + } + else // If the window is completely off-screen, bring it just onto the screen. + { + if (windowRect.width == 0) windowRect.width = 1; + if (windowRect.height == 0) windowRect.height = 1; + if (windowRect.x >= Screen.width) windowRect.x = Screen.width - 1; + if (windowRect.y >= Screen.height) windowRect.y = Screen.height - 1; + if (windowRect.x + scaledWindowSize.x < 1) windowRect.x = 1 - scaledWindowSize.x; + if (windowRect.y + scaledWindowSize.y < 1) windowRect.y = 1 - scaledWindowSize.y; + } + 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(float.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 (MouseIsInRect(topGui, inverseMousePos)) return true; + if (BDArmorySetup.windowBDAToolBarEnabled && MouseIsInRect(BDArmorySetup.WindowRectToolbar, inverseMousePos)) + return true; + if (ModuleTargetingCamera.windowIsOpen && MouseIsInRect(BDArmorySetup.WindowRectTargetingCam, inverseMousePos)) + return true; + MissileFire wm = BDArmorySetup.Instance.OnGUIWM; + if (wm != null) + { + if (wm.vesselRadarData && wm.vesselRadarData.guiEnabled) + { + if (MouseIsInRect(BDArmorySetup.WindowRectRadar, inverseMousePos)) return true; + if (wm.vesselRadarData.linkWindowOpen && MouseIsInRect(wm.vesselRadarData.linkWindowRect, inverseMousePos)) + return true; + } + if (wm.rwr && wm.rwr.rwrEnabled && wm.rwr.displayRWR && MouseIsInRect(BDArmorySetup.WindowRectRwr, inverseMousePos)) + return true; + if (wm.wingCommander && wm.wingCommander.showGUI) + { + if (MouseIsInRect(BDArmorySetup.WindowRectWingCommander, inverseMousePos)) return true; + if (wm.wingCommander.showAGWindow && MouseIsInRect(wm.wingCommander.agWindowRect, inverseMousePos)) + return true; + } + + } + if (extraGUIRects != null) + { + foreach (var guiRect in extraGUIRects.Values) + { + if (!guiRect.visible) continue; + if (MouseIsInRect(guiRect.rect, 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(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MouseIsInRect(Rect rect) + { + Vector2 inverseMousePos = new(Input.mousePosition.x, Screen.height - Input.mousePosition.y); + return MouseIsInRect(rect, inverseMousePos); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MouseIsInRect(Rect rect, Vector2 inverseMousePos) + { + Rect scaledRect = new(rect.position, BDArmorySettings.UI_SCALE_ACTUAL * rect.size); + return scaledRect.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; + } + + /// + /// Wrapper for HorizontalSlider for UI_FloatSemiLogRange fields. + /// + /// + /// + /// + /// + /// + /// + /// + /// A cache of tuples to avoid needlessly recalculating semi-log values. Can initially be null. + /// + public static float HorizontalSemiLogSlider(Rect rect, float value, float minValue, float maxValue, float sigFig, bool withZero, bool reducedPrecisionAtMin, ref (float, float)[] cache) + { + if (cache == null || cache.Length != 3) + { + cache = [ + (value, UI_FloatSemiLogRange.ToSliderValue(value, minValue, sigFig, withZero, reducedPrecisionAtMin)), + (minValue, UI_FloatSemiLogRange.ToSliderValue(withZero ? 0 : minValue, minValue, sigFig, withZero, reducedPrecisionAtMin)), + (maxValue, UI_FloatSemiLogRange.ToSliderValue(maxValue, minValue, sigFig, withZero, reducedPrecisionAtMin)) + ]; + } + else + { + if (value != cache[0].Item1) cache[0] = (value, UI_FloatSemiLogRange.ToSliderValue(value, minValue, sigFig, withZero, reducedPrecisionAtMin)); + if (minValue != cache[1].Item1) cache[1] = (minValue, UI_FloatSemiLogRange.ToSliderValue(withZero ? 0 : minValue, minValue, sigFig, withZero, reducedPrecisionAtMin)); + if (maxValue != cache[2].Item1) cache[2] = (maxValue, UI_FloatSemiLogRange.ToSliderValue(maxValue, minValue, sigFig, withZero, reducedPrecisionAtMin)); + } + float sliderValue = cache[0].Item2; + if (sliderValue != (sliderValue = GUI.HorizontalSlider(rect, sliderValue, cache[1].Item2, cache[2].Item2))) + { + cache[0] = (value, sliderValue); + return UI_FloatSemiLogRange.FromSliderValue(sliderValue, minValue, sigFig, withZero, reducedPrecisionAtMin); + } + else return value; + } + + /// + /// Wrapper for HorizontalSlider for UI_FloatLogRange fields. + /// + /// + /// + /// + /// + /// + /// + /// + /// A cache of tuples to avoid needlessly recalculating log values. Can initially be null. + /// + public static float HorizontalFloatLogSlider(Rect rect, float value, float minValue, float maxValue, int steps, ref (float, float)[] cache) + { + if (cache == null || cache.Length != 3) + { + cache = [ + (value, UI_FloatLogRange.ToSliderValue(value, minValue, maxValue, steps)), + (minValue, UI_FloatLogRange.ToSliderValue(0, minValue, maxValue, steps)), + (maxValue, UI_FloatLogRange.ToSliderValue(maxValue, minValue, maxValue, steps)) + ]; + } + else + { + if (value != cache[0].Item1) cache[0] = (value, UI_FloatLogRange.ToSliderValue(value, minValue, maxValue, steps)); + if (minValue != cache[1].Item1) cache[1] = (minValue, UI_FloatLogRange.ToSliderValue(0, minValue, maxValue, steps)); + if (maxValue != cache[2].Item1) cache[2] = (maxValue, UI_FloatLogRange.ToSliderValue(maxValue, minValue, maxValue, steps)); + } + float sliderValue = cache[0].Item2; + if (sliderValue != (sliderValue = GUI.HorizontalSlider(rect, sliderValue, cache[1].Item2, cache[2].Item2))) + { + cache[0] = (value, sliderValue); + return UI_FloatLogRange.FromSliderValue(sliderValue, minValue, maxValue, steps); + } + else return value; + } + + /// + /// Wrapper for HorizontalSlider for UI_FloatPowerRange fields. + /// + /// + /// + /// + /// + /// + /// + /// A cache of tuples to avoid needlessly recalculating power values. Can initially be null. + /// + public static float HorizontalPowerSlider(Rect rect, float value, float minValue, float maxValue, float power, int sigFig, ref (float, float)[] cache) + { + if (cache == null || cache.Length != 3) + { + cache = [ + (value, UI_FloatPowerRange.ToSliderValue(value, power)), + (minValue, UI_FloatPowerRange.ToSliderValue(minValue, power)), + (maxValue, UI_FloatPowerRange.ToSliderValue(maxValue, power)) + ]; + } + else + { + if (value != cache[0].Item1) cache[0] = (value, UI_FloatPowerRange.ToSliderValue(value, power)); + if (minValue != cache[1].Item1) cache[1] = (minValue, UI_FloatPowerRange.ToSliderValue(minValue, power)); + if (maxValue != cache[2].Item1) cache[2] = (maxValue, UI_FloatPowerRange.ToSliderValue(maxValue, power)); + } + float sliderValue = cache[0].Item2; + if (sliderValue != (sliderValue = GUI.HorizontalSlider(rect, sliderValue, cache[1].Item2, cache[2].Item2))) + { + cache[0] = (value, sliderValue); + return UI_FloatPowerRange.FromSliderValue(sliderValue, power, sigFig, maxValue); + } + else return value; + } + + [KSPAddon(KSPAddon.Startup.EveryScene, false)] + internal class GUIUtilsInstance : MonoBehaviour + { + public bool MouseIsOnGUI + { + get + { + if (!_mouseIsOnGUICheckedThisFrame) + { + _mouseIsOnGUI = _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() + { + 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..e8ae5a350 --- /dev/null +++ b/BDArmory/Utils/KrakensbaneUtils.cs @@ -0,0 +1,139 @@ +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 Start() + { + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Earlyish, CheckActiveVessel); // Check for there being an active vessel before the Normal timing phase. + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.BetterLateThanNever, Reset); // Reset the flags at the end of the frame. + GameEvents.onVesselChange.Add(OnVesselSwitch); + GameEvents.onVesselWillDestroy.Add(OnVesselWillDestroy); + } + + void OnDestroy() + { + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Earlyish, CheckActiveVessel); // Check for there being an active vessel before the Normal timing phase. + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.BetterLateThanNever, Reset); + GameEvents.onVesselChange.Remove(OnVesselSwitch); + GameEvents.onVesselWillDestroy.Remove(OnVesselWillDestroy); + } + + void Reset() + { + _frameVelocityCheckedThisFrame = false; + _floatingOriginOffsetCheckedThisFrame = false; + _floatingOriginOffsetNonKrakensbaneCheckedThisFrame = false; + _isActiveCheckedThisFrame = false; + _switchedFromDeadVesselThisFrame = false; + _activeVesselDied = false; + } + + void OnVesselSwitch(Vessel v) + { + Reset(); + CheckActiveVessel(); + _switchedFromDeadVesselThisFrame = _haveActiveVessel && _activeVesselWasDead; + _activeVesselWasDead = false; + } + bool _switchedFromDeadVesselThisFrame = false; + + void OnVesselWillDestroy(Vessel v) + { + if (!v.isActiveVessel) return; + _activeVesselDied = true; + _activeVesselWasDead = true; + } + bool _activeVesselDied = false, _activeVesselWasDead = false; + + void CheckActiveVessel() + { + _haveActiveVessel = FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.gameObject.activeInHierarchy; + } + bool _haveActiveVessel = false; + + 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; + // If KSP doesn't have an active vessel, it doesn't set the frame velocity to zero immediately. Similarly, the frame velocity isn't immediately set once KSP has an active vessel again. + isActive = _haveActiveVessel && !_activeVesselDied && (!FloatingOriginOffset.IsZero() || !GetFrameVelocityV3f.IsZero() || _switchedFromDeadVesselThisFrame); + _isActiveCheckedThisFrame = true; + return isActive; + } + } + bool isActive = false; + bool _isActiveCheckedThisFrame = false; + } +} \ No newline at end of file diff --git a/BDArmory.Core/Utils/LayerMask.cs b/BDArmory/Utils/LayerMask.cs similarity index 57% rename from BDArmory.Core/Utils/LayerMask.cs rename to BDArmory/Utils/LayerMask.cs index fa3131390..9f2f0b6f6 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 { @@ -23,10 +23,28 @@ public static int ToLayer(int bitmask) return result; } } + + // LayerMasks for raycasts. Use as (int)(Parts|EVA|Scenery). + 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? PhysicalObjects? + RootPart = 1 << 21, + Unknown23 = 1 << 23, // Why are some raycasts using this layer? AeroFXIgnore? + Wheels = 1 << 26 // WheelCollidersIgnore? + // 1 << 27 WheelColliders? + }; // Scenery includes terrain and buildings. + // Commonly used values: + // 163840 = (1 << 15) | (1 << 17) + // 557057 = (1 << 0) | (1 << 15) | (1 << 19) = Parts|Scenery|??? + // 9076737 = (1 << 0) | (1 << 15) | (1 << 17) | (1 << 19) | (1 << 23) = Parts|Scenery|EVA|???|??? } /* -Layer masks: +Layer mask names (doesn't actually seem to be correct when testing raycasts): 0: Default 1: TransparentFX 2: Ignore Raycast diff --git a/BDArmory/Misc/ObjectPool.cs b/BDArmory/Utils/ObjectPool.cs similarity index 84% rename from BDArmory/Misc/ObjectPool.cs rename to BDArmory/Utils/ObjectPool.cs index 4a02cab1f..7a8eff8cc 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; } - Debug.Log("[ObjectPool]: Resizing " + poolObjectName + " pool to " + size); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ObjectPool]: Resizing " + poolObjectName + " pool to " + size); } private void AddObjectsToPool(int count) @@ -65,7 +66,7 @@ private void AddObjectsToPool(int count) private void ReplacePoolObject(int index) { - Debug.Log("[ObjectPool]: Object of type " + poolObjectName + " was null at position " + index + ", replacing it."); + Debug.LogWarning("[BDArmory.ObjectPool]: Object of type " + poolObjectName + " was null at position " + index + ", replacing it."); GameObject obj = Instantiate(poolObject); obj.transform.SetParent(transform); obj.SetActive(false); @@ -105,7 +106,7 @@ public GameObject GetPooledObject() if (canGrow) { var size = (int)(pool.Count * 1.2) + 1; // Grow by 20% + 1 - Debug.Log("[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); @@ -115,6 +116,7 @@ public GameObject GetPooledObject() if (forceReUse) // Return an old entry that is already being used. { lastIndex = (lastIndex + 1) % pool.Count; + if (pool[lastIndex].transform.parent != null) pool[lastIndex].transform.parent = null; // Disassociate the object from any previous parent. pool[lastIndex].SetActive(false); if (disableAfterDelay > 0f) DisableAfterDelay(pool[lastIndex], disableAfterDelay); return pool[lastIndex]; @@ -130,7 +132,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 +142,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) { - Debug.Log("[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..bab2b193c --- /dev/null +++ b/BDArmory/Utils/OtherUtils.cs @@ -0,0 +1,264 @@ +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 int[] ParseToIntArray(string intString) + { + string[] intStrings = intString.Split(new char[] { ',' }); + int[] intArray = new int[intStrings.Length]; + for (int i = 0; i < intStrings.Length; i++) + { + intArray[i] = int.Parse(intStrings[i]); + } + + return intArray; + } + /// + /// Parse a comma-separated string as an array of the given enum. + /// + /// The enum type to parse as. + /// The comma-separated enum names or values. + /// An array of enums. + public static T[] ParseEnumArray(string enumString) where T : Enum + { + string[] enumStrings = enumString.Split(new char[] { ',' }); // Split the string on the commas. + string[] enumNames = Enum.GetNames(typeof(T)); // Get the enum names. + for (int i = 0; i < enumStrings.Length; i++) //legacy support for int-based enum strings (e.g. antiradtargetTypes = 0.5 vs antiradTargetTypes = SAM,Detection) + { + if (int.TryParse(enumStrings[i], out int intValue)) + { + if (Enum.IsDefined(typeof(T), intValue)) + enumStrings[i] = Enum.GetName(typeof(T), intValue); //if there's ints in the string, convert them + } + } + T[] enumArray = [.. enumStrings.Where(enumNames.Contains).Select(e => Enum.Parse(typeof(T), e)).Cast()]; // then feed the enum names into an Enum array + if (!enumStrings.All(enumNames.Contains)) // Check for invalid values. + Debug.LogError($"[BDArmory.OtherUtils]: Invalid enum ({typeof(T)}) values: {string.Join(", ", enumStrings.Where(e => !enumNames.Contains(e)))}"); + return enumArray; + } + + 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; + } + + // LINQ.All() returns true for empty collections. Sometimes we want it to be false in those cases. + public static bool AllAndNotEmpty(this IEnumerable source, Func predicate) + { + return source.Any() && source.All(predicate); + } + } + + /// + /// 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 + /// + /// Notes: + /// - 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. + /// - These "wait" enumerators always wait at least one cycle. If immediately continuing is desired, use a manual WaitForFixedUpdate loop. + /// + 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. + + + /// + /// For serializing List> + /// + [Serializable] + public struct StringList + { + public List ls; + } + + /// + /// Comparer for raycast hit sorting. + /// + public class RaycastHitComparer : IComparer + { + int IComparer.Compare(RaycastHit left, RaycastHit right) + { + return left.distance.CompareTo(right.distance); + } + public static RaycastHitComparer raycastHitComparer = new RaycastHitComparer(); + } +} \ No newline at end of file diff --git a/BDArmory/Utils/PartExploderSystem.cs b/BDArmory/Utils/PartExploderSystem.cs new file mode 100644 index 000000000..f68bee411 --- /dev/null +++ b/BDArmory/Utils/PartExploderSystem.cs @@ -0,0 +1,47 @@ +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 + { + // Remove parts that are already gone. + nowExploding.AddRange(ExplodingParts.Where(p => p is null || p.packed || (p.vessel is not null && !p.vessel.loaded))); + ExplodingParts.ExceptWith(nowExploding); + nowExploding.Clear(); + // Explode outer-most parts first to avoid creating new vessels needlessly. + nowExploding.AddRange(ExplodingParts.Where(p => !ExplodingParts.Contains(p.parent))); + foreach (var part in nowExploding) + part.explode(); + ExplodingParts.ExceptWith(nowExploding); + nowExploding.Clear(); + } while (ExplodingParts.Count > 0); + } + } +} diff --git a/BDArmory/Utils/ProjectileUtils.cs b/BDArmory/Utils/ProjectileUtils.cs new file mode 100644 index 000000000..a9387a211 --- /dev/null +++ b/BDArmory/Utils/ProjectileUtils.cs @@ -0,0 +1,1209 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.GameModes; +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + class ProjectileUtils + { + public static string settingsConfigURL = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/PartsBlacklists.cfg")); + public static void SetUpPartsHashSets() + { + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode == 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")) + { + 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", "bdVTOLAI", "bdOrbitalAI", "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 (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); + } + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ProjectileUtils]: Ignored Parts: " + string.Join(", ", IgnoredPartNames)); + } + return IgnoredPartNames.Contains(part.partInfo.name); + } + static HashSet armorParts; + public static bool IsArmorPart(Part part) + { + if (part == null) return false; + 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); + } + public static void SetUpWeaponReporting() + { + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode == null) // Note: this shouldn't happen since SetUpPartsHashSets is called before SetUpWeaponReporting. + { + 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. + bool addDefaultParts = false; + if (!fileNode.HasNode("AnnouncerGuns")) + { + fileNode.AddNode("AnnouncerGuns", announcerGunsComment); + addDefaultParts = true; + } + ConfigNode Iparts = fileNode.GetNode("AnnouncerGuns"); + Iparts.comment = announcerGunsComment; + var partNames = Iparts.GetValues().ToHashSet(); // Get the existing part names, then add our ones. + if (addDefaultParts) + { + 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", "bdVTOLAI", "bdOrbitalAI", "missileController", "bdammGuidanceModule", "PotatoRoid" }; + + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode.HasNode("MaterialsBlacklist")) + { + ConfigNode parts = fileNode.GetNode("MaterialsBlacklist"); + //Debug.Log($"[BDArmory.ProjectileUtils]: partsBlacklist.cfg BlacklistParts count: " + parts.CountValues); + for (int i = 0; i < parts.CountValues; i++) + { + 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); + } + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ProjectileUtils]: Part Material blacklist: " + string.Join(", ", materialsBlacklist)); + } + return materialsBlacklist.Contains(Part.partInfo.name); + } + static HashSet reportingWeaponList; + public static bool isReportingWeapon(Part part) + { + if (part == null) return false; + if (reportingWeaponList == null) + { + reportingWeaponList = new HashSet { }; + + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode.HasNode("AnnouncerGuns")) + { + ConfigNode parts = fileNode.GetNode("AnnouncerGuns"); + for (int i = 0; i < parts.CountValues; i++) + { + 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); + } + } + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ProjectileUtils]: Weapon Reporting List: " + string.Join(", ", reportingWeaponList)); + } + return reportingWeaponList.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 + //No struts, they cause weird bugs :) -BahamutoD + if (hitPart == null) return; + if (hitPart.partInfo.name.Contains("Strut")) return; + if (IsIgnoredPart(hitPart)) return; // Ignore ignored parts. + + // Add decals + if (BDArmorySettings.BULLET_HITS) + { + BulletHitFX.CreateBulletHit(hitPart, hit.point, hit, hit.normal, hasRichocheted, caliber, penetrationfactor, team); + } + // 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); + + string sourceVesselName = sourceVessel != null ? sourceVessel.GetName() : null; + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, caliber, penetrationfactor, explosive, incendiary, sourceVesselName, hit, partAlreadyHit, cockpitPen); + } + + // Update scoring structures + //if (firstHit) + //{ + ApplyScore(hitPart, sourceVesselName, 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) + { + var aName = sourceVessel;//.GetName(); + var tName = hitPart.vessel.GetName(); + + switch (ExplosionSource) + { + case ExplosionSourceType.Bullet: + if (newhit) BDACompetitionMode.Instance.Scores.RegisterBulletHit(aName, tName, name, distanceTraveled); + BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, damage); + break; + case ExplosionSourceType.Rocket: + //if (newhit) BDACompetitionMode.Instance.Scores.RegisterRocketStrike(aName, tName); + 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, hitPart.vessel, damage); + break; + } + } + public static float CalculateArmorPenetration(Part hitPart, float penetration, float thickness) + { + /////////////////////////////////////////////////////////////////////// + // Armor Penetration + /////////////////////////////////////////////////////////////////////// + //if (thickness < 0) thickness = (float)hitPart.GetArmorThickness(); //returns mm + //want thickness of armor, modified by angle of hit, use thickness val fro projectile + if (thickness <= 0) + { + thickness = 1; + } + var penetrationFactor = penetration / thickness; + + 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.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{Armor Penetration}]: Bullet Stopped by Armor"); + } + } + return penetrationFactor; + } + public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, float caliber, float hardness, float ductility, float density, float impactVel, string sourceVesselName, ExplosionSourceType explosionSource, int armorType) + { + /// + /// Calculate damage to armor from kinetic impact based on armor mechanical properties + /// Sufficient penetration by bullet will result in armor spalling or failure + /// + 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 + + double volumeToReduce = -1; + float caliberModifier = 1; //how many calibers wide is the armor loss/spall? + float spallMass = 0; + float spallCaliber = 1; + //Spalling/Armor damage + if (ductility > 0.20f) + { + if (penetrationFactor > 2) //material can't stretch fast enough, necking/point embrittlelment/etc, material tears + { + if (thickness < 2 * caliber) + { + caliberModifier = 4; // - bullet capped by necked material, add to caliber/bulletmass + } + else + { + caliberModifier = 2; + } + spallCaliber = caliber * (caliberModifier / 2); //mm + spallMass = (spallCaliber * spallCaliber * Mathf.PI / 400) * (thickness / 10) * (density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD;//mm -> kg + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils]: " + hitPart + ", " + hitPart.vessel.GetName() + ": Armor spalling! Diameter: " + spallCaliber + "mm; mass: " + spallMass + "kg"); + } + } + if (penetrationFactor > 0.75 && penetrationFactor < 2) //material deformed around impact point + { + caliberModifier = 2; + } + } + else //ductility < 0.20 + { + if (hardness > 500) + { + if (penetrationFactor > 1) + { + 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.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: Armor failure on " + hitPart + ", " + hitPart.vessel.GetName() + "! Lost: " + volumeToReduce / ((float)hitPart.GetArmorMaxThickness() / 10) + "m2"); + } + } + else //0.05-0.19 ductility - harder steels, etc + { + caliberModifier = (20 / (ductility * 100)) * Mathf.Clamp(penetrationFactor, 1, 3); + } + } + if (penetrationFactor > 0.66 && penetrationFactor < 1) + { + spallCaliber = ((1 - penetrationFactor) + 1) * (caliber * caliber * Mathf.PI / 400); + + volumeToReduce = spallCaliber; //cm3 + spallMass = spallCaliber * (density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //kg + 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 + "mm; mass: " + spallMass + "kg"); + } + } + } + //else //low hardness non ductile materials (i.e. kevlar/aramid) not going to spall + } + + if (volumeToReduce < 0) + { + var modifiedCaliber = 0.5f * caliber * caliberModifier; + volumeToReduce = modifiedCaliber * modifiedCaliber * Mathf.PI / 400 * (thickness / 10); //cm3 + } + 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.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: Bullet Stopped by Armor"); + } + } + if (spallMass > 0) + { + float damage = hitPart.AddBallisticDamage(spallMass, spallCaliber, 1, 1.1f, 1, (impactVel / 2), explosionSource); + 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, float thickness = -1) + { + /// + /// Calculates damage from flak/shrapnel, based on HEmass and projMass, of both contact and airburst detonations. + /// 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 + /// + if (BDArmorySettings.PAINTBALL_MODE) return; //don't damage armor if paintball mode + if (thickness < 0) thickness = (float)hitPart.GetArmorThickness(); + if (thickness < 1) + { + thickness = 1; //prevent divide by zero or other odd behavior + } + double volumeToReduce = 0; + var Armor = hitPart.FindModuleImplementing(); + if (Armor != null) + { + if (!IsArmorPart(hitPart)) + { + if (Armor.ArmorTypeNum == 1) return; //ArmorType "None"; no armor to block/reduce blast, take full damage + } + float Ductility = Armor.Ductility; + float hardness = Armor.Hardness; + float Strength = Armor.Strength; + float Density = Armor.Density; + int armorType = (int)Armor.ArmorTypeNum; + //Spalling/Armor damage + //minimum armor thickness to stop shrapnel is 0.08 calibers for 1.4-3.5% HE by mass; 0.095 calibers for 3.5-5.99% HE by mass; and .11 calibers for 6% HE by mass, assuming detonation is > 5calibers away + //works out to y = 0.0075x^(1.05)+0.06 + //20mm Vulcan is HE fraction 13%, so 0.17 calibers(3.4mm), GAU ~0.19, or 0.22calibers(6.6mm), AbramsHe 80%, so 0.8calibers(96mm) + //HE contact detonation penetration; minimum thickness of armor to receive caliber sized hole: thickness = (2.576 * 10 ^ -20) * Caliber * ((velocity/3.2808) ^ 5.6084) * Cos(2 * angle - 45)) +(0.156 * diameter) + //TL;Dr; armor thickness needed is .156*caliber, and if less than, will generate a caliber*proj length hole. half the min thickness will yield a 2x size hole + //angle and impact vel have negligible impact on hole size + //if the round penetrates, increased damage; min thickness of .187 calibers to prevent armor cracking //is this per the 6% HE fraction above, or ? could just do the shrapnelfraction * 1.41/1.7 + float HERatio = 0.06f; + if (projmass < HEmass) + { + projmass = HEmass * 1.25f; //sanity check in case this is 0 + } + 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) * (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) + { + shrapnelCount = Mathf.Clamp((frangibility / (4 * Mathf.PI * detonationDist * detonationDist)) * (float)(radiativeArea / 3), 0, (frangibility * .4f)); //fragments/m2 + } + else //srf detonation + { + shrapnelCount = frangibility * 0.4f; + } + //shrapnelCount *= (float)(radiativeArea / 3); //shrapnelhits/part + float shrapnelMass = ((projmass * (1 - HERatio)) / frangibility) * shrapnelCount; + float damage; + // 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 (thickness < shrapnelThickness && shrapnelCount > 0) + { + //armor penetration by subcaliber shrapnel; use dist to abstract # of fragments that hit to calculate damage, assuming 5k fragments for now + + volumeToReduce = (((caliber * caliber) * 1.5f) / shrapnelCount * thickness) / 1000; //rough approximation of volume / # of fragments + hitPart.ReduceArmor(volumeToReduce); + 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{CalcShrapnel}]: " + hitPart.name + " on " + hitPart.vessel.GetName() + ", detonationDist: " + detonationDist + "; " + shrapnelCount + " shrapnel hits; Armor damage: " + volumeToReduce + "cm3; part damage: " + damage); + } + ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", explosionSource); + 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 (thickness < (shrapnelThickness * 1.41f)) + { + //armor breach + + volumeToReduce = ((caliber * thickness * (caliber * 4)) / 1000); //cm3 + hitPart.ReduceArmor(volumeToReduce); + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: 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); + } + 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 //detonates on/in armor + { + if (penetrationFactor < 1 && penetrationFactor > 0) + { + thickness *= (1 - penetrationFactor); //armor thickness reduced from projectile penetrating some distance, less distance from proj to back of plate + if (thickness < (shrapnelThickness * 1.41f)) + { + //armor breach + volumeToReduce = ((caliber * thickness * (caliber * 4)) * 2) / 1000; //cm3 + hitPart.ReduceArmor(volumeToReduce); + + 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 + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Shrapnel penetration from on-armor detonation, " + hitPart.name + ", " + hitPart.vessel.GetName() + "; Armor damage: " + volumeToReduce + "; part damage: " + damage); + } + 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 + { + damage = hitPart.AddBallisticDamage((projmass * (1 - HERatio)), 0.1f, 1, 1.9f, 1, 430, explosionSource); //internal det catches entire shrapnel mass + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Through-armor detonation in " + hitPart.name + ", " + hitPart.vessel.GetName() + "; part damage: " + damage); + } + ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", 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 false; //don't damage armor if paintball mode. Returns false (damage passes armor) so misiles can still be damaged in Paintball mode + float thickness = (float)hitPart.GetArmorMaxThickness(); + //Since BDA armor is some sort of weird ablatitive model, need to adjust thickness to original level before volume calcs to ensure a hit that should remove X mass will continue to + //remove x mass subsequent hits, instead of diminishing amounts, so using getmaxThickness isntead of GetThickness + + 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 = 0; + var Armor = hitPart.FindModuleImplementing(); + if (Armor != null) + { + if (IsArmorPart(hitPart)) + { + armorArea = Armor.armorVolume; // pseudo-surface area, m2 + if (double.IsNaN(armorArea) || Armor.Armor <= 0) + return false; //no armor to stop explosion + spallArea = Mathf.Min(armorArea, radius * radius * 1.5f); //clamp based on max size of explosion, m2 + } + else + { + if (Armor.ArmorTypeNum == 1) return false;//ArmorType "None"; no armor to block/reduce blast, take full damage + armorArea = !double.IsNaN(hitPart.radiativeArea) ? (float)hitPart.radiativeArea : hitPart.GetArea();// m2 // * 10000; //cm2 + spallArea = Mathf.Min(armorArea / 3, radius * radius * 1.5f);//m2 + } + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: part: " + hitPart + " armorArea: " + armorArea + "m2; expl. radius: " + radius + "m; spallArea: " + spallArea + "m2"); + + if (double.IsNaN(hitPart.radiativeArea)) + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: radiative area of part " + hitPart + " was NaN, using approximate area " + hitPart.GetArea() + "m2 instead."); + } + float ductility = Armor.Ductility; + float hardness = Armor.Hardness; + float Strength = Armor.Strength; + float Density = Armor.Density; + + float ArmorTolerance = (((Strength * (1 + ductility)) + Density) / 1000f) * (float)hitPart.GetArmorThickness(); //either this or blowthrough factor should probably get reviewed at some point + + ArmorTolerance *= BDArmorySettings.EXP_PEN_RESIST_MULT; + + float blowthroughFactor = (float)BlastPressure / ArmorTolerance; //as damage to armor might be a bit high. Otoh, how much damage is to be expected from, say, a 5" naval HE shell vs 50mm of armor? + //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 (distance < 0.5) spallArea = Mathf.Clamp(spallArea, 0.1f, 1 * blowthroughFactor * ductility); //contact detonations against armor scaled by how much excess energy the blast has after penning armor, modded by material ductility + // high energy blasts vs more strtchy material will result in larger, but sill localized holes + if (spallArea >= armorArea) thickness = hitPart.GetArmorThickness(); //if armor larger than blast area, use max thickness to ensure currect armor reduction from HE hits (siming plate thickness reduction as more localized cratering + //than the entire panel delaminating layers of armor) + //if blast area encompasses entire plate, then max thickness is whatever the current thickness is, + //tl;dr 'thickness' at present is really more accurately 'average thickness'. Should probably refactor armor at somepoint to maintain a constant thickness + //and have armor degredation solely represented by armor integrity. + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log($"[BDArmory.ProjectileUtils{{CalculateExplosiveArmorDamage}}]: Beginning ExplosiveArmorDamage(); {hitPart.name}, ArmorType: {Armor.ArmorTypeNum}; Armor Thickness: {Armor.Armor}mm; BlastPressure: {BlastPressure}; BlowthroughFactor: {blowthroughFactor}"); ; + } + //is BlastUtils maxpressure in MPa? confirm blast pressure from ExplosionUtils on same scale/magnitude as armorTolerance + + if (ductility > 0.20f) + { + if (BlastPressure >= ArmorTolerance) //material stress tolerance exceeded, armor rupture + { + spallMass = spallArea * (thickness / 10) * 10000; //entirety of armor lost, cm3 + hitPart.ReduceArmor(spallMass); //cm3 + spallMass *= (Density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //cm3 -> kg + + float spallCaliber = BDAMath.Sqrt(spallArea) * 1000; //m2 -> mm + damage = hitPart.AddBallisticDamage(spallMass, spallCaliber, 1, blowthroughFactor, 1, 422.75f, explosionSource); + ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor rupture on " + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "m2; mass: " + spallMass + "kg; spall dmg: " + damage); + } + + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, spallCaliber, blowthroughFactor, true, false, sourcevessel, hit); + } + return false; + } + if (blowthroughFactor > 0.66) + { + spallArea *= ((1 - ductility) * blowthroughFactor); //m2 + + spallMass = Mathf.Min(spallArea, armorArea) * 10000 * ((thickness / 10) * (blowthroughFactor - 0.66f)) * (Density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //lose up to 1/3rd thickness from spalling, based on severity of blast; m2 -> cm2 -> cm3 -> kg + if (spallArea > armorArea) spallArea = armorArea; //m2 + + float spallCaliber = BDAMath.Sqrt(spallArea) * 1000; //m2 -> mm + if (hardness > 500)//armor holds, but spalling + { + damage = hitPart.AddBallisticDamage(spallMass, spallCaliber, 1, blowthroughFactor, 1, 422.75f, explosionSource); + ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); + } + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Explosive Armor spalling" + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "m2; mass: " + spallMass + "kg; spall dmg: " + damage); + } + //else soft enough to not spall. Armor has suffered some deformation, though, weakening it. + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, spallCaliber, blowthroughFactor, false, false, sourcevessel, hit); + } + spallArea *= 10000 * (thickness / 10) * (blowthroughFactor - 0.66f); //m2 -> cm2 -> cm3 + hitPart.ReduceArmor(spallArea); //cm3 + return true; + } + } + else //ductility < 0.20 + { + if (blowthroughFactor >= 1) + { + if (ductility < 0.05f) //ceramics + { + var volumeToReduce = Mathf.CeilToInt(spallArea / 0.25f) * 2500 * (thickness / 10);//total failue of 50x50cm armor tile(s) + // m2 - > 50x50cm tiles -> cm2 -> cm3 + if (hardness > 500) + { + spallMass = volumeToReduce * (Density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //cm3 -> kg + damage = hitPart.AddBallisticDamage(spallMass, 500, 1, blowthroughFactor, 1, 422.75f, explosionSource); + ApplyScore(hitPart, sourcevessel, 0, damage, "Armor Shatter", explosionSource); + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, 500, blowthroughFactor, true, false, sourcevessel, hit); + } + } + //soft stuff like Aramid not likely to cause major damage + hitPart.ReduceArmor(volumeToReduce); //cm3 + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor destruction on " + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + BDAMath.Sqrt(volumeToReduce / (thickness / 10)) + "m2; mass: " + volumeToReduce * (Density / 1000000) + "kg; spall dmg: " + damage); + } + } + else //0.05-0.19 ductility - harder steels, etc + { + spallArea *= (1.2f - ductility) * blowthroughFactor; //m2 + spallMass = Mathf.Min(spallArea, armorArea) * 10000 * (thickness / 10) * (Density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //m2 -> cm2 -> cm3 -> kg + if (spallArea > armorArea) spallArea = armorArea; //m2 + float spallCaliber = BDAMath.Sqrt(spallArea) * 1000; //m2 -> mm + + hitPart.ReduceArmor(spallArea); //cm3 + damage = hitPart.AddBallisticDamage(spallMass, spallCaliber, 1, blowthroughFactor, 1, 422.75f, explosionSource); + ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor sundered, " + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "m2; mass: " + spallMass + "kg; spall dmg: " + damage); + } + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, spallCaliber, blowthroughFactor, true, false, sourcevessel, hit); + } + } + return false; + } + else + { + if (blowthroughFactor > 0.33) + { + if (ductility < 0.05f && hardness < 500) //flexible, non-ductile materials aren't going to absorb or deflect blast; + { + return false; + //but at least they aren't going to be taking much armor damage + } + } + if (blowthroughFactor > 0.66) + { + if (ductility < 0.05f) //should really have this modified by thickness/blast force + { + var volumeToReduce = Mathf.CeilToInt(spallArea / 0.25f) * 2500 * (thickness / 10);//total failue of 50x50cm armor tile(s) + // m2 - > 50x50cm tiles -> cm2 -> cm3 + if (hardness > 500) + { + spallMass = volumeToReduce * (Density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //kg + damage = hitPart.AddBallisticDamage(spallMass, 500, 1, blowthroughFactor, 1, 422.75f, explosionSource); + ApplyScore(hitPart, sourcevessel, 0, damage, "Armor Shatter", explosionSource); + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, 500, blowthroughFactor, true, false, sourcevessel, hit); + } + } + //soft stuff like Aramid not likely to cause major damage + hitPart.ReduceArmor(volumeToReduce); //cm3 + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor destruction on " + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + BDAMath.Sqrt(volumeToReduce / (thickness / 10)) + "m2; mass: " + volumeToReduce * (Density / 1000000) + "kg; spall dmg: " + damage); + } + } + else //0.05-0.19 ductility - harder steels, etc + { + spallArea *= 10000 * ((1.2f - ductility) * blowthroughFactor) * (blowthroughFactor - 0.66f); //cm2 + if (spallArea > armorArea * 10000) spallArea = armorArea; //cm2 + float spallCaliber = BDAMath.Sqrt(spallArea) * 10; //cm2 -> mm + spallArea *= thickness / 10; //cm2 -> cm3 + if (hardness > 500) + { + //blowtrhoughFactor - 1 * 100 + spallMass = spallArea * (Density / 1000000) * BDArmorySettings.ARMOR_MASS_MOD; //kg + damage = hitPart.AddBallisticDamage(spallMass, spallCaliber, 1, blowthroughFactor, 1, 422.75f, explosionSource); + ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(hitPart, spallCaliber, blowthroughFactor, true, false, sourcevessel, hit); + } + } + hitPart.ReduceArmor(spallArea); //cm3 + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor holding. Barely!, " + hitPart.name + ", " + hitPart.vessel.GetName() + "!; area lost: " + spallArea + "cm3; mass: " + spallArea * (Density / 1000000) + "kg; spall dmg: " + damage); + } + } + return true; + } + } + } + } + return true; + } + /* + public static float CalculatePenetration(float caliber, float projMass, float impactVel, float apBulletMod = 1) + { + float penetration = 0; + if (apBulletMod <= 0) // sanity check/legacy compatibility + { + apBulletMod = 1; + } + + if (caliber > 5) //use the "krupp" penetration formula for anything larger than HMGs + { + penetration = (float)(16f * impactVel * Math.Sqrt(projMass / 1000) / Math.Sqrt(caliber) * apBulletMod); //APBulletMod now actually implemented, serves as penetration multiplier, 1 being neutral, <1 for soft rounds, >1 for AP penetrators + } + + return penetration; + } + */ + 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.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils]: Bullet Energy: " + bulletEnergy + "; mass: " + projMass + "; vel: " + impactVel); + } + return bulletEnergy; + } + + public static float CalculateArmorStrength(float caliber, float thickness, float Ductility, float Strength, float Density, float SafeTemp, Part hitpart) + { + /// + /// Armor Penetration calcs for new Armor system + /// return modified caliber, velocity for penetrating rounds + /// Math is very much game-ified abstract rather than real-world calcs, but returns numbers consistant with legacy armor system, assuming legacy armor is mild steel (UST ~950 MPa, BHN ~200) + /// so for now, Good Enough For Government Work^tm + /// + //initial impact calc + //determine yieldstrength of material + float yieldStrength; + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils]: properties: Tensile:" + Strength + "; Ductility: " + Ductility + "; density: " + Density + "; thickness: " + thickness + "; caliber: " + caliber); + } + if (thickness < 1) + { + thickness = 1; //prevent divide by zero or other odd behavior + } + if (caliber < 1) + { + caliber = 20; //prevent divide by zero or other odd behavior + } + var modifiedCaliber = (0.5f * caliber) + (0.5f * caliber) * (2f * Ductility * Ductility); + yieldStrength = modifiedCaliber * modifiedCaliber * Mathf.PI / 100f * Strength * (Density / 7850f) * thickness; + //assumes bullet is perfect cyl, modded by ductility spreading impact over larger area, times strength/cm2 for threshold energy required to penetrate armor material + // Ductility is a measure of brittleness, the lower the brittleness, the more the material is willing to bend before fracturing, allowing energy to be spread over more area + 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 + } + if (hitpart.skinTemperature > SafeTemp) //has the armor started melting/denaturing/whatever? + { + yieldStrength *= 0.75f; + if (hitpart.skinTemperature > SafeTemp * 1.5f) + { + yieldStrength *= 0.5f; + } + } + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils]: Armor yield Strength: " + yieldStrength); + } + + return yieldStrength; + } + + 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 + { + //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.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 * 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? - 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.DEBUG_ARMOR) Debug.Log("[BDArmory.ProjectileUtils{Calc Deformation}]: bullet now " + (newCaliber) + " mm"); + return newCaliber; + } + } + public static bool CalculateBulletStatus(float projMass, float newCaliber, bool sabot = false) + { + //does the bullet suvive its impact? + //calculate bullet lengh, in mm + float density = 11.34f; + if (sabot) + { + 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.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils]: Bullet deformed past usable limit"); + } + return false; + } + else return true; + } + + + 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) + { + thickness = 1; //prevent divide by zero or other odd behavior + } + //the harder the material, the more the bullet is deformed, and the more energy it needs to expend to deform the armor + 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... + { + 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.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}]: 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 > 0.001f ? anglemultiplier : 0.001f), 1); + return Mathf.Max(thickness / Mathf.Abs(anglemultiplier), 1); + } + public static bool CheckGroundHit(Part hitPart, RaycastHit hit, float caliber) + { + if (hitPart == null) + { + if (BDArmorySettings.BULLET_HITS) + { + BulletHitFX.CreateBulletHit(hitPart, hit.point, hit, hit.normal, true, caliber, 0, null); + } + + return true; + } + return false; + } + public static bool CheckBuildingHit(RaycastHit hit, float projMass, Vector3 currentVelocity, float DmgMult) + { + DestructibleBuilding building = null; + try + { + building = hit.collider.gameObject.GetComponentUpwards(); + //if (building != null) + // building.damageDecay = 600f; //check if new method is still subject to building regen + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.ProjectileUtils]: Exception thrown in CheckBuildingHit: " + e.Message + "\n" + e.StackTrace); + } + + 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.BALLISTIC_DMG_FACTOR + * 1e-4f); + damageToBuilding /= 8f; + 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.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; + } + return false; + } + + public static bool CheckBuildingHit(RaycastHit hit, float laserDamage, bool pulselaser) + { + DestructibleBuilding building = null; + try + { + building = hit.collider.gameObject.GetComponentUpwards(); + //if (building != null) + // building.damageDecay = 600f; //check if new method is still subject to building regen + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.ProjectileUtils]: Exception thrown in CheckBuildingHit: " + e.Message + "\n" + e.StackTrace); + } + + if (building != null && building.IsIntact) + { + if (BDArmorySettings.BUILDING_DMG_MULTIPLIER == 0) return true; + if (laserDamage > 0) + { + float damageToBuilding = (laserDamage * (pulselaser ? 1 : TimeWarp.fixedDeltaTime)) * Mathf.Clamp((1 - (BDAMath.Sqrt(10 * 2.4f) * 200) / laserDamage), 0.005f, 1) //rough estimates of concrete at 10 Diffusivity, 2400kg/m3, and 20cm thick walls + * (BDArmorySettings.DMG_MULTIPLIER / 100); //will probably need to goose the numbers, quick back-of-the-envelope calc has the ABL doing ~3.4 DPS, BDA-E plasma, ~ 85 DPS + //damageToBuilding /= 8f; + 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 energy damage! Dmg to building: " + building.Damage); + building.Demolish(); + } + 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; + } + } + return false; + } + + public static void CheckPartForExplosion(Part hitPart) + { + if (!hitPart.FindModuleImplementing()) return; + + switch (hitPart.GetExplodeMode()) + { + case "Always": + CreateExplosion(hitPart); + break; + + case "Dynamic": + float probability = CalculateExplosionProbability(hitPart); + if (probability >= 3) + CreateExplosion(hitPart); + break; + + case "Never": + break; + } + } + + public static float CalculateExplosionProbability(Part part) + { + /////////////////////////////////////////////////////////////// + float probability = 0; + float fuelPct = 0; + for (int i = 0; i < part.Resources.Count; i++) + { + PartResource current = part.Resources[i]; + switch (current.resourceName) + { + case "LiquidFuel": + fuelPct = (float)(current.amount / current.maxAmount); + break; + //case "Oxidizer": + // probability += (float) (current.amount/current.maxAmount); + // break; + } + } + + if (fuelPct > 0 && fuelPct <= 0.60f) + { + probability = BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 5f }); + } + else + { + probability = BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 2f }); + } + + if (fuelPct == 1f || fuelPct == 0f) + probability = 0f; + + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.ProjectileUtils]: Explosive Probablitliy " + probability); + } + + return probability; + } + + public static void CreateExplosion(Part part) //REVIEW - remove/only activate if BattleDaamge fire disabled? + { + float explodeScale = 0; + IEnumerator resources = part.Resources.GetEnumerator(); + while (resources.MoveNext()) + { + if (resources.Current == null) continue; + switch (resources.Current.resourceName) + { + case "LiquidFuel": + explodeScale += (float)resources.Current.amount; + break; + + case "Oxidizer": + explodeScale += (float)resources.Current.amount; + break; + } + } + + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.ProjectileUtils]: Penetration of bullet detonated fuel!"); + } + + resources.Dispose(); + + explodeScale /= 100; + part.explosionPotential = explodeScale; + + PartExploderSystem.AddPartToExplode(part); + } + } +} 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..05c062311 --- /dev/null +++ b/BDArmory/Utils/SmoothingUtils.cs @@ -0,0 +1,115 @@ +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; + readonly float rate; + public Vector3 Value => 2f * S1 - S2; // The value at the current time. + + /// + /// Constructor for Brown's double exponential smoothing. + /// + /// Smoothing factor. 0 = no smoothing. + /// The initial value to use. + /// The constant rate at which the values are updated. Time.fixedDeltaTime is used if this is 0.= 0) SetAlpha(newAlpha); + S1 = alpha * value + beta * S1; + S2 = alpha * S1 + beta * S2; + } + + public void Reset(Vector3 initialValue = default) + { + S1 = initialValue; + S2 = initialValue; + } + + void SetAlpha(float alpha) + { + this.alpha = Mathf.Clamp01(alpha); + beta = 1f - alpha; + } + + /// + /// Estimate the value at a time later than the most recent update. + /// For the value at the current time, use Value instead. + /// + /// 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 > 0 ? rate : Time.fixedDeltaTime) * b; + } + } +} diff --git a/BDArmory/Utils/SoundUtils.cs b/BDArmory/Utils/SoundUtils.cs new file mode 100644 index 000000000..a63185db9 --- /dev/null +++ b/BDArmory/Utils/SoundUtils.cs @@ -0,0 +1,58 @@ +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. + /// Don't log an error if the sound file doesn't exist, just log it instead. + /// The AudioClip. + public static AudioClip GetAudioClip(string soundPath, bool allowMissing = false) + { + if (!audioClips.TryGetValue(soundPath, out AudioClip audioClip) || audioClip is null) + { + audioClip = GameDatabase.Instance.GetAudioClip(soundPath); + if (audioClip is null) + { + if (allowMissing) Debug.Log($"[BDArmory.SoundUtils]: {soundPath} did not give a valid audioclip."); + else 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/Utils/SplineUtils.cs b/BDArmory/Utils/SplineUtils.cs new file mode 100644 index 000000000..a81cd0c04 --- /dev/null +++ b/BDArmory/Utils/SplineUtils.cs @@ -0,0 +1,69 @@ +using UnityEngine; + +namespace BDArmory.Utils +{ + public static class SplineUtils + { + // TODO Add checks for when time-deltas are really small that might give NaNs/Infs and handle them gracefully + #region Vector3 + /// + /// Evaluate the function p(x) = h00(t)*p0 + h10(t)*(x1-x0)*m0 + h01(t)*p1 + h11(t)*(x1-x0)*m1, where + /// h00, h10, h01, h11 are the Hermite polynomial basis functions, + /// p0, p1, m0, m1 are the points and their slopes, + /// t is the normalised distance [0,1] between the points (x0 and x1) to perform the interpolation. + /// https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Interpolation_on_an_arbitrary_interval + /// The start point + /// The slope at the start point + /// The end point + /// The slope at the end point + /// The point in the interval (between tStart and tStop) to evaluate the function + /// The start of the interval + /// The end of the interval + /// + public static Vector3 EvaluateSpline(Vector3 point1, Vector3 slope1, Vector3 point2, Vector3 slope2, float t, float tStart, float tStop) + { + var dt = tStop - tStart; + t = Mathf.Clamp01((t - tStart) / dt); // Rescale the t paramter and enforce that it is in the correct range. + var t2 = t * t; + var t3 = t2 * t; + var h00 = 2 * t3 - 3 * t2 + 1; + var h10 = t3 - 2 * t2 + t; + var h01 = -2 * t3 + 3 * t2; + var h11 = t3 - t2; + return h00 * point1 + h10 * dt * slope1 + h01 * point2 + h11 * dt * slope2; + } + + /// + /// Slope estimation using 3-point finite difference. + /// https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Finite_difference + /// + /// + /// + /// + /// + /// + /// + public static Vector3 EstimateSlope(Vector3 point0, Vector3 point1, Vector3 point2, float dt01 = 0, float dt12 = 0) + { + // If the time deltas between the points aren't specified, treat the time parameter as distance. + if (dt01 == 0) dt01 = (point1 - point0).magnitude; + if (dt12 == 0) dt12 = (point2 - point1).magnitude; + return 0.5f * ((point2 - point1) / dt12 + (point1 - point0) / dt01); + } + + /// + /// Slope estimation using 2-point finite difference. + /// + /// + /// + /// + /// + public static Vector3 EstimateSlope(Vector3 point0, Vector3 point1, float dt = 0) + { + // If the time delta between the points isn't specified, treat the time parameter as distance. + if (dt == 0) dt = (point1 - point0).magnitude; + return (point1 - point0) / dt; + } + #endregion + } +} \ No newline at end of file diff --git a/BDArmory/Utils/StringUtils.cs b/BDArmory/Utils/StringUtils.cs new file mode 100644 index 000000000..3e633e782 --- /dev/null +++ b/BDArmory/Utils/StringUtils.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using KSP.Localization; +using UnityEngine; + +namespace BDArmory.Utils +{ + public static class StringUtils + { + static readonly Dictionary localizedStrings = []; // 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..0d4d8be26 --- /dev/null +++ b/BDArmory/Utils/UIControlUtils.cs @@ -0,0 +1,1277 @@ +using System.Collections.Generic; +using System.Collections; +using System.Reflection; +using System; +using TMPro; +using UniLinq; +using UnityEngine.SceneManagement; +using UnityEngine.UI; +using UnityEngine; +using BDArmory.Extensions; +using KSP.UI; + +namespace BDArmory.Utils +{ + #region FloatLogRange + /// + /// 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(); + } + public (float, float, float, int) GetLimits() => (minValue, maxValue, 0, steps); + + public static float ToSliderValue(float value, float minValue, float maxValue, int steps) + { + float logMinValue = Mathf.Log10(minValue); + float logMaxValue = Mathf.Log10(maxValue); + float sliderStepSize = (logMaxValue - logMinValue) / steps; + return Mathf.Clamp(BDAMath.RoundToUnit(Mathf.Log10(value) - logMinValue, sliderStepSize) + logMinValue, logMinValue, logMaxValue); + } + public static float FromSliderValue(float value, float minValue, float maxValue, int steps) + { + float logMinValue = Mathf.Log10(minValue); + float logMaxValue = Mathf.Log10(maxValue); + float sliderStepSize = (logMaxValue - logMinValue) / steps; + return Mathf.Pow(10f, Mathf.Clamp(BDAMath.RoundToUnit(value - logMinValue, sliderStepSize) + logMinValue, logMinValue, logMaxValue)); + } + } + + [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; + } + } + #endregion + + #region FloatSemiLogRange + /// + /// 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 float sigFig = 2; // 2 sig.fig. gives: ..., 9.8, 9.9, 10, 11, 12, ...; the fractional component (if non-zero) determines the rounding amount. + public bool withZero = false; // Include a special 0 value and sets reducedPrecisionAtMin to true. + public bool reducedPrecisionAtMin = false; // Lower the sigFig for the lowest values. + public UI_FloatSemiLogRange() { } + + /// + /// Update the limits. + /// Call this instead of directly setting min/max value or sigFig to properly update the slider. + /// + /// + /// + /// + /// + /// (Automatically set to true if withZero is true and sigFig>1.) + public void UpdateLimits(float minValue, float maxValue, float sigFig = 0, Toggle withZero = Toggle.NoChange, Toggle reducedPrecisionAtMin = Toggle.NoChange) + { + // Sanitise input. + this.minValue = Mathf.Min(minValue, maxValue); + this.maxValue = Mathf.Max(minValue, maxValue); + if (sigFig > 0) this.sigFig = sigFig; + this.withZero = withZero switch { Toggle.On => true, Toggle.Off => false, Toggle.Toggle => !this.withZero, _ => this.withZero }; + this.reducedPrecisionAtMin = this.sigFig > 1 && (this.withZero || reducedPrecisionAtMin switch { Toggle.On => true, Toggle.Off => false, Toggle.Toggle => !this.reducedPrecisionAtMin, _ => this.reducedPrecisionAtMin }); + var partActionFieldItem = (UIPartActionFloatSemiLogRange)partActionItem; + if (partActionFieldItem != null) partActionFieldItem.UpdateLimits(); + } + + public (float, float, float, float, bool, bool) GetLimits() => (minValue, maxValue, Mathf.Max(10f * (sigFig % 1f), 1f), sigFig, withZero, reducedPrecisionAtMin); + + /// + /// Static function for converting linear values to semi-log values. + /// + /// The value to convert. + /// The minimum value of the slider. + /// The number of significant figures (for integer rounding). Default=2. + /// The semi-log value. + public static float FromSliderValue(float value, float minValue, float sigFig = 2, bool withZero = false, bool reducedPrecisionAtMin = false) + { + reducedPrecisionAtMin = sigFig > 1 && (reducedPrecisionAtMin || withZero); + int sigfig = Mathf.CeilToInt(sigFig); + float rounding = Mathf.Max(10f * (sigFig % 1f), 1f); + float minStepSize = Mathf.Pow(10f, Mathf.Floor(Mathf.Log10(minValue)) + (reducedPrecisionAtMin && rounding == 1 ? 1 : 0)); + float sliderStepSize = Mathf.Pow(10f, 1 - sigfig); + float sliderMinValue = BDAMath.RoundToUnit(reducedPrecisionAtMin ? 1 - (11 - 10 * minValue / minStepSize) * sliderStepSize : minValue / minStepSize - (withZero ? sliderStepSize : 0), sliderStepSize); + + value = BDAMath.RoundToUnit(value, sliderStepSize); + if (withZero && value < sliderMinValue + sliderStepSize / 2f) return 0; + else if (reducedPrecisionAtMin && value <= 1f + sliderStepSize / 2f) value *= minStepSize; + else value = Mathf.Pow(10f, Mathf.Floor((value - 1f) / 9f)) * (1f + (value - 1f) % 9f) * minStepSize; + + value = BDAMath.RoundToUnit(value, rounding * Mathf.Pow(10, Mathf.CeilToInt(Mathf.Log10(value)) - sigfig)); // Round to the rounding units. + if (Mathf.Log10(value) - (sigfig - 1) > 0) value = Mathf.Round(value); // Round whole numbers properly. + return value; + } + /// + /// Static function for converting semi-log values to linear values. + /// + /// The value to convert. + /// The minimum value of the slider. + /// The linear value. + public static float ToSliderValue(float value, float minValue, float sigFig = 2, bool withZero = false, bool reducedPrecisionAtMin = false) + { + reducedPrecisionAtMin = sigFig > 1 && (reducedPrecisionAtMin || withZero); + int sigfig = Mathf.CeilToInt(sigFig); + float rounding = Mathf.Max(10f * (sigFig % 1f), 1f); + float minStepSize = Mathf.Pow(10f, Mathf.Floor(Mathf.Log10(minValue)) + (reducedPrecisionAtMin && rounding == 1 && sigfig > 1 ? 1 : 0)); + float sliderStepSize = Mathf.Pow(10f, 1 - sigfig); + if (withZero || reducedPrecisionAtMin) + { + float sliderMinValue = BDAMath.RoundToUnit(sigfig > 1 ? 1 - (11 - 10 * minValue / minStepSize) * sliderStepSize : minValue / minStepSize - sliderStepSize, sliderStepSize); + if (value < minValue) return sliderMinValue; + if (value < minStepSize) return BDAMath.RoundToUnit(value / minStepSize, sliderStepSize); + } + value /= minStepSize; + float factor = Mathf.Floor(Mathf.Log10(value)); + return BDAMath.RoundToUnit(factor * 9f + value / Mathf.Pow(10f, factor), sliderStepSize); + } + } + + [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; + private bool withZero = false; + private bool reducedPrecisionAtMin = false; + private int sigFig; + private float rounding; + + 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{Mathf.Max(Mathf.CeilToInt(semiLogFloatRange.sigFig) + 2, Mathf.CeilToInt(Mathf.Log10(semiLogFloatRange.maxValue)) + 1)}"; // Show at most 2 digits beyond the requested sig. fig. or enough for the largest number. + float value = GetFieldValue(); + SetFieldValue(value); + UpdateDisplay(value); + 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 void CheckSlider(float value) + { + var toValue = ToSliderValue(value); + var fromValue = FromSliderValue(toValue); + // Debug.Log($"DEBUG SemiLog: value {value} -> {toValue} -> {fromValue}, static ToSlider: {UI_FloatSemiLogRange.ToSliderValue(value, semiLogFloatRange.minValue, semiLogFloatRange.sigFig, semiLogFloatRange.withZero, semiLogFloatRange.reducedPrecisionAtMin)}, FromSlider: {UI_FloatSemiLogRange.FromSliderValue(toValue, semiLogFloatRange.minValue, semiLogFloatRange.sigFig, semiLogFloatRange.withZero, semiLogFloatRange.reducedPrecisionAtMin)}"); + } + float FromSliderValue(float value) + { + value = BDAMath.RoundToUnit(value, sliderStepSize); + if (withZero && value < slider.minValue + sliderStepSize / 2f) return 0; + else if (reducedPrecisionAtMin && value <= 1f + sliderStepSize / 2f) value *= minStepSize; + else value = Mathf.Pow(10f, Mathf.Floor((value - 1f) / 9f)) * (1f + (value - 1f) % 9f) * minStepSize; + + value = BDAMath.RoundToUnit(value, rounding * Mathf.Pow(10, Mathf.CeilToInt(Mathf.Log10(value)) - sigFig)); // Round to the rounding units. + if (Mathf.Log10(value) - (sigFig - 1) > 0) value = Mathf.Round(value); // Round whole numbers properly. + return value; + } + float ToSliderValue(float value) + { + if (withZero || reducedPrecisionAtMin) + { + if (value < semiLogFloatRange.minValue) return slider.minValue; + if (value < minStepSize) return BDAMath.RoundToUnit(value / minStepSize, sliderStepSize); + } + value /= minStepSize; + float factor = Mathf.Floor(Mathf.Log10(value)); + return BDAMath.RoundToUnit(factor * 9f + value / Mathf.Pow(10f, factor), sliderStepSize); + } + 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 = ToSliderValue(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 = FromSliderValue(slider.value); + // CheckSlider(value); + SetFieldValue(value); + UpdateDisplay(value); + } + private void OnNumericSubmitted(string str) + { + if (float.TryParse(str, out float value)) + { + value = Mathf.Clamp(value, withZero ? 0 : 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. + sigFig = Mathf.CeilToInt(semiLogFloatRange.sigFig); + rounding = Mathf.Max(10f * (semiLogFloatRange.sigFig % 1f), 1f); + withZero = semiLogFloatRange.withZero; + reducedPrecisionAtMin = sigFig > 1 && (semiLogFloatRange.reducedPrecisionAtMin || withZero); + var minStepSizePower = Mathf.Floor(Mathf.Log10(semiLogFloatRange.minValue)) + (reducedPrecisionAtMin && rounding == 1 ? 1 : 0); + 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 - sigFig); + slider.minValue = BDAMath.RoundToUnit(reducedPrecisionAtMin ? 1 - (10 + (withZero ? 1 : 0) - 10 * semiLogFloatRange.minValue / minStepSize) * sliderStepSize : semiLogFloatRange.minValue / minStepSize - (withZero ? sliderStepSize : 0), sliderStepSize); + slider.maxValue = BDAMath.RoundToUnit(9f * (maxStepSizePower - minStepSizePower) + semiLogFloatRange.maxValue / maxStepSize, sliderStepSize); + semiLogFloatRange.stepIncrement = sliderStepSize; + fieldFormatString = $"G{Mathf.Max(sigFig + 2, Mathf.CeilToInt(Mathf.Log10(semiLogFloatRange.maxValue)) + 1)}"; // Show at most 2 digits beyond the requested sig. fig. or enough for the largest number. + SetFieldValue(value); // Restore the unclamped value. + UpdateDisplay(value); + // Debug.Log($"DEBUG value is {value} with limits {semiLogFloatRange.minValue}—{semiLogFloatRange.maxValue}, with zero: {withZero}, sigFig: {sigFig}, rounding: {rounding}, reducedPrecisionAtMin: {reducedPrecisionAtMin}"); + // Debug.Log($"DEBUG slider has value {slider.value} with limits {slider.minValue}—{slider.maxValue}"); + } + } + + [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; + } + } + #endregion + + #region FloatPowerRange + /// + /// Power-scaling FloatRange slider. + /// Specify minValue, maxValue, power and sigFig. The stepIncrement is automatically calculated. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_FloatPowerRange : UI_FloatRange + { + private const string UIControlName = "FloatPowerRange"; + public float power; + public int sigFig = 2; + public UI_FloatPowerRange() { } + + /// + /// 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, float power = 0, int sigFig = 0) + { + // Sanitise input. + minValue = Mathf.Max(0, minValue); // Values can't be negative. + maxValue = Mathf.Max(0, maxValue); + this.minValue = Mathf.Min(minValue, maxValue); + this.maxValue = Mathf.Max(minValue, maxValue); + if (power > 0) this.power = power; + if (sigFig > 0) this.sigFig = sigFig; + var partActionFieldItem = (UIPartActionFloatPowerRange)partActionItem; + if (partActionFieldItem != null) partActionFieldItem.UpdateLimits(); + } + + public (float, float, float, int) GetLimits() => (minValue, maxValue, Mathf.Pow(10f, Mathf.CeilToInt(Mathf.Log10(maxValue)) - sigFig - 2), sigFig); + + /// + /// Static function for converting linear values to power values. + /// + /// + /// + /// + public static float FromSliderValue(float value, float power, int sigFig, float maxValue) + { + if (value > 0) + { + value = Mathf.Pow(value, power); + var rounding = Mathf.Max(Mathf.Pow(10f, Mathf.CeilToInt(Mathf.Log10(value)) - sigFig), Mathf.Pow(10f, Mathf.CeilToInt(Mathf.Log10(maxValue)) - sigFig - 2)); + return BDAMath.RoundToUnit(value, rounding); + } + else return 0; + } + /// + /// Static function for converting power values to linear values. + /// + /// + /// + /// + /// + public static float ToSliderValue(float value, float power) + { + if (value > 0) return Mathf.Pow(value, 1 / power); + else return 0; + } + } + + [UI_FloatPowerRange] + public class UIPartActionFloatPowerRange : UIPartActionFieldItem + { + protected UI_FloatPowerRange powerFloatRange { get { return (UI_FloatPowerRange)control; } } + public TextMeshProUGUI fieldName; + public TextMeshProUGUI fieldValue; + public Slider slider; + private float minValue, maxValue, power, sigFig, roundingLimit; + private bool blockSliderUpdate; + private bool numericSliders = false; + private string fieldFormatString = "G4"; + 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 UIPartActionFloatPowerRange CreateTemplate() + { + // Create the control + GameObject gameObject = new GameObject("UIPartActionFloatPowerRange", VersionTaggedType(typeof(UIPartActionFloatPowerRange))); + UIPartActionFloatPowerRange partActionFloatPowerRange = 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); + partActionFloatPowerRange.fieldName = fieldNameGO.GetComponent(); + list.TryGetValue(partActionFloatRange.fieldAmount.gameObject, out GameObject fieldValueGO); + partActionFloatPowerRange.fieldValue = fieldValueGO.GetComponent(); + list.TryGetValue(partActionFloatRange.slider.gameObject, out GameObject sliderGO); + partActionFloatPowerRange.slider = sliderGO.GetComponent(); + list.TryGetValue(partActionFloatRange.numericContainer, out partActionFloatPowerRange.numericContainer); + list.TryGetValue(partActionFloatRange.inputField.gameObject, out GameObject inputFieldGO); + partActionFloatPowerRange.inputField = inputFieldGO.GetComponent(); + list.TryGetValue(partActionFloatRange.fieldNameNumeric.gameObject, out GameObject fieldNameNumericGO); + partActionFloatPowerRange.fieldNameNumeric = fieldNameNumericGO.GetComponent(); + + return partActionFloatPowerRange; + } + + 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{Mathf.Max(powerFloatRange.sigFig + 2, Mathf.CeilToInt(Mathf.Log10(powerFloatRange.maxValue)) + 1)}"; // Show at most 2 digits beyond the requested sig. fig. or enough for the largest number. + float value = GetFieldValue(); + SetFieldValue(value); + UpdateDisplay(value); + 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; + } + float FromSliderValue(float value) + { + if (value > 0) + { + value = Mathf.Pow(value, power); + var rounding = Mathf.Max(Mathf.Pow(10f, Mathf.CeilToInt(Mathf.Log10(value)) - sigFig), roundingLimit); + return BDAMath.RoundToUnit(value, rounding); + } + else return 0; + } + float ToSliderValue(float value) + { + if (value > 0) return Mathf.Pow(value, 1 / power); + else return 0; + } + 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 = ToSliderValue(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 = FromSliderValue(slider.value); + SetFieldValue(value); + UpdateDisplay(value); + } + private void OnNumericSubmitted(string str) + { + if (float.TryParse(str, out float value)) + { + value = Mathf.Clamp(value, minValue, 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. + minValue = powerFloatRange.minValue; + maxValue = powerFloatRange.maxValue; + power = powerFloatRange.power; + sigFig = powerFloatRange.sigFig; + roundingLimit = Mathf.Pow(10f, Mathf.CeilToInt(Mathf.Log10(maxValue)) - sigFig - 2); + blockSliderUpdate = true; // Block the slider from updating while we adjust things (unblocks in UpdateDisplay). + slider.minValue = Mathf.Pow(minValue, 1 / power); + slider.maxValue = Mathf.Pow(maxValue, 1 / power); + fieldFormatString = $"G{Mathf.Max(sigFig + 2, Mathf.CeilToInt(Mathf.Log10(maxValue)) + 1)}"; // Show at most 2 digits beyond the requested sig. fig. or enough for the largest number. + SetFieldValue(value); // Restore the unclamped value. + UpdateDisplay(value); + // Debug.Log($"DEBUG value is {value} with limits {minValue}—{maxValue}"); + // Debug.Log($"DEBUG slider has value {slider.value} with limits {slider.minValue}—{slider.maxValue}"); + } + } + + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class UIPartActionFloatPowerRangeRegistration : 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(UIPartActionFloatPowerRange.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_FloatPowerRange)); + + isRunning = false; + } + } + #endregion + + #region ActionGroup + /// + /// Action group slider. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_ActionGroup : UI_ChooseOption + { + private const string UIControlName = "ActionGroup"; + public UI_ActionGroup() + { + options = [.. ActionGroups.Values.Select(ag => ag.ToString()).Select(ag => ag.StartsWith("Custom") ? $"AG{ag.Substring(6)}" : ag)]; + } + public static readonly Dictionary ActionGroups = new() { + { 0, KSPActionGroup.None }, + { 1, KSPActionGroup.Custom01 }, + { 2, KSPActionGroup.Custom02 }, + { 3, KSPActionGroup.Custom03 }, + { 4, KSPActionGroup.Custom04 }, + { 5, KSPActionGroup.Custom05 }, + { 6, KSPActionGroup.Custom06 }, + { 7, KSPActionGroup.Custom07 }, + { 8, KSPActionGroup.Custom08 }, + { 9, KSPActionGroup.Custom09 }, + { 10, KSPActionGroup.Custom10 }, + { 11, KSPActionGroup.Gear }, + { 12, KSPActionGroup.Light }, + { 13, KSPActionGroup.RCS }, + { 14, KSPActionGroup.SAS }, + { 15, KSPActionGroup.Brakes }, + { 16, KSPActionGroup.Abort } + }; + } + + [UI_ActionGroup] + public class UIPartActionActionGroup : UIPartActionFieldItem + { + protected UI_ActionGroup actionGroup { get { return (UI_ActionGroup)control; } } + public TextMeshProUGUI fieldName; + public TextMeshProUGUI fieldValue; + public Slider slider; + public UIButtonToggle inc; + public UIButtonToggle dec; + private bool blockSliderUpdate; + + 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 UIPartActionActionGroup CreateTemplate() + { + // Create the control + GameObject gameObject = new GameObject("UIPartActionActionGroup", VersionTaggedType(typeof(UIPartActionActionGroup))); + UIPartActionActionGroup partActionActionGroup = GetTaggedComponent(gameObject); + gameObject.SetActive(false); + + // Find the template for FloatRange + UIPartActionChooseOption partActionChooseOption = (UIPartActionChooseOption)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionChooseOption)); + + // Copy UI elements + RectTransform rtc = gameObject.AddComponent(); + RectTransform rt = partActionChooseOption.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 = partActionChooseOption.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(partActionChooseOption.gameObject, gameObject.transform, ref list); + list.TryGetValue(partActionChooseOption.fieldName.gameObject, out GameObject fieldNameGO); + partActionActionGroup.fieldName = fieldNameGO.GetComponent(); + list.TryGetValue(partActionChooseOption.fieldValue.gameObject, out GameObject fieldValueGO); + partActionActionGroup.fieldValue = fieldValueGO.GetComponent(); + list.TryGetValue(partActionChooseOption.slider.gameObject, out GameObject sliderGO); + partActionActionGroup.slider = sliderGO.GetComponent(); + list.TryGetValue(partActionChooseOption.inc.gameObject, out GameObject incGO); + partActionActionGroup.inc = incGO.GetComponent(); + list.TryGetValue(partActionChooseOption.dec.gameObject, out GameObject decGO); + partActionActionGroup.dec = decGO.GetComponent(); + + return partActionActionGroup; + } + + 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); + slider.minValue = UI_ActionGroup.ActionGroups.Keys.Min(); + slider.maxValue = UI_ActionGroup.ActionGroups.Keys.Max(); + fieldName.text = field.guiName; + var (index, value) = GetValueFromField(); + SetFieldValue(value); + UpdateDisplay(index); + slider.onValueChanged.AddListener(OnValueChanged); + inc.onToggle.AddListener(OnTap_inc); + dec.onToggle.AddListener(OnTap_dec); + } + + private (int, int) GetValueFromField() + { + var value = (KSPActionGroup)field.GetValue(field.host); + var index = UI_ActionGroup.ActionGroups.ToDictionary(kvp => kvp.Value, kvp => kvp.Key).GetValueOrDefault(value, 0); // Filter out bitwise combinations, e.g., Gear | Brakes. + return (index, (int)UI_ActionGroup.ActionGroups.GetValueOrDefault(index, KSPActionGroup.None)); + } + private void UpdateDisplay(int index) + { + blockSliderUpdate = true; + fieldValue.text = actionGroup.options[index]; + slider.value = index; + 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; } + int index = Mathf.RoundToInt(slider.value); + int value = (int)UI_ActionGroup.ActionGroups.GetValueOrDefault(index, KSPActionGroup.None); + SetFieldValue(value); + UpdateDisplay(index); + } + private void OnTap_dec() + { + var index = Mathf.RoundToInt(slider.value); + if (index <= slider.minValue) return; + --index; + int value = (int)UI_ActionGroup.ActionGroups.GetValueOrDefault(index, KSPActionGroup.None); + SetFieldValue(value); + UpdateDisplay(index); + } + private void OnTap_inc() + { + var index = Mathf.RoundToInt(slider.value); + if (index >= slider.maxValue) return; + ++index; + int value = (int)UI_ActionGroup.ActionGroups.GetValueOrDefault(index, KSPActionGroup.None); + SetFieldValue(value); + UpdateDisplay(index); + } + } + + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class UIPartActionActionGroupRegistration : 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(UIPartActionChooseOption))) + yield return false; + + // Register prefabs + controller.fieldPrefabs.Add(UIPartActionActionGroup.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_ActionGroup)); + + isRunning = false; + } + } + #endregion +} diff --git a/BDArmory/Utils/VectorUtils.cs b/BDArmory/Utils/VectorUtils.cs new file mode 100644 index 000000000..b5406b648 --- /dev/null +++ b/BDArmory/Utils/VectorUtils.cs @@ -0,0 +1,637 @@ +using System; +using UnityEngine; + +using BDArmory.Extensions; +using System.Runtime.CompilerServices; + +namespace BDArmory.Utils +{ + public static class VectorUtils + { + private static System.Random RandomGen = new System.Random(); + + /// + /// A slightly more efficient `Vector3.Sign` function, still requires a sqrt so it is best replaced with + /// `VectorUtils.GetAngleOnPlane`, however that requires orthogonality from `fromDirection`. This function + /// may be used even if `referenceRight` is not orthogonal to `fromDirection`. This function also does not + /// require the magnitudes of any of its inputs to be specified in some way. + /// + /// Right compared to fromDirection, make sure it's not orthogonal to toDirection, or you'll get unstable signs + public static float SignedAngle(Vector3 fromDirection, Vector3 toDirection, Vector3 referenceRight) + { + float angle = Angle(fromDirection, toDirection); + float sign = Mathf.Sign(Vector3.Dot(toDirection, referenceRight)); + float finalAngle = sign * angle; + return finalAngle; + } + + /// + /// Same as SignedAngle, just using double precision for the cosine calculation. + /// For very small angles the floating point precision starts to matter, as the cosine is close to 1, not to 0. + /// + public static float SignedAngleDP(Vector3 fromDirection, Vector3 toDirection, Vector3 referenceRight) + { + float angle = (float)Vector3d.Angle(fromDirection, toDirection); + float sign = Mathf.Sign(Vector3.Dot(toDirection, referenceRight)); + float finalAngle = sign * angle; + return finalAngle; + } + + /// + /// Convert an angle to be between -180 and 180. + /// + public static float ToAngle(this float angle) + { + angle = (angle + 180) % 360; + return angle > 0 ? angle - 180 : angle + 180; + } + + //from howlingmoonsoftware.com + //calculates how long it will take for a target to be where it will be when a bullet fired now can reach it. + //delta = initial relative position, vr = relative velocity, muzzleV = bullet velocity. + public static float CalculateLeadTime(Vector3 delta, Vector3 vr, float muzzleV) + { + // Quadratic equation coefficients a*t^2 + b*t + c = 0 + float a = Vector3.Dot(vr, vr) - muzzleV * muzzleV; + float b = 2f * Vector3.Dot(vr, delta); + float c = Vector3.Dot(delta, delta); + + float det = b * b - 4f * a * c; + + // If the determinant is negative, then there is no solution + if (det > 0f) + { + return 2f * c / (BDAMath.Sqrt(det) - b); + } + else + { + return -1f; + } + } + + /// + /// Returns a value between -1 and 1 via Perlin noise. + /// + /// Returns a value between -1 and 1 via Perlin noise. + /// The x coordinate. + /// The y coordinate. + public static float FullRangePerlinNoise(float x, float y) + { + float perlin = Mathf.PerlinNoise(x, y); + + perlin -= 0.5f; + perlin *= 2; + + return perlin; + } + + public static Vector3 RandomDirectionDeviation(Vector3 direction, float maxAngle) + { + return Vector3.RotateTowards(direction, UnityEngine.Random.rotation * direction, UnityEngine.Random.Range(0, maxAngle * Mathf.Deg2Rad), 0).normalized; + } + + public static Vector3 WeightedDirectionDeviation(Vector3 direction, float maxAngle) + { + float random = UnityEngine.Random.Range(0f, 1f); + float maxRotate = maxAngle * (random * random); + maxRotate = Mathf.Clamp(maxRotate, 0, maxAngle) * Mathf.Deg2Rad; + return Vector3.RotateTowards(direction, UnityEngine.Random.onUnitSphere.ProjectOnPlane(direction), maxRotate, 0).normalized; + } + + /// + /// Returns the original vector rotated in a random direction using the give standard deviation. + /// + /// mean direction + /// standard deviation in degrees + /// Randomly adjusted Vector3 + /// + /// Technically, this is calculated using the chi-squared distribution in polar coordinates, + /// which, incidentally, makes the math easier too. + /// However a chi-squared (k=2) distance from center distribution produces a vector distributed normally + /// on any chosen axis orthogonal to the original vector, which is exactly what we want. + /// + public static Vector3 GaussianDirectionDeviation(Vector3 direction, float standardDeviation) + { + return Quaternion.AngleAxis(UnityEngine.Random.Range(-180f, 180f), direction) + * Quaternion.AngleAxis(Rayleigh() * standardDeviation, + new Vector3(-1 / direction.x, -1 / direction.y, 2 / direction.z)) // orthogonal vector + * direction; + } + + /// Random float distributed with an approximated standard normal distribution + /// https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform + /// Note a standard normal variable is technically unbounded + 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 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 + Debug.LogWarning("[BDArmory.VectorUtils]: Exception thrown in Gaussian: " + e.Message + "\n" + e.StackTrace); + return 0; + } + } + + /// + /// 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( + mean.x + stdDev.x * Math.Sqrt(-2 * Math.Log(1 - RandomGen.NextDouble())) * Math.Cos(Math.PI * RandomGen.NextDouble()), + mean.y + stdDev.y * Math.Sqrt(-2 * Math.Log(1 - RandomGen.NextDouble())) * Math.Cos(Math.PI * RandomGen.NextDouble()), + mean.z + stdDev.z * Math.Sqrt(-2 * Math.Log(1 - RandomGen.NextDouble())) * Math.Cos(Math.PI * RandomGen.NextDouble()) + ); + } + + /// + /// Random float distributed with the chi-squared distribution with two degrees of freedom + /// aka the Rayleigh distribution. + /// Multiply by deviation for best results. + /// + /// https://en.wikipedia.org/wiki/Rayleigh_distribution + /// Note a chi-square distributed variable is technically unbounded + public static float Rayleigh() + { + // Technically this will raise an exception if the random produces a zero, which should almost never happen + try + { + 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 + Debug.LogWarning("[BDArmory.VectorUtils]: Exception thrown in Rayleigh: " + e.Message + "\n" + e.StackTrace); + return 0; + } + } + + /// + /// Converts world position to Lat,Long,Alt form. + /// + /// The position in geo coords. + /// World position. + /// Body. + public static Vector3d WorldPositionToGeoCoords(Vector3d worldPosition, CelestialBody body) + { + if (!body) + { + return Vector3d.zero; + } + + double lat = body.GetLatitude(worldPosition); + double longi = body.GetLongitude(worldPosition); + double alt = body.GetAltitude(worldPosition); + return new Vector3d(lat, longi, alt); + } + + /// + /// Calculates the coordinates of a point a certain distance away in a specified direction. + /// + /// Starting point coordinates, in Lat,Long,Alt form + /// The body on which the movement is happening + /// Bearing to move in, in degrees, where 0 is north and 90 is east + /// Distance to move, in meters + /// Ending point coordinates, in Lat,Long,Alt form + public static Vector3 GeoCoordinateOffset(Vector3 start, CelestialBody body, float bearing, float distance) + { + //https://stackoverflow.com/questions/2637023/how-to-calculate-the-latlng-of-a-point-a-certain-distance-away-from-another + float lat1 = start.x * Mathf.Deg2Rad; + float lon1 = start.y * Mathf.Deg2Rad; + bearing *= Mathf.Deg2Rad; + distance /= ((float)body.Radius + start.z); + + float lat2 = Mathf.Asin(Mathf.Sin(lat1) * Mathf.Cos(distance) + Mathf.Cos(lat1) * Mathf.Sin(distance) * Mathf.Cos(bearing)); + float lon2 = lon1 + Mathf.Atan2(Mathf.Sin(bearing) * Mathf.Sin(distance) * Mathf.Cos(lat1), Mathf.Cos(distance) - Mathf.Sin(lat1) * Mathf.Sin(lat2)); + + return new Vector3(lat2 * Mathf.Rad2Deg, lon2 * Mathf.Rad2Deg, start.z); + } + + /// + /// Calculate the bearing going from one point to another + /// + /// Starting point coordinates, in Lat,Long,Alt form + /// Destination point coordinates, in Lat,Long,Alt form + /// Bearing when looking at destination from start, in degrees, where 0 is north and 90 is east + public static float GeoForwardAzimuth(Vector3 start, Vector3 destination) + { + //http://www.movable-type.co.uk/scripts/latlong.html + float lat1 = start.x * Mathf.Deg2Rad; + float lon1 = start.y * Mathf.Deg2Rad; + float lat2 = destination.x * Mathf.Deg2Rad; + float lon2 = destination.y * Mathf.Deg2Rad; + return Mathf.Atan2(Mathf.Sin(lon2 - lon1) * Mathf.Cos(lat2), Mathf.Cos(lat1) * Mathf.Sin(lat2) - Mathf.Sin(lat1) * Mathf.Cos(lat2) * Mathf.Cos(lon2 - lon1)) * Mathf.Rad2Deg; + } + + /// + /// Calculate the distance from one point to another on a globe + /// + /// Starting point coordinates, in Lat,Long,Alt form + /// Destination point coordinates, in Lat,Long,Alt form + /// The body on which the distance is calculated + /// distance between the two points + public static float GeoDistance(Vector3 start, Vector3 destination, CelestialBody body) + { + //http://www.movable-type.co.uk/scripts/latlong.html + float lat1 = start.x * Mathf.Deg2Rad; + float lat2 = destination.x * Mathf.Deg2Rad; + 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(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) + { + Vector3 line = pointToRotate - pivotPoint; + line = Quaternion.AngleAxis(angle, axis) * line; + return pivotPoint + line; + } + + public static Vector3 GetNorthVector(Vector3 position, CelestialBody body) + { + 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) + { + if (!body) + { + return Vector3.zero; + } + return body.GetWorldSurfacePosition(geoPosition.x, geoPosition.y, geoPosition.z); + } + + /// + /// Get the up direction at a position. + /// Note: If the position is a vessel's position, then this is the same as vessel.up, which is precomputed. Use that instead! + /// + /// + /// The normalized up direction at the position. + public static Vector3 GetUpDirection(Vector3 position) + { + if (FlightGlobals.currentMainBody == null) return Vector3.up; + return (position - FlightGlobals.currentMainBody.position).normalized; + } + + /// + /// Get the up direction and altitude at a position. + /// Note: If the position is a vessel's position, then this is the same as vessel.up and vessel.altitude, which are precomputed. Use those instead! + /// + /// + /// + /// The normalized up direction at the position. + public static Vector3 GetUpDirection(Vector3 position, out double altitude) + { + if (FlightGlobals.currentMainBody == null) + { + altitude = 0; + return Vector3.up; + } + Vector3 upDir; + (altitude, upDir) = (position - FlightGlobals.currentMainBody.position).MagNorm(); + altitude -= FlightGlobals.currentMainBody.Radius; + + return upDir; + } + + public static bool SphereRayIntersect(Ray ray, Vector3 sphereCenter, double sphereRadius, out double distance) + { + Vector3 o = ray.origin; + Vector3 l = ray.direction; + Vector3d c = sphereCenter; + double r = sphereRadius; + + double d; + + 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)) + { + distance = 0; + return false; + } + else + { + distance = d; + return true; + } + } + + public static bool CheckClearOfSphere(Ray ray, Vector3 sphereCenter, float sphereRadius) + { + // Return true if no sphere intersections, false if sphere intersections + // Better handling of conditions when ray origin is inside sphere or direction is away from sphere than SphereRayIntersect + + if ((ray.origin - sphereCenter).sqrMagnitude < (sphereRadius * sphereRadius)) + return false; + + bool intersect = SphereRayIntersect(ray, sphereCenter, (double)sphereRadius, out double distance); + + if (!intersect) + return true; + else + { + if (distance > 0) // Valid intersection + return false; + else // -ray intersects, but +ray does not + return true; + } + } + + /// + /// A more accurate Angle that is maintains precision down to an angle of 1e-5 + /// (as compared to (float)Vector3d.Angle) instead of the 1e-2 that Vector3.Angle gives. + /// Additionally, it's around 30% faster than Vector3.Angle and 12% faster than (float)Vector3d(from, to). + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Angle(Vector3 from, Vector3 to) + { + double num = ((Vector3d)from).sqrMagnitude * ((Vector3d)to).sqrMagnitude; + if (num < 1e-30) + { + return 0f; + } + + double num2 = BDAMath.Clamp(Vector3d.Dot(from, to) / Math.Sqrt(num), -1.0, 1.0); + return (float)(Math.Acos(num2) * 57.295779513082325); + } + + /// + /// Get angle between two pre-normalized vectors. + /// + /// This implementation assumes that the input vectors are already normalized, + /// skipping such checks and normalization that Vector3.Angle does. + /// IMPORTANT NOTE: Unlike Vector3.Angle(), this returns 90° if one or both + /// vectors are zero vectors! Vector3.Angle() returns 0° instead. + /// If this behavior is undesireable, the "AnglePreNormalized" function which takes + /// in the two original vectors and their magnitudes should be used instead. + /// + /// First vector. + /// Second vector. + /// The angle between the two vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float AnglePreNormalized(Vector3 from, Vector3 to) + { + float num2 = Mathf.Clamp(Vector3.Dot(from, to), -1f, 1f); + return Mathf.Acos(num2) * 57.29578f; + } + + /// + /// Get angle between two vectors, with known magnitudes. + /// + /// This implementation assumes that the magnitude of the input vectors is known, + /// skipping some checks and normalization that Vector3.Angle does. It is not + /// truly more efficient, however it is slightly more efficient when both + /// magnitudes are already known. + /// + /// First vector. + /// Second vector. + /// First vector magnitude. + /// Second vector magnitude. + /// The angle between the two vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float AnglePreNormalized(Vector3 from, Vector3 to, float fromMag, float toMag) + { + float num = fromMag * toMag; + if (num < 1E-15f) + return 0f; + + float num2 = Mathf.Clamp(Vector3.Dot(from, to) / (fromMag * toMag), -1f, 1f); + return Mathf.Acos(num2) * Mathf.Rad2Deg; + } + + /// + /// Get AoA and Sideslip of a vector, relative to axes defined by forward and up. + /// Note that forward and up are expected to be unit vectors, however dir does not have + /// to be a unit vector! + /// + /// + /// Direction vector. + /// Aircraft aligned forward vector. + /// Aircraft aligned up/lift vector. + /// AoA output. + /// Sideslip output. + /// The AoA and Sideslip angle, in degrees, of "dir" relative to the axes defined by forward and up. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GetAoASideslip(Vector3 dir, Vector3 forward, Vector3 up, out float AoA, out float sideslip) + { + // Get the left vector to fully define the coordinate system + Vector3 left = Vector3.Cross(up, forward); + + // Get the projections + float x = Vector3.Dot(dir, forward); + float y = Vector3.Dot(dir, left); + float z = Vector3.Dot(dir, up); + + // Return the AoA/sideslip + AoA = -Mathf.Rad2Deg * Mathf.Atan2(z, x); + sideslip = -Mathf.Rad2Deg * Mathf.Atan2(y, x); + } + + /// + /// Get angle of a vector, projected on a plane defined by a forward and a left vector. + /// Note that forward and left must have equal magnitudes but do not have to be unit + /// vectors (though unit vectors are most likely the most convenient for this purpose). + /// dir does not have to be a unit vector. + /// + /// + /// Direction vector. + /// Forward vector. + /// Left vector. + /// The angle of "dir" relative to "forward", in degrees, projected onto a plane defined by "forward" and "left". + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetAngleOnPlane(Vector3 dir, Vector3 forward, Vector3 left) + { + // Get the projections + float x = Vector3.Dot(dir, forward); + float y = Vector3.Dot(dir, left); + + // Check for if the desired vector is straight up/down + if (Mathf.Abs(x) < 2f * Vector3.kEpsilon && Mathf.Abs(y) < 2f * Vector3.kEpsilon) + return 0f; + + // Return the azimuth/elevation + return Mathf.Rad2Deg * Mathf.Atan2(y, x); + } + + /// + /// Get elevation angle of a vector, relative to an up vector. + /// Note that this basically an alternate form of AnglePreNormalized. + /// + /// + /// Direction vector. + /// Up vector. + /// Magnitude of the direction vector. + /// Magnitude of the up vector, defaults to 1. + /// The angle of "dir" relative to "up", in degrees, as an elevation angle, with range -90° to 90°. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetElevation(Vector3 dir, Vector3 up, float dist, float upMag = 1.0f) + { + return 90f - AnglePreNormalized(up, dir, upMag, dist); + } + + /// + /// Get elevation angle of a vector, relative to an up vector. + /// Note that this basically an alternate form of Vector3.Angle, + /// somewhat optimized for the case where the up vector is a + /// unit vector (skipping a mere "sqrMagnitude" call). If the + /// magnitude of the direction vector is known, the overload + /// with this magnitude is preferred: + /// GetElevation(dir, up, dist, upMag) + /// + /// + /// Direction vector. + /// Up vector. + /// The angle of "dir" relative to "up", in degrees, as an elevation angle, with range -90° to 90°. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetElevation(Vector3 dir, Vector3 up) + { + float dirMag = dir.magnitude; + if (dirMag < 1E-15f) + { + return 0f; + } + + float num2 = Mathf.Clamp(Vector3.Dot(up, dir) / dirMag, -1f, 1f); + return 90f - (float)Math.Acos(num2) * 57.29578f; + } + + /// + /// Get normalized difference between two vectors, useful for direction vectors. + /// + /// First vector. + /// Second vector. + /// (v1 - v2).normalized. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 NormalizedDiff(Vector3 v1, Vector3 v2) + { + float x = v1.x - v2.x, y = v1.y - v2.y, z = v1.z - v2.z; + float normalizationFactor = 1f / BDAMath.Sqrt(x * x + y * y + z * z); + return new Vector3(x * normalizationFactor, y * normalizationFactor, z * normalizationFactor); + } + + /// + /// Get normalized difference between two vectors with given distance, useful for direction vectors. + /// + /// First vector. + /// Second vector. + /// Distance. + /// (v1 - v2).normalized. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 NormalizedDiff(Vector3 v1, Vector3 v2, float dist) + { + float x = v1.x - v2.x, y = v1.y - v2.y, z = v1.z - v2.z; + float normalizationFactor = 1f / dist; + return new Vector3(x * normalizationFactor, y * normalizationFactor, z * normalizationFactor); + } + + /// + /// Get square distance between two vectors, in cases where the vector difference isn't needed. + /// + /// First vector. + /// Second vector. + /// (v1 - v2).sqrMagnitude. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SqrDist(Vector3 v1, Vector3 v2) + { + float x = v1.x - v2.x, y = v1.y - v2.y, z = v1.z - v2.z; + return x * x + y * y + z * z; + } + + /// + /// Rotates a Vector2 in 2D about (0,0). + /// + /// Vector. + /// Angle. + /// v rotated by theta degrees (anti-clockwise positive). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Rotate2DVec2(Vector2 v, float theta) + { + float x = v.x, y = v.y; + float cos = Mathf.Cos(theta * Mathf.Deg2Rad); + float sin = BDAMath.Sqrt(1 - cos * cos); + return new Vector2(x * cos - y * sin, x * sin + y * cos); + } + + /// + /// Rotates a Vector2 in 2D about a given point. + /// + /// Vector to rotate. + /// Point to rotate about. + /// Angle. + /// v rotated by theta degrees (anti-clockwise positive) about p. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Rotate2DVec2(Vector2 v, Vector2 p, float theta) + { + float x = v.x - p.x, y = v.y - p.y; + float cos = Mathf.Cos(theta); + float sin = BDAMath.Sqrt(1 - cos * cos); + return new Vector2(x * cos - y * sin + p.x, x * sin + y * cos + p.y); + } + + /// + /// Compute the 1-norm of a Vector3. + /// + /// The 1-norm. + public static float OneNorm(this Vector3 v) + { + return Mathf.Abs(v.x) + Mathf.Abs(v.y) + Mathf.Abs(v.z); + } + + /// + /// Round the Vector3 to the given unit. + /// + /// The unit to round to. + /// The modified Vector3. + public static Vector3 Round(this ref Vector3 v, float unit) + { + if (unit == 0) return v; + v.x = Mathf.Round(v.x / unit) * unit; + v.y = Mathf.Round(v.y / unit) * unit; + v.z = Mathf.Round(v.z / unit) * unit; + return v; + } + + /// + /// Non-modifying version of Vector3.Round. + /// + /// The unit to round to. + /// A new Vector3 rounded to the unit. + public static Vector3 Rounded(this Vector3 v, float unit) => v.Round(unit); + } +} diff --git a/BDArmory/Utils/VesselModuleRegistry.cs b/BDArmory/Utils/VesselModuleRegistry.cs new file mode 100644 index 000000000..59aa451e2 --- /dev/null +++ b/BDArmory/Utils/VesselModuleRegistry.cs @@ -0,0 +1,1229 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using System.Runtime.CompilerServices; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using BDArmory.UI; +using BDArmory.VesselSpawning; + +namespace BDArmory.Utils +{ + /// + /// A registry over all the asked for modules in all the asked for vessels. + /// The lists are automatically updated whenever needed. + /// Querying for a vessel or module that isn't yet in the registry causes the vessel or module to be added and tracked. + /// + /// This removes the need for each module to scan for such modules, which often causes GC allocations and performance losses. + /// The exception to this is that there is a race condition for functions triggering on the onVesselPartCountChanged event. + /// Other functions that trigger on onVesselPartCountChanged or onPartJointBreak events should call OnVesselModified first before performing their own actions. + /// + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class VesselModuleRegistry : MonoBehaviour + { + #region Fields + static public VesselModuleRegistry Instance; + static public Dictionary>> registry; + static public Dictionary updateModuleCallbacks; + public static readonly HashSet IgnoredVesselTypes = [VesselType.Debris, VesselType.SpaceObject]; + public static readonly HashSet ValidVesselTypes = [VesselType.Plane, VesselType.Ship, VesselType.Rover, VesselType.Lander, VesselType.Base]; // Valid vessel types for competitions. + static readonly HashSet ModuleTypesToSortByProximityToRoot = [ + typeof(BDModulePilotAI), + typeof(BDModuleSurfaceAI), + typeof(BDModuleVTOLAI), + typeof(BDModuleOrbitalAI), + typeof(MissileFire), + typeof(IBDAIControl) + ]; + + // Specialised registries to avoid the boxing/unboxing GC allocations on frequently used module types. + static public Dictionary> registryMissileFire; + static public Dictionary> registryMissileBase; + static public Dictionary> registryBDModulePilotAI; + static public Dictionary> registryBDModuleSurfaceAI; + static public Dictionary> registryIBDAIControl; + static public Dictionary> registryModuleWeapon; + static public Dictionary> registryIBDWeapon; + static public Dictionary> registryModuleEngines; + static public Dictionary> registryModuleIntakes; + static public Dictionary> registryModuleCommand; + static public Dictionary> registryKerbalSeat; + static public Dictionary> registryKerbalEVA; + static public Dictionary> registryRepulsorModule; + + // Named Modules (where the modules are only known by name, we don't actually have instances of them; they come from DLLs that aren't dependencies). + static public Dictionary>> registryNamedModuleParts; // Parts per vessel containing the named module. + + static Dictionary vesselPartCounts; + #endregion + + #region Monobehaviour methods + void Awake() + { + if (Instance != null) { Destroy(Instance); } + Instance = this; + + registry ??= []; + registryMissileFire ??= []; + registryMissileBase ??= []; + registryModuleWeapon ??= []; + registryIBDWeapon ??= []; + registryModuleEngines ??= []; + registryModuleIntakes ??= []; + registryBDModulePilotAI ??= []; + registryBDModuleSurfaceAI ??= []; + registryIBDAIControl ??= []; + registryModuleCommand ??= []; + registryKerbalSeat ??= []; + registryKerbalEVA ??= []; + registryRepulsorModule ??= []; + updateModuleCallbacks ??= []; + vesselPartCounts ??= []; + registryNamedModuleParts ??= []; + } + + void Start() + { + GameEvents.onVesselPartCountChanged.Add(OnVesselModifiedHandler); + GameEvents.onVesselLoaded.Add(OnVesselLoaded); + } + + void OnDestroy() + { + GameEvents.onVesselPartCountChanged.Remove(OnVesselModifiedHandler); + GameEvents.onVesselLoaded.Remove(OnVesselLoaded); + + registry.Clear(); + registryMissileFire.Clear(); + registryMissileBase.Clear(); + registryModuleWeapon.Clear(); + registryIBDWeapon.Clear(); + registryModuleEngines.Clear(); + registryModuleIntakes.Clear(); + registryBDModulePilotAI.Clear(); + registryBDModuleSurfaceAI.Clear(); + registryIBDAIControl.Clear(); + registryModuleCommand.Clear(); + registryKerbalSeat.Clear(); + registryKerbalEVA.Clear(); + registryRepulsorModule.Clear(); + registryNamedModuleParts.Clear(); + + updateModuleCallbacks.Clear(); + vesselPartCounts.Clear(); + } + #endregion + + #region Private methods + /// + /// Add a vessel to track to the registry. + /// + /// The vessel. + void AddVesselToRegistry(Vessel vessel) + { + registry.Add(vessel, []); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to registry."); + } + + /// + /// Add a module type to track to a vessel in the registry. + /// + /// The module type to track. + /// The vessel. + void AddVesselModuleTypeToRegistry(Vessel vessel) where T : class + { + if (!registry[vessel].ContainsKey(typeof(T))) + { + registry[vessel].Add(typeof(T), []); + updateModuleCallbacks[typeof(T)] = typeof(VesselModuleRegistry).GetMethod(nameof(UpdateVesselModulesInRegistry), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).MakeGenericMethod(typeof(T)); + } + } + + /// + /// Update the list of modules of the given type in the registry for the given vessel. + /// + /// The module type. + /// The vessel. + void UpdateVesselModulesInRegistry(Vessel vessel) where T : class + { + if (!registry.ContainsKey(vessel)) { AddVesselToRegistry(vessel); } + if (!registry[vessel].ContainsKey(typeof(T))) { AddVesselModuleTypeToRegistry(vessel); } + if (ModuleTypesToSortByProximityToRoot.Contains(typeof(T))) + { + if (typeof(T) == typeof(IBDAIControl)) // Specialisation due to IBDAI being an interface instead of a proper class. + { + var modules = vessel.FindPartModulesImplementing(); + registry[vessel][typeof(T)] = SortByProximityToRootIBDAI(ref modules).ConvertAll(m => m as UnityEngine.Object); + } + else + { + var modules = vessel.FindPartModulesImplementing().ConvertAll(m => m as PartModule); + registry[vessel][typeof(T)] = SortByProximityToRoot(ref modules).ConvertAll(m => m as UnityEngine.Object); + } + } + else { registry[vessel][typeof(T)] = vessel.FindPartModulesImplementing().ConvertAll(m => m as UnityEngine.Object); } + 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}."); + } + + /// + /// Add a named module type to track to a vessel in the named modules registry. + /// + /// The vessel. + /// The name of the module type to track. + void AddVesselNamedModuleTypeToRegistry(Vessel vessel, string moduleName) + { + if (!registryNamedModuleParts.ContainsKey(vessel)) registryNamedModuleParts.Add(vessel, []); + if (!registryNamedModuleParts[vessel].ContainsKey(moduleName)) { registryNamedModuleParts[vessel].Add(moduleName, []); } + } + + /// + /// Update the list of parts in the registry containing the named module type for the given vessel. + /// + /// The vessel. + /// The named module type. + void UpdateVesselModulesInNamedModuleRegistry(Vessel vessel, string moduleName) + { + if (!registryNamedModuleParts.ContainsKey(vessel) || !registryNamedModuleParts[vessel].ContainsKey(moduleName)) { AddVesselNamedModuleTypeToRegistry(vessel, moduleName); } + registryNamedModuleParts[vessel][moduleName] = vessel.Parts.Where(part => part.Modules.Contains(moduleName)).ToList(); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Registry entry for {vessel.vesselName} updated to have {registryNamedModuleParts[vessel][moduleName].Count} parts with modules of type {moduleName}."); + } + + /// + /// Update the registry entries when a tracked vessel gets modified. + /// + /// The vessel that was modified. + void OnVesselModifiedHandler(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return; + if (vesselPartCounts.ContainsKey(vessel) && vessel.Parts.Count == vesselPartCounts[vessel]) return; // Already done. + + var partsAdded = vesselPartCounts.ContainsKey(vessel) && vessel.Parts.Count > vesselPartCounts[vessel]; + vesselPartCounts[vessel] = vessel.Parts.Count; + + if (registry.ContainsKey(vessel)) + { + foreach (var moduleType in registry[vessel].Keys.ToList()) + { + if (!partsAdded && registry[vessel][moduleType].Count == 0) continue; // Part loss shouldn't give more modules. + // Invoke the specific callback to update the registry for this type of module. + updateModuleCallbacks[moduleType].Invoke(this, [vessel]); + } + } + + // Specialised registries. + if (registryMissileFire.ContainsKey(vessel) && (partsAdded || registryMissileFire[vessel].Count > 0)) + { + var missileFires = vessel.FindPartModulesImplementing(); + registryMissileFire[vessel] = SortByProximityToRoot(ref missileFires); + 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.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)) + { + var pilotAIModules = vessel.FindPartModulesImplementing(); + registryBDModulePilotAI[vessel] = SortByProximityToRoot(ref pilotAIModules); + 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)) + { + var surfaceAIModules = vessel.FindPartModulesImplementing(); + registryBDModuleSurfaceAI[vessel] = SortByProximityToRoot(ref surfaceAIModules); + 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)) + { + var IBDAIControls = vessel.FindPartModulesImplementing(); + registryIBDAIControl[vessel] = SortByProximityToRootIBDAI(ref IBDAIControls); + 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.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.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.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.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.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.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.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}."); + } + + // Named module registry. + if (registryNamedModuleParts.ContainsKey(vessel)) + { + foreach (var moduleName in registryNamedModuleParts[vessel].Keys.ToList()) + { + if (!partsAdded && registryNamedModuleParts[vessel][moduleName].Count == 0) continue; // Part loss shouldn't give more modules. + UpdateVesselModulesInNamedModuleRegistry(vessel, moduleName); + } + } + } + + public void OnVesselLoaded(Vessel vessel) + { + if (vessel == null || !registry.ContainsKey(vessel)) return; // If the vessel is null or isn't in the registry, ignore it. + OnVesselModified(vessel, true); // Force re-scanning the vessel. + } + #endregion + + #region Public methods + /// + /// Static interface to triggering the OnVesselModified handler. + /// + /// The vessel that was modified. + /// Update the registry even if the part count hasn't changed. + public static void OnVesselModified(Vessel vessel, bool force = false) + { + if (vessel == null || !vessel.loaded) return; + if (force) { vesselPartCounts[vessel] = -1; } + Instance.OnVesselModifiedHandler(vessel); + } + + /// + /// Get an enumerable over the modules of the specified type in the specified vessel. + /// This is about 15-30 times faster than FindPartModulesImplementing, but still requires around the same amount of GC allocations due to boxing/unboxing. + /// + /// The module type to get. + /// The vessel to get the modules from. + /// An enumerable for use in foreach loops or .ToList. + public static List GetModules(Vessel vessel) where T : class + { + if (vessel == null || !vessel.loaded) return []; // Return empty list. + + if (typeof(T) == typeof(MissileFire)) { return GetMissileFires(vessel) as List; } + if (typeof(T) == typeof(MissileBase)) { return GetMissileBases(vessel) as List; } + if (typeof(T) == typeof(BDModulePilotAI)) { return GetBDModulePilotAIs(vessel) as List; } + if (typeof(T) == typeof(IBDAIControl)) { return GetIBDAIControls(vessel) as List; } + if (typeof(T) == typeof(BDModuleSurfaceAI)) { return GetBDModuleSurfaceAIs(vessel) as List; } + if (typeof(T) == typeof(ModuleWeapon)) { return GetModuleWeapons(vessel) as List; } + if (typeof(T) == typeof(IBDWeapon)) { return GetIBDWeapons(vessel) as List; } + if (typeof(T) == typeof(ModuleEngines)) { return GetModuleEngines(vessel) as List; } + if (typeof(T) == typeof(ModuleResourceIntake)) { return GetModuleIntakes(vessel) as List; } + 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); } + + if (!registry[vessel].ContainsKey(typeof(T))) + { Instance.UpdateVesselModulesInRegistry(vessel); } + + return registry[vessel][typeof(T)].ConvertAll(m => m as T); + } + + /// + /// Get the first module of the specified type in the specified vessel. + /// + /// The module type. + /// The vessel. + /// The first module or the first non-null module (may still be null if none are found). + /// The first module if it exists, else null. + public static T GetModule(Vessel vessel, bool firstNonNull = false) where T : class + { + var modules = GetModules(vessel); + if (modules == null) return null; + if (!firstNonNull) return modules.FirstOrDefault(); + foreach (var module in modules) + { if (module != null) return module; } + return null; + } + + /// + /// Get the number of modules of the given type on the vessel. + /// + /// The module type. + /// The vessel. + /// The number of modules of that type on the vessel. + public static int GetModuleCount(Vessel vessel) where T : class + { + if (vessel == null || !vessel.loaded) return 0; + if (typeof(T) == typeof(MissileFire)) { return GetMissileFires(vessel).Count; } + if (typeof(T) == typeof(MissileBase)) { return GetMissileBases(vessel).Count; } + if (typeof(T) == typeof(BDModulePilotAI)) { return GetBDModulePilotAIs(vessel).Count; } + if (typeof(T) == typeof(BDModuleSurfaceAI)) { return GetBDModuleSurfaceAIs(vessel).Count; } + if (typeof(T) == typeof(IBDAIControl)) { return GetIBDAIControls(vessel).Count; } + if (typeof(T) == typeof(ModuleWeapon)) { return GetModuleWeapons(vessel).Count; } + if (typeof(T) == typeof(IBDWeapon)) { return GetIBDWeapons(vessel).Count; } + if (typeof(T) == typeof(ModuleEngines)) { return GetModuleEngines(vessel).Count; } + if (typeof(T) == typeof(ModuleResourceIntake)) { return GetModuleIntakes(vessel).Count; } + 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; + } + + /// + /// Get the number of parts containing the given named module on the vessel. + /// Notes: + /// Parts with multiple modules of the same type only count as 1. + /// Named modules are those that come from DLLs that we don't have a dependency on. + /// + /// The vessel. + /// The named module. + /// The number of parts containing the named module. + public static int GetModuleCount(Vessel vessel, string moduleName) + { + if (vessel == null || !vessel.loaded) return 0; + if (!registryNamedModuleParts.ContainsKey(vessel) || !registryNamedModuleParts[vessel].ContainsKey(moduleName)) { Instance.UpdateVesselModulesInNamedModuleRegistry(vessel, moduleName); } + return registryNamedModuleParts[vessel][moduleName].Count; + } + + /// + /// Get the list of parts on the vessel that contain the named module. + /// Note: Named modules are those that come from DLLs that we don't have a dependency on. + /// + /// The vessel. + /// The named module. + /// The list of parts containing the named module. + public static List GetModuleParts(Vessel vessel, string moduleName) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryNamedModuleParts.ContainsKey(vessel) || !registryNamedModuleParts[vessel].ContainsKey(moduleName)) { Instance.UpdateVesselModulesInNamedModuleRegistry(vessel, moduleName); } + return registryNamedModuleParts[vessel][moduleName]; + } + + /// + /// Clean out the registries and drop null vessels. + /// + public static void CleanRegistries() + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Cleaning registries."); + // General registry. + foreach (var vessel in registry.Keys.ToList()) { registry[vessel] = registry[vessel].Where(kvp => kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } // Remove empty module lists. + registry = registry.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + // Specialised registries. + registryMissileFire = registryMissileFire.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryMissileBase = registryMissileBase.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryBDModulePilotAI = registryBDModulePilotAI.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryBDModuleSurfaceAI = registryBDModuleSurfaceAI.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryIBDAIControl = registryIBDAIControl.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryModuleWeapon = registryModuleWeapon.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryIBDWeapon = registryIBDWeapon.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryModuleEngines = registryModuleEngines.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryModuleIntakes = registryModuleIntakes.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + 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. + // Named module registry. + registryNamedModuleParts = registryNamedModuleParts.Where(kvp => kvp.Key != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null vessel entries. We can't clear the empty entries as we want to know if there are none. + } + + /// + /// Sort a list of part modules by their proximity to the root part. + /// Note: we use OrderBy here to get a stable sort. + /// + /// + /// + public static List SortByProximityToRoot(ref List modules, Part root = null) where T : PartModule + { + if (modules.Count == 0) return modules; + if (root == null) root = modules.First().vessel.rootPart; + modules = [.. modules.OrderBy(m => ProximityToRoot(m.part, root))]; + return modules; + } + + /// + /// Specialisation for IBDAI due to it being an interface. + /// + /// + /// + /// + public static List SortByProximityToRootIBDAI(ref List modules, Part root = null) where T : IBDAIControl + { + if (modules.Count == 0) return modules; + if (root == null) root = modules.First().vessel.rootPart; + modules = [.. modules.OrderBy(m => ProximityToRoot(m.part, root))]; + return modules; + } + + /// + /// Get the proximity to the root part. + /// + /// + /// Proximity to the root part or int.MaxValue if no root part was found. + public static int ProximityToRoot(Part part, Part root) + { + int proximity = 0; + Part currentPart = part; + while (currentPart is not null && currentPart != root) + { + currentPart = currentPart.parent; + ++proximity; + } + if (currentPart is null) + return int.MaxValue; + return proximity; + } + + #region Specialised methods + // This would be much easier if C# implemented proper C++ style template specialisation. + // These specialised methods give an extra speed boost by avoiding the boxing/unboxing associated with storing the modules as objects in the main registry. + // They will be automatically used via the general method, but even more speed can be obtained by accessing them directly, particularly the ones returning a single item. + + public static List GetMissileFires(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryMissileFire.ContainsKey(vessel)) + { + var missileFires = vessel.FindPartModulesImplementing(); + registryMissileFire.Add(vessel, SortByProximityToRoot(ref missileFires)); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(MissileFire).Name} registry with {registryMissileFire[vessel].Count} modules."); + } + return registryMissileFire[vessel]; + } + public static MissileFire GetMissileFire(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetMissileFires(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryMissileFire.ContainsKey(vessel)) { return GetMissileFires(vessel).FirstOrDefault(); } + return registryMissileFire[vessel].FirstOrDefault(); + } + + public static List GetMissileBases(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryMissileBase.ContainsKey(vessel)) + { + registryMissileBase.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(MissileBase).Name} registry with {registryMissileBase[vessel].Count} modules."); + } + return registryMissileBase[vessel]; + } + public static MissileBase GetMissileBase(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetMissileBases(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryMissileBase.ContainsKey(vessel)) { return GetMissileBases(vessel).FirstOrDefault(); } + return registryMissileBase[vessel].FirstOrDefault(); + } + + public static List GetBDModulePilotAIs(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryBDModulePilotAI.ContainsKey(vessel)) + { + var pilotAIModules = vessel.FindPartModulesImplementing(); + registryBDModulePilotAI.Add(vessel, SortByProximityToRoot(ref pilotAIModules)); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(BDModulePilotAI).Name} registry with {registryBDModulePilotAI[vessel].Count} modules."); + } + return registryBDModulePilotAI[vessel]; + } + public static BDModulePilotAI GetBDModulePilotAI(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetBDModulePilotAIs(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryBDModulePilotAI.ContainsKey(vessel)) { return GetBDModulePilotAIs(vessel).FirstOrDefault(); } + return registryBDModulePilotAI[vessel].FirstOrDefault(); + } + + public static List GetBDModuleSurfaceAIs(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryBDModuleSurfaceAI.ContainsKey(vessel)) + { + var surfaceAIModules = vessel.FindPartModulesImplementing(); + registryBDModuleSurfaceAI.Add(vessel, SortByProximityToRoot(ref surfaceAIModules)); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(BDModuleSurfaceAI).Name} registry with {registryBDModuleSurfaceAI[vessel].Count} modules."); + } + return registryBDModuleSurfaceAI[vessel]; + } + public static BDModuleSurfaceAI GetBDModuleSurfaceAI(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetBDModuleSurfaceAIs(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryBDModuleSurfaceAI.ContainsKey(vessel)) { return GetBDModuleSurfaceAIs(vessel).FirstOrDefault(); } + return registryBDModuleSurfaceAI[vessel].FirstOrDefault(); + } + + public static List GetIBDAIControls(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryIBDAIControl.ContainsKey(vessel)) + { + var IBDAIControls = vessel.FindPartModulesImplementing(); + registryIBDAIControl.Add(vessel, SortByProximityToRootIBDAI(ref IBDAIControls)); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(IBDAIControl).Name} registry with {registryIBDAIControl[vessel].Count} modules."); + } + return registryIBDAIControl[vessel]; + } + public static IBDAIControl GetIBDAIControl(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetIBDAIControls(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryIBDAIControl.ContainsKey(vessel)) { return GetIBDAIControls(vessel).FirstOrDefault(); } + return registryIBDAIControl[vessel].FirstOrDefault(); + } + + public static List GetModuleWeapons(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryModuleWeapon.ContainsKey(vessel)) + { + registryModuleWeapon.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleWeapon).Name} registry with {registryModuleWeapon[vessel].Count} modules."); + } + return registryModuleWeapon[vessel]; + } + + public static List GetIBDWeapons(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryIBDWeapon.ContainsKey(vessel)) + { + registryIBDWeapon.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(IBDWeapon).Name} registry with {registryIBDWeapon[vessel].Count} modules."); + } + return registryIBDWeapon[vessel]; + } + + public static List GetModuleEngines(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryModuleEngines.ContainsKey(vessel)) + { + registryModuleEngines.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleEngines).Name} registry with {registryModuleEngines[vessel].Count} modules."); + } + return registryModuleEngines[vessel]; + } + + public static List GetModuleIntakes(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryModuleIntakes.ContainsKey(vessel)) + { + registryModuleIntakes.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleResourceIntake).Name} registry with {registryModuleIntakes[vessel].Count} modules."); + } + return registryModuleIntakes[vessel]; + } + public static List GetModuleCommands(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryModuleCommand.ContainsKey(vessel)) + { + registryModuleCommand.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleCommand).Name} registry with {registryModuleCommand[vessel].Count} modules."); + } + return registryModuleCommand[vessel]; + } + public static ModuleCommand GetModuleCommand(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetModuleCommands(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryModuleCommand.ContainsKey(vessel)) { return GetModuleCommands(vessel).FirstOrDefault(); } + return registryModuleCommand[vessel].FirstOrDefault(); + } + + public static List GetKerbalSeats(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryKerbalSeat.ContainsKey(vessel)) + { + registryKerbalSeat.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(KerbalSeat).Name} registry with {registryKerbalSeat[vessel].Count} modules."); + } + return registryKerbalSeat[vessel]; + } + public static KerbalSeat GetKerbalSeat(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetKerbalSeats(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryKerbalSeat.ContainsKey(vessel)) { return GetKerbalSeats(vessel).FirstOrDefault(); } + return registryKerbalSeat[vessel].FirstOrDefault(); + } + + public static List GetKerbalEVAs(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + if (!registryKerbalEVA.ContainsKey(vessel)) + { + registryKerbalEVA.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(KerbalEVA).Name} registry with {registryKerbalEVA[vessel].Count} modules."); + } + return registryKerbalEVA[vessel]; + } + public static KerbalEVA GetKerbalEVA(Vessel vessel, bool firstNonNull = false) + { + if (vessel == null || !vessel.loaded) return null; + if (firstNonNull) + { + foreach (var module in GetKerbalEVAs(vessel)) + { if (module != null) return module; } + return null; + } + if (!registryKerbalEVA.ContainsKey(vessel)) { return GetKerbalEVAs(vessel).FirstOrDefault(); } + return registryKerbalEVA[vessel].FirstOrDefault(); + } + public static List GetRepulsorModules(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return []; + 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 with {registryRepulsorModule[vessel].Count} modules."); + } + return registryRepulsorModule[vessel]; + } + #endregion + +#if DEBUG + public IEnumerator PerformanceTest() + { + var wait = new WaitForSeconds(0.1f); + { + // Note: this test has significant GC allocations due to the allocation of an intermediate list. + int count = 0; + int iters = 100000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { foreach (var mf in FlightGlobals.ActiveVessel.FindPartModulesImplementing()) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via vessel.FindPartModulesImplementing()"); + } + yield return wait; + { + int count = 0; + int iters = 100000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { if (FlightGlobals.ActiveVessel.FindPartModuleImplementing() != null) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via vessel.FindPartModuleImplementing()"); + } + yield return wait; + { + int count = 0; + int iters = 10000000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { foreach (var mf in VesselModuleRegistry.GetModules(FlightGlobals.ActiveVessel)) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via VesselModuleRegistry.GetModules(vessel)"); + } + yield return wait; + { + int count = 0; + int iters = 10000000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { foreach (var mf in VesselModuleRegistry.GetMissileFires(FlightGlobals.ActiveVessel)) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via VesselModuleRegistry.GetMissileFires(vessel)"); + } + yield return wait; + { + int count = 0; + int iters = 10000000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { if (VesselModuleRegistry.GetModule(FlightGlobals.ActiveVessel) != null) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via VesselModuleRegistry.GetModule(vessel)"); + } + yield return wait; + { + int count = 0; + int iters = 10000000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { if (VesselModuleRegistry.GetModule(FlightGlobals.ActiveVessel, true) != null) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via VesselModuleRegistry.GetModule(vessel, true)"); + } + yield return wait; + { + int count = 0; + int iters = 10000000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { if (VesselModuleRegistry.GetMissileFire(FlightGlobals.ActiveVessel) != null) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via VesselModuleRegistry.GetMissileFire(vessel)"); + } + yield return wait; + { + int count = 0; + int iters = 10000000; + var startTime = Time.realtimeSinceStartup; + for (int i = 0; i < iters; ++i) { if (VesselModuleRegistry.GetMissileFire(FlightGlobals.ActiveVessel, true) != null) ++count; } + Debug.Log($"DEBUG {FlightGlobals.ActiveVessel} has {count / iters} weapon managers, checked at {iters / (Time.realtimeSinceStartup - startTime)}/s via VesselModuleRegistry.GetMissileFire(vessel, true)"); + } + BDACompetitionMode.Instance.competitionStatus.Add("VesselModuleRegistry performance test complete."); + } + + public void DumpRegistriesFor(Vessel vessel) + { + if (registry.ContainsKey(vessel)) + { + foreach (var type in registry[vessel].Keys) + Debug.Log($"DEBUG {vessel.vesselName} has {registry[vessel][type].Count} {type} modules in the general registry"); + } + else { Debug.Log($"DEBUG {vessel.vesselName} isn't in the general registry"); } + + if (registryBDModulePilotAI.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} BDModulePilotAI special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryBDModuleSurfaceAI.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} BDModuleSurfaceAI special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryIBDAIControl.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} IBDAIControl special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryIBDWeapon.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} IBDWeapon special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryKerbalEVA.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} KerbalEVA special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryKerbalSeat.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} KerbalSeat special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryMissileBase.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} MissileBase special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryMissileFire.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} MissileFire special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryModuleCommand.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} ModuleCommand special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryModuleEngines.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} ModuleEngines special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryModuleIntakes.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} ModuleIntakes special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + if (registryModuleWeapon.ContainsKey(vessel)) { var moduleCount = GetModuleCount(vessel); var modules = vessel.FindPartModulesImplementing(); Debug.Log($"DEBUG {vessel.vesselName} has {moduleCount} ModuleWeapon special registry modules" + (modules.Count != moduleCount ? $", but {modules.Count} modules found" : "")); } + + if (registryNamedModuleParts.ContainsKey(vessel)) + { + foreach (var moduleName in registryNamedModuleParts[vessel].Keys) + Debug.Log($"DEBUG {vessel.vesselName} has {GetModuleCount(vessel, moduleName)} parts with module {moduleName} in the named module registry."); + } + else { Debug.Log($"DEBUG {vessel.vesselName} isn't in the named module registry"); } + } +#endif + #endregion + } + + /// + /// This class maintains an overview and control of which WM and AI modules are the primary ones controlling a vessel. + /// The primary AI is either the one that was most recently activated or the one closest to the root of the vessel. + /// + /// Usage tips: + /// 1. Access pattern for parts that need the primary WM every frame (about 6x faster than querying vessel.ActiveController().WM each time): + /// MissileFire WeaponManager + /// { + /// get + /// { + /// if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + /// _weaponManager = (vessel != null && vessel.loaded) ? vessel.ActiveController().WM : null; + /// if (_weaponManager != null && _weaponManager.vessel != vessel) _weaponManager = null; + /// return _weaponManager; + /// } + /// } + /// MissileFire _weaponManager; + /// Note: Take a local copy if accessing it repeatedly without the possibility of it changing. + /// Note: The secondary check is necessary if vessel is FlightGlobals.ActiveVessel due to the DeathCam switch delay while the vessel is being removed. + /// + /// 2. Access pattern for parts that need the primary AI every frame: + /// public IBDAIControl AI + /// { + /// get + /// { + /// if (_AI == null || !_AI.pilotEnabled || _AI.vessel != vessel) _AI = vessel.ActiveController().AI; + /// return _AI; + /// } + /// } + /// IBDAIControl _AI; + /// Note: Take a local copy if accessing it repeatedly without the possibility of it changing. + /// + /// 3. Accessing a field of the active AI, depending on the AI's type: + /// var ai = vessel.ActiveController().AI; + /// var myField = ai != null && ai.pilotEnabled ? ai.aiType switch + /// { + /// AIType.PilotAI => (ai as BDModulePilotAI).myField, + /// AIType.SurfaceAI => (ai as BDModuleSurfaceAI).myField, + /// AIType.VTOLAI => (ai as BDModuleVTOLAI).myField, + /// AIType.OrbitalAI => (ai as BDModuleOrbitalAI).myField, + /// _ => default + /// } : default; + /// + /// 4. Accessing the active AI as a specific type: + /// var ai = vessel.ActiveController().AI; + /// var pilotAI = ai != null && ai.pilotEnabled && ai.aiType == AIType.PilotAI ? ai as BDModulePilotAI : null; + /// + /// 5. Accessing the active AI as multiple types when switching on AI.aiType isn't appropriate: + /// var ai = vessel.ActiveController().AI; + /// BDModulePilotAI pilotAI = null; + /// BDModuleSurfaceAI surfaceAI = null; + /// BDModuleVTOLAI vtolAI = null; + /// BDModuleOrbitalAI orbitalAI = null; + /// if (ai != null && ai.pilotEnabled) switch(ai.aiType) + /// { + /// case AIType.PilotAI: pilotAI = ai as BDModulePilotAI; break; + /// case AIType.SurfaceAI: surfaceAI = ai as BDModuleSurfaceAI; break; + /// case AIType.VTOLAI: vtolAI = ai as BDModuleVTOLAI; break; + /// case AIType.OrbitalAI: orbitalAI = ai as BDModuleOrbitalAI; break; + /// } + /// + /// 6. Accessing the primary AI of a certain type (regardless of which type is active): + /// var pilotAI = vessel.ActiveController().PilotAI; + /// if (pilotAI && pilotAI.pilotEnabled) {} + /// + public class ActiveController : VesselModule + { + /// + /// Get the active controller vessel module for the vessel. + /// Note: there is an extension method vessel.GetActiveController(). + /// This is slightly faster than going via VesselModuleRegistry.GetMissileFire(vessel). + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActiveController GetActiveController(Vessel vessel) + { + if (vessel == null) return null; + if (!registry.ContainsKey(vessel)) + registry.Add(vessel, vessel.gameObject.GetComponent()); + return registry[vessel]; + } + + static Dictionary registry = []; + + public MissileFire WM { get; private set; } // Use this for accessing the primary WM. Use VesselModuleRegistry.GetMissileFires to get all WMs on a craft. + public IBDAIControl AI { get; private set; } // The active AI (if any are active) or the closest AI to the root. + public bool VesselNamingDeconflictionHasBeenApplied { get; set; } = false; // Whether vessel naming deconfliction has been applied to this vessel or not. + public string VesselName { get; set; } = null; // The vesselName of this vessel. This is to revert KSP's automatic renaming of vessels when we don't want it to. + + // Note: If using these below, check that ai.pilotEnabled is true to see if it's the active AI. + public BDModulePilotAI PilotAI { get; private set; } // The primary or most recently active pilot AI. + public BDModuleSurfaceAI SurfaceAI { get; private set; } // The primary or most recently active surface AI. + public BDModuleVTOLAI VTOLAI { get; private set; } // The primary or most recently active VTOL AI. + public BDModuleOrbitalAI OrbitalAI { get; private set; } // The primary or most recently active orbital AI. + + bool updateRequired = true; + public bool IsFighter = false; // Whether the vessel is a detached fighter. + + // Activate module on valid vessels during flight. + public override Activation GetActivation() => Vessel.vesselType == VesselType.SpaceObject ? Activation.Never : Activation.FlightScene; + + void UpdateModules() + { + if (!updateRequired) return; + + // Make sure the registry is up-to-date. + VesselModuleRegistry.OnVesselModified(Vessel); + + // Set only the closest WM to the root part as the active WM. + WM = VesselModuleRegistry.GetMissileFire(Vessel); + foreach (var wm in VesselModuleRegistry.GetMissileFires(Vessel)) + { + wm.IsPrimaryWM = wm == WM; + if (!wm.IsPrimaryWM) wm.ParentWM = WM; + } + + // Update the AIs. + // Find the primary of each type of AI: the first active one or the first one, sorted by proximity to the root. + // Then disable all but the overall primary (the first active primary in the order: pilot, surface, VTOL, orbital), reactivating it if necessary (as deactivating the others may have side effects). + PilotAI = VesselModuleRegistry.GetBDModulePilotAIs(Vessel).Where(ai => ai.pilotEnabled).FirstOrDefault(); // Select the first active one. + if (PilotAI == null) PilotAI = VesselModuleRegistry.GetBDModulePilotAI(Vessel); // Or default to the first one. + SurfaceAI = VesselModuleRegistry.GetBDModuleSurfaceAIs(Vessel).Where(ai => ai.pilotEnabled).FirstOrDefault(); + if (SurfaceAI == null) SurfaceAI = VesselModuleRegistry.GetBDModuleSurfaceAI(Vessel); + VTOLAI = VesselModuleRegistry.GetModules(Vessel).Where(ai => ai.pilotEnabled).FirstOrDefault(); + if (VTOLAI == null) VTOLAI = VesselModuleRegistry.GetModule(Vessel); + OrbitalAI = VesselModuleRegistry.GetModules(Vessel).Where(ai => ai.pilotEnabled).FirstOrDefault(); + if (OrbitalAI == null) OrbitalAI = VesselModuleRegistry.GetModule(Vessel); + UpdateAIModules(true); + + // Update the registry. + registry = registry.Where(kvp => kvp.Key != null && kvp.Key == kvp.Value.Vessel).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove any null or non-matching vessels. + + updateRequired = false; + if (BDArmorySettings.DEBUG_OTHER) + { + var vesselName = Vessel.GetName(); + if (string.IsNullOrEmpty(vesselName)) vesselName = "new vessel"; + Debug.Log($"[BDArmory.ActiveController]: ActiveController modules updated on {(string.IsNullOrEmpty(vesselName) ? Vessel.rootPart.partInfo.name : vesselName)} ({Vessel.persistentId}, {Vessel.vesselType}), WM: {WM != null}, PilotAI: {PilotAI != null}, SurfaceAI: {SurfaceAI != null}, VTOLAI: {VTOLAI != null}, OrbitalAI: {OrbitalAI != null}, AI: {AI}"); + } + LoadedVesselSwitcher.Instance.UpdateWMs(); // Flag that the WMs in the VS need refreshing. + } + + /// + /// Set AI to the first active AI in the order Pilot, Surface, VTOL, Orbital, otherwise the AI closest to the root part on the vessel. + /// AIs other than the primary one get deactivated. + /// In order to activate a lower priority AI, the higher priority ones need to be disabled first. SetActiveAI below takes care of this. + /// Reactivate the active AI in case deactivating others disables some stuff. + /// + public void UpdateAIModules(bool reactivate = false) + { + var vesselName = Vessel.GetName(); + if (string.IsNullOrEmpty(vesselName)) vesselName = "new vessel"; + if (PilotAI != null && PilotAI.pilotEnabled) AI = PilotAI; + else if (SurfaceAI != null && SurfaceAI.pilotEnabled) AI = SurfaceAI; + else if (VTOLAI != null && VTOLAI.pilotEnabled) AI = VTOLAI; + else if (OrbitalAI != null && OrbitalAI.pilotEnabled) AI = OrbitalAI; + else AI = VesselModuleRegistry.GetIBDAIControl(Vessel); + if (AI != null) // Then, deactivate any other AIs to avoid any control conflicts. + { + foreach (var ai in VesselModuleRegistry.GetIBDAIControls(Vessel)) + { + if (ai == null || ai == AI || !ai.pilotEnabled) continue; + ScreenMessages.PostScreenMessage($"Deactivating non-primary {ai.aiType} on {vesselName}", 3); + Debug.Log($"[BDArmory.ActiveController]: Deactivating non-primary {ai.aiType} ({ai.part.persistentId}) on {vesselName}"); + ai.DeactivatePilot(); + } + if (reactivate && AI.pilotEnabled) + { + AI.ActivatePilot(); // Reactivate the AI in case deactivating the others disabled any common stuff. + } + } + if (BDArmoryAIGUI.Instance != null) BDArmoryAIGUI.Instance.checkForAI = true; // Update the AI GUI on the next frame. + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Precalc, UpdateVesselType); // Reclassify the vessel if needed on the next frame. + } + + /// + /// Set a specific AI as the active one, disabling all the rest. + /// This sets the AI as the primary of this AI type so long as it remains active through any vessel modifications. + /// + /// + public void SetActiveAI(IBDAIControl ai) + { + if (ai == null) return; + if (ai.vessel != Vessel) return; + foreach (var otherAI in VesselModuleRegistry.GetIBDAIControls(Vessel)) + { + if (otherAI == ai) continue; + if (otherAI.pilotEnabled) otherAI.DeactivatePilot(); + } + switch (ai.aiType) // Switch the primary of this type of AI to this one. + { + case AIType.PilotAI: PilotAI = ai as BDModulePilotAI; break; + case AIType.SurfaceAI: SurfaceAI = ai as BDModuleSurfaceAI; break; + case AIType.VTOLAI: VTOLAI = ai as BDModuleVTOLAI; break; + case AIType.OrbitalAI: OrbitalAI = ai as BDModuleOrbitalAI; break; + } + ai.ActivatePilot(); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Precalc, UpdateVesselType); // Reclassify the vessel if needed on the next frame. + } + + /// + /// Update a vessel's type to match its AI (or lack thereof). + /// Vessels with a WM must be one of VesselModuleRegistry.ValidVesselTypes. + /// Note: unmanned probes are not considered a separate type from their manned equivalents. + /// + void UpdateVesselType() + { + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Precalc, UpdateVesselType); // Do it only once. + if (Vessel == null) return; + var origType = Vessel.vesselType; + Vessel.StripTypeFromName(); + if (AI != null) + { + switch (AI.aiType) + { + case AIType.PilotAI: + case AIType.VTOLAI: + Vessel.vesselType = VesselType.Plane; + break; + case AIType.OrbitalAI: + Vessel.vesselType = VesselType.Ship; + break; + case AIType.SurfaceAI: + switch ((AI as BDModuleSurfaceAI).SurfaceType) + { + case AIUtils.VehicleMovementType.Land: + case AIUtils.VehicleMovementType.Amphibious: + Vessel.vesselType = VesselType.Rover; + break; + case AIUtils.VehicleMovementType.Stationary: + Vessel.vesselType = VesselType.Lander; + break; + case AIUtils.VehicleMovementType.Water: + case AIUtils.VehicleMovementType.Submarine: + Vessel.vesselType = VesselType.Ship; + break; + } + break; + } + } + else if (WM != null) + { + Vessel.vesselType = VesselType.Base; // Fixed weapon emplacement. + } + if (BDArmorySettings.DEBUG_OTHER && origType != Vessel.vesselType) Debug.Log($"[BDArmory.ActiveController]: Reclassifying vessel type of {Vessel.GetName()} from {origType} to {Vessel.vesselType}."); + } + + /// + /// Set the craft file that all the WMs on this craft originate from. + /// This is set via BDA's spawner. Craft spawned otherwise won't have this set. + /// + /// The URL of the craft file. + public void SetSourceURL(string sourceURL) + { + foreach (var wm in VesselModuleRegistry.GetMissileFires(Vessel)) + wm.SourceVesselURL = sourceURL; + } + + /// + /// This is called whenever a new vessel is created (both spawning and undocking / parts falling off / firing missiles / etc.). + /// + public override void OnLoadVessel() + { + base.OnLoadVessel(); + GameEvents.onVesselPartCountChanged.Add(OnVesselPartCountChanged); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Precalc, UpdateModules); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.ObscenelyEarly, GetVesselName); + updateRequired = true; + UpdateModules(); + + if (WM != null) + { + // If the vessel detached from a craft that had an active WM/AI, then we should activate the current WM/AI (if it has one). + if (WM.ParentWM != null && WM.ParentWM.IsPrimaryWM) + { + // Set the vessels AI and WM state based on what the parent was doing. + // Note: we don't need to assign the team as doing so for the parent applies it to all WM on the vessel. + if (WM.guardMode) WM.ToggleGuardMode(); + if (WM.ParentWM.guardMode) WM.ToggleGuardMode(); + if (AI != null && WM.ParentWM.AI != null) + { + if (WM.ParentWM.AI.pilotEnabled) + { + AI.ActivatePilot(); + switch (WM.ParentWM.AI.currentCommand) + { + case PilotCommands.Free: + AI.ReleaseCommand(true, false); + break; + case PilotCommands.Attack: + AI.CommandAttack(WM.ParentWM.AI.commandGPS); + break; + case PilotCommands.FlyTo: // Not planning on attacking something, so just follow the leader. + case PilotCommands.Waypoints: // If the parent was running waypoints, then just follow them. + case PilotCommands.Follow: // Parent was following someone, we'll follow the parent as a sub-formation. + AI.CommandFollow(WM.ParentWM.wingCommander, WM.ParentWM.wingCommander.GetFreeWingIndex()); + break; + default: + Debug.LogError($"[BDArmory.VesselModuleRegistry]: Invalid PilotCommand!"); + break; + } + } + else AI.DeactivatePilot(); + } + if (BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting) + BDACompetitionMode.Instance.AddToCompetitionWhenReady(WM, false); // We've already set the AI/WM state, so don't go weapons-free when adding them to the competition. + IsFighter = true; // Detached craft are "fighters". + WM.CheckMissiles(); + WM.ParentWM = null; // Clear the parent WM at the end of frame in case the WM is not on the root part since losing the root part will trigger OnLoadVessel again. + } + } + } + + /// + /// This is called when parts fall off or when docking occurs. + /// + /// + void OnVesselPartCountChanged(Vessel vessel) + { + if (vessel != Vessel) return; + updateRequired = true; + if (WM != null && !string.IsNullOrEmpty(VesselName) && vessel.vesselName != VesselName) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ActiveController]: Reverting name change of {VesselName} ({vessel.persistentId}) from {vessel.vesselName}"); + vessel.vesselName = VesselName; + } + } + + // The vessel name of new vessels gets assigned during the ObscenelyEarly timing phase. + void GetVesselName() + { + VesselName = vessel.vesselName; + if (!string.IsNullOrEmpty(VesselName)) + { + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.ObscenelyEarly, GetVesselName); + } + } + + /// + /// Clean up the event handlers. + /// + public void RemoveHandlers() + { + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Precalc, UpdateModules); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.ObscenelyEarly, GetVesselName); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Precalc, UpdateVesselType); + GameEvents.onVesselPartCountChanged.Remove(OnVesselPartCountChanged); + } + + public override void OnUnloadVessel() + { + RemoveHandlers(); + base.OnUnloadVessel(); + } + + void OnDestroy() // Make sure stuff gets removed if the vessel module is destroyed without unloading the vessel (e.g., docking, fast quit, etc.). + { + RemoveHandlers(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Utils/VesselUtils.cs b/BDArmory/Utils/VesselUtils.cs new file mode 100644 index 000000000..646038529 --- /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.GetName()); + foreach (var missileFire in VesselModuleRegistry.GetMissileFires(v)) + { + PartExploderSystem.AddPartToExplode(missileFire.part); + ExplosionFx.CreateExplosion(missileFire.part.transform.position, 1f, explModelPath, explSoundPath, ExplosionSourceType.Other, 0, missileFire.part, sourceVelocity: v.Velocity()); + 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..b1e6d4ee0 --- /dev/null +++ b/BDArmory/VesselSpawning/CircularSpawning.cs @@ -0,0 +1,513 @@ +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; + } + + // Team spawn + if (teamSpawnCoroutine != null) + { + StopCoroutine(teamSpawnCoroutine); + teamSpawnCoroutine = 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, float refHeading = 0, 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, refHeading)); + } + + 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. + /// + /// Note: this is only called via the deprecated RemoteOrchestration SpawnStrategies. + /// + /// The spawn config to use. + public IEnumerator SpawnAllVesselsOnceAsCoroutine(CircularSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + SpawnUtils.ResetVesselNamingDeconfliction(); + 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(Path.GetFullPath(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(Path.GetFullPath(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.GetFullPath(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; + var spawnBody = FlightGlobals.Bodies[spawnConfig.worldIndex]; + var spawnInOrbit = spawnConfig.altitude >= spawnBody.MinSafeAltitude(); // Min safe orbital altitude + var withInitialVelocity = spawnAirborne && BDArmorySettings.VESSEL_SPAWN_INITIAL_VELOCITY; + var spawnPitch = (withInitialVelocity || spawnInOrbit) ? 0f : -80f; + 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 < 1000 ? $"{spawnConfig.altitude:G5}m" : $"{spawnConfig.altitude / 1000:G5}km")} ({(spawnInOrbit ? "in orbit" : spawnAirborne ? "airborne" : "landed")}){(spawnConfig.craftFiles.Count > 8 ? ", this may take some time..." : ".")}"); + #endregion + + yield return AcquireSpawnPoint(spawnConfig, 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 = new Func(() => + { // Uses https://www.movable-type.co.uk/scripts/latlong.html to calculate the GPS point at a distance of 100m along the requested heading from the spawn point. + // This loses accuracy near the poles, but is correct within ±89° latitude. + float delta = 100f / (float)FlightGlobals.currentMainBody.Radius; + float theta = Mathf.Deg2Rad * spawnConfig.refHeading; + float phi1 = Mathf.Deg2Rad * (float)spawnConfig.latitude; + float phi2 = Mathf.Asin(Mathf.Sin(phi1) * Mathf.Cos(delta) + Mathf.Cos(phi1) * Mathf.Sin(delta) * Mathf.Cos(theta)); + var lat = Mathf.Rad2Deg * phi2; + var lon = spawnConfig.longitude + Mathf.Rad2Deg * Mathf.Atan2(Mathf.Sin(theta) * Mathf.Sin(delta) * Mathf.Cos(phi1), Mathf.Cos(delta) - Mathf.Sin(phi1) * Mathf.Sin(phi2)); + var offset = FlightGlobals.currentMainBody.GetWorldSurfacePosition(lat, lon, FlightGlobals.currentMainBody.GetAltitude(spawnPoint)); + return (offset - spawnPoint).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + })(); + if (refDirection == Vector3.zero) refDirection = Vector3.forward; // This only happens within 0.02° of planetary poles (~100m). + var vesselSpawnConfigs = new List(); + if (spawnConfig.teamsSpecific == null) + { + foreach (var craftUrl in spawnConfig.craftFiles) + { + // Figure out spawn point and orientation + var heading = 360f * spawnedVesselCount / Mathf.Max(1, 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 (!spawnInOrbit && 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 + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67 && craftUrl.Contains(BDArmorySettings.PINATA_NAME)) + vesselSpawnConfigs.Add(new VesselSpawnConfig( + craftUrl, + position, + direction, + altitude: 25000, + pitch: spawnPitch, + airborne: true, + inOrbit: false, + reuseURLVesselName: (BDATournament.Instance.tournamentStatus == TournamentStatus.Running && !BDATournament.Instance.fullTeams) || TournamentCoordinator.Instance.IsRunning + )); + else + vesselSpawnConfigs.Add(new VesselSpawnConfig( + craftUrl, + position, + direction, + (float)spawnConfig.altitude, + spawnPitch, + spawnAirborne, + spawnInOrbit, + reuseURLVesselName: (BDATournament.Instance.tournamentStatus == TournamentStatus.Running && !BDATournament.Instance.fullTeams) || TournamentCoordinator.Instance.IsRunning + )); + } + } + 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 = (!spawnInOrbit && 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 (staggered similarly to formation and slightly spread depending on how closely starting to each other). + ++teamSpawnCount; + var position = teamSpawnPosition + + intraTeamSeparation * (teamSpawnCount % 2 == 1 ? -teamSpawnCount / 2 : teamSpawnCount / 2) * spreadDirection + + intraTeamSeparation / 3f * (team.Count / 2 - teamSpawnCount / 2) * facingDirection; + var individualFacingDirection = Quaternion.AngleAxis((teamSpawnCount % 2 == 1 ? -teamSpawnCount / 2 : teamSpawnCount / 2) * 200f / (20f + intraTeamSeparation), radialUnitVector) * facingDirection; + vesselSpawnConfigs.Add(new VesselSpawnConfig( + craftUrl, + position, + individualFacingDirection, + (float)spawnConfig.altitude, + spawnPitch, + spawnAirborne, + spawnInOrbit, + reuseURLVesselName: (BDATournament.Instance.tournamentStatus == TournamentStatus.Running && !BDATournament.Instance.fullTeams) || TournamentCoordinator.Instance.IsRunning + )); + ++spawnedVesselCount; + } + ++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 SpawnUtils.SpawnedVesselURLs.Keys) + SpawnUtils.originalTeams[vesselName] = Path.GetFileName(Path.GetDirectoryName(SpawnUtils.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 SpawnUtils.SpawnedVesselURLs.Keys) + SpawnUtils.originalTeams[vesselName] = Path.GetFileNameWithoutExtension(SpawnUtils.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; + } + } + } + } + + // Revert back to the KSP's proper camera. + SpawnUtils.RevertSpawnLocationCamera(true); + + yield return PostSpawnMainSequence(spawnConfig, spawnAirborne, withInitialVelocity); + if (spawnFailureReason != SpawnFailureReason.None) + { + LogMessage("Vessel spawning FAILED! " + spawnFailureReason); + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + 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); + + 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, float refHeading = 0, 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, refHeading)); + } + 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) + { + SpawnUtils.ResetVesselNamingDeconfliction(); + 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; + SpawnUtils.ResetVesselNamingDeconfliction(); + // 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 + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/ContinuousSpawning.cs b/BDArmory/VesselSpawning/ContinuousSpawning.cs new file mode 100644 index 000000000..4c1bc62be --- /dev/null +++ b/BDArmory/VesselSpawning/ContinuousSpawning.cs @@ -0,0 +1,622 @@ +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 (spawnVesselsContinuouslyCoroutine != null) + { + StopCoroutine(spawnVesselsContinuouslyCoroutine); + spawnVesselsContinuouslyCoroutine = null; + if (vesselsSpawningContinuously) + { + if (BDACompetitionMode.Instance != null) BDACompetitionMode.Instance.StopCompetition(); + continuousSpawningScores.Clear(); + LogMessage("Continuous vessel spawning cancelled."); + } + } + vesselsSpawningContinuously = false; + } + + public override void PreSpawnInitialisation(SpawnConfig spawnConfig) + { + base.PreSpawnInitialisation(spawnConfig); + + vesselsSpawningContinuously = true; + vesselsSpawning = true; + spawnFailureReason = SpawnFailureReason.None; // Reset the spawn failure reason. + continuousSpawningScores = new Dictionary(); + RecomputeScores(); + 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(preSpawn: true); // Reset competition scores. + SpawnUtilsInstance.Instance.gunGameProgress.Clear(); // Clear gun-game progress. + ScoreWindow.SetMode(ScoreWindow.Mode.ContinuousSpawn, Toggle.Off); + } + + protected override void ResetInternals() + { + base.ResetInternals(); + SpawnUtils.ResetVesselNamingDeconfliction(); + currentlySpawning.Clear(); + craftURLToVesselName.Clear(); + continuousSpawnedVesselCount = 0; + currentlySpawningCount = 0; + } + + public void SpawnVesselsContinuously(CircularSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + LogMessage($"[BDArmory.VesselSpawner]: Triggering continuous vessel spawning at {spawnConfig.latitude:G6}, {spawnConfig.longitude:G6} on {FlightGlobals.Bodies[spawnConfig.worldIndex].name}, with altitude {spawnConfig.altitude:0}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; + // 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.GetFullPath(Path.Combine(AutoSpawnPath, spawnConfig.folder)), "*.craft").ToList(); + if (spawnConfig.craftFiles.Count == 0) + { + LogMessage("Vessel spawning: found no craft files in " + Path.GetFullPath(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 spawnBody = FlightGlobals.Bodies[spawnConfig.worldIndex]; + var spawnInOrbit = spawnConfig.altitude >= spawnBody.MinSafeAltitude(); // Min safe orbital altitude + 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 < 1000 ? $"{spawnConfig.altitude:G5}m" : $"{spawnConfig.altitude / 1000:G5}km")}{(spawnConfig.craftFiles.Count > 8 ? ", this may take some time..." : ".")}"); + else + LogMessage($"Spawning {Math.Min(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS, spawnConfig.craftFiles.Count)} of {spawnConfig.craftFiles.Count} vessels at an altitude of {(spawnConfig.altitude < 1000 ? $"{spawnConfig.altitude:G5}m" : $"{spawnConfig.altitude / 1000:G5}km")} with rolling-spawning."); + #endregion + + yield return AcquireSpawnPoint(spawnConfig, spawnDistance, true); + if (spawnFailureReason != SpawnFailureReason.None) + { + vesselsSpawningContinuously = false; + yield break; + } + + #region Spawning + ResetInternals(); + Vector3 craftSpawnPosition; + 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); + Queue spawnQueue = []; + Queue craftToSpawn = []; + double currentUpdateTick; + bool hasSetTeamColours = BDTISettings.STORE_TEAM_COLORS; + var sufficientCraftTimer = Time.time; + 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. + { + // 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) && // Still has lives + !spawnQueue.Contains(craftURL) && !currentlySpawning.Contains(craftURL) && ( // Not already in the queue or currently spawning + !craftURLToVesselName.ContainsKey(craftURL) || // Hasn't spawned yet + ( + BDACompetitionMode.Instance.Scores.ScoreData.TryGetValue(craftURLToVesselName[craftURL], out var sd) && sd.deathTime >= 0 && // Has died + !LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Any(wm => wm != null && wm.vessel != null && wm.SourceVesselURL == craftURL) // Fighters have died + ) + ))) + { + 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.Count; // Just count the number of teams. + 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 after the final grace period. + { + if (Time.time - sufficientCraftTimer > BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD) + { + LogMessage("Spawn queue is empty and not enough vessels are active, ending competition.", false); + BDACompetitionMode.Instance.StopCompetition(); + if ((BDArmorySettings.AUTO_RESUME_TOURNAMENT || BDArmorySettings.AUTO_RESUME_CONTINUOUS_SPAWN) && BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT && TournamentAutoResume.Instance != null) + { + TournamentAutoResume.AutoQuit(5); + var message = "Quitting KSP in 5s due to reaching the end of a tournament."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogWarning("[BDArmory.BDATournament]: " + message); + } + break; + } + } + else sufficientCraftTimer = Time.time; + {// Perform a "bubble shuffle" (randomly swap pairs of craft moving through the queue). + List shufflePool = [], shuffleSelection = []; + Queue bubbleShuffleQueue = new(); + while (spawnQueue.Count > 0) + { + shufflePool.Add(spawnQueue.Dequeue()); // Take craft from the spawn queue. + if (shufflePool.Count > 1) // Use a pool of size 2 for shuffling. + { + shufflePool.Shuffle(); + // Prioritise craft that have had fewer spawns/deaths. + int fewestSpawns = shufflePool.Min(craftUrl => spawnCounts[craftUrl]); + shuffleSelection = shufflePool.Where(craftUrl => spawnCounts[craftUrl] == fewestSpawns).ToList(); + string selected = shuffleSelection.First(); + bubbleShuffleQueue.Enqueue(selected); + shufflePool.Remove(selected); + } + } + foreach (var craft in shufflePool) bubbleShuffleQueue.Enqueue(craft); // Add any remaining craft in the shuffle pool. + while (bubbleShuffleQueue.Count > 0) spawnQueue.Enqueue(bubbleShuffleQueue.Dequeue()); // Re-insert the craft into the spawn queue from the bubble shuffle queue. + } + while (craftToSpawn.Count + currentlySpawningCount + currentlyActive < spawnSlots.Count && spawnQueue.Count > 0) + craftToSpawn.Enqueue(spawnQueue.Dequeue()); +#if DEBUG + 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) && v.ActiveController().WM != null).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); + } + } +#endif + if (craftToSpawn.Count > 0) + { + VesselModuleRegistry.CleanRegistries(); // Clean out any old entries. + yield return new WaitWhileFixed(() => LoadedVesselSwitcher.Instance.currentVesselDied); // Wait for the death camera to finish so we don't cause lag for it, then give it an extra second. + yield return new WaitForSecondsFixed(1); + + // 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; + if (BDArmorySettings.VESSEL_SPAWN_CS_FOLLOWS_CENTROID) // Allow the spawn point to drift, but bias it back to the original spawn point. + { + var vessels = LoadedVesselSwitcher.Instance.Vessels.Values.SelectMany(v => v).Where(v => v != null).ToList(); + foreach (var vessel in vessels) spawnPoint += vessel.CoM; + spawnPoint /= 1 + vessels.Count; + radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + spawnPoint += (spawnConfig.altitude - BodyUtils.GetRadarAltitudeAtPos(spawnPoint)) * radialUnitVector; // Reset the altitude to the desired spawn altitude. + } + 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. + // Configure vessel spawn configs + foreach (var craftURL in craftToSpawn) + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"Spawning vessel from {craftURL.Substring(AutoSpawnPath.Length - AutoSpawnFolder.Length)} for the {spawnCounts[craftURL]}{spawnCounts[craftURL] switch { 1 => "st", 2 => "nd", 3 => "rd", _ => "th" }} time.", true); + 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; + StartCoroutine(SpawnCraft(new VesselSpawnConfig( + craftURL, + craftSpawnPosition, + direction, + (float)spawnConfig.altitude, + pitch: -80f, + airborne: true, + inOrbit: spawnInOrbit, + teamIndex: 0, + reuseURLVesselName: true + ))); + } + craftToSpawn.Clear(); // Clear the queue since we just spawned all those vessels. + } + if (vesselsSpawning) // Wait for the initial spawn to be ready before letting CameraTools take over. + { + yield return new WaitWhileFixed(() => currentlySpawningCount > 0); + vesselsSpawning = false; + if (!hasSetTeamColours) + { + BDTISetup.Instance.ResetColors(); + hasSetTeamColours = true; + } + } + + // 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); + } + + readonly List currentlySpawning = []; + Dictionary craftURLToVesselName = []; + IEnumerator SpawnCraft(VesselSpawnConfig vesselSpawnConfig) + { + ++currentlySpawningCount; + currentlySpawning.Add(vesselSpawnConfig.craftURL); + // Spawn vessel + yield return SpawnSingleVessel(vesselSpawnConfig); + if (spawnFailureReason != SpawnFailureReason.None) + { + currentlySpawning.Remove(vesselSpawnConfig.craftURL); + --currentlySpawningCount; + yield break; + } + var vessel = GetSpawnedVesselsName(vesselSpawnConfig.craftURL); + if (vessel == null) + { + currentlySpawning.Remove(vesselSpawnConfig.craftURL); + --currentlySpawningCount; + yield break; + } + + // Perform post-spawn stuff. + yield return PostSpawnMainSequence(vessel, true, BDArmorySettings.VESSEL_SPAWN_INITIAL_VELOCITY, false); + if (spawnFailureReason != SpawnFailureReason.None) + { + currentlySpawning.Remove(vesselSpawnConfig.craftURL); + --currentlySpawningCount; + yield break; + } + + // Spawning went fine. Time to blow stuff up! + BDACompetitionMode.Instance.AddToActiveCompetition(vessel); + + currentlySpawning.Remove(vesselSpawnConfig.craftURL); + craftURLToVesselName = SpawnUtils.SpawnedVesselURLs.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); // Update the vesselName-to-craftURL dictionary for the latest spawn. + --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 int cumulativePartsLostToAsteroids = 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); + continuousSpawningScores[vesselName].cumulativePartsLostToAsteroids = scoreData.Sum(kvp => kvp.Value.partsLostToAsteroids); + 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.Count == 0) return; + foreach (var vesselName in continuousSpawningScores.Keys) + UpdateCompetitionScores(continuousSpawningScores[vesselName].vessel); + RecomputeScores(); // Update the scores for the score window. + if (BDArmorySettings.DEBUG_COMPETITION) 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.Where(kvp => kvp.Value.deathTime >= 0).Select(kvp => $"{kvp.Key}:{kvp.Value.aliveState}:{kvp.Value.deathTime: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 Asteroids + var partsLostToAsteroids = string.Join(", ", scoreData.Where(kvp => kvp.Value.partsLostToAsteroids > 0).Select(kvp => $"{kvp.Key}:{kvp.Value.partsLostToAsteroids}")); + if (!string.IsNullOrEmpty(partsLostToAsteroids)) logStrings.Add($"[BDArmory.VesselSpawner:{BDACompetitionMode.Instance.CompetitionID}]: PARTSLOSTTOASTEROIDS: {partsLostToAsteroids}"); + #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.GetFullPath(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(); + ContinuousSpawningScores score; + foreach (var vesselName in continuousSpawningScores.Keys) + { + score = continuousSpawningScores[vesselName]; + var vessel = score.vessel; + if (vessel == null) continue; // Vessel hasn't been respawned yet. + var weaponManager = vessel.ActiveController().WM; + 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); + } + } + } + + #region Scoring (in-game) + public static readonly Dictionary defaultWeights = new() + { + {"Clean Kills", 3f}, + {"Assists", 1.5f}, + {"Deaths", -1f}, + {"Hits", 0.004f}, + {"Bullet Damage", 0.0001f}, + {"Bullet Damage Taken", 4e-05f}, + {"Rocket Hits", 0.01f}, + {"Rocket Parts Hit", 0.0005f}, + {"Rocket Damage", 0.0001f}, + {"Rocket Damage Taken", 4e-05f}, + {"Missile Hits", 0.15f}, + {"Missile Parts Hit", 0.002f}, + {"Missile Damage", 3e-05f}, + {"Missile Damage Taken", 1.5e-05f}, + {"Ram Score", 0.075f}, + {"Parts Lost To Asteroids", 0f}, + // FIXME Add tag fields? + }; + public static Dictionary weights = new(defaultWeights); + + public static void SaveWeights() + { + ConfigNode fileNode = ConfigNode.Load(ScoreWindow.scoreWeightsURL) ?? new ConfigNode(); + + if (!fileNode.HasNode("CtsScoreWeights")) + { + fileNode.AddNode("CtsScoreWeights"); + } + + ConfigNode settings = fileNode.GetNode("CtsScoreWeights"); + + foreach (var kvp in weights) + { + settings.SetValue(kvp.Key, kvp.Value.ToString(), true); + } + fileNode.Save(ScoreWindow.scoreWeightsURL); + } + + public static void LoadWeights() + { + ConfigNode fileNode = ConfigNode.Load(ScoreWindow.scoreWeightsURL); + if (fileNode == null || !fileNode.HasNode("CtsScoreWeights")) return; + ConfigNode settings = fileNode.GetNode("CtsScoreWeights"); + + foreach (var key in weights.Keys.ToList()) + { + if (!settings.HasValue(key)) continue; + + object parsedValue = BDAPersistentSettingsField.ParseValue(typeof(float), settings.GetValue(key), key); + if (parsedValue != null) + { + weights[key] = (float)parsedValue; + } + } + } + + public List<(string, int, float)> Scores { get; private set; } = []; // Name, deaths, score + /// + /// Update the scores for the score window based on the score weights. + /// This is called whenever the cts-*.log file is dumped or whenever the weights are changed. + /// This should give the same scores as the parse_CS_log_files.py script for the most recent cts-*.log file. + /// + public void RecomputeScores() + { + Scores.Clear(); + foreach (var player in continuousSpawningScores.Keys) + { + var (deaths, score) = ComputeScore(player, continuousSpawningScores); + Scores.Add((player, deaths, score)); + } + Scores.Sort((a, b) => b.Item3.CompareTo(a.Item3)); + } + (int, float) ComputeScore(string player, Dictionary data) + { + AliveState[] specialKills = [AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal]; // Clean kill types. + GMKillReason[] gmKillReasons = [GMKillReason.BigRedButton, GMKillReason.GM, GMKillReason.OutOfAmmo]; // GM kill reasons not to count as assists. + float score = 0; + score += weights["Clean Kills"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Where(sd => specialKills.Contains(sd.aliveState) && sd.lastPersonWhoDamagedMe == player).Count()); + score += weights["Assists"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Where(sd => sd.aliveState == AliveState.AssistedKill && !gmKillReasons.Contains(sd.gmKillReason) && (sd.damageFromGuns.GetValueOrDefault(player) > 0 || sd.damageFromRockets.GetValueOrDefault(player) > 0 || sd.damageFromMissiles.GetValueOrDefault(player) > 0 || sd.rammingPartLossCounts.GetValueOrDefault(player) > 0)).Count()); + int deaths = data[player].scoreData.Values.Where(sd => sd.deathTime >= 0).Count(); + score += weights["Deaths"] * deaths; + score += weights["Hits"] * data[player].scoreData.Values.Sum(sd => sd.hits); + score += weights["Bullet Damage"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.damageFromGuns.GetValueOrDefault(player))); + score += weights["Bullet Damage Taken"] * data[player].scoreData.Values.Sum(sd => sd.damageFromGuns.Values.Sum()); + score += weights["Rocket Hits"] * data[player].scoreData.Values.Sum(sd => sd.rocketStrikes); + score += weights["Rocket Parts Hit"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.rocketPartDamageCounts.GetValueOrDefault(player))); + score += weights["Rocket Damage"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.damageFromRockets.GetValueOrDefault(player))); + score += weights["Rocket Damage Taken"] * data[player].scoreData.Values.Sum(sd => sd.damageFromRockets.Values.Sum()); + score += weights["Missile Hits"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.missileHitCounts.GetValueOrDefault(player))); + score += weights["Missile Parts Hit"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.missilePartDamageCounts.GetValueOrDefault(player))); + score += weights["Missile Damage"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.damageFromMissiles.GetValueOrDefault(player))); + score += weights["Missile Damage Taken"] * data[player].scoreData.Values.Sum(sd => sd.damageFromMissiles.Values.Sum()); + score += weights["Ram Score"] * data.Where(other => other.Key != player).Sum(other => other.Value.scoreData.Values.Sum(sd => sd.rammingPartLossCounts.GetValueOrDefault(player))); + score += weights["Parts Lost To Asteroids"] * data[player].scoreData.Values.Sum(sd => sd.partsLostToAsteroids); + return (deaths, score); + } + #endregion + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/CustomSpawnTemplate.cs b/BDArmory/VesselSpawning/CustomSpawnTemplate.cs new file mode 100644 index 000000000..6ba4bc804 --- /dev/null +++ b/BDArmory/VesselSpawning/CustomSpawnTemplate.cs @@ -0,0 +1,1125 @@ +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 = []; + bool startCompetitionAfterSpawning = false; + protected override void Awake() + { + base.Awake(); + if (Instance != null) Destroy(Instance); + Instance = this; + + LoadTemplate(customSpawnConfig?.name, 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() + { + if (customSpawnConfig != null && customSpawnConfig.includeCraftURLs) RestoreCraftURLsFromCache(); // Reinstate the cached craft URLs prior to saving so we don't overwrite with whatever the current values are. + 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(); + var spawnAirborne = spawnConfig.altitude > 10f; + var spawnBody = FlightGlobals.Bodies[spawnConfig.worldIndex]; + var spawnInOrbit = spawnConfig.altitude >= spawnBody.MinSafeAltitude(); // Min safe orbital altitude + var withInitialVelocity = spawnAirborne && BDArmorySettings.VESSEL_SPAWN_INITIAL_VELOCITY; + var spawnPitch = withInitialVelocity ? 0f : -80f; + LogMessage($"Spawning {spawnConfig.craftFiles.Count} vessels at an altitude of {(spawnConfig.altitude < 1000 ? $"{spawnConfig.altitude:G5}m" : $"{spawnConfig.altitude / 1000:G5}km")} ({(spawnInOrbit ? "in orbit" : spawnAirborne ? "airborne" : "landed")})."); + #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, + spawnPitch, + spawnAirborne, + spawnInOrbit, + customVesselSpawnConfig.teamIndex, + reuseURLVesselName: BDATournament.Instance.tournamentStatus == TournamentStatus.Running || TournamentCoordinator.Instance.IsRunning, + crew: 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, spawnAirborne, withInitialVelocity, !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) + 1; ++i) + teamVesselNames.Add(spawnedVesselsTeamIndex.Where(kvp => kvp.Value == i).Select(kvp => kvp.Key).ToList()); + bool useOriginalTeamNames = spawnConfig.assignTeams && (spawnConfig.numberOfTeams == 1 || spawnConfig.numberOfTeams == -1); // Flag to use per file / per team organisation. + if (useOriginalTeamNames) + { + if (spawnConfig.numberOfTeams == 1) // Folders + { + foreach (var vesselName in SpawnUtils.SpawnedVesselURLs.Keys) + SpawnUtils.originalTeams[vesselName] = Path.GetFileName(Path.GetDirectoryName(SpawnUtils.SpawnedVesselURLs[vesselName])); + } + else // Files as folders + { + foreach (var vesselName in SpawnUtils.SpawnedVesselURLs.Keys) + SpawnUtils.originalTeams[vesselName] = Path.GetFileNameWithoutExtension(SpawnUtils.SpawnedVesselURLs[vesselName]); + } + } + if (BDArmorySettings.VESSEL_SPAWN_SMART_REASSIGN_TEAMS && spawnConfig.assignTeams && spawnConfig.numberOfTeams == 11) + { + HashSet teamNames = []; + SpawnUtils.originalTeams.Clear(); + foreach (var team in teamVesselNames) + { + foreach (var vesselName in team) + { + if (!spawnedVessels.ContainsKey(vesselName)) continue; + var wm = spawnedVessels[vesselName].ActiveController().WM; + if (wm == null) continue; + if (wm.Team.Name.Length < 2) continue; // If it's a one-letter name, ignore it. + if (teamNames.Contains(wm.Team.Name)) continue; // The team name already exists, ignore it. + teamNames.Add(wm.Team.Name); // Found a valid non-default team name that isn't already taken. + foreach (var otherVesselName in team) // Set all the craft in this team to this team name. + SpawnUtils.originalTeams[otherVesselName] = wm.Team.Name; + break; // Go to the next team. + } + } + // Do a MassTeamSwitch without using original teams to give everyone A, B, ... + LoadedVesselSwitcher.Instance.MassTeamSwitch(true, false, null, teamVesselNames); + useOriginalTeamNames = true; // Next call of MassTeamSwitch with originalTeams=true to override the defaults with the custom team names. + } + LoadedVesselSwitcher.Instance.MassTeamSwitch(true, useOriginalTeamNames, 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 static CustomSpawnConfig customSpawnConfig = null; + static List> cachedCustomVesselSpawnConfigURLs; + static void StoreCraftURLsToCache() + { + if (customSpawnConfig != null && customSpawnConfig.includeCraftURLs) + cachedCustomVesselSpawnConfigURLs = customSpawnConfig.customVesselSpawnConfigs?.Select(team => team.Select(member => member.craftURL).ToList()).ToList(); + else + cachedCustomVesselSpawnConfigURLs = null; + } + static void RestoreCraftURLsFromCache() + { + if (cachedCustomVesselSpawnConfigURLs == null || customSpawnConfig.customVesselSpawnConfigs == null) return; + using var cachedURLTeam = cachedCustomVesselSpawnConfigURLs.GetEnumerator(); + using var configTeam = customSpawnConfig.customVesselSpawnConfigs.GetEnumerator(); + while (cachedURLTeam.MoveNext() && configTeam.MoveNext()) + { + using var cachedURLMember = cachedURLTeam.Current.GetEnumerator(); + using var configMember = configTeam.Current.GetEnumerator(); + while (cachedURLMember.MoveNext() && configMember.MoveNext()) + configMember.Current.craftURL = cachedURLMember.Current; + } + } + /// + /// 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 static void LoadTemplate(string templateName = null, bool fromDisk = false) + { + if (fromDisk) // Reload the templates from disk. + CustomSpawnTemplateField.Load(); + else if (templateName != null && templateName == customSpawnConfig.name) + { + if (Instance) Instance.RefreshSelectedCrew(); + if (customSpawnConfig.includeCraftURLs) CustomSpawnTemplateField.Load(); // We need to get the craft URLs from disk again. + else 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 + ), + [] + ); + } + if (Instance) Instance.RefreshSelectedCrew(); + StoreCraftURLsToCache(); + if (customSpawnConfig.includeCraftURLs) Instance.PopulateEntriesFromConfig(customSpawnConfig); + } + + /// + /// 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. + bool includedCraftURLs = customSpawnConfig.includeCraftURLs; // If the previously saved template included craft URLs, update them. + 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( + 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; + } + PopulateEntriesFromLVS(); // Populate the slots to show the layout. + if (!customSpawnConfigs.Contains(customSpawnConfig)) customSpawnConfigs.Add(customSpawnConfig); // Add the template if it isn't already there. + if (includedCraftURLs) + { + SaveCraftToTemplate(); // Update the craft URLs and cached copy before saving. + } + else + { + cachedCustomVesselSpawnConfigURLs = null; + CustomSpawnTemplateField.Save(); + } + } + + /// + /// Save the currently populated craft slots as part of the template. + /// + public void SaveCraftToTemplate() + { + // If at least one slot is filled, add craftURLs to the template, otherwise remove the craftURLs from the template. + customSpawnConfig.includeCraftURLs = customSpawnConfig.customVesselSpawnConfigs.Any(team => team.Any(member => !string.IsNullOrEmpty(member.craftURL))); + // Store a cached copy so that we can reinstate it before saving if any changes are made before then. + StoreCraftURLsToCache(); + CustomSpawnTemplateField.Save(); + } + + /// + /// 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(); + cachedCustomVesselSpawnConfigURLs = null; + + // 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 + ), + [] + ); + 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() + { + static int distanceToRoot(Part p) { return p.parent != null ? distanceToRoot(p.parent) : 0; } + + if (CustomCraftBrowserDialog.shipNames.Count == 0) + { + craftBrowser = new CustomCraftBrowserDialog(); + craftBrowser.UpdateList(); + } + 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; + } + } + } + } + + /// + /// Populate the spawn slots from a custom spawn config with existing craftURL values. + /// + /// The custom spawn config to populate the entries from. + public void PopulateEntriesFromConfig(CustomSpawnConfig customSpawnConfig) + { + foreach (var team in customSpawnConfig.customVesselSpawnConfigs) + foreach (var member in team) + { + if (string.IsNullOrEmpty(member.craftURL)) continue; + if (CustomCraftBrowserDialog.shipNames.ContainsKey(member.craftURL)) continue; // Already exists. + var craftMeta = $"{Path.GetFileNameWithoutExtension(member.craftURL)}.loadmeta"; + var craftProfile = new CraftProfileInfo(); + if (File.Exists(craftMeta) && File.GetLastWriteTime(craftMeta) > File.GetLastWriteTime(member.craftURL)) // If the loadMeta file exists and has a timestamp that's later than the craft file (because WTF KSP‽), use it, otherwise generate one. + { + craftProfile.LoadFromMetaFile(craftMeta); + } + else + { + var craftNode = ConfigNode.Load(member.craftURL); + craftProfile.LoadDetailsFromCraftFile(craftNode, member.craftURL); + if (File.Exists(craftMeta)) // If the file existed, but was out of date, update it. Otherwise, don't to avoid polluting folders with .loadmeta files. + craftProfile.SaveToMetaFile(craftMeta); + } + CustomCraftBrowserDialog.shipNames.Add(member.craftURL, craftProfile.shipName); + } + CustomTemplateSpawning.customSpawnConfig.customVesselSpawnConfigs = customSpawnConfig.customVesselSpawnConfigs; + } + + /// + /// 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.Max(BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 2f); + customSpawnConfig.killEverythingFirst = true; + customSpawnConfig.numberOfTeams = BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS; + + this.startCompetitionAfterSpawning = startCompetitionAfterSpawning; + return true; + } + + #endregion + + #region UI + void OnGUI() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (!BDArmorySetup.GAME_UI_ENABLED) return; + var guiMatrix = GUI.matrix; + if (showTemplateSelection) + { + if (Event.current.type == EventType.MouseDown && !templateSelectionWindowRect.Contains(Event.current.mousePosition)) + HideTemplateSelection(); + else + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) { GUI.matrix = guiMatrix; GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, templateSelectionWindowRect.position); } + 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 + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) { GUI.matrix = guiMatrix; GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, crewSelectionWindowRect.position); } + crewSelectionWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), crewSelectionWindowRect, CrewSelectionWindow, StringUtils.Localize("#LOC_BDArmory_VesselMover_CrewSelection"), BDArmorySetup.BDGuiSkin.window); + } + } + if (showVesselSelection) + { + if (Event.current.type == EventType.MouseDown && !vesselSelectionWindowRect.Contains(Event.current.mousePosition)) + HideVesselSelection(); + else + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) { GUI.matrix = guiMatrix; GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, vesselSelectionWindowRect.position); } + vesselSelectionWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), vesselSelectionWindowRect, VesselSelectionWindow, StringUtils.Localize("#LOC_BDArmory_VesselMover_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 + BDArmorySettings.UI_SCALE_ACTUAL * 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(0, 0, 600, 800); + Vector2 vesselSelectionScrollPos = default; + string selectionFilter = ""; + bool focusFilterField = false; + bool folderSelectionMode = false; // Show SPH/VAB and folders instead of craft files. + List FilteredCraft + { + get + { + if (_filteredCraft.Item1 != selectionFilter || _filteredCraft.Item2 < craftBrowser.craftListUpdateTimestamp) + { + // Something changed, update the filtered list. + _filteredCraft.Item1 = selectionFilter; + _filteredCraft.Item2 = craftBrowser.craftListUpdateTimestamp; + _filteredCraft.Item3 = [.. craftBrowser.craftList.Where(kvp => kvp.Key != null && kvp.Value != null && kvp.Value.shipName.ToLower().Contains(selectionFilter.ToLower())).Select(kvp => kvp.Key)]; + } + return _filteredCraft.Item3; + } + } + (string, float, List) _filteredCraft = ("", 0, []); + + CustomCraftBrowserDialog craftBrowser; + GUIStyle ButtonStyle = new(CustomCraftBrowserDialog.ButtonStyle); + GUIStyle InfoStyle = new(CustomCraftBrowserDialog.InfoStyle); + public static 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(); + ButtonStyle.fontSize = 18; + InfoStyle.fontSize = 12; + } + vesselSelectionWindowRect.position = position + BDArmorySettings.UI_SCALE_ACTUAL * 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; + craftBrowser.CheckCurrent(); + 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.MaxWidth(vesselSelectionWindowRect.width - 15), GUILayout.MaxHeight(vesselSelectionWindowRect.height - 60)); + if (folderSelectionMode) + { + GUILayout.BeginHorizontal(); + if (GUILayout.Button("SPH", CustomCraftBrowserDialog.ButtonStyle, GUILayout.Height(80))) craftBrowser.ChangeFolder(EditorFacility.SPH); + if (GUILayout.Button("VAB", CustomCraftBrowserDialog.ButtonStyle, GUILayout.Height(80))) craftBrowser.ChangeFolder(EditorFacility.VAB); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.Label(craftBrowser.DisplayFolder, CustomCraftBrowserDialog.LabelStyle, GUILayout.Height(50), GUILayout.ExpandWidth(true)); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_Select"), CustomCraftBrowserDialog.ButtonStyle, GUILayout.Height(50), GUILayout.MaxWidth(vesselSelectionWindowRect.width / 3))) folderSelectionMode = false; + GUILayout.EndHorizontal(); + foreach (var folder in craftBrowser.subfolders) + { + if (GUILayout.Button($"{folder}", CustomCraftBrowserDialog.ButtonStyle, GUILayout.MaxHeight(60))) + { + craftBrowser.ChangeFolder(craftBrowser.Facility, folder); + break; // The enumerator can't continue since subfolders has changed. + } + } + } + else + { + foreach (var vesselURL in FilteredCraft) + { + if (!craftBrowser.craftList.TryGetValue(vesselURL, out var vesselInfo)) continue; // This shouldn't happen. + GUILayout.BeginHorizontal(); // Vessel buttons + if (GUILayout.Button($"{vesselInfo.shipName}", ButtonStyle, GUILayout.MaxHeight(48), GUILayout.Width(vesselSelectionWindowRect.width - 240))) + { + 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(VesselMover.Instance.VesselInfoEntry(vesselURL, vesselInfo, false), InfoStyle, GUILayout.Width(142)); + GUILayout.Label(craftBrowser.craftThumbnails.GetValueOrDefault(vesselURL), InfoStyle, GUILayout.Height(48), GUILayout.Width(48)); + GUILayout.EndHorizontal(); + } + } + GUILayout.EndScrollView(); + 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.BeginHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Clear"), BDArmorySetup.BDGuiSkin.button)) + { + currentVesselSpawnConfig.craftURL = null; + HideVesselSelection(); + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_CraftBrowser_ClearAll"), BDArmorySetup.BDGuiSkin.button)) + { + foreach (var team in customSpawnConfig.customVesselSpawnConfigs) + foreach (var member in team) + member.craftURL = null; + } + if (GUILayout.Button(folderSelectionMode ? StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Craft") : StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Folder"), folderSelectionMode ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle, GUILayout.Width(vesselSelectionWindowRect.width / 6))) + { + folderSelectionMode = !folderSelectionMode; + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Refresh"), BDArmorySetup.BDGuiSkin.button, GUILayout.Width(vesselSelectionWindowRect.width / 6))) + { + craftBrowser.UpdateList(); + } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUIUtils.RepositionWindow(ref vesselSelectionWindowRect); + GUIUtils.UpdateGUIRect(vesselSelectionWindowRect, _vesselGUICheckIndex); + GUIUtils.UseMouseEventInRect(vesselSelectionWindowRect); + if (bringVesselSelectionToFront) + { + bringVesselSelectionToFront = false; + GUI.BringWindowToFront(windowID); + } + } + + #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 + BDArmorySettings.UI_SCALE_ACTUAL * 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_CraftBrowser_Clear"), BDArmorySetup.BDGuiSkin.button)) + { + SelectedCrewMembers.Remove(currentVesselSpawnConfig.kerbalName); + currentVesselSpawnConfig.kerbalName = null; + HideCrewSelection(); + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_CraftBrowser_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_CraftBrowser_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 (spawnTemplate.includeCraftURLs && !string.IsNullOrEmpty(member.craftURL)) + { + memberNode.AddValue("craftURL", member.craftURL); + } + } + } + } + + 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 = []; + 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)) + ), + [] + ); + 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; + var config = 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 (memberNode.HasValue("craftURL")) // Check the memberNode for a craftURL. + { // If we find one, flag the config as being craft specific. + config.craftURL = (string)ParseField(memberNode, "craftURL", typeof(string)); + customSpawnConfig.includeCraftURLs = true; + } + team.Add(config); + } + 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..2a5f6db24 --- /dev/null +++ b/BDArmory/VesselSpawning/SingleVesselSpawning.cs @@ -0,0 +1,114 @@ +using UnityEngine; +using System.Collections; +using System.IO; +using System.Linq; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Settings; +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); + + // This is only used by the deprecated RemoteOrchestration SpawnStrategies + public override IEnumerator Spawn(SpawnConfig spawnConfig) + { + if (spawnConfig.craftFiles == null || spawnConfig.craftFiles.Count == 0) + { + var spawnFolder = Path.GetFullPath(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. + SpawnUtils.ResetVesselNamingDeconfliction(); + } + + public IEnumerator SpawnVessel(string craftUrl, double latitude, double longitude, double altitude, float initialHeading = 90f, float initialPitch = 0f) + { + // Convert the parameters to a VesselSpawnConfig. + var spawnBody = FlightGlobals.currentMainBody; + var terrainAltitude = spawnBody.TerrainAltitude(latitude, longitude); + var spawnPoint = spawnBody.GetWorldSurfacePosition(latitude, longitude, terrainAltitude + altitude); + var radialUnitVector = (spawnPoint - spawnBody.transform.position).normalized; + var north = VectorUtils.GetNorthVector(spawnPoint, spawnBody); + var direction = (Quaternion.AngleAxis(initialHeading, radialUnitVector) * north).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + var airborne = altitude > 10; + var spawnInOrbit = altitude >= spawnBody.MinSafeAltitude(); // Min safe orbital altitude + var withInitialVelocity = airborne && BDArmorySettings.VESSEL_SPAWN_INITIAL_VELOCITY; + VesselSpawnConfig vesselSpawnConfig = new VesselSpawnConfig( + craftUrl, + spawnPoint, + direction, + (float)altitude, + initialPitch, + airborne, + spawnInOrbit, + reuseURLVesselName: BDATournament.Instance.tournamentStatus == TournamentStatus.Running || TournamentCoordinator.Instance.IsRunning + ); + + // 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, withInitialVelocity); + 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; + } + + if (!airborne) SpawnUtils.AirborneActivation(vessel, false); // Activate ground-spawned craft (air-spawned craft are already active). + BDACompetitionMode.Instance.AddToActiveCompetition(vessel); + } + + 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..4c966b452 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnConfig.cs @@ -0,0 +1,154 @@ +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) + { + worldIndex = other.worldIndex; + latitude = other.latitude; + longitude = other.longitude; + altitude = other.altitude; + killEverythingFirst = other.killEverythingFirst; + assignTeams = other.assignTeams; + numberOfTeams = other.numberOfTeams; + teamCounts = other.teamCounts; + teamsSpecific = other.teamsSpecific; + folder = other.folder; + 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(string craftURL, Vector3 position, Vector3 direction, float altitude, float pitch, bool airborne, bool inOrbit, int teamIndex = 0, bool reuseURLVesselName = false, bool deconflictVesselName = true, List crew = null) + { + public string craftURL = craftURL; // The craft file. + public Vector3 position = position; // World-space coordinates (x,y,z) to place the vessel once spawned (before adjusting for terrain altitude). + public Vector3 direction = direction; // Direction to point the plane horizontally (i.e., heading). + public float altitude = altitude; // Altitude above terrain / water to adjust spawning position to. + public float pitch = pitch; // Pitch if spawning airborne. + public bool airborne = airborne; // Whether the vessel should be spawned in an airborne configuration or not. + public bool inOrbit = inOrbit; // Whether the vessel should be spawned in orbit or not (overrides airborne). + public int teamIndex = teamIndex; // Index for team assignment. + public bool reuseURLVesselName = reuseURLVesselName; // Reuse the vesselName for the same craftURL (for continuous spawning / tournaments). + public bool deconflictVesselName = deconflictVesselName; // Apply vessel name deconfliction during spawning for consistent and unique vessel naming. + public List crew = crew?.ToList(); // Override the crew. + public EditorFacility editorFacility = EditorFacility.SPH; // Which editorFacility the craft belongs to (found out during spawning). + } + + /// + /// 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, float refHeading = 0) : base(spawnConfig) + { + this.distance = distance; + this.absDistanceOrFactor = absDistanceOrFactor; + this.refHeading = refHeading; + } + public CircularSpawnConfig(CircularSpawnConfig other) : base(other) + { + this.distance = other.distance; + this.absDistanceOrFactor = other.absDistanceOrFactor; + this.refHeading = other.refHeading; + } + public CircularSpawnConfig(int worldIndex, double latitude, double longitude, double altitude, float distance, bool absDistanceOrFactor, float refHeading = 0, 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, refHeading) { } // 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. + public float refHeading; // Reference heading for the first craft. + } + + /// + /// 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; + } + /// + /// Note: this only makes a shallow copy of customVesselSpawnConfigs. + /// + /// + public CustomSpawnConfig(CustomSpawnConfig other) : base(other) + { + name = other.name; + customVesselSpawnConfigs = other.customVesselSpawnConfigs?.Select(config => config?.ToList()).ToList(); + includeCraftURLs = other.includeCraftURLs; + } + public string name; + public List> customVesselSpawnConfigs; + public bool includeCraftURLs = false; + public override string ToString() => $"{{name: {name}, worldIndex: {worldIndex}, lat: {latitude:F3}, lon: {longitude:F3}, alt: {altitude:F0}, URLs: {includeCraftURLs}; {(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/VesselSpawning/SpawnLocations.cs b/BDArmory/VesselSpawning/SpawnLocations.cs new file mode 100644 index 000000000..8e595055f --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnLocations.cs @@ -0,0 +1,218 @@ +using System; +using System.IO; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; +using BDArmory.Settings; + +namespace BDArmory.VesselSpawning +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class SpawnLocations : MonoBehaviour + { + private static SpawnLocations Instance; + + // Interesting spawn locations on Kerbin. + [VesselSpawnerField] public static bool UpdateSpawnLocations = true; + [VesselSpawnerField] public static List spawnLocations; + + void Awake() + { + if (Instance != null) + Destroy(Instance); + Instance = this; + + VesselSpawnerField.Load(); + } + + void OnDestroy() + { + VesselSpawnerField.Save(); + } + } + + public class SpawnLocation + { + public static string oldSpawnLocationsCfg = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/spawn_locations.cfg")); + public static string spawnLocationsCfg = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/spawn_locations.cfg")); + + public string name; + public Vector2d location; + public int worldIndex; + + public SpawnLocation(string _name, Vector2d _location, int _worldIndex) { name = _name; location = _location; worldIndex = _worldIndex; } + public override string ToString() { return name + "; " + location.ToString("G6") + "; " + worldIndex.ToString(); } + } + + [AttributeUsage(AttributeTargets.Field)] + public class VesselSpawnerField : Attribute + { + public VesselSpawnerField() { } + //static Dictionary defaultLocations = new Dictionary{ + static List defaultLocations = new List{ + new SpawnLocation("KSC", new Vector2d(-0.04762, -74.8593), 1), + new SpawnLocation("Inland KSC", new Vector2d(20.5939, -146.567), 1), + new SpawnLocation("Desert Runway", new Vector2d(-6.44958, -144.038), 1), + new SpawnLocation("Kurgan's spot", new Vector2d(-28.4595, -9.15156), 1), + new SpawnLocation("Alpine Lake", new Vector2d(-23.48, 119.83), 1), + new SpawnLocation("Big Canyon", new Vector2d(6.97865, -170.804), 1), + new SpawnLocation("Bowl 1", new Vector2d(35.6559, -77.4941), 1), + new SpawnLocation("Bowl 2", new Vector2d(3.8744, -78.0039), 1), + new SpawnLocation("Bowl 3", new Vector2d(0.268284, -80.5195), 1), + new SpawnLocation("Bowl 4", new Vector2d(-2.962, 179.91), 1), + new SpawnLocation("Bowl 5", new Vector2d(47.16, 134.08), 1), + new SpawnLocation("Canyon", new Vector2d(-52.7592, -4.71081), 1), + new SpawnLocation("Colorado", new Vector2d(41.715, 82.29), 1), + new SpawnLocation("Crater Isle", new Vector2d(8.159, 179.65), 1), + new SpawnLocation("Crater Lake", new Vector2d(-18.86, 66.47), 1), + new SpawnLocation("Crater Sea", new Vector2d(7.213, -177.34), 1), + new SpawnLocation("East Peninsula", new Vector2d(-1.57, -39.12), 1), + new SpawnLocation("Great Lake", new Vector2d(-31.958, 81.654), 1), + new SpawnLocation("Half-pipe", new Vector2d(-21.1388, 72.6437), 1), + new SpawnLocation("Ice field", new Vector2d(80.3343, -32.0119), 1), + new SpawnLocation("Kermau-Sur-Mer", new Vector2d(33.911, -172.26), 1), + new SpawnLocation("Land Bridge", new Vector2d(-48.055, 13.33), 1), + new SpawnLocation("Lonely Mt", new Vector2d(24.48, -116.444), 1), + new SpawnLocation("Manley Delta", new Vector2d(39.0705, -136.193), 1), + new SpawnLocation("Manley Valley", new Vector2d(45.6, -137.3), 1), + 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.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.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), + new SpawnLocation("Ice Floe 1", new Vector2d(-73.0986, -114.983), 1), + new SpawnLocation("Ice Floe 2", new Vector2d(-71.0594, 60.3108), 1), + new SpawnLocation("Joolian Skies", new Vector2d(0.05096, -74.8016), 8), + new SpawnLocation("Great Sea", new Vector2d(0.05096, -74.8016), 9), + new SpawnLocation("Impact Basin", new Vector2d(15.667, -65.1566), 9), + new SpawnLocation("Crater Cove", new Vector2d(34.7921, 161.095), 9), + new SpawnLocation("Battle Pond", new Vector2d(1.33667, 150.643), 9), + new SpawnLocation("Tri-Eye Isle", new Vector2d(5.16308, -169.655), 9), + new SpawnLocation("Bayou", new Vector2d(33.306, -130.172), 5), + new SpawnLocation("Poison Pond", new Vector2d(33.3616, -67.2242), 5), + new SpawnLocation("Crater Isle", new Vector2d(6.03858, 2.62539), 5), + new SpawnLocation("Sunken Crater", new Vector2d(23.1178, -42.8307), 5), + new SpawnLocation("Bowl 1", new Vector2d(36.0241, 105.294), 5), + new SpawnLocation("Polar Bowl", new Vector2d(45.9978, 115.843), 6), + new SpawnLocation("Grand Canyon", new Vector2d(9.32963, 167.071), 6), + new SpawnLocation("Grand Canal", new Vector2d(-0.08521, -60.6124), 6), + new SpawnLocation("Polar Bowl 2", new Vector2d(-53.655, -32.4155), 6), + }; + public static void Save() + { + ConfigNode fileNode = ConfigNode.Load(SpawnLocation.spawnLocationsCfg); + if (fileNode == null) + fileNode = new ConfigNode(); + if (!fileNode.HasNode("Config")) + fileNode.AddNode("Config"); + + ConfigNode settings = fileNode.GetNode("Config"); + foreach (var field in typeof(SpawnLocations).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly)) + { + if (field == null || !field.IsDefined(typeof(VesselSpawnerField), false)) continue; + if (field.Name == "spawnLocations") 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("BDASpawnLocations")) + fileNode.AddNode("BDASpawnLocations"); + + ConfigNode spawnLocations = fileNode.GetNode("BDASpawnLocations"); + + spawnLocations.ClearValues(); + foreach (var spawnLocation in SpawnLocations.spawnLocations) + spawnLocations.AddValue("LOCATION", spawnLocation.ToString()); + + if (!Directory.GetParent(SpawnLocation.spawnLocationsCfg).Exists) + { Directory.GetParent(SpawnLocation.spawnLocationsCfg).Create(); } + var success = fileNode.Save(SpawnLocation.spawnLocationsCfg); + if (success && File.Exists(SpawnLocation.oldSpawnLocationsCfg)) // Remove the old settings if it exists and the new settings were saved. + { File.Delete(SpawnLocation.oldSpawnLocationsCfg); } + } + + public static void Load() + { + ConfigNode fileNode = ConfigNode.Load(SpawnLocation.spawnLocationsCfg); + if (fileNode == null) + { + fileNode = ConfigNode.Load(SpawnLocation.oldSpawnLocationsCfg); // Try the old location. + } + SpawnLocations.spawnLocations = new List(); + if (fileNode != null) + { + if (fileNode.HasNode("Config")) + { + ConfigNode settings = fileNode.GetNode("Config"); + foreach (var field in typeof(SpawnLocations).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly)) + { + if (field == null || !field.IsDefined(typeof(VesselSpawnerField), false)) continue; + if (field.Name == "spawnLocations") continue; // We'll do the spawn locations separately. + if (!settings.HasValue(field.Name)) continue; + object parsedValue = ParseValue(field.FieldType, settings.GetValue(field.Name), field.Name); + if (parsedValue != null) + { + field.SetValue(null, parsedValue); + } + } + } + + if (fileNode.HasNode("BDASpawnLocations")) + { + ConfigNode settings = fileNode.GetNode("BDASpawnLocations"); + foreach (var spawnLocation in settings.GetValues("LOCATION")) + { + var parsedValue = (SpawnLocation)ParseValue(typeof(SpawnLocation), spawnLocation, "SpawnLocation"); + if (parsedValue != null) + { + SpawnLocations.spawnLocations.Add(parsedValue); + } + } + } + } + + // Add defaults if they're missing and we're not instructed not to. + if (SpawnLocations.UpdateSpawnLocations) + { + foreach (var location in defaultLocations.ToList()) + if (!SpawnLocations.spawnLocations.Select(l => l.name).ToList().Contains(location.name)) + SpawnLocations.spawnLocations.Add(location); + } + } + + public static object ParseValue(Type type, string value, string what) + { + try + { + if (type == typeof(SpawnLocation)) + { + string[] parts; + if (!value.Contains(';')) parts = value.Split(new char[] { ',' }, 2); // Old spawn location format. + else parts = value.Split(new char[] { ';' }); // New spawn location format. + if (parts.Length > 1) + { + var name = (string)ParseValue(typeof(string), parts[0], "SpawnLocation Name"); + var location = (Vector2d)ParseValue(typeof(Vector2d), parts[1], "SpawnLocation Coords"); + var worldIndex = parts.Length > 2 ? (int)ParseValue(typeof(int), parts[2], "SpawnLocation World Index") : 1; // Default to Kerbin for upgrading old spawn locations. + if (name != null && location != null) + return new SpawnLocation(name, location, worldIndex); + } + } + else return BDAPersistentSettingsField.ParseValue(type, value, what); + } + catch (Exception e) + { + Debug.LogException(e); + } + Debug.LogError("[BDArmory.SpawnLocations]: Failed to parse settings field of type " + type + " and value " + value); + return null; + } + } +} diff --git a/BDArmory/VesselSpawning/SpawnStrategies/CircularSpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/CircularSpawnStrategy.cs new file mode 100644 index 000000000..39ef0bf18 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnStrategies/CircularSpawnStrategy.cs @@ -0,0 +1,67 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition.RemoteOrchestration; +using BDArmory.Settings; + +namespace BDArmory.VesselSpawning.SpawnStrategies +{ + public class CircularSpawnStrategy : SpawnStrategy + { + private VesselSource vesselSource; + private List vesselIds; + private int bodyIndex; + private double latitude; + private double longitude; + private double altitude; + private float radius; + private bool success = false; + + public CircularSpawnStrategy(VesselSource vesselSource, List vesselIds, int bodyIndex, double latitude, double longitude, double altitude, float radius) + { + this.vesselSource = vesselSource; + this.vesselIds = vesselIds; + this.bodyIndex = bodyIndex; + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.radius = radius; + } + + 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 + CircularSpawnConfig spawnConfig = new CircularSpawnConfig( + new SpawnConfig( + bodyIndex, + latitude, + longitude, + altitude, + true, + craftFiles: new List(craftUrls) + ), + radius, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + BDArmorySettings.VESSEL_SPAWN_REF_HEADING + ); + yield return spawner.Spawn(spawnConfig); + + if (!spawner.vesselSpawnSuccess) + { + Debug.Log("[BDArmory.BDAScoreService] Vessel spawning failed."); + yield break; + } + + success = true; + } + + public bool DidComplete() + { + return success; + } + } +} diff --git a/BDArmory/VesselSpawning/SpawnStrategies/ListSpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/ListSpawnStrategy.cs new file mode 100644 index 000000000..6d586e488 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnStrategies/ListSpawnStrategy.cs @@ -0,0 +1,31 @@ +using System.Collections; +using System.Collections.Generic; + +namespace BDArmory.VesselSpawning.SpawnStrategies +{ + public class ListSpawnStrategy : SpawnStrategy + { + private List strategies; + private bool success = false; + + public ListSpawnStrategy(List strategies) + { + this.strategies = strategies; + } + + public bool DidComplete() + { + return success; + } + + public IEnumerator Spawn(VesselSpawnerBase spawner) + { + success = false; + foreach (var item in strategies) + { + yield return item.Spawn(spawner); + } + success = true; + } + } +} diff --git a/BDArmory/VesselSpawning/SpawnStrategies/PointSpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/PointSpawnStrategy.cs new file mode 100644 index 000000000..73f21efe1 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnStrategies/PointSpawnStrategy.cs @@ -0,0 +1,60 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Settings; + +namespace BDArmory.VesselSpawning.SpawnStrategies +{ + public class PointSpawnStrategy : SpawnStrategy + { + private string craftUrl; + private double latitude, longitude, altitude; + private float heading, pitch; + private bool success = false; + + public PointSpawnStrategy(string craftUrl, double latitude, double longitude, double altitude, float heading, float pitch = -0.7f) + { + this.craftUrl = craftUrl; + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.heading = heading; + this.pitch = pitch; + } + + public IEnumerator Spawn(VesselSpawnerBase spawner) + { + Debug.Log("[BDArmory.BDAScoreService] PointSpawnStrategy spawning."); + + // TODO: support body targeting; fixed as Kerbin for now + var worldIndex = FlightGlobals.GetBodyIndex(FlightGlobals.GetBodyByName("Kerbin")); + + // spawn the given craftUrl at the given location/heading/pitch + // yield return spawner.SpawnVessel(craftUrl, latitude, longitude, altitude, heading, pitch); + + // AUBRANIUM, in order to make the VesselSpawner abstract class fit with the way you've defined the SpawnStrategy interface, I found it necessary to shoe-horn the single craft spawning in like this. + // This is far from optimal and really needs a better solution. + // 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, false, false, 0, null, null, "", new List{craftUrl})); + + // wait for spawner to finish + yield return new WaitWhile(() => spawner.vesselsSpawning); + + if (!spawner.vesselSpawnSuccess) + { + Debug.Log("[BDArmory.BDAScoreService] PointSpawnStrategy failed."); + yield break; + } + + success = true; + } + + public bool DidComplete() + { + return success; + } + } +} diff --git a/BDArmory/VesselSpawning/SpawnStrategies/SpawnConfigStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/SpawnConfigStrategy.cs new file mode 100644 index 000000000..13f312a70 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnStrategies/SpawnConfigStrategy.cs @@ -0,0 +1,34 @@ +using System.Collections; +using UnityEngine; + +namespace BDArmory.VesselSpawning.SpawnStrategies +{ + /// + /// A simple pass-through strategy to be able to use the current spawning functions properly. + /// + public class SpawnConfigStrategy : SpawnStrategy + { + public SpawnConfig spawnConfig; + private bool success = false; + + public SpawnConfigStrategy(SpawnConfig spawnConfig) { this.spawnConfig = spawnConfig; } + + public IEnumerator Spawn(VesselSpawnerBase spawner) + { + yield return spawner.Spawn(spawnConfig); + + if (!spawner.vesselSpawnSuccess) + { + Debug.Log($"[BDArmory.SpawnConfigStrategy]: Vessel spawning failed: {spawner.spawnFailureReason}"); + yield break; + } + + success = true; + } + + public bool DidComplete() + { + return success; + } + } +} diff --git a/BDArmory/VesselSpawning/SpawnStrategies/SpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/SpawnStrategy.cs new file mode 100644 index 000000000..547ec7642 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnStrategies/SpawnStrategy.cs @@ -0,0 +1,18 @@ +using System.Collections; + +namespace BDArmory.VesselSpawning.SpawnStrategies +{ + public interface SpawnStrategy + { + /// + /// Part 1 of Remote Orchestration + /// + /// Spawns craft according to a provided strategy and prepares them for flight. + /// + /// + /// + 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..49d293062 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnUtils.cs @@ -0,0 +1,1428 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +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; +using BDArmory.Weapons; +using BDArmory.Damage; +using BDArmory.FX; + +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 && CircularSpawning.Instance.vesselsSpawning || CircularSpawning.Instance.vesselsSpawningOnceContinuously) + { CircularSpawning.Instance.CancelSpawning(); } + + // Continuous spawn + if (ContinuousSpawning.Instance && ContinuousSpawning.Instance.vesselsSpawningContinuously) + { ContinuousSpawning.Instance.CancelSpawning(); } + + 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) { if (SpawnUtilsInstance.Instance) 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) { if (SpawnUtilsInstance.Instance) SpawnUtilsInstance.Instance.RevertSpawnLocationCamera(keepTransformValues, revertIfDead); } + #endregion + + #region Teams + public static Dictionary originalTeams = []; + 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, bool andOperational = false) + { + return VesselModuleRegistry.GetModuleEngines(vessel).Where(engine => engine.EngineIgnited && (!andOperational || engine.isOperational)).ToList().Count + FireSpitter.CountActiveEngines(vessel); + } + + public static void ActivateAllEngines(Vessel vessel, bool activate = true, bool ignoreModularMissileEngines = true) + { + foreach (var engine in VesselModuleRegistry.GetModuleEngines(vessel)) + { + if (ignoreModularMissileEngines && IsModularMissilePart(engine.part)) 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); + foreach (var repulsor in VesselModuleRegistry.GetModules(vessel)) + { + repulsor.ToggleRepulsor(); + } + } + + public static bool IsModularMissilePart(Part 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 Mutators + public static void ApplyMutatorsOnNewVessels(bool enable) => SpawnUtilsInstance.Instance.ApplyMutatorsOnNewVessels(enable); + public static void ApplyMutators(Vessel vessel, bool enable) => SpawnUtilsInstance.Instance.ApplyMutators(vessel, enable); + #endregion + + #region RWP Stuff + public static void ApplyRWPonNewVessels(bool enable) => SpawnUtilsInstance.Instance.ApplyRWPonNewVessels(enable); + public static void ApplyRWP(Vessel vessel) => SpawnUtilsInstance.Instance.ApplyRWP(vessel); // Applying RWP can't be undone + #endregion + #region CompCheck Stuff + public static void ApplyCompCheckonNewVessels(bool enable) => SpawnUtilsInstance.Instance.ApplyCompCheckOnNewVessels(enable); + public static void ApplyCompSettingsChecks(Vessel vessel) => SpawnUtilsInstance.Instance.ApplyCompSettingsChecks(vessel); // Applying these can't be undone + + #endregion + + #region HallOfShame + public static void ApplyHOSOnNewVessels(bool enable) => SpawnUtilsInstance.Instance.ApplyHOSOnNewVessels(enable); + public static void ApplyHOS(Vessel vessel) => SpawnUtilsInstance.Instance.ApplyHOS(vessel); // Applying HOS can't be undone. + #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); + + /// + /// 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. + /// + /// + public static void AirborneActivation(Vessel vessel, bool withInitialVelocity) + { + // 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 = vessel.ActiveController().WM; + if (weaponManager != null) + { + var ai = weaponManager.AI; + if (ai != null) + { + ai.ActivatePilot(); + ai.CommandTakeOff(); + if (withInitialVelocity) + { + var pilot = ai as BDModulePilotAI; + if (pilot != null) { vessel.SetWorldVelocity(pilot.idleSpeed * vessel.transform.up); } + } + var orbitalAI = ai as BDModuleOrbitalAI; + if (orbitalAI && vessel.altitude > vessel.mainBody.MinSafeAltitude()) // In space with an orbital AI. Set it in a circular orbit. + { + Vector3d orbitVelocity = Math.Sqrt(FlightGlobals.getGeeForceAtPosition(vessel.CoM, vessel.mainBody).magnitude * (vessel.mainBody.Radius + vessel.altitude)) * FlightGlobals.currentMainBody.getRFrmVel(vessel.CoM).normalized; + if (BDKrakensbane.IsActive) orbitVelocity -= BDKrakensbane.FrameVelocityV3f; + vessel.SetWorldVelocity(orbitVelocity); + } + } + if (weaponManager.guardMode) + { + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.SpawnUtils]: 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 && 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) Debug.Log($"[BDArmory.SpawnUtils]: {vessel.vesselName} didn't activate engines on AG10! Activating ALL their engines."); + ActivateAllEngines(vessel); + } + else if (BDArmorySettings.NO_ENGINES && CountActiveEngines(vessel) > 0) // Vessel had some active engines. Turn them off if possible. + { + ActivateAllEngines(vessel, false); + } + } + #endregion + + #region Name Deconfliction + public static Dictionary SpawnedVesselURLs = []; // Deconflicted vessel name => URL. Vessels not spawned via BDA's spawners will have a null URL. + public static Dictionary DeconflictionSuffixes = []; // Deconflicted vessel name => suffix added to deconflict it. (For applying deconfliction to VESSELNAMING parts when reusing names.) + public static HashSet FighterNames = []; // Deconflicted vessel names of fighters. + + /// + /// Reset the vessel name deconfliction dictionaries. + /// + public static void ResetVesselNamingDeconfliction(bool fightersOnly = false) + { + if (!fightersOnly) + { + SpawnedVesselURLs.Clear(); + DeconflictionSuffixes.Clear(); + } + FighterNames.Clear(); + } + /// + /// Deconflict vessel names by appending a suffix. + /// If the vessel has detached from a parent craft, then the suffix is of the form "_Fn" for "fighters", otherwise the suffix is of the form "_n" for some integer n. + /// + /// Notes: + /// - VESSELNAMING requires that the vessel's parts list has been populated, which takes a few frames after spawning. + /// - Deconfliction occurs during spawning (if not disabled in the SpawnConfig) and when adding to a running competition (for detached vessels). + /// - Spawning via the VM disables deconfliction for that vessel unless a competition is active to avoid messing with names unnecessarily. + /// - Deconfliction also occurs when resetting a competition prior to initialising scores. + /// - The deconfliction dictionaries are reset under various conditions: + /// - Starting a tournament (during tournaments, the deconfliction dictionaries are stored as part of the tournament state in case of interruption). + /// - Starting a continuous spawn tournament. (reuse=true for cts spawn.) + /// - Performing a group spawn outside of a tournament when killEverythingFirst is true. + /// + /// The vessel for which to deconflict naming. + /// Reuse the previously deconflicted name for the vessel (if it exists). + public static void DeconflictVesselName(Vessel vessel, bool reuse = false) + { + // Before anything else, strip the type from the vessel's name. This avoids names like "Some craft name Rover", but also means "Jeb's Plane" isn't a valid name for a plane. + vessel.StripTypeFromName(); + + // If vessel naming deconfliction has previously been applied to this vessel, don't make further changes. + var ac = vessel.ActiveController(); + if (ac.VesselNamingDeconflictionHasBeenApplied) return; + ac.VesselNamingDeconflictionHasBeenApplied = true; + if (ac.WM == null) return; // Not a valid craft for competitions so don't bother deconflicting its name. + + // Start by deconflicting VESSELNAMING within the vessel. + var vesselNamingParts = DeconflictPartVesselNaming(vessel); + if (vesselNamingParts.Count > 0) + { + var vesselNamingName = vesselNamingParts.Select(p => p.vesselNaming.vesselName).First(); + if (!string.IsNullOrEmpty(vesselNamingName)) + { + if (BDArmorySettings.DEBUG_SPAWNING && vessel.vesselName != vesselNamingName) Debug.Log($"[BDArmory.SpawnUtils]: Overriding vesselName of {vessel.vesselName} with {vesselNamingName} from VESSELNAMING."); + vessel.vesselName = vesselNamingName; // Override vesselName with the highest priority VESSELNAMING name (since that's what KSP does). + } + } + + // Then make sure all the names are truly unique between vessels. + var craftURL = ac.WM.SourceVesselURL; + var isFighter = ac.IsFighter; + if (reuse && !isFighter && craftURL != null && SpawnedVesselURLs.ContainsValue(craftURL)) + { // A unique name has previously been found and we should just reuse it. + var potentialName = SpawnedVesselURLs.Where(kvp => kvp.Value == craftURL).Select(kvp => kvp.Key).First(); + if (BDArmorySettings.DEBUG_SPAWNING && vessel.vesselName != potentialName) Debug.Log($"[BDArmory.SpawnUtils]: Renaming {vessel.vesselName} to {potentialName} due to reusing previously spawned name."); + vessel.vesselName = potentialName; + if (vesselNamingParts.Count > 0) + { + if (BDArmorySettings.DEBUG_SPAWNING && vesselNamingParts.First().vesselNaming.vesselName != potentialName) + { + var part = vesselNamingParts.First(); + Debug.Log($"[BDArmory.SpawnUtils]: Renaming VESSELNAMING {part.vesselNaming.vesselName} on {part.partInfo.name} to {potentialName} due to reusing previously spawned name."); + } + vesselNamingParts.First().vesselNaming.vesselName = potentialName; // Override the VESSELNAMING of the primary part that was originally used to set the name. + foreach (var part in vesselNamingParts.Skip(1)) + { + var oldName = part.vesselNaming.vesselName; + part.vesselNaming.vesselName += DeconflictionSuffixes.GetValueOrDefault(potentialName); + if (BDArmorySettings.DEBUG_SPAWNING && oldName != part.vesselNaming.vesselName) Debug.Log($"[BDArmory.SpawnUtils]: Renaming VESSELNAMING {oldName} on {part.partInfo.name} to {part.vesselNaming.vesselName} due to parent being renamed."); + } + } + } + else + { + var suffix = isFighter ? "_F" : "_"; + var count = 1; + if (isFighter) + { + bool IsNameUsed(string name) => SpawnedVesselURLs.ContainsKey(name) || + (reuse ? + FlightGlobals.Vessels.Where(v => v != null && v.loaded && v != vessel && v.ActiveController().WM != null).Select(v => v.vesselName).Contains(name) : // If reusing fighter names, only check active vessels. + FighterNames.Contains(name) + ); + if (IsNameUsed(vessel.vesselName)) + { + var baseName = vessel.vesselName; + // If the baseName conforms to the fighter naming pattern, strip the suffix to avoid "SomeName_F1_F1" names. + // This typically happens if two fighters are attached to a parent and the parent gets destroyed before they detach. + var suffixIndex = baseName.LastIndexOf(suffix); + if (suffixIndex > -1 && int.TryParse(baseName.Substring(suffixIndex + 2), NumberStyles.None, CultureInfo.InvariantCulture, out _)) + { baseName = baseName.Remove(suffixIndex); } + var potentialName = $"{baseName}{suffix}{count}"; + while (IsNameUsed(potentialName)) + potentialName = $"{baseName}{suffix}{++count}"; // Note: The computer will have long since run out of memory before we exhaust the integers. + if (BDArmorySettings.DEBUG_SPAWNING && vessel.vesselName != potentialName) + { + Debug.Log($"[BDArmory.SpawnUtils]: Renaming {vessel.vesselName} ({vessel.persistentId}) to {potentialName} due to naming conflict."); + // Debug.Log($"DEBUG SpawnedVesselURLs: {string.Join(", ", SpawnedVesselURLs.Keys)}"); + // Debug.Log($"DEBUG Active FighterNames: {string.Join(", ", FighterNames.Where(name => FlightGlobals.Vessels.Where(v => v != null && v.loaded && v != vessel && v.ActiveController().WM != null).Select(v => v.vesselName).Contains(name)))}"); + } + vessel.vesselName = potentialName; + } + FighterNames.Add(vessel.vesselName); + } + else + { + if (SpawnedVesselURLs.ContainsKey(vessel.vesselName) || FighterNames.Contains(vessel.vesselName)) + { + var potentialName = $"{vessel.vesselName}{suffix}{count}"; + while (SpawnedVesselURLs.ContainsKey(potentialName) || FighterNames.Contains(potentialName)) + potentialName = $"{vessel.vesselName}{suffix}{++count}"; // Note: The computer will have long since run out of memory before we exhaust the integers. + if (BDArmorySettings.DEBUG_SPAWNING && vessel.vesselName != potentialName) Debug.Log($"[BDArmory.SpawnUtils]: Renaming {vessel.vesselName} ({vessel.persistentId}) to {potentialName} due to naming conflict."); + vessel.vesselName = potentialName; + DeconflictionSuffixes.Add(potentialName, $"{suffix}{count}"); + if (vesselNamingParts.Count > 0) + { + vesselNamingParts.First().vesselNaming.vesselName = potentialName; // Override the VESSELNAMING of the primary part that was originally used to set the name. + foreach (var part in vesselNamingParts.Skip(1)) // Append the same suffix to fighters for consistency. + { + var oldName = part.vesselNaming.vesselName; + part.vesselNaming.vesselName += DeconflictionSuffixes.GetValueOrDefault(potentialName); + if (BDArmorySettings.DEBUG_SPAWNING && oldName != part.vesselNaming.vesselName) Debug.Log($"[BDArmory.SpawnUtils]: Renaming VESSELNAMING {oldName} on {part.partInfo.name} to {part.vesselNaming.vesselName} due to parent being renamed."); + } + } + } + SpawnedVesselURLs.Add(vessel.vesselName, craftURL); // Only add the URL for the originally spawned craft. + } + } + + // Update the VesselName in the ActiveController to prevent KSP from messing with it. + ac.VesselName = vessel.vesselName; + } + /// + /// Deconflict VESSELNAMING by adding "_Fn" suffixes to conflicting names other than the highest priority one. + /// + /// + /// A list of the parts with VESSELNAMING in order of descending priority with deconflicted names. + static List DeconflictPartVesselNaming(Vessel vessel) + { + if (vessel.Parts.Count == 0) { Debug.LogWarning($"[BDArmory.SpawnUtils]: {vessel.GetName()}'s parts list isn't loaded yet, unable to deconflict vessel naming."); return []; } // Nothing to do. + + var partNamingPriority = vessel.Parts.Where(p => p.vesselNaming != null && !string.IsNullOrEmpty(p.vesselNaming.vesselName)).OrderByDescending(p => p.vesselNaming.namingPriority).ToList(); + var partNamingNames = partNamingPriority.Select(p => p.vesselNaming.vesselName).ToList(); + List names = []; + foreach (var part in partNamingPriority) + { + int count = 0; + if (names.Contains(part.vesselNaming.vesselName) || partNamingNames.Count(name => name == part.vesselNaming.vesselName) > 1) + { + var potentialName = $"{part.vesselNaming.vesselName}_F{++count}"; + while (names.Contains(potentialName)) { potentialName = $"{part.vesselNaming.vesselName}_F{++count}"; } + if (BDArmorySettings.DEBUG_SPAWNING && part.vesselNaming.vesselName != potentialName) Debug.Log($"[BDArmory.SpawnUtils]: Renaming VESSELNAMING {part.vesselNaming.vesselName} on {part.partInfo.name} to {potentialName}"); + part.vesselNaming.vesselName = potentialName; + } + names.Add(part.vesselNaming.vesselName); + } + return partNamingPriority; + } + public static string GetNameOfFirstSpawnedVesselFrom(string craftURL) + { + // Find the first vesselName corresponding to the craft URL. + return SpawnedVesselURLs.Where(kvp => kvp.Value == craftURL).Select(kvp => kvp.Key).FirstOrDefault(); + } + #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(); + public static void DisableAllBulletsAndRockets() => SpawnUtilsInstance.Instance.DisableAllBulletsAndRockets(); + #endregion + + #region AI/WM stuff for RWP + public static bool CheckAIWMPlacement(Vessel vessel) + { + var message = ""; + List failureStrings = []; + + var AI = vessel.ActiveController().AI; + var WM = vessel.ActiveController().WM; + 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 (!( + vessel.rootPart.IsKerbalSeat() // The root part is a seat. + || vessel.rootPart.protoModuleCrew.Any(crew => crew != null) // The root part is a cockpit. + || vessel.rootPart.children.Any(part => part.IsKerbalSeat()) // The root part has a seat attached to it (this should be fine as the chair will be killed if it detaches). + )) + { + message += $"{(message.Length > 0 ? " and its" : "'s")} cockpit isn't the 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 = Math.Max(1, removeVesselsPending + 1); + 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 (AsteroidUtils.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() + { + DisableAllBulletsAndRockets(); // First get rid of any bullets and rockets flying around (missiles count as vessels). + var vesselsToKill = FlightGlobals.Vessels.ToList(); + // Spawn in the SpawnProbe at the camera position. + var spawnProbe = VesselSpawner.SpawnSpawnProbe(); + var tic = Time.time; + 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) && Time.time - tic < 30); + // Switch to the spawn probe. Give up after 30s. + while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe && Time.time - tic < 30) + { + try + { + LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnProbe); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.SpawnUtils]: Failed to switch to the SpawnProbe, proceeding with trying to kill everything.\n{e.Message}\n{e.StackTrace}"); + break; + } + 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 up to 30s for everything to be removed. + SpawnUtils.originalTeams.Clear(); + tic = Time.time; + yield return new WaitWhile(() => removeVesselsPending > 0 && Time.time - tic < 30); + } + + public void DisableAllBulletsAndRockets() + { + if (ModuleWeapon.bulletPool != null && ModuleWeapon.bulletPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.SpawnUtils]: Setting {ModuleWeapon.bulletPool.pool.Count(b => b != null && b.activeInHierarchy)} bullets inactive."); + foreach (var bullet in ModuleWeapon.bulletPool.pool) + { + if (bullet == null) continue; + bullet.SetActive(false); + } + } + if (ModuleWeapon.shellPool != null && ModuleWeapon.shellPool.pool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.SpawnUtils]: Setting {ModuleWeapon.shellPool.pool.Count(s => s != null && s.activeInHierarchy)} shells inactive."); + foreach (var shell in ModuleWeapon.shellPool.pool) + { + if (shell == null) continue; + shell.SetActive(false); + } + } + if (ModuleWeapon.rocketPool != null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.SpawnUtils]: Setting {ModuleWeapon.rocketPool.Values.Where(rocketPool => rocketPool != null && rocketPool.pool != null).Sum(rocketPool => rocketPool.pool.Count(s => s != null && s.activeInHierarchy))} rockets inactive."); + foreach (var rocketPool in ModuleWeapon.rocketPool.Values) + { + if (rocketPool == null || rocketPool.pool == null) continue; + foreach (var rocket in rocketPool.pool) + { + if (rocket == null) continue; + rocket.SetActive(false); + } + } + } + } + #endregion + + #region Camera Adjustment + GameObject spawnLocationCamera; + Transform originalCameraParentTransform; + float originalCameraNearClipPlane; + float originalCameraDistance; + 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.DEBUG_SPAWNING) Debug.Log($"[BDArmory.SpawnUtils]: Showing spawn point ({latitude:G3}, {longitude:G3}, {altitude:G6}) on {FlightGlobals.Bodies[worldIndex].name}"); + 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 (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.PatchedConicsAttached) FlightGlobals.ActiveVessel.DetachPatchedConicsSolver(); + if (!spawning) + { + var overLand = (worldIndex != -1 ? FlightGlobals.Bodies[worldIndex] : FlightGlobals.currentMainBody).TerrainAltitude(latitude, longitude) > 0; + var easeToSurface = altitude <= 10; + 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, easeToSurface, easeToSurface ? 0.1 : 1); // 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; + originalCameraDistance = flightCamera.Distance; + } + 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.SetDistanceImmediate(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) + { + var mainCamera = GUIUtils.GetMainCamera(); + if (keepTransformValues && flightCamera.transform != null && flightCamera.transform.parent != null) + { + originalCameraParentTransform.position = flightCamera.transform.parent.position; + originalCameraParentTransform.rotation = flightCamera.transform.parent.rotation; + if (mainCamera) originalCameraNearClipPlane = mainCamera.nearClipPlane; + originalCameraDistance = flightCamera.Distance; + } + flightCamera.transform.parent = originalCameraParentTransform; + if (mainCamera) mainCamera.nearClipPlane = originalCameraNearClipPlane; + flightCamera.SetDistanceImmediate(originalCameraDistance); + 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 30°/s."); + 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 (vessel.ActiveController().WM != 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(); + kal.ControlledActions.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; + // First restore the controlled axes. + 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; + } + } + } + // Then restore the controlled actions. + var controlledActions = protoPartModuleSnapshot.moduleValues.GetNode("CONTROLLEDACTIONS"); + kal.ControlledActions.Clear(); // Clear the existing actions (they should be clear already due to mismatching part persistent IDs, but better safe than sorry). + rowIndex = 0; + foreach (var actionNode in controlledActions.GetNodes("ACTION")) // For each action to be controlled, locate the part in the spawned vessel that has the correct module. + if (uint.TryParse(actionNode.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 actionName = actionNode.GetValue("actionName"); + foreach (var action in partModule.Actions) + if (action.name == actionName) // Found the action in a module in a part being controlled by this KAL. + { + actionNode.SetValue("persistentId", part.persistentId.ToString()); // Update the ConfigNode in the ProtoPartModuleSnapshot + actionNode.SetValue("partNickName", part.partInfo.title); // Set the nickname to the part title (note: this will override custom nicknames). + actionNode.SetValue("rowIndex", rowIndex++); + var controlledAction = new Expansions.Serenity.ControlledAction(part, partModule, action, kal); // Link the part, module, field and KAL together. + controlledAction.Load(actionNode); // Load the new config into the action. + kal.ControlledActions.Add(controlledAction); // Add the action to the KAL. + break; + } + } + } + } + } + #endregion + + #region Mutators + public void ApplyMutatorsOnNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(ApplyMutatorEventHandler); + } + else + { + GameEvents.onVesselLoaded.Remove(ApplyMutatorEventHandler); + } + } + void ApplyMutatorEventHandler(Vessel vessel) => ApplyMutators(vessel, true); + + public Dictionary gunGameProgress = []; + public void ApplyMutators(Vessel vessel, bool enable) + { + if (vessel == null || !vessel.loaded) return; + var MM = vessel.rootPart.FindModuleImplementing(); + if (enable && BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_LIST.Count > 0) + { + if (MM == null) + { + MM = (BDAMutator)vessel.rootPart.AddModule("BDAMutator"); + } + if (BDArmorySettings.MUTATOR_APPLY_GUNGAME) //gungame + { + if (!BDArmorySettings.GG_CYCLE_LIST && MM.progressionIndex > BDArmorySettings.MUTATOR_LIST.Count - 1) return; // Already at the end of the list. + if (BDArmorySettings.GG_PERSISTANT_PROGRESSION) MM.progressionIndex = gunGameProgress.GetValueOrDefault(vessel.vesselName, 0); + if (MM.progressionIndex > BDArmorySettings.MUTATOR_LIST.Count - 1) MM.progressionIndex = BDArmorySettings.GG_CYCLE_LIST ? 0 : BDArmorySettings.MUTATOR_LIST.Count - 1; + Debug.Log($"[BDArmory.SpawnUtils]: Applying mutator {BDArmorySettings.MUTATOR_LIST[MM.progressionIndex]} to {vessel.vesselName}"); + MM.EnableMutator(BDArmorySettings.MUTATOR_LIST[MM.progressionIndex]); // Apply the mutator. + MM.progressionIndex++; //increment to next mutator on list + if (BDArmorySettings.GG_PERSISTANT_PROGRESSION) gunGameProgress[vessel.vesselName] = MM.progressionIndex; + } + else + { + if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) //selected mutator applied globally + { + MM.EnableMutator(BDACompetitionMode.Instance.currentMutator); + } + else //mutator applied on a per-craft basis, APPLY_TIMER/APPLY_KILL + { + MM.EnableMutator(); //random mutator + } + } + BDACompetitionMode.Instance.competitionStatus.Add($"{vessel.vesselName} gains {MM.mutatorName}{(BDArmorySettings.MUTATOR_DURATION > 0 ? $" for {BDArmorySettings.MUTATOR_DURATION * 60} seconds!" : "!")}"); + } + else if (MM != null) + { + MM.DisableMutator(); + } + } + #endregion + + #region HOS + public void ApplyHOSOnNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(ApplyHOSEventHandler); + } + else + { + GameEvents.onVesselLoaded.Remove(ApplyHOSEventHandler); + } + } + void ApplyHOSEventHandler(Vessel vessel) => ApplyHOS(vessel); + + public void ApplyHOS(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return; + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Count > 0) + { + if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vessel.GetName())) + { + using (List.Enumerator part = vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (BDArmorySettings.HOS_FIRE > 0.1f) + { + BulletHitFX.AttachFire(part.Current.transform.position, part.Current, BDArmorySettings.HOS_FIRE * 50, "GM", BDArmorySettings.COMPETITION_DURATION * 60, 1, true); + } + if (BDArmorySettings.HOS_MASS != 0) + { + var MM = part.Current.FindModuleImplementing(); + if (MM == null) + { + MM = (ModuleMassAdjust)part.Current.AddModule("ModuleMassAdjust"); + } + MM.duration = BDArmorySettings.COMPETITION_DURATION * 60; + MM.massMod += (float)(BDArmorySettings.HOS_MASS / vessel.Parts.Count); //evenly distribute mass change across entire vessel + } + if (BDArmorySettings.HOS_DMG != 1) + { + 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(vessel).GetEnumerator()) + while (engine.MoveNext()) + { + engine.Current.thrustPercentage = BDArmorySettings.HOS_THRUST; + } + } + if (!string.IsNullOrEmpty(BDArmorySettings.HOS_MUTATOR)) + { + var MM = vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)vessel.rootPart.AddModule("BDAMutator"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: adding Mutator module {vessel.vesselName}"); + } + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: Applying ({BDArmorySettings.HOS_MUTATOR})"); + MM.EnableMutator(BDArmorySettings.HOS_MUTATOR, true); + } + } + } + } + } + #endregion + + #region RWP Specific + public void ApplyRWPonNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(ApplyRWPEventHandler); + } + else + { + GameEvents.onVesselLoaded.Remove(ApplyRWPEventHandler); + } + } + void ApplyRWPEventHandler(Vessel vessel) => ApplyRWP(vessel); + + public void ApplyRWP(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return; + if (BDArmorySettings.RUNWAY_PROJECT) + { + float torqueQuantity = 0; + int APSquantity = 0; + SpawnUtils.HackActuators(vessel, true); + + using (List.Enumerator part = 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.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) + { + if (!vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) + { + ModuleCommand MC; + MC = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0 && MC.minimumCrew == 0 && !SpawnUtils.IsModularMissilePart(part.Current)) //Non-MMG 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 == 78) + { + part.Current.sameVesselCollision = true; + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60) + { + var nuke = vessel.rootPart.FindModuleImplementing(); + if (nuke == null) + { + nuke = (BDModuleNuke)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 {vessel.GetName()}"); + } + BDModulePilotAI pilotAI = vessel.ActiveController().PilotAI; + 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.SpawnUtils]: Setting SpaceMode AI settings on {vessel.GetName()}"); + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 67) + { + if (vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) + { + HitpointTracker armor = vessel.rootPart.GetComponent(); + if (armor != null) + { + armor.maxHitPoints = BDArmorySettings.MAX_ACTIVE_RADAR_RANGE; //not used by RWP, so can be hacked to serve as a asteroid Hp value + armor.SetupPrefab(); + } + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 74) + { + var wm = vessel.ActiveController().WM; + if (wm != null) + { + if (BDArmorySettings.DEBUG_COMPETITION && wm.targetWeightAttackVIP != 10) Debug.Log($"[BDArmory.SpawnUtils]: Overriding VIP target priority to 10 on {vessel.GetName()}"); + wm.targetWeightAttackVIP = 10; + } + } + } + } + #endregion + + #region Competition AI/WM Settings Compliance + public void ApplyCompCheckOnNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(ApplyCompCheckEventHandler); + } + else + { + GameEvents.onVesselLoaded.Remove(ApplyCompCheckEventHandler); + } + } + void ApplyCompCheckEventHandler(Vessel vessel) => ApplyCompSettingsChecks(vessel); + + public void ApplyCompSettingsChecks(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return; + if (BDArmorySettings.COMP_CONVENIENCE_CHECKS) + { + int cockpitSeatCount = 0; + + using (List.Enumerator part = vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (CompSettings.CompOverrides.TryGetValue("DISABLE_SAS", out float dSAS) && dSAS > 0) + { + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; + SAS = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0) + SAS.authorityLimiter = 0; + } + } + if (part.Current.GetComponent() != null) + { + if (part.Current.CrewCapacity > 0 && part.Current.CrewCapacity > cockpitSeatCount) cockpitSeatCount = part.Current.CrewCapacity; + } + } + var pilotAI = vessel.ActiveController().PilotAI; + if (pilotAI != null) + { + if (CompSettings.CompOverrides.TryGetValue("extendDistanceAirToAir", out float dATA) && dATA > 0) + pilotAI.extendDistanceAirToAir = Mathf.Min(pilotAI.extendDistanceAirToAir, dATA); + if (CompSettings.CompOverrides.TryGetValue("collisionAvoidanceThreshold", out float cAT) && cAT >= 0) + pilotAI.collisionAvoidanceThreshold = Mathf.Max(pilotAI.collisionAvoidanceThreshold, cAT); + if (CompSettings.CompOverrides.TryGetValue("vesselCollisionAvoidanceLookAheadPeriod", out float vCAL) && vCAL >= 0) + pilotAI.vesselCollisionAvoidanceLookAheadPeriod = Mathf.Max(pilotAI.vesselCollisionAvoidanceLookAheadPeriod, vCAL); + if (CompSettings.CompOverrides.TryGetValue("vesselCollisionAvoidanceStrength", out float vCAS) && vCAS >= 0) + pilotAI.vesselCollisionAvoidanceStrength = Mathf.Max(pilotAI.vesselCollisionAvoidanceStrength, vCAS); + if (CompSettings.CompOverrides.TryGetValue("idleSpeed", out float iS) && iS > 0) + pilotAI.idleSpeed = Mathf.Max(pilotAI.idleSpeed, iS); + if (CompSettings.CompOverrides.TryGetValue("extensionCutoffTime", out float eCT) && eCT > 0) + pilotAI.extensionCutoffTime = Mathf.Max(pilotAI.extensionCutoffTime, eCT); + } + var WM = vessel.ActiveController().WM; + if (WM != null) + { + if (cockpitSeatCount == 1) + { + if (CompSettings.CompOverrides.TryGetValue("MONOCOCKPIT_VIEWRANGE", out float gR1) && gR1 > 0) + WM.guardRange = Mathf.Min(WM.guardRange, gR1); + if (CompSettings.CompOverrides.TryGetValue("guardAngle", out float gA) && gA > 0) + WM.guardAngle = Mathf.Min(WM.guardAngle, gA); + } + else //this would cause dual-seat visual range to also apply to dronecore controlled craft, but those should be caught by overall building rules... + { + if (CompSettings.CompOverrides.TryGetValue("DUALCOCKPIT_VIEWRANGE", out float gR2) && gR2 > 0) + WM.guardRange = Mathf.Min(WM.guardRange, gR2); + } + } + } + } + #endregion + } +} diff --git a/BDArmory/VesselSpawning/VesselMover.cs b/BDArmory/VesselSpawning/VesselMover.cs new file mode 100644 index 000000000..6ed7b1420 --- /dev/null +++ b/BDArmory/VesselSpawning/VesselMover.cs @@ -0,0 +1,1764 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using KSP.UI.Screens; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +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(); + HashSet movingVessels = []; + HashSet loweringVessels = []; + readonly List jumpToAltitudes = [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(); + SetupMoveIndicator(); + GameEvents.onVesselChange.Add(OnVesselChanged); + + if (BDArmorySettings.VM_TOOLBAR_BUTTON) AddToolbarButton(); + } + + void SetupMoveIndicator() + { + 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; + } + + 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); + position += (lowerBound - previousLowerBound) * up; + vessel.SetPosition(position); + vessel.SetWorldVelocity(Vector3d.zero); + } + + // 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.acceleration = 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; + } + + public void PickUpAndDrop(Vessel vessel, float altitude) { StartCoroutine(PickUpAndDropCoroutine(vessel, altitude)); } + public IEnumerator PickUpAndDropCoroutine(Vessel vessel, float altitude) + { + StartCoroutine(MoveVessel(vessel)); // Pick it up. + yield return new WaitForSecondsFixed(0.25f); // Wait a quarter-sec (the initial pick-up should only take 0.2s). + DropVessel(vessel); // Drop it. + vessel.SetPosition(vessel.transform.position + (altitude - (float)vessel.radarAltitude) * (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized); + vessel.IgnoreGForces(1); + vessel.IgnoreSpeed(1); + } + + /// + /// 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 radius = vessel.GetRadius(); + var maxDim = 2f * radius; + 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(); + 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 = []; + int crewCapacity = -1; + string vesselNameToSpawn = ""; + CustomCraftBrowserDialog craftBrowser; + bool abortCraftSelection = false; + bool resizingSelectionWindow = 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}"); + + // Wait for the vessel to be usable. + // Note: Smart parts that are pre-enabled can trigger events that break craft while they spawn (particularly altitude and speed based ones). Those should be set active on AG10 instead. + while (spawnedVessel != null && (!spawnedVessel.loaded || spawnedVessel.packed)) yield return wait; + + // Reposition the vessel to where it should be. + 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 null; + } + } + + IEnumerator GetSpawnPoint() + { + messageState = Messages.ChoosingSpawnPoint; + GameObject indicatorObject = SetupSpawnPointIndicator(); + + Vector3 mouseAim, point; + Ray ray; + 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); + bool altitudeCorrection; + 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 null; + 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 null; + } + Destroy(indicatorObject); + } + + GameObject SetupSpawnPointIndicator() + { + // Use the same indicator as the original VesselMover for familiarity. + GameObject indicatorObject = new(); + 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 = [Vector3.zero, 10 * Vector3.forward]; + lr.SetPositions(positions); + lr.positionCount = positions.Length; + lr.startWidth = 0.1f; + lr.endWidth = 1f; + + return 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( + craftUrl, + spawnPoint, + direction, + (float)altitude, + initialPitch, + airborne: false, + inOrbit: false, + deconflictVesselName: BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting, // Deconflict name only if spawning into an active competition. + crew: crew + ); + ResetInternals(); // Reset spawner internals. + + // 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; + float previousVesselMoverWindowHeight = 0; + + private void OnGUI() + { + if (!(ready && BDArmorySetup.GAME_UI_ENABLED && HighLogic.LoadedSceneIsFlight)) + return; + + if (resizingSelectionWindow && Event.current.type == EventType.MouseUp) { resizingSelectionWindow = false; } + if (BDArmorySetup.showVesselMoverGUI) + { + BDArmorySetup.SetGUIOpacity(); + var guiMatrix = GUI.matrix; // Store and restore the GUI.matrix so we can apply a different scaling for the WM window. + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectVesselMover.position); + 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) + ); + GUI.matrix = guiMatrix; + previousVesselMoverWindowHeight = BDArmorySetup.WindowRectVesselMover.height; + if (showVesselSelection) + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, BDArmorySetup.WindowRectVesselMoverVesselSelection.position); + BDArmorySetup.WindowRectVesselMoverVesselSelection = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + BDArmorySetup.WindowRectVesselMoverVesselSelection, + VesselSelectionWindow, + StringUtils.Localize("#LOC_BDArmory_VesselMover_VesselSelection"), + BDArmorySetup.BDGuiSkin.window + ); + GUI.matrix = guiMatrix; + } + else if (showCrewSelection) + { + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, crewSelectionWindowRect.position); + crewSelectionWindowRect = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + crewSelectionWindowRect, + CrewSelectionWindow, + StringUtils.Localize("#LOC_BDArmory_VesselMover_CrewSelection"), + BDArmorySetup.BDGuiSkin.window + ); + GUI.matrix = guiMatrix; + } + 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, previousVesselMoverWindowHeight); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselMover, guiCheckIndex); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectVesselMover); + } + + /// + /// Reset the height of the window so that it shrinks. + /// + void ResetWindowHeight() + { + BDArmorySetup.WindowRectVesselMover.height = 0; + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselMover, previousVesselMoverWindowHeight); + } + + 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; + Vector2 vesselSelectionScrollPos = default; + float selectionTimer = 0; + string selectedVesselURL = ""; + string selectionFilter = ""; + bool focusFilterField = false; // Focus the filter text field. + bool folderSelectionMode = false; // Show SPH/VAB and folders instead of craft files. + + public void ShowVesselSelection(Action selectedCallback = null, Action cancelledCallback = null) + { + if (craftBrowser == null) + { + craftBrowser = new CustomCraftBrowserDialog(); + craftBrowser.UpdateList(); + } + craftBrowser.selectFileCallback = selectedCallback; + craftBrowser.cancelledCallback = cancelledCallback; + selectedVesselURL = ""; + showVesselSelection = true; + focusFilterField = true; + selectionTimer = Time.realtimeSinceStartup; + craftBrowser.CheckCurrent(); + GUIUtils.SetGUIRectVisible(_vesselGUICheckIndex, true); + } + + public void HideVesselSelection() + { + showVesselSelection = false; + GUIUtils.SetGUIRectVisible(_vesselGUICheckIndex, false); + } + + List FilteredCraft + { + get + { + if (_filteredCraft.Item1 != selectionFilter || _filteredCraft.Item2 < craftBrowser.craftListUpdateTimestamp) + { + // Something changed, update the filtered list. + _filteredCraft.Item1 = selectionFilter; + _filteredCraft.Item2 = craftBrowser.craftListUpdateTimestamp; + _filteredCraft.Item3 = [.. craftBrowser.craftList.Where(kvp => kvp.Key != null && kvp.Value != null && kvp.Value.shipName.ToLower().Contains(selectionFilter.ToLower())).Select(kvp => kvp.Key)]; + } + return _filteredCraft.Item3; + } + } + (string, float, List) _filteredCraft = ("", 0, []); + + public void VesselSelectionWindow(int windowID) + { + GUI.DragWindow(new Rect(0, 0, BDArmorySetup.WindowRectVesselMoverVesselSelection.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(BDArmorySetup.WindowRectVesselMoverVesselSelection.width - 15), GUILayout.MaxHeight(BDArmorySetup.WindowRectVesselMoverVesselSelection.height - 60)); + if (folderSelectionMode) + { + GUILayout.BeginHorizontal(); + if (GUILayout.Button("SPH", CustomCraftBrowserDialog.ButtonStyle, GUILayout.Height(80))) craftBrowser.ChangeFolder(EditorFacility.SPH); + if (GUILayout.Button("VAB", CustomCraftBrowserDialog.ButtonStyle, GUILayout.Height(80))) craftBrowser.ChangeFolder(EditorFacility.VAB); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.Label(craftBrowser.DisplayFolder, CustomCraftBrowserDialog.LabelStyle, GUILayout.Height(50), GUILayout.ExpandWidth(true)); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_Select"), CustomCraftBrowserDialog.ButtonStyle, GUILayout.Height(50), GUILayout.MaxWidth(BDArmorySetup.WindowRectVesselMoverVesselSelection.width / 3))) folderSelectionMode = false; + GUILayout.EndHorizontal(); + foreach (var folder in craftBrowser.subfolders) + { + if (GUILayout.Button($"{folder}", CustomCraftBrowserDialog.ButtonStyle, GUILayout.MaxHeight(60))) + { + craftBrowser.ChangeFolder(craftBrowser.Facility, folder); + break; // The iteration can't continue since subfolders has changed. + } + } + } + else + { + foreach (var vesselURL in FilteredCraft) + { + if (!craftBrowser.craftList.TryGetValue(vesselURL, out var vesselInfo)) continue; // This shouldn't happen. + GUILayout.BeginHorizontal(); // Vessel buttons + if (GUILayout.Button($"{vesselInfo.shipName}", selectedVesselURL == vesselURL ? CustomCraftBrowserDialog.SelectedButtonStyle : CustomCraftBrowserDialog.ButtonStyle, GUILayout.MaxHeight(64), GUILayout.MaxWidth(BDArmorySetup.WindowRectVesselMoverVesselSelection.width - 230))) + { + if (Time.realtimeSinceStartup - selectionTimer < 0.5f) + { + craftBrowser.selectFileCallback?.Invoke(vesselURL); + HideVesselSelection(); + } + else if (selectedVesselURL == vesselURL) { selectedVesselURL = ""; } + else { selectedVesselURL = vesselURL; } + selectionTimer = Time.realtimeSinceStartup; + } + GUILayout.Label(VesselInfoEntry(vesselURL, vesselInfo, true), CustomCraftBrowserDialog.InfoStyle, GUILayout.Width(166)); + GUILayout.Label(craftBrowser.craftThumbnails.GetValueOrDefault(vesselURL), CustomCraftBrowserDialog.InfoStyle, GUILayout.Height(64), GUILayout.Width(64)); + GUILayout.EndHorizontal(); + } + } + GUILayout.EndScrollView(); + 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(folderSelectionMode ? StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Craft") : StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Folder"), folderSelectionMode ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle, GUILayout.Width(BDArmorySetup.WindowRectVesselMoverVesselSelection.width / 6))) + { + folderSelectionMode = !folderSelectionMode; + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Refresh"), BDArmorySetup.ButtonStyle, GUILayout.Width(BDArmorySetup.WindowRectVesselMoverVesselSelection.width / 6))) + { + craftBrowser.UpdateList(); + } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + #region Resizing + var resizeRect = new Rect(BDArmorySetup.WindowRectVesselMoverVesselSelection.width - 16, BDArmorySetup.WindowRectVesselMoverVesselSelection.height - 16, 16, 16); + GUI.DrawTexture(resizeRect, GUIUtils.resizeTexture, ScaleMode.StretchToFill, true); + if (Event.current.type == EventType.MouseDown && resizeRect.Contains(Event.current.mousePosition)) + { + resizingSelectionWindow = true; + } + if (resizingSelectionWindow && Event.current.type == EventType.Repaint) + { BDArmorySetup.WindowRectVesselMoverVesselSelection.size += Mouse.delta / BDArmorySettings.UI_SCALE_ACTUAL; } + #endregion + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselMoverVesselSelection); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselMoverVesselSelection, _vesselGUICheckIndex); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectVesselMoverVesselSelection); + } + + readonly StringBuilder vesselInfoEntry = new(); + public string VesselInfoEntry(string vesselURL, CraftProfileInfo vesselInfo, bool withCrewCount) + { + vesselInfoEntry.Clear(); + vesselInfoEntry.AppendLine( + $"{StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Parts")}: {( + vesselInfo.partCount < 101 ? vesselInfo.partCount : + vesselInfo.partCount < 201 ? $"{vesselInfo.partCount}" : + vesselInfo.partCount < 301 ? $"{vesselInfo.partCount}" : + $"{vesselInfo.partCount}" + )}, {StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Mass")}: {( + vesselInfo.totalMass < 1000f ? $"{vesselInfo.totalMass:G3}t" : + $"{vesselInfo.totalMass / 1000f:G3}kt" + )}" + ); + if (withCrewCount) + vesselInfoEntry.AppendLine($"Crew count: {(craftBrowser.crewCounts.ContainsKey(vesselURL) ? craftBrowser.crewCounts[vesselURL].ToString() : "unknown")}"); + vesselInfoEntry.Append( + vesselInfo.UnavailableShipParts.Count > 0 ? $"{StringUtils.Localize("#LOC_BDArmory_CraftBrowser_InvalidParts")}" : + $"{StringUtils.Localize("#LOC_BDArmory_CraftBrowser_Version")}: {(vesselInfo.compatibility == VersionCompareResult.COMPATIBLE ? $"{vesselInfo.version}" : + $"{vesselInfo.version}")}{(vesselInfo.UnavailableShipPartModules.Count > 0 ? $" {StringUtils.Localize("#LOC_BDArmory_CraftBrowser_UnknownModules")}" : "")}" + ); + return vesselInfoEntry.ToString(); + } + + #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). + crewMember.courage = 0.5f; + } + 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) // Just update the callbacks for the current instance. + { + button.onTrue = ShowVMGUI; + button.onFalse = HideVMGUI; + yield break; + } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. + 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 + } + + internal class CustomCraftBrowserDialog + { + // Keep some of these as static so that they're remembered between instances of showing the dialog. + static EditorFacility facility = EditorFacility.None; + static string profile = HighLogic.SaveFolder; + static string baseFolder; + static string displayFolder; + static string currentFolder; + + // Public getters + public string CurrentFolder => currentFolder; + public string BaseFolder => baseFolder; + public string DisplayFolder => displayFolder; + public string GameName => profile; + public EditorFacility Facility => facility; + + string _currentFolder; // For checking if the current folder has changed between instances and thus the craftList needs refreshing. + public float craftListUpdateTimestamp = 0; // Timestamp for when the craftList was last updated. + public Dictionary craftList = []; + public Dictionary crewCounts = []; + public Dictionary craftThumbnails = []; + public List subfolders = []; + public Action selectFileCallback = null; + public Action cancelledCallback = null; + public static Dictionary shipNames = []; // craftURLs to ship names. + public static GUIStyle ButtonStyle = new(BDArmorySetup.ButtonStyle); + public static GUIStyle SelectedButtonStyle = new(BDArmorySetup.SelectedButtonStyle); + public static GUIStyle InfoStyle = new(BDArmorySetup.BDGuiSkin.label); + public static GUIStyle LabelStyle = new(BDArmorySetup.BDGuiSkin.label); + public void UpdateList() + { + if (CheckCurrent()) return; // If CheckCurrent changes the folder, then UpdateList gets called internally. + if (string.IsNullOrEmpty(currentFolder) || !Directory.Exists(currentFolder)) ChangeFolder(facility); // Default to the current base folder if something is wrong. + craftList = Directory.GetFiles(currentFolder, "*.craft").ToDictionary(craft => craft, craft => new CraftProfileInfo()); + if (craftList.ContainsKey(Path.Combine(currentFolder, "Auto-Saved Ship.craft"))) craftList.Remove(Path.Combine(currentFolder, "Auto-Saved Ship.craft")); // Ignore the Auto-Saved Ship. + subfolders = Directory.GetDirectories(currentFolder).Select(Path.GetFileName).ToList(); + if (currentFolder != baseFolder) subfolders.Insert(0, ".."); + CraftProfileInfo.PrepareCraftMetaFileLoad(); + var thumbURLSubDir = $"/{Path.Combine(facility.ToString(), currentFolder.Substring(baseFolder.Length).Trim('/'))}"; + foreach (var craft in craftList.Keys.ToList()) + { + var craftMeta = Path.Combine(currentFolder, $"{Path.GetFileNameWithoutExtension(craft)}.loadmeta"); + if (File.Exists(craftMeta) && File.GetLastWriteTime(craftMeta) > File.GetLastWriteTime(craft)) // If the loadMeta file exists and has a timestamp that's later than the craft file (because WTF KSP‽), use it, otherwise generate one. + { + craftList[craft].LoadFromMetaFile(craftMeta); + } + else + { + var craftNode = ConfigNode.Load(craft); + craftList[craft].LoadDetailsFromCraftFile(craftNode, craft); + craftList[craft].SaveToMetaFile(craftMeta); + } + var thumbURL = $"/thumbs/{GetPlayerCraftThumbnailName(profile, thumbURLSubDir, Path.GetFileNameWithoutExtension(craft))}"; + craftThumbnails[craft] = ShipConstruction.GetThumbnail(thumbURL); + } + var failedToParse = craftList.Where(kvp => kvp.Value is null || kvp.Value.partNames is null).ToList(); + if (failedToParse.Count > 0) Debug.LogError($"[BDArmory.VesselMover]: Failed to properly parse some loadmeta files:\n{string.Join("\n ", failedToParse)}"); + crewCounts = craftList.ToDictionary(kvp => kvp.Key, kvp => (kvp.Value is null || kvp.Value.partNames is null) ? 0 : kvp.Value.partNames.Where(p => SpawnUtils.PartCrewCounts.ContainsKey(p)).Sum(p => SpawnUtils.PartCrewCounts[p])); + ButtonStyle.stretchHeight = true; + ButtonStyle.fontSize = 24; + SelectedButtonStyle.stretchHeight = true; + SelectedButtonStyle.fontSize = 24; + InfoStyle.fontSize = 14; + InfoStyle.richText = true; + LabelStyle.fontSize = 24; + LabelStyle.alignment = TextAnchor.MiddleCenter; + LabelStyle.normal.textColor = Color.green; + shipNames.Where(kvp => File.Exists(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove any no longer valid ship names. + foreach (var craft in craftList.Keys) + shipNames[craft] = craftList[craft].shipName; + craftListUpdateTimestamp = Time.realtimeSinceStartup; + } + + public void ChangeFolder(EditorFacility facility, string subfolder = null, bool relative = true) + { + if (facility == EditorFacility.None) // Very first time used, default to the VAB if the current vessel was launched from there or fall back to the SPH. + { + facility = FlightDriver.LaunchSiteName == "LaunchPad" ? EditorFacility.VAB : EditorFacility.SPH; + } + if (facility != CustomCraftBrowserDialog.facility) + { + subfolder = null; // Revert to the base folder when changing facilities. + CustomCraftBrowserDialog.facility = facility; + } + baseFolder = Path.GetFullPath(GetShipsPathFor(profile, facility).TrimEnd(['/'])); + if (!Directory.Exists(baseFolder)) + { + var message = $"The base folder for the {facility} doesn't exist! Your KSP install is broken!"; + Debug.LogError($"[BDArmory.VesselMover]: {message}"); + BDACompetitionMode.Instance.competitionStatus.Add(message); + return; + } + if (!relative || string.IsNullOrEmpty(currentFolder)) currentFolder = baseFolder; + if (string.IsNullOrEmpty(subfolder)) + { + currentFolder = baseFolder; + } + else + { + var newFolder = Path.GetFullPath(Path.Combine(currentFolder, subfolder.TrimStart(['/']))); + if (Directory.Exists(newFolder)) currentFolder = newFolder; + else currentFolder = baseFolder; + } + _currentFolder = currentFolder; + displayFolder = currentFolder.Substring(baseFolder.Length - facility.ToString().Length); + UpdateList(); + } + + public bool CheckCurrent() + { + if (profile != HighLogic.SaveFolder) // The user changed saves after having opened the craft browser. Reset to the default. + { + profile = HighLogic.SaveFolder; + baseFolder = null; + displayFolder = null; + currentFolder = null; + ChangeFolder(EditorFacility.None); + return true; + } + if (_currentFolder != currentFolder) + { + ChangeFolder(facility, currentFolder.Substring(baseFolder.Length), false); // Another instance changed the current folder, so switch to match it. + return true; + } + return false; + } + + public static string GetPlayerCraftThumbnailName(string profile, string thumbURLSubDir, string shipName) + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 11) || Versioning.version_major > 1) // Introduced in 1.12 + return GetPlayerCraftThumbnailName_1_12(profile, thumbURLSubDir, shipName); + return $"{profile}{thumbURLSubDir.Replace('/', '_')}_{shipName}"; + } + public static string GetPlayerCraftThumbnailName_1_12(string profile, string thumbURLSubDir, string shipName) => ShipConstruction.GetPlayerCraftThumbnailName(profile, thumbURLSubDir, shipName); + public static string GetShipsPathFor(string profile, EditorFacility facility) + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 11) || Versioning.version_major > 1) // Introduced in 1.12 + return GetShipsPathFor_1_12(profile, facility); + return Path.Combine(KSPUtil.ApplicationRootPath, "saves", profile, "Ships", ShipConstruction.GetShipsSubfolderFor(facility)); + } + public static string GetShipsPathFor_1_12(string profile, EditorFacility facility) => ShipConstruction.GetShipsPathFor(profile, facility); + } + + internal class CraftBrowserMissingThumbnailGenerator : MonoBehaviour + { + static CraftBrowserMissingThumbnailGenerator instance; + public static CraftBrowserMissingThumbnailGenerator Instance + { + get + { + if (instance == null) + { + GameObject gameObject = new() { name = "CraftBrowserMissingThumbnailGenerator" }; + instance = gameObject.AddComponent(); + } + return instance; + } + } + public static bool recurse = false; + + public void GenerateMissingThumbnails(EditorFacility facility) => StartCoroutine(GenerateMissingThumbnailsWorker(facility)); + IEnumerator GenerateMissingThumbnailsWorker(EditorFacility facility) + { + var craftBrowser = new CustomCraftBrowserDialog(); + craftBrowser.ChangeFolder(facility); + var thumbURLRoot = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "thumbs")); + + List folders = [craftBrowser.BaseFolder]; + if (recurse) folders.AddRange(Directory.GetDirectories(craftBrowser.BaseFolder, "*", SearchOption.AllDirectories)); + foreach (var folder in folders) + { + craftBrowser.ChangeFolder(facility, folder, true); + var thumbURLSubDir = $"/{Path.Combine(facility.ToString(), craftBrowser.CurrentFolder.Substring(craftBrowser.BaseFolder.Length).Trim('/'))}"; + bool hasShownMessage = false; + foreach (var craft in craftBrowser.craftList.Keys) + { + var craftInfo = craftBrowser.craftList[craft]; + if (craftInfo.UnavailableShipParts.Count > 0) + { + Debug.Log($"[BDArmory.VesselMover]: Craft {craftInfo.shipName} has missing parts, unable to generate thumbnail."); + continue; + } + var thumbURL = Path.Combine(thumbURLRoot, $"{ShipConstruction.GetPlayerCraftThumbnailName(craftBrowser.GameName, thumbURLSubDir, Path.GetFileNameWithoutExtension(craft))}.png"); // Actual URL. + if (!File.Exists(thumbURL)) + { + if (!hasShownMessage) + { + ScreenMessages.PostScreenMessage($"{StringUtils.Localize("#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingForCraftIn")} {craftBrowser.DisplayFolder}", 5); + hasShownMessage = true; + } + // Load the ship and take a thumbnail of it. + Debug.Log($"[BDArmory.VesselMover]: Generating thumbnail for {craftInfo.shipName} at KSP{thumbURL.Substring(KSPUtil.ApplicationRootPath.Length)}"); + ScreenMessages.PostScreenMessage($"{StringUtils.Localize("#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsGeneratingFor")} {craftInfo.shipName}.", 3); + yield return null; + try + { + EditorLogic.LoadShipFromFile(craft); + ShipConstruction.CaptureThumbnail(EditorLogic.fetch.ship, $"/thumbs", ShipConstruction.GetPlayerCraftThumbnailName(craftBrowser.GameName, thumbURLSubDir, Path.GetFileNameWithoutExtension(craft))); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.VesselMover]: Error capturing thumbnail of {craftInfo.shipName} {e.Message}\n{e.StackTrace}"); + ScreenMessages.PostScreenMessage($"{StringUtils.Localize("#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFailure")} {craftInfo.shipName}", 5); + } + } + } + } + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_CraftBrowser_GenerateMissingThumbnailsFinished"), 5); + EditorLogic.LoadShipFromFile(null); + } + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/VesselSpawner.cs b/BDArmory/VesselSpawning/VesselSpawner.cs new file mode 100644 index 000000000..03fe9a7fd --- /dev/null +++ b/BDArmory/VesselSpawning/VesselSpawner.cs @@ -0,0 +1,530 @@ +using UnityEngine; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Weapons; + +namespace BDArmory.VesselSpawning +{ + /// + /// A static class for doing the actual spawning of a vessel from a craft file into KSP. + /// This is mostly taken from VesselSpawner. + /// For proper vessel placement and orientation (including accounting for control point orientation), the vessel should be immediately set as not landed, + /// then wait a couple of fixed updates for the root part's transform to be assigned and assign this as the vessel's reference transform, then the proper + /// 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 VesselSpawner + { + public static string spawnProbeLocation + { + get + { + if (_spawnProbeLocation != null) return _spawnProbeLocation; + _spawnProbeLocation = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "craft", "SpawnProbe.craft")); // SpaceDock location + if (!File.Exists(_spawnProbeLocation)) _spawnProbeLocation = Path.GetFullPath(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 = 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. + /// + /// + /// + /// + /// + /// + /// out parameter containing the vessel's EditorFacility (VAB/SPH) + /// + /// + public static Vessel SpawnVesselFromCraftFile(string craftURL, Vector3d gpsCoords, float heading, float pitch, float roll, out EditorFacility shipFacility, List crewData = null) + { + VesselData newData = new VesselData(); + + newData.craftURL = craftURL; + newData.latitude = gpsCoords.x; + newData.longitude = gpsCoords.y; + newData.altitude = gpsCoords.z; + + newData.body = FlightGlobals.currentMainBody; + newData.heading = heading; + newData.pitch = pitch; + newData.roll = roll; + newData.orbiting = false; + newData.flagURL = HighLogic.CurrentGame.flagURL; + newData.owned = true; + newData.vesselType = VesselType.Ship; + + newData.crew = new List(); + + 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; + //Set additional info for landed vessels + bool landed = false; + if (!vesselData.orbiting) + { + landed = true; + if (vesselData.altitude == null || vesselData.altitude < 0) + { + vesselData.altitude = 35; + } + + Vector3d pos = vesselData.body.GetRelSurfacePosition(vesselData.latitude, vesselData.longitude, vesselData.altitude.Value); + + vesselData.orbit = new Orbit(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, vesselData.body); + vesselData.orbit.UpdateFromStateVectors(pos, vesselData.body.getRFrmVel(pos), vesselData.body, Planetarium.GetUniversalTime()); + } + else + { + vesselData.orbit.referenceBody = vesselData.body; + } + + ConfigNode[] partNodes; + ShipConstruct shipConstruct = null; + if (!string.IsNullOrEmpty(vesselData.craftURL)) + { + var craftNode = ConfigNode.Load(vesselData.craftURL); + if (craftNode == null) + { + Debug.LogError($"[BDArmory.VesselSpawner]: Failed to load {vesselData.craftURL}."); + return null; + } + shipConstruct = new ShipConstruct(); + if (!shipConstruct.LoadShip(craftNode)) + { + Debug.LogError("[BDArmory.VesselSpawner]: Ship file error!"); + return null; + } + + // Set the name + if (string.IsNullOrEmpty(vesselData.name)) + { + vesselData.name = shipConstruct.shipName; + } + + // Sort the parts into top-down tree order. + shipConstruct.parts = SortPartTree(shipConstruct.parts); + + // Set some parameters that need to be at the part level + uint missionID = (uint)Guid.NewGuid().GetHashCode(); + uint launchID = HighLogic.CurrentGame.launchID++; + foreach (Part p in shipConstruct.parts) + { + p.flightID = ShipConstruction.GetUniqueFlightID(HighLogic.CurrentGame.flightState); + p.missionID = missionID; + p.launchID = launchID; + p.flagURL = vesselData.flagURL ?? HighLogic.CurrentGame.flagURL; + + // Had some issues with this being set to -1 for some ships - can't figure out + // why. End result is the vessel exploding, so let's just set it to a positive + // value. + p.temperature = 1.0; + } + + // Add crew + 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 = 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) // 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; + } + case 2: // All crewable control points plus crewable weapons. + { + crewParts = shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity && (p.FindModuleImplementing() || p.FindModuleImplementing() || ((crewedWeapon = p.FindModuleImplementing()) && crewedWeapon.crewserved))).ToList(); + break; + } + case 3: // All crewable parts. + { + crewParts = shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity).ToList(); + break; + } + default: + throw new IndexOutOfRangeException("Invalid Fill Seats value"); + } + 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). + newCrewMember.courage = 0.5f; //make their G-tolerance identical; 0.5 Courage BadS Pilot yields 20.5G tolerance + 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 ? + 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) + { + 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). + crewMember.courage = 0.5f; + + // 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 + // slightly different format than those required for a ProtoVessel (seriously + // Squad?!?). + ConfigNode empty = new ConfigNode(); + ProtoVessel dummyProto = new ProtoVessel(empty, null); + Vessel dummyVessel = new Vessel(); + dummyVessel.parts = shipConstruct.Parts; + dummyProto.vesselRef = dummyVessel; + + // Create the ProtoPartSnapshot objects and then initialize them + foreach (Part p in shipConstruct.parts) + { + dummyVessel.loaded = false; + p.vessel = dummyVessel; + + dummyProto.protoPartSnapshots.Add(new ProtoPartSnapshot(p, dummyProto, true)); + UnityEngine.Object.Destroy(p.gameObject); // Destroy the prefab. + } + foreach (ProtoPartSnapshot p in dummyProto.protoPartSnapshots) + { + p.storePartRefs(); + } + + // Create the ship's parts + List partNodesL = new List(); + foreach (ProtoPartSnapshot snapShot in dummyProto.protoPartSnapshots) + { + ConfigNode node = new ConfigNode("PART"); + snapShot.Save(node); + partNodesL.Add(node); + } + partNodes = partNodesL.ToArray(); + } + else + { + // Create crew member array + ProtoCrewMember[] crewArray = new ProtoCrewMember[vesselData.crew.Count]; + int i = 0; + foreach (CrewData cd in vesselData.crew) + { + // Create the ProtoCrewMember + ProtoCrewMember crewMember = HighLogic.CurrentGame.CrewRoster.GetNextOrNewKerbal(ProtoCrewMember.KerbalType.Crew); + if (cd.name != null) + { + crewMember.KerbalRef.name = cd.name; + } + 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). + crewMember.courage = 0.5f; + crewArray[i++] = crewMember; + } + + // Create part nodes + uint flightId = ShipConstruction.GetUniqueFlightID(HighLogic.CurrentGame.flightState); + partNodes = new ConfigNode[1]; + partNodes[0] = ProtoVessel.CreatePartNode(vesselData.craftPart.name, flightId, crewArray); + + // Default the size class + //sizeClass = UntrackedObjectClass.A; + + // Set the name + if (string.IsNullOrEmpty(vesselData.name)) + { + vesselData.name = vesselData.craftPart.name; + } + } + + // Create additional nodes + ConfigNode[] additionalNodes = new ConfigNode[0]; + + // Create the config node representation of the ProtoVessel + 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) + { + bool splashed = false;// = landed && terrainHeight < 0.001; + + // Create the config node representation of the ProtoVessel + // Note - flying is experimental, and so far doesn't work + protoVesselNode.SetValue("sit", (splashed ? Vessel.Situations.SPLASHED : landed ? + Vessel.Situations.LANDED : Vessel.Situations.FLYING).ToString()); + protoVesselNode.SetValue("landed", (landed && !splashed).ToString()); + protoVesselNode.SetValue("splashed", splashed.ToString()); + protoVesselNode.SetValue("lat", vesselData.latitude.ToString()); + protoVesselNode.SetValue("lon", vesselData.longitude.ToString()); + protoVesselNode.SetValue("alt", vesselData.altitude.ToString()); + protoVesselNode.SetValue("landedAt", vesselData.body.name); + + // Figure out the additional height to subtract + float lowest = float.MaxValue; + if (shipConstruct != null) + { + foreach (Part p in shipConstruct.parts) + { + foreach (Collider collider in p.GetComponentsInChildren()) + { + if (collider.gameObject.layer != 21 && collider.enabled) + { + lowest = Mathf.Min(lowest, collider.bounds.min.y); + } + } + } + } + else + { + foreach (Collider collider in vesselData.craftPart.partPrefab.GetComponentsInChildren()) + { + if (collider.gameObject.layer != 21 && collider.enabled) + { + lowest = Mathf.Min(lowest, collider.bounds.min.y); + } + } + } + + if (lowest == float.MaxValue) + { + lowest = 0; + } + + // Figure out the surface height and rotation + Quaternion normal = Quaternion.LookRotation((Vector3)vesselData.body.GetRelSurfaceNVector(vesselData.latitude, vesselData.longitude)); + Quaternion rotation = Quaternion.identity; + float heading = vesselData.heading; + if (shipConstruct == null) + { + // Debug.Log("[BDArmory.VesselSpawner]: initial rotation override: null"); + rotation = Quaternion.FromToRotation(Vector3.up, Vector3.back); //FIXME add a check if spawning in null-atmo to have craft spawn horizontal, not nose-down + } + else if (shipConstruct.shipFacility == EditorFacility.SPH) + { + //Debug.Log("[BDArmory.VesselSpawner]: initial rotation override: SPH"); + rotation = Quaternion.FromToRotation(Vector3.forward, Vector3.back); // Orient the SPH vessel upright, facing (vessel.up) south. + rotation = Quaternion.AngleAxis(180f, Vector3.back) * rotation; // Face north (heading 0°). + } + else + { + // Debug.Log("[BDArmory.VesselSpawner]: initial rotation override: VAB"); + rotation = Quaternion.FromToRotation(Vector3.up, Vector3.forward); // Orient the SPH vessel upright, facing (-vessel.forward) south. + rotation = Quaternion.AngleAxis(180f, Vector3.back) * rotation; // Face north (heading 0°). + } + + rotation = Quaternion.AngleAxis(vesselData.roll, Vector3.down) * rotation; // Note: roll direction is inverted. + rotation = Quaternion.AngleAxis(vesselData.pitch, Vector3.right) * rotation; + rotation = Quaternion.AngleAxis(heading, Vector3.forward) * rotation; + + // Set the height and rotation + if (landed || splashed) + { + float hgt = (shipConstruct != null ? shipConstruct.parts[0] : vesselData.craftPart.partPrefab).localRoot.attPos0.y - lowest; + hgt += vesselData.height + 35; + protoVesselNode.SetValue("hgt", hgt.ToString(), true); + } + protoVesselNode.SetValue("rot", KSPUtil.WriteQuaternion(normal * rotation), true); + + // Set the normal vector relative to the surface + Vector3 nrm = (rotation * Vector3.forward); + protoVesselNode.SetValue("nrm", nrm.x + "," + nrm.y + "," + nrm.z, true); + protoVesselNode.SetValue("prst", false.ToString(), true); + } + + // Add vessel to the game + ProtoVessel protoVessel = HighLogic.CurrentGame.AddVessel(protoVesselNode); + + // Set the vessel size (FIXME various other vessel fields appear to not be set, e.g. CoM) + protoVessel.vesselRef.vesselSize = shipConstruct.shipSize; + shipFacility = shipConstruct.shipFacility; + switch (shipFacility) + { + case EditorFacility.SPH: + protoVessel.vesselRef.vesselType = VesselType.Plane; + break; + case EditorFacility.VAB: + protoVessel.vesselRef.vesselType = VesselType.Ship; + break; + default: + break; + } + + // Store the id for later use + vesselData.id = protoVessel.vesselRef.id; + // StartCoroutine(PlaceSpawnedVessel(protoVessel.vesselRef)); + + return protoVessel.vesselRef; + } + + static List SortPartTree(List parts) + { + List Parts = [.. parts.Where(p => p.parent == null)]; // There can be only one. + while (Parts.Count() < parts.Count()) + { + var partsToAdd = parts.Where(p => !Parts.Contains(p) && Parts.Contains(p.parent)); + if (partsToAdd.Count() == 0) + { + Debug.LogError($"[BDArmory.VesselSpawner]: Part count mismatch when sorting the part-tree: {Parts.Count()} vs {parts.Count()}"); + break; + } + Parts.AddRange(partsToAdd); + } + return Parts; + } + + internal class CrewData + { + public string name = null; + public ProtoCrewMember.Gender? gender = null; + public bool addToRoster = true; + + public CrewData() { } + public CrewData(CrewData cd) + { + name = cd.name; + gender = cd.gender; + addToRoster = cd.addToRoster; + } + } + + internal class VesselData + { + public string name = null; + public Guid? id = null; + public string craftURL = null; + public AvailablePart craftPart = null; + public string flagURL = null; + public VesselType vesselType = VesselType.Ship; + public CelestialBody body = null; + public Orbit orbit = null; + public double latitude = 0.0; + public double longitude = 0.0; + public double? altitude = null; + public float height = 0.0f; + public bool orbiting = false; + public bool owned = false; + public List crew = new List(); + public PQSCity pqsCity = null; + public Vector3d pqsOffset = Vector3d.zero; + public float heading = 0f; + public float pitch = 0f; + public float roll = 0f; + } + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/VesselSpawnerBase.cs b/BDArmory/VesselSpawning/VesselSpawnerBase.cs new file mode 100644 index 000000000..a3fb60e8f --- /dev/null +++ b/BDArmory/VesselSpawning/VesselSpawnerBase.cs @@ -0,0 +1,898 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.GameModes; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.UI; +using BDArmory.ModIntegration; + +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) + || (ContinuousSpawning.Instance != null && ContinuousSpawning.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; + [Obsolete("Use ModIntegration.CameraTools.InhibitCameraTools instead.")] public static bool inhibitCameraTools => vesselsSpawning; // [Deprecated] 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; + public 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 static void LogMessageFrom(string derivedClassName, string message, bool toScreen, bool toLog) + { + if (toScreen) BDACompetitionMode.Instance.competitionStatus.Add(message); + if (toLog) Debug.Log($"[BDArmory.{derivedClassName}]: " + message); + } + static 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(preSpawn: true); // Reset competition scores. + } + + // Reset the random seed as KSP restores the random seed from the previous save. + UnityEngine.Random.InitState((int)DateTime.Now.Ticks); + } + #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 spawnDistance, bool spawnAirborne) + { + // Sanitise the viewDistance to at most one third the PRE range. + var preRange = PhysicsRangeExtender.GetPRERange(); + if (preRange < 2.5f * spawnDistance) + { + preRange = 2.5f * spawnDistance; + PhysicsRangeExtender.SetPRERange((int)preRange); + LogMessage($"PRE range is insufficient for the spawn distance ({spawnDistance / 1000:0}km), increasing PRE range to {preRange / 1000:0}km"); + } + var viewDistance = Mathf.Min(1.5f * spawnDistance, preRange / 3); // Try to capture the entire spawn circle, within reason. + 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); + BDArmorySetup.DisableAllFXAndProjectiles(); + } + 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).sqrMagnitude > preRange * preRange) + { 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 && spawnConfig.altitude < 10000) // Not over the ocean or on a surfaceless body and spawning at less than 10k altitude. + { + // 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; + } + } + else + { + yield return waitForFixedUpdate; // Wait a couple of frames so that the floating origin shift has time to do its thing. + yield return waitForFixedUpdate; + } + } + #endregion + + #region Spawning + public int vesselsSpawningCount = 0; + protected string latestSpawnedVesselName = ""; + protected Dictionary spawnedVessels = []; // Vessel name => vessel instance. + protected Dictionary spawnedVesselsTeamIndex = []; // Vessel name => team index + protected Dictionary spawnedVesselPartCounts = []; // Vessel name => part count. + protected Dictionary finalSpawnPositions = []; // Vessel name => final spawn position as geo-coordinates (for later reuse). + protected Dictionary finalSpawnRotations = []; // Vessel name => final spawn rotation (for later reuse). + protected virtual void ResetInternals() + { + // Clear our internal collections and counters. + vesselsSpawningCount = 0; + spawnedVessels.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 = []; + 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. + var spawnBody = FlightGlobals.currentMainBody; + spawnBody.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. + var heightFromTerrain = vessel.GetHeightFromTerrain() - 35f; // The SpawnVesselFromCraftFile routine adds 35m for some reason. + + // Wait until the vessel's part list gets updated. + var vesselName = vessel.vesselName; + 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; + } + vessel.ActiveController().SetSourceURL(vesselSpawnConfig.craftURL); + if (vesselSpawnConfig.deconflictVesselName) SpawnUtils.DeconflictVesselName(vessel, vesselSpawnConfig.reuseURLVesselName); + vesselName = vessel.vesselName; + latestSpawnedVesselName = vesselName; + spawnedVesselsTeamIndex[vesselName] = vesselSpawnConfig.teamIndex; // For specific team assignments. + 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. + vesselSpawnConfig.position = VectorUtils.GetWorldSurfacePostion(craftGeoCoords, spawnBody); // Reacquire the spawn point as floating origin changes may have shifted it. + var ray = new Ray(vesselSpawnConfig.position, -radialUnitVector); + RaycastHit hit; + var distanceToCoMainBody = (ray.origin - spawnBody.transform.position).magnitude; + float distance; + var spawnInOrbit = vesselSpawnConfig.altitude >= spawnBody.MinSafeAltitude(); // Min safe orbital altitude + 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 || spawnInOrbit) ? -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 || spawnInOrbit) ? vessel.ReferenceTransform.up : -vessel.ReferenceTransform.forward, vesselSpawnConfig.direction, localSurfaceNormal), localSurfaceNormal) * vessel.transform.rotation); // Re-orient the vessel to the right direction. + if (vesselSpawnConfig.airborne && !spawnInOrbit && !BDArmorySettings.SF_GRAVITY && !BDArmorySettings.SF_REPULSOR) + { vessel.SetRotation(Quaternion.AngleAxis(-vesselSpawnConfig.pitch, vessel.ReferenceTransform.right) * vessel.transform.rotation); } + // Position + if (spawnBody.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] = VectorUtils.WorldPositionToGeoCoords(vesselSpawnConfig.position, spawnBody); + 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 withInitialVelocity, bool ignoreValidity = false) + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Checking vessel validity", false); + yield return CheckVesselValidity(spawnedVessels, ignoreValidity); + 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, ignoreValidity); + if (spawnFailureReason != SpawnFailureReason.None) 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?) + 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); + var vesselsToPlace = spawnedVessels.Values.ToList(); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 67) vesselsToPlace = vesselsToPlace.Where(vessel => !vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)).ToList(); // Exclude specific vessels (e.g., some piñatas). + yield return PlaceSpawnedVessels(vesselsToPlace); + 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); + foreach (var vessel in spawnedVessels.Select(v => v.Value)) + SpawnUtils.AirborneActivation(vessel, withInitialVelocity); + } + 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); + foreach (var vessel in spawnedVessels.Values.Where(v => v.GetName().Contains(BDArmorySettings.PINATA_NAME))) + { + LogMessage($"Spawning Piñata: {vessel.GetName()}"); // Warn about spawning piñatas in case of poorly named craft. + SpawnUtils.ApplyRWP(vessel); + } + 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, bool continueAnyway) + { + var startTime = Time.time; + Dictionary invalidVessels; + // Check that the spawned vessels are valid craft + do + { + yield return waitForFixedUpdate; + ResetFinalSpawnPositionsAndRotations(spawnedVessels, finalSpawnPositions, finalSpawnRotations); // Don't drop the vessels while we're waiting. + 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); + if (!continueAnyway) 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, bool continueAnyway) + { + var vesselsToCheck = vessels.Where(kvp => kvp.Value.ActiveController().WM != null).Select(kvp => kvp.Key).ToList(); // Only check the vessels that actually have weapon managers. + var allWeaponManagersAssigned = false; + var startTime = Time.time; + do + { + yield return waitForFixedUpdate; + ResetFinalSpawnPositionsAndRotations(spawnedVessels, finalSpawnPositions, finalSpawnRotations); // Don't drop the vessels while we're waiting. + + // 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)})"))); + if (!continueAnyway) + { + 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 = vessels[vesselName].ActiveController().WM; + 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); + if (!continueAnyway) 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! + SpawnUtils.CheckForRenamedVessels(vessels); + foreach (var vesselName in vessels.Keys) + { + if (vessels[vesselName] == null) continue; + vessels[vesselName].SetPosition(VectorUtils.GetWorldSurfacePostion(positions[vesselName], FlightGlobals.currentMainBody)); + vessels[vesselName].SetRotation(rotations[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; + } + #endregion + + #region Single vessel post-spawn functions + /// + /// Get the first vessel corresponding to the craftURL from the SpawnUtils.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 = SpawnUtils.GetNameOfFirstSpawnedVesselFrom(craftURL); + if (string.IsNullOrEmpty(vesselName) || !spawnedVessels.ContainsKey(vesselName)) + { + spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; + if (!string.IsNullOrEmpty(vesselName)) + { + foreach (var vessel in FlightGlobals.Vessels) // If the vessel was partially spawned, find and remove it. + { + if (vessel == null) continue; + if (vessel.vesselName == vesselName) RemoveVessel(vessel); + } + 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, bool withInitialVelocity, bool revertSpawnCamera = true) + { + 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(VectorUtils.GetWorldSurfacePostion(finalSpawnPositions[vesselName], FlightGlobals.currentMainBody)); + vessel.SetRotation(finalSpawnRotations[vesselName]); + + // Undo any camera adjustment and reset the camera distance. This has an internal check so that it only occurs once. + if (revertSpawnCamera) 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 while lowering craft to terrain: {partCount - SpawnUtils.PartCount(vessel)}"); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + if (vessel != null) RemoveVessel(vessel); + yield break; + } + } + else SpawnUtils.AirborneActivation(vessel, withInitialVelocity); + + // 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); + + foreach (var v in spawnedVessels.Values.Where(v => v.GetName().Contains(BDArmorySettings.PINATA_NAME))) + { + LogMessage($"Spawning Piñata: {vessel.GetName()}"); // Warn about spawning piñatas in case of poorly named craft. + SpawnUtils.ApplyRWP(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(VectorUtils.GetWorldSurfacePostion(finalSpawnPositions[vessel.vesselName], FlightGlobals.currentMainBody)); // 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 = vessel.ActiveController().WM; + var vesselName = vessel.vesselName; // In case it disappears. + var sourceURL = weaponManager.SourceVesselURL; + var assigned = weaponManager != null && LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Contains(weaponManager); + if (vessel.PatchedConicsAttached) vessel.DetachPatchedConicsSolver(); // Detach the patched conic solver to avoid it calculating NaN orbits. + while (!assigned && Time.time - startTime < 10 && vessel != null) + { + yield return waitForFixedUpdate; + int partCount = SpawnUtils.PartCount(vessel); + if (vessel == null || partCount != spawnedVesselPartCounts[vesselName]) + { + if (vessel == null) LogMessage($"{vesselName} disappeared while waiting for the weapon manager to appear in the Vessel Switcher."); + else LogMessage($"Part-count of {vesselName} changed after spawning: {spawnedVesselPartCounts[vesselName] - partCount}"); + spawnedVesselPartCounts[vesselName] = partCount; // Reset the part count to avoid spamming. + // This can happen if a vessel with fighters spawns, but the parent vessel gets detached for some reason, leaving the fighters in a weird state that breaks KSP. + foreach (var otherVessel in FlightGlobals.Vessels.ToList()) + { + if (otherVessel == null || otherVessel == vessel) continue; + var wm = otherVessel.ActiveController().WM; + if (wm != null && wm.SourceVesselURL == sourceURL) + { + LogMessage($"Removing fighter craft '{otherVessel.GetName()}' due to spawn failure of parent craft '{vesselName}'."); + RemoveVessel(otherVessel); + } + } + if (!BDArmorySettings.COMPETITION_START_DESPITE_FAILURES) + { + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + } + if (vessel != null) + { + if (weaponManager == null) weaponManager = vessel.ActiveController().WM; + assigned = weaponManager != null && LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Contains(weaponManager); + vessel.SetPosition(VectorUtils.GetWorldSurfacePostion(finalSpawnPositions[vessel.vesselName], FlightGlobals.currentMainBody)); // Prevent the vessel from falling. + } + } + if (!assigned) + { + LogMessage($"Timed out waiting for {vesselName}'s weapon manager to appear in the Vessel Switcher."); + spawnFailureReason = SpawnFailureReason.TimedOut; + if (vessel != null) RemoveVessel(vessel); + } + } + + /// + /// [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; + } + #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 (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 + } +} diff --git a/BDArmory/VesselSpawning/_description b/BDArmory/VesselSpawning/_description new file mode 100644 index 000000000..342e0d8da --- /dev/null +++ b/BDArmory/VesselSpawning/_description @@ -0,0 +1,2 @@ +Vessel spawning stuff. +- FIXME There is some overlap with SpawnStrategies that will presumably be merged/resolved at some point once the spawn strategies become more fully developed. \ No newline at end of file diff --git a/BDArmory/Modules/BDAdjustableRail.cs b/BDArmory/WeaponMounts/BDAdjustableRail.cs similarity index 87% rename from BDArmory/Modules/BDAdjustableRail.cs rename to BDArmory/WeaponMounts/BDAdjustableRail.cs index 2539c539c..7e3c36a00 100644 --- a/BDArmory/Modules/BDAdjustableRail.cs +++ b/BDArmory/WeaponMounts/BDAdjustableRail.cs @@ -1,8 +1,9 @@ +using BDArmory.Weapons.Missiles; using System.Collections; using System.Collections.Generic; using UnityEngine; -namespace BDArmory.Modules +namespace BDArmory.WeaponMounts { public class BDAdjustableRail : PartModule { @@ -12,6 +13,7 @@ public class BDAdjustableRail : PartModule Transform railLengthTransform; Transform railHeightTransform; + Transform launchTransform; [KSPField] public string stackNodePosition; @@ -22,6 +24,13 @@ public override void OnStart(StartState state) railLengthTransform = part.FindModelTransform("Rail"); railHeightTransform = part.FindModelTransform("RailSleeve"); + var launcher = part.FindModuleImplementing(); + if (launcher) + { + var tempTransform = part.FindModelTransform(launcher.launchTransformName); + if (tempTransform.IsChildOf(railLengthTransform)) + launchTransform = tempTransform; + } railLengthTransform.localScale = new Vector3(1, railLength, 1); railHeightTransform.localPosition = new Vector3(0, railHeight, 0); @@ -47,7 +56,7 @@ void ParseStackNodePosition() IEnumerator DelayedUpdateStackNode() { - yield return null; + yield return new WaitForFixedUpdate(); UpdateStackNode(false); } @@ -90,6 +99,8 @@ public void IncreaseLength() { railLength = Mathf.Clamp(railLength + 0.2f, 0.4f, 2f); railLengthTransform.localScale = new Vector3(1, railLength, 1); + if (launchTransform != null) + launchTransform.localScale = new Vector3(1, 1 / railLength, 1); List.Enumerator sym = part.symmetryCounterparts.GetEnumerator(); while (sym.MoveNext()) { @@ -104,6 +115,8 @@ public void DecreaseLength() { railLength = Mathf.Clamp(railLength - 0.2f, 0.4f, 2f); railLengthTransform.localScale = new Vector3(1, railLength, 1); + if (launchTransform != null) + launchTransform.localScale = new Vector3(1, 1 / railLength, 1); List.Enumerator sym = part.symmetryCounterparts.GetEnumerator(); while (sym.MoveNext()) { @@ -125,6 +138,8 @@ public void UpdateLength(float length) { railLength = length; railLengthTransform.localScale = new Vector3(1, railLength, 1); + if (launchTransform != null) + launchTransform.localScale = new Vector3(1, 1 / railLength, 1); } public void UpdateStackNode(bool updateChild) diff --git a/BDArmory/WeaponMounts/BDDeployableRail.cs b/BDArmory/WeaponMounts/BDDeployableRail.cs new file mode 100644 index 000000000..1ecdc6701 --- /dev/null +++ b/BDArmory/WeaponMounts/BDDeployableRail.cs @@ -0,0 +1,454 @@ +using System.Collections; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; +using BDArmory.Extensions; + +namespace BDArmory.WeaponMounts +{ + public class BDDeployableRail : PartModule + { + [KSPField] + public string deployAnimName = "deployAnim"; + AnimationState deployState; + + public Coroutine deployRoutine; + public Coroutine retractRoutine; + + Transform deployTransform; + [KSPField] + public string deployTransformName = "deployTransform"; + + public bool IsDeployed => (deployed && !deploying) || (!deployed && deploying); // Deploying or being deployed. !IsDeployed = retracted or retracting. + bool deployed = false; + bool deploying = false; + [KSPField] public float rotationDelay = 0.15f; + + [KSPField] + public bool hideMissiles = false; + + public int missileCount; + MissileLauncher[] missileChildren; + Transform[] missileTransforms; + Transform[] missileReferenceTransforms; + + public MissileLauncher nextMissile; + + Dictionary comOffsets; + + bool rdyToFire; + bool setupComplete = false; + public bool readyToFire + { + get { return rdyToFire; } + } + MissileLauncher rdyMissile; + public MissileLauncher readyMissile + { + get { return rdyMissile; } + } + + MissileFire WeaponManager + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; + } + } + 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() + { + UpdateMissileChildren(); + DeployRail(false); + } + public override void OnStart(StartState state) + { + base.OnStart(state); + + part.force_activate(); + setupComplete = false; + deployTransform = part.FindModelTransform(deployTransformName); + deployState = GUIUtils.SetUpSingleAnimation(deployAnimName, part); + + deployState.enabled = true; + deployState.speed = 0; + deployState.normalizedTime = 1; + deployed = true; + + UpdateMissileChildren(); + + if (HighLogic.LoadedSceneIsFlight) + { + //DisableRail(); //In SPH, missiletransforms got, then retracting works fine. in flight, transforms got, and retracting moves it a littlebit, then deploying reveals an offset in where the transforms are. wth + //DeployRail(true); //this works when called manually later, but not as part of initial spawn... + //...but does need to occur before RCS shapshot takes place. hrm. + StartCoroutine(OnStartDeploy()); + } + //setupComplete = true; + } + + public void DeployRail(bool externallycalled, Toggle state = Toggle.Toggle) + { + switch (state) + { + case Toggle.Toggle: // This allows interrupting deploy/retract. + StopRoutines(); // StopRoutines sets deployed taking into account deploying in progress. + if (!deployed) + deployRoutine = StartCoroutine(Deploy()); + else + retractRoutine = StartCoroutine(Retract()); + break; + case Toggle.On: + if (IsDeployed) return; // Already deployed or deploying. + StopRoutines(); + deployRoutine = StartCoroutine(Deploy()); + break; + case Toggle.Off: + if (!IsDeployed) return; // Already retracted or retracting. + StopRoutines(); + retractRoutine = StartCoroutine(Retract()); + break; + } + if (externallycalled) return; + using List.Enumerator p = part.symmetryCounterparts.GetEnumerator(); + while (p.MoveNext()) + { + if (p.Current == null || p.Current == part) continue; + var rail = p.Current.FindModuleImplementing(); + rail.UpdateMissileChildren(); + rail.DeployRail(true, state); + } + } + public IEnumerator OnStartDeploy() + { + 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, Toggle.Off); // Close bays on flight startup + } + public IEnumerator Deploy() + { + deployState.enabled = true; + deployState.speed = 1; + deploying = true; + for (int i = 0; i < missileChildren.Length; i++) + { + if (!missileTransforms[i] || !missileChildren[i] || missileChildren[i].HasFired) continue; + Part missilePart = missileChildren[i].part; + missilePart.ShieldedFromAirstream = false; + if (hideMissiles) missilePart.SetOpacity(1); + } + while (deployState.normalizedTime < 1) + { + UpdateChildrenPos(); + yield return new WaitForFixedUpdate(); + } + deployState.normalizedTime = 1; + deployState.speed = 0; + deployState.enabled = false; + deployed = true; + deploying = false; + if (HighLogic.LoadedSceneIsFlight) + { + yield return new WaitForSecondsFixed(rotationDelay); + rdyToFire = true; + } + if (HighLogic.LoadedSceneIsEditor) + { + //have attachnode toggle on when deployed, and off when stowed + using List.Enumerator node = part.attachNodes.GetEnumerator(); + while (node.MoveNext()) + { + if (node.Current.id.ToLower().Contains("rail")) + { + node.Current.nodeType = AttachNode.NodeType.Stack; + node.Current.radius = 1f; + } + } + } + } + public IEnumerator Retract() + { + if (HighLogic.LoadedSceneIsEditor) + { + //have attachnode toggle on when deployed, and off when stowed + using (List.Enumerator node = part.attachNodes.GetEnumerator()) + while (node.MoveNext()) + { + if (node.Current.id.ToLower().Contains("rail")) + { + node.Current.nodeType = AttachNode.NodeType.Dock; + node.Current.radius = 0.001f; + } + } + } + rdyToFire = false; + deployState.enabled = true; + deployState.speed = -1; + deploying = true; + while (deployState.normalizedTime > 0) + { + UpdateChildrenPos(); + yield return new WaitForFixedUpdate(); + } + deployState.normalizedTime = 0; + deployState.speed = 0; + deployState.enabled = false; + deployed = false; + deploying = false; + setupComplete = true; + for (int i = 0; i < missileChildren.Length; i++) + { + if (!missileTransforms[i] || !missileChildren[i]) continue; + Part missilePart = missileChildren[i].part; + missilePart.ShieldedFromAirstream = true; + if (hideMissiles) missilePart.SetOpacity(0); + } + } + public void StopRoutines() + { + if (deploying) deployed = deployRoutine != null; // If partway through deploying, consider it as fully deployed/retracted for toggle logic. + deploying = false; + + if (retractRoutine != null) + { + StopCoroutine(retractRoutine); + retractRoutine = null; + } + + if (deployRoutine != null) + { + StopCoroutine(deployRoutine); + deployRoutine = null; + } + } + public void EnableRail() + { + if (!setupComplete) return; + DeployRail(true, Toggle.On); + } + public void DisableRail() + { + if (!setupComplete) return; + DeployRail(true, Toggle.Off); + } + + public void UpdateChildrenPos() + { + /* + using (List.Enumerator p = part.children.GetEnumerator()) + while (p.MoveNext()) + { + if (p.Current == null) continue; + Transform mTf = p.Current.FindModelTransform("missileTransform"); + if (mTf == null) continue; + mTf.position = deployTransform.position; + mTf.rotation = deployTransform.rotation; + } + */ + if (missileCount == 0) + { + return; + } + + for (int i = 0; i < missileChildren.Length; i++) + { + 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 + //missileTransforms[i].localScale = new Vector3(scaleVector, scaleVector, 1); + Part missilePart = missileChildren[i].part; + Vector3 newCoMOffset = + missilePart.transform.InverseTransformPoint( + missileTransforms[i].TransformPoint(comOffsets[missilePart])); + missilePart.CoMOffset = newCoMOffset; + } + } + public void UpdateMissileChildren() + { + missileCount = 0; + + if (comOffsets == null) + { + comOffsets = new Dictionary(); + } + + if (missileReferenceTransforms != null) + { + for (int i = 0; i < missileReferenceTransforms.Length; i++) + { + if (missileReferenceTransforms[i]) + { + Destroy(missileReferenceTransforms[i].gameObject); + } + } + } + + List msl = new List(); + 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; + + MissileLauncher ml = child.Current.FindModuleImplementing(); + + if (!ml) continue; + + 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++) + { + 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); + + ml.MissileReferenceTransform = mTf; + ml.deployableRail = this; + + ml.decoupleForward = false; + ml.dropTime = Mathf.Max(ml.dropTime, 0.2f); + + if (!comOffsets.ContainsKey(ml.part)) + { + comOffsets.Add(ml.part, ml.part.CoMOffset); + } + missileCount++; + } + + missileChildren = msl.ToArray(); + missileCount = missileChildren.Length; + missileTransforms = mtfl.ToArray(); + missileReferenceTransforms = mrl.ToArray(); //one of these, either the missile transform, or the deploytransform, is getting offset a bit + } + + public void PrepMissileForFire(MissileLauncher ml) + { + int index = IndexOfMissile(ml); + + if (index >= 0) + { + PrepMissileForFire(index); + } + } + + void PrepMissileForFire(int index) + { + missileTransforms[index].localPosition = Vector3.zero; + missileTransforms[index].localRotation = Quaternion.identity; + missileChildren[index].part.partTransform.position = missileReferenceTransforms[index].position; + missileChildren[index].part.partTransform.rotation = missileReferenceTransforms[index].rotation; + + missileChildren[index].part.CoMOffset = comOffsets[missileChildren[index].part]; + } + + public void FireMissile(int missileIndex, Vessel targetVessel, MissileFire.TargetData targetData = null) //this is causing it not to fire, determine how missileindex is assigned + { + if (!readyToFire) return; + + if (missileIndex < missileCount && missileChildren != null && missileChildren[missileIndex] != null) + { + PrepMissileForFire(missileIndex); + + var wm = WeaponManager; + if (wm) + { + wm.SendTargetDataToMissile(missileChildren[missileIndex], targetVessel, true, targetData, true); + wm.PreviousMissile = missileChildren[missileIndex]; + } + + missileChildren[missileIndex].FireMissile(); + + rdyMissile = null; + + UpdateMissileChildren(); + + if (wm) // If the primary WM changes, the list will automatically update. + { + wm.UpdateList(); + } + } + } + + public void FireMissile(MissileLauncher ml, Vessel targetVessel, MissileFire.TargetData targetData = null) + { + if (!readyToFire) + { + return; + } + + int index = IndexOfMissile(ml); + if (index >= 0) + { + FireMissile(index, targetVessel, targetData); + } + } + + private int IndexOfMissile(MissileLauncher ml) + { + if (missileCount == 0) return -1; + + for (int i = 0; i < missileCount; i++) + { + if (missileChildren[i] && missileChildren[i] == ml) + { + return i; + } + } + return -1; + } + public bool ContainsMissileOfType(MissileLauncher ml) + { + if (!ml) return false; + if (missileCount == 0) return false; + + for (int i = 0; i < missileCount; i++) + { + if ((missileChildren[i]) && missileChildren[i].part.name == ml.part.name) + { + return true; + } + } + return false; + } + } +} diff --git a/BDArmory/Modules/BDRotaryRail.cs b/BDArmory/WeaponMounts/BDRotaryRail.cs similarity index 86% rename from BDArmory/Modules/BDRotaryRail.cs rename to BDArmory/WeaponMounts/BDRotaryRail.cs index 49b02ccfd..fd776bb8d 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 { @@ -66,25 +71,16 @@ public MissileLauncher readyMissile get { return rdyMissile; } } - MissileFire wm; - - public MissileFire weaponManager + MissileFire WeaponManager { get { - if (wm && wm.vessel == vessel) return wm; - wm = null; - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - wm = mf.Current; - break; - } - mf.Dispose(); - return wm; + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; } } + MissileFire _weaponManager; [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_RailsPlus")]//Rails++ public void RailsPlus() @@ -265,7 +261,7 @@ public void MoveEndStackNode(float offset) IEnumerator DelayedMoveStackNode(float offset) { - yield return null; + yield return new WaitForFixedUpdate(); MoveEndStackNode(offset); } @@ -274,24 +270,22 @@ void UpdateRails(int railAmount) if (rails.Count == 0) { rails.Add(part.FindModelTransform("railTransform")); - IEnumerator t = part.FindModelTransforms("newRail").AsEnumerable().GetEnumerator(); - while (t.MoveNext()) - { - if (t.Current == null) continue; - rails.Add(t.Current); - } - t.Dispose(); + using (var t = part.FindModelTransforms("newRail").AsEnumerable().GetEnumerator()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + rails.Add(t.Current); + } } for (int i = 1; i < rails.Count; i++) { - IEnumerator t = rails[i].GetComponentsInChildren().AsEnumerable().GetEnumerator(); - while (t.MoveNext()) - { - if (t.Current == null) continue; - t.Current.name = "deleted"; - } - t.Dispose(); + using (var t = rails[i].GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + t.Current.name = "deleted"; + } Destroy(rails[i].gameObject); } @@ -364,6 +358,18 @@ public override void OnStart(StartState state) RotateToIndex(railIndex, true); } + void OnDestroy() + { + if (rails != null) + { + foreach (var rail in rails) + { + if (rail != null && rail.gameObject != null) + { Destroy(rail.gameObject); } + } + } + } + void OnAttach() { UpdateRails(Mathf.RoundToInt(numberOfRails)); @@ -376,7 +382,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; } @@ -456,6 +462,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; @@ -485,40 +492,34 @@ 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) { StopCoroutine(rotationRoutine); } - // if(railIndex == index && readyToFire) return; - + nextMissile = null; if (missileCount > 0) { - if (railToMissileIndex.ContainsKey(index)) + var railCount = Mathf.RoundToInt(numberOfRails); + for (int i = index; i < index + numberOfRails; ++i) { - nextMissile = missileChildren[railToMissileIndex[index]]; + if (railToMissileIndex.ContainsKey(index % railCount) && (nextMissile = missileChildren[railToMissileIndex[index % railCount]]) != null) + { + rotationRoutine = StartCoroutine(RotateToIndexRoutine(index, instant)); + return; + } } + UpdateMissileChildren(); //missile destroyed before it could be fired, remove from count + Debug.LogWarning("[BDArmory.BDRotaryRail]: No missiles found, but missile count is non-zero."); } - else - { - nextMissile = null; - } - - if (!nextMissile && missileCount > 0) - { - RotateToIndex(Mathf.RoundToInt(Mathf.Repeat(index + 1, numberOfRails)), instant); - return; - } - - rotationRoutine = StartCoroutine(RotateToIndexRoutine(index, instant)); } Coroutine rotationRoutine; @@ -529,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); @@ -564,7 +565,8 @@ IEnumerator RotateToIndexRoutine(int index, bool instant) rdyToFire = true; nextMissile = null; - if (weaponManager) + var wm = WeaponManager; + if (wm) { if (wm.weaponIndex > 0 && wm.selectedWeapon.GetPart().name == rdyMissile.part.name) { @@ -579,7 +581,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; } @@ -591,13 +593,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; @@ -613,7 +615,7 @@ void PrepMissileForFire(int index) missileChildren[index].rotaryRail = this; } - public void FireMissile(int missileIndex) + public void FireMissile(int missileIndex, Vessel targetVessel, MissileFire.TargetData targetData = null) { int nextRailIndex = 0; @@ -633,9 +635,11 @@ public void FireMissile(int missileIndex) PrepMissileForFire(missileIndex); - if (weaponManager) + var wm = WeaponManager; + if (wm) { - wm.SendTargetDataToMissile(missileChildren[missileIndex]); + wm.SendTargetDataToMissile(missileChildren[missileIndex], targetVessel, true, targetData, true); + wm.PreviousMissile = missileChildren[missileIndex]; } string firedMissileName = missileChildren[missileIndex].part.name; @@ -648,9 +652,9 @@ public void FireMissile(int missileIndex) nextRailIndex = Mathf.RoundToInt(Mathf.Repeat(missileToRailIndex[missileIndex] + 1, numberOfRails)); - UpdateMissileChildren(); + if (!missileChildren[missileIndex].reloadableRail) UpdateMissileChildren(); - if (wm) + if (wm) // If the primary WM changes, the list will automatically update. { wm.UpdateList(); } @@ -671,7 +675,7 @@ IEnumerator RotateToIndexAtEndOfFrame(int index, bool instant) RotateToIndex(index, instant); } - public void FireMissile(MissileLauncher ml) + public void FireMissile(MissileLauncher ml, Vessel targetVessel, MissileFire.TargetData targetData = null) { if (!readyToFire || ml != readyMissile) { @@ -681,12 +685,12 @@ public void FireMissile(MissileLauncher ml) int index = IndexOfMissile(ml); if (index >= 0) { - //Debug.Log("Firing missile index: " + index); - FireMissile(index); + //Debug.Log("[BDArmory.BDRotaryRail]: Firing missile index: " + index); + FireMissile(index, targetVessel, targetData); } 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."); } } @@ -760,7 +764,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(); @@ -797,7 +801,7 @@ public void UpdateMissileChildren() UpdateIndexDictionary(); } - void UpdateMissilePositions() + public void UpdateMissilePositions() { if (missileCount == 0) { @@ -806,7 +810,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 64% rename from BDArmory/Modules/MissileTurret.cs rename to BDArmory/WeaponMounts/MissileTurret.cs index d4987f225..a89f9bc1a 100644 --- a/BDArmory/Modules/MissileTurret.cs +++ b/BDArmory/WeaponMounts/MissileTurret.cs @@ -1,11 +1,18 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Guidances; 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; +using BDArmory.Targeting; +using BDArmory.Extensions; + +namespace BDArmory.WeaponMounts { public class MissileTurret : PartModule { @@ -14,7 +21,11 @@ public class MissileTurret : PartModule [KSPField] public int turretID = 0; - ModuleTurret turret; + public ModuleTurret turret; + + public MissileLauncher missilepod; + + public MissileBase activeMissile; [KSPField(guiActive = true, guiName = "#LOC_BDArmory_TurretEnabled")] public bool turretEnabled;//Turret Enabled @@ -26,6 +37,14 @@ public class MissileTurret : PartModule UI_Toggle(scene = UI_Scene.Editor)] public bool autoReturn = true; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TurretLoft"), + UI_Toggle(scene = UI_Scene.All)] + public bool turretLoft = false; // Turret fires at a lofted trajectory + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TurretLoftFac"), + UI_FloatRange(minValue = 0, maxValue = 1, stepIncrement = 0.05f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.Editor)] + public float turretLoftFac = 0.5f; // Factor for optimumVelocity for lofting, lower means a more lofted trajectory + bool hasReturned = true; [KSPField] public float railLength = 3; @@ -40,6 +59,8 @@ public class MissileTurret : PartModule Dictionary comOffsets; public bool slaved; + public bool slavedGuard = false; + public bool manuallyControlled = false; public Vector3 slavedTargetPosition; @@ -54,10 +75,21 @@ public class MissileTurret : PartModule [KSPField] public bool mouseControllable = true; + [KSPField] public bool deployBlocksReload = false; // Turret must stow/"undeploy" itself before reloading + [KSPField] public bool deployBlocksYaw = false; // Turret must deploy before yawing, turret must return to yaw standby position to stow/"undeploy". + [KSPField] public bool deployBlocksPitch = false; // Turret must deploy before pitching, turret must return to pitch standby position to stow/"undeploy". + public bool isReloading = false; + [KSPField] public bool startsDeployed = false; //Turret starts in deployed position and only uses deploy anim for relaoding. TODO: proper reload anim support for turrets independent of deployAnim + //animation [KSPField] public string deployAnimationName; AnimationState deployAnimState; - bool hasDeployAnimation; + public bool hasDeployAnimation; + + public bool isDeployed() + { + return hasDeployAnimation && deployAnimState.normalizedTime > 0; + } [KSPField] public float deployAnimationSpeed = 1; bool editorDeployed; Coroutine deployAnimRoutine; @@ -65,37 +97,28 @@ public class MissileTurret : PartModule //special [KSPField] public bool activeMissileOnly = false; - MissileFire wm; - - public MissileFire weaponManager + MissileFire WeaponManager { get { - if (wm && wm.vessel == vessel) return wm; - wm = null; - - List.Enumerator mf = vessel.FindPartModulesImplementing().GetEnumerator(); - while (mf.MoveNext()) - { - if (mf.Current == null) continue; - wm = mf.Current; - break; - } - mf.Dispose(); - return wm; + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; } } + 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; @@ -104,15 +127,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; @@ -121,14 +141,15 @@ IEnumerator DeployAnimation(bool forward) deployAnimState.speed = 0; } - public void EnableTurret() + public void EnableTurret(MissileBase currMissile, bool manualControl) { if (!HighLogic.LoadedSceneIsFlight) { return; } - if (returnRoutine != null) + activeMissile = currMissile; + if (!(isReloading && deployBlocksReload && hasDeployAnimation) && returnRoutine != null) { StopCoroutine(returnRoutine); returnRoutine = null; @@ -136,6 +157,7 @@ public void EnableTurret() turretEnabled = true; hasReturned = false; + manuallyControlled = manuallyControlled |= manualControl; if (hasAttachedRadar) { @@ -148,7 +170,7 @@ public void EnableTurret() Events["ReturnTurret"].guiActive = false; } - if (hasDeployAnimation) + if ((hasDeployAnimation && !startsDeployed) && !(isReloading && deployBlocksReload)) { if (deployAnimRoutine != null) { @@ -162,10 +184,11 @@ public void EnableTurret() public void DisableTurret() { turretEnabled = false; + activeMissile = null; + manuallyControlled = false; if (autoReturn) { - hasReturned = true; if (returnRoutine != null) { StopCoroutine(returnRoutine); @@ -175,33 +198,23 @@ public void DisableTurret() if (hasAttachedRadar) { - attachedRadar.lockingYaw = true; - attachedRadar.lockingPitch = true; + attachedRadar.lockingYaw = !(hasDeployAnimation && deployBlocksYaw && disableRadarYaw); + attachedRadar.lockingPitch = !(hasDeployAnimation && deployBlocksPitch && disableRadarPitch); } if (!autoReturn) { Events["ReturnTurret"].guiActive = true; } - - if (hasDeployAnimation) - { - if (deployAnimRoutine != null) - { - StopCoroutine(deployAnimRoutine); - } - - deployAnimRoutine = StartCoroutine(DeployAnimation(false)); - } } [KSPEvent(guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ReturnTurret")]//Return Turret public void ReturnTurret() { - if (!turretEnabled) + if (!turretEnabled || isReloading) { + if (returnRoutine != null) StopCoroutine(returnRoutine); returnRoutine = StartCoroutine(ReturnRoutine()); - hasReturned = true; } } @@ -220,24 +233,52 @@ public void EditorToggleAnimation() IEnumerator ReturnRoutine() { - if (turretEnabled) + if (turretEnabled && !isReloading) { hasReturned = false; yield break; } - yield return new WaitForSeconds(0.25f); + hasReturned = true; + + bool retract = isDeployed() && ((!turretEnabled && !startsDeployed) || (isReloading && deployBlocksReload)); + + if (retract && !(deployBlocksYaw || deployBlocksPitch)) + { + if (deployAnimRoutine != null) + { + StopCoroutine(deployAnimRoutine); + } + + deployAnimRoutine = StartCoroutine(DeployAnimation(false)); + } + + yield return new WaitForSecondsFixed(0.25f); while (pausingAfterShot) { yield return new WaitForFixedUpdate(); } - while (turret != null && !turret.ReturnTurret()) + // If the turret is enabled, then we're here because we're reloading, so we only need to return the turret's yaw/pitch if it's blocking the reload. + bool pitch = !turretEnabled || (deployBlocksPitch && deployBlocksReload); + bool yaw = !turretEnabled || (deployBlocksYaw && deployBlocksReload); + + while (turret != null && !turret.ReturnTurret(pitch, yaw)) { UpdateMissilePositions(); yield return new WaitForFixedUpdate(); } + + if (retract && (deployBlocksYaw || deployBlocksPitch)) + { + if (deployAnimRoutine != null) + { + StopCoroutine(deployAnimRoutine); + } + + deployAnimRoutine = StartCoroutine(DeployAnimation(false)); + } } public override void OnStart(StartState state) @@ -250,8 +291,8 @@ public override void OnStart(StartState state) if (!string.IsNullOrEmpty(deployAnimationName)) { hasDeployAnimation = true; - deployAnimState = Misc.Misc.SetUpSingleAnimation(deployAnimationName, part); - if (state == StartState.Editor) + deployAnimState = GUIUtils.SetUpSingleAnimation(deployAnimationName, part); + if (state == StartState.Editor && !startsDeployed) { Events["EditorToggleAnimation"].guiActiveEditor = true; } @@ -268,10 +309,23 @@ public override void OnStart(StartState state) break; } tur.Dispose(); - + List.Enumerator mml = part.FindModulesImplementing().GetEnumerator(); + while (mml.MoveNext()) + { + if (mml.Current == null) continue; + missilepod = mml.Current; + break; + } + mml.Dispose(); attachedRadar = part.FindModuleImplementing(); if (attachedRadar) hasAttachedRadar = true; + if (hasAttachedRadar && hasDeployAnimation) + { + attachedRadar.lockingYaw = !deployBlocksYaw; + attachedRadar.lockingPitch = !deployBlocksPitch; + } + finalTransform = part.FindModelTransform(finalTransformName); UpdateMissileChildren(); @@ -286,12 +340,23 @@ public override void OnStart(StartState state) public override void OnFixedUpdate() { base.OnFixedUpdate(); - if (turretEnabled) { - hasReturned = false; + if (!isReloading) + { + if (hasDeployAnimation && deployBlocksReload && !(deployAnimState.normalizedTime > 0)) + { + if (deployAnimRoutine != null) + { + StopCoroutine(deployAnimRoutine); + } + + deployAnimRoutine = StartCoroutine(DeployAnimation(true)); + } + hasReturned = false; + } - if (missileCount == 0) + if ((missilepod == null && missileCount == 0) || (missilepod != null && missilepod.multiLauncher.missileSpawner.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) { DisableTurret(); return; @@ -307,18 +372,16 @@ public override void OnFixedUpdate() } else { - if (Quaternion.FromToRotation(finalTransform.forward, turret.yawTransform.parent.parent.forward) != + if (Quaternion.FromToRotation(finalTransform.forward, turret.yawTransform.parent.parent.forward) != Quaternion.identity) { UpdateMissilePositions(); } - if (autoReturn && !hasReturned) { DisableTurret(); } } - pausingAfterShot = (Time.time - timeFired < firePauseTime); } @@ -332,12 +395,13 @@ void Aim() } else { - if (weaponManager && wm.guardMode) + var wm = WeaponManager; + if (wm && wm.guardMode) { return; } - if (mouseControllable && vessel.isActiveVessel) + if (mouseControllable && vessel.isActiveVessel && manuallyControlled) { MouseAim(); } @@ -348,25 +412,43 @@ void UpdateTarget() { slaved = false; - if (weaponManager && wm.slavingTurrets && wm.CurrentMissile) + var wm = WeaponManager; + if (wm && wm.CurrentMissile) { - slaved = true; - slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(wm.CurrentMissile, wm.slavedPosition, - wm.slavedVelocity); + if (wm.slavingTurrets) + { + slaved = true; + //slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(wm.CurrentMissile, wm.slavedPosition, wm.slavedVelocity); + slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(activeMissile, wm.slavedPosition, wm.slavedVelocity, turretLoft, turretLoftFac); + return; + } + else if (wm.mainTGP != null && ModuleTargetingCamera.windowIsOpen && wm.mainTGP.slaveTurrets) + { + slaved = true; + //slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(wm.CurrentMissile, wm.slavedPosition, wm.slavedVelocity); + slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(activeMissile, wm.mainTGP.targetPointPosition, wm.mainTGP.lockedVessel ? wm.mainTGP.lockedVessel.Velocity() : Vector3.zero, turretLoft, turretLoftFac); + return; + } + if (wm.guardMode) + slaved = slavedGuard; + else + slavedGuard = false; } } public void SlavedAim() { if (pausingAfterShot) return; + bool deployCond = hasDeployAnimation && (deployAnimState.normalizedTime < 1 || isReloading); - turret.AimToTarget(slavedTargetPosition); + turret.AimToTarget(slavedTargetPosition, !(deployCond && deployBlocksPitch), !(deployCond && deployBlocksYaw)); } + const int mouseAimLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); void MouseAim() { if (pausingAfterShot) return; - + Vector3 targetPosition; float maxTargetingRange = 5000; @@ -386,7 +468,7 @@ void MouseAim() // } //} - if (Physics.Raycast(ray, out hit, maxTargetingRange, 9076737)) + if (Physics.Raycast(ray, out hit, maxTargetingRange, mouseAimLayerMask)) { targetPosition = hit.point; @@ -404,7 +486,8 @@ void MouseAim() FlightCamera.fetch.mainCamera.transform.position; } - turret.AimToTarget(targetPosition); + bool deployCond = hasDeployAnimation && (deployAnimState.normalizedTime < 1 || isReloading); + turret.AimToTarget(targetPosition, !(deployCond && deployBlocksPitch), !(deployCond && deployBlocksYaw)); } public void UpdateMissileChildren() @@ -463,8 +546,8 @@ public void UpdateMissileChildren() while (t.MoveNext()) { if (t.Current == null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory] : MissileTurret moving transform: " + t.Current.gameObject.name); + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log("[BDArmory.MissileTurret] : MissileTurret moving transform: " + t.Current.gameObject.name); t.Current.parent = mTf; } t.Dispose(); @@ -505,7 +588,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; @@ -522,46 +605,49 @@ void UpdateMissilePositions() } } - public void FireMissile(int index) + public void FireMissile(int index, Vessel targetVessel, MissileFire.TargetData targetData = null) { if (index < missileCount && missileChildren != null && missileChildren[index] != null) { PrepMissileForFire(index); - if (weaponManager) + var wm = WeaponManager; + if (wm) { - wm.SendTargetDataToMissile(missileChildren[index]); + wm.SendTargetDataToMissile(missileChildren[index], targetVessel, true, targetData, true); + wm.PreviousMissile = missileChildren[index]; } missileChildren[index].FireMissile(); - StartCoroutine(MissileRailRoutine(missileChildren[index])); - if (wm) + StartCoroutine(MissileRailRoutine(missileChildren[index])); //turret is stil getting thrusted away despite this being behind a !relaodableRail conditional. investigate + if (wm) // If the primary WM changes, the list will automatically update. { wm.UpdateList(); } - UpdateMissileChildren(); + if (!missileChildren[index].reloadableRail) UpdateMissileChildren(); timeFired = Time.time; } } - public void FireMissile(MissileLauncher ml) + public void FireMissile(MissileLauncher ml, Vessel targetVessel, MissileFire.TargetData targetData = null) { int index = IndexOfMissile(ml); if (index >= 0) { - Debug.Log("[BDArmory] : Firing missile index: " + index); - FireMissile(index); + Debug.Log("[BDArmory.MissileTurret] : Firing missile index: " + index); + FireMissile(index, targetVessel, targetData); } else { - Debug.Log("[BDArmory] : Tried to fire a missile that doesn't exist or is not attached to the turret."); + Debug.Log("[BDArmory.MissileTurret] : Tried to fire a missile that doesn't exist or is not attached to the turret."); } } 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); @@ -581,9 +667,10 @@ 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 + //Why is this a thing? MissileLauncher is already imparting forward vel from jettison. If we really need to impart some vel, setWorldvel is absolutely not the method to use. + //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); @@ -592,7 +679,7 @@ IEnumerator MissileRailRoutine(MissileLauncher ml) void PrepMissileForFire(int index) { - Debug.Log("[BDArmory] : Prepping missile for turret fire."); + Debug.Log("[BDArmory.MissileTurret] : Prepping missile for turret fire."); missileTransforms[index].localPosition = Vector3.zero; missileTransforms[index].localRotation = Quaternion.identity; missileChildren[index].part.partTransform.position = missileReferenceTransforms[index].position; @@ -611,7 +698,7 @@ public void PrepMissileForFire(MissileLauncher ml) } else { - Debug.Log("[BDArmory] : Tried to prep a missile for firing that doesn't exist or is not attached to the turret."); + Debug.Log("[BDArmory.MissileTurret] : Tried to prep a missile for firing that doesn't exist or is not attached to the turret."); } } @@ -637,7 +724,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/WeaponMounts/ModuleCustomTurret.cs b/BDArmory/WeaponMounts/ModuleCustomTurret.cs new file mode 100644 index 000000000..d8c7463c0 --- /dev/null +++ b/BDArmory/WeaponMounts/ModuleCustomTurret.cs @@ -0,0 +1,358 @@ +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Guidances; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using Expansions.Serenity; +using System; +using System.Collections.Generic; +using UnityEngine; +using static BDArmory.Weapons.Missiles.MissileBase; + +namespace BDArmory.WeaponMounts +{ + public class ModuleCustomTurret : PartModule + { + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_TurretID"),//Max Pitch + UI_FloatRange(minValue = 0f, maxValue = 20f, stepIncrement = 1f, scene = UI_Scene.All)] + public float turretID; + /* + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissileTurretFireFOV"), + UI_FloatRange(minValue = 1, maxValue = 180, stepIncrement = 1, scene = UI_Scene.All)] + public float fireFOV = 5; // Fire when pointing within 5� of target. + */ + [KSPField] public string pitchTransformName = "TopJoint"; + public Transform pitchTransform; + + [KSPField] public string yawTransformName = "TopJoint"; + public Transform yawTransform; + + [KSPField] public string baseTransformName = "BottomJoint"; + public Transform bottomTransform; + + Transform referenceTransform; //set this to gun's fireTransform + + public float maxPitch = 0; + public float minPitch = 0; + public float maxYaw = 0; + public float minYaw = 0; + public bool fullRotation = false; + + [KSPField(isPersistant = true)] public float minPitchLimit = 400; + [KSPField(isPersistant = true)] public float maxPitchLimit = 400; + [KSPField(isPersistant = true)] public float yawRangeLimit = 400; + + ModuleRoboticServoHinge Hinge; + ModuleRoboticRotationServo Servo; + + public Vector3 baseForward; + Vector3 pitchForward; + public Vector3 yawNormal; + + public Vector3 slavedTargetPosition; + public bool slaved; + public bool manuallyControlled = false; + public bool isYawRotor => Servo != null; + MissileFire WeaponManager + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; + } + } + MissileFire _weaponManager; + public override void OnStart(StartState state) + { + base.OnStart(state); + yawTransform = part.FindModelTransform(yawTransformName); + var hinge = part.FindModuleImplementing(); + if (hinge != null) + { + Hinge = hinge; + minPitch = Mathf.Min(hinge.softMinMaxAngles.x, hinge.softMinMaxAngles.y); + maxPitch = Mathf.Max(hinge.softMinMaxAngles.x, hinge.softMinMaxAngles.y); + + pitchTransform = part.FindModelTransform(hinge.servoTransformName); + bottomTransform = part.FindModelTransform(hinge.baseTransformName); + if (!pitchTransform) + { + Debug.LogWarning("[BDArmory.ModuleCustomTurret]: " + part.partInfo.title + " has no pitchTransform"); + } + if (!bottomTransform) + { + Debug.LogWarning("[BDArmory.ModuleCustomTurret]: " + part.partInfo.title + " has no bottomTransform"); + } + } + var servo = part.FindModuleImplementing(); + if (servo != null) + { + Servo = servo; + if (servo.allowFullRotation) + fullRotation = true; + else + { + minYaw = Mathf.Min(servo.softMinMaxAngles.x, servo.softMinMaxAngles.y); + maxYaw = Mathf.Max(servo.softMinMaxAngles.x, servo.softMinMaxAngles.y); + } + yawTransform = part.FindModelTransform(servo.servoTransformName); + bottomTransform = part.FindModelTransform(servo.baseTransformName); + if (!yawTransform) + { + Debug.LogWarning("[BDArmory.ModuleCustomTurret]: " + part.partInfo.title + " has no yawTransform"); + } + if (!bottomTransform) + { + Debug.LogWarning("[BDArmory.ModuleCustomTurret]: " + part.partInfo.title + " has no bottomTransform"); + } + } + if (!referenceTransform) + { + if (pitchTransform) + SetReferenceTransform(pitchTransform); + else if (yawTransform) + SetReferenceTransform(yawTransform); + else + Debug.LogWarning("[BDArmory.ModuleCustomTurret]: " + part.partInfo.title + " has no referenceTransform"); + } + if (!bottomTransform) bottomTransform = part.transform; + + yawNormal = yawTransform.up; + //because ofc Squad can't have consistant standard for servo/hinge axis transform orientation... + //Also need to account for rotation/facing; ModuleTurret is agnostic, but targetAngle in the hinge module is not. + /* + if (Hinge) + { + yawNormal = Hinge.mainAxis switch + { + "X" => pitchTransform.forward, + "Z" => pitchTransform.right, + _ => yawTransform.up + }; + baseForward = Hinge.mainAxis switch + { + "X" => bottomTransform.up, + "Z" => bottomTransform.forward, + _ => -bottomTransform.right + }; + pitchForward = Hinge.mainAxis switch + { + "X" => pitchTransform.up, + "Z" => pitchTransform.forward, + _ => -pitchTransform.right + }; + } + */ + } + + void FixedUpdate() + { + if (!HighLogic.LoadedSceneIsFlight) return; + var wm = WeaponManager; + if (wm && wm.CurrentMissile && wm.CurrentMissile.customTurret.Count > 0 && wm.CurrentMissile.customTurret.Contains(this)) + { + if (wm.slavingTurrets) + { + slaved = true; + slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(wm.CurrentMissile, wm.slavedPosition, wm.slavedVelocity, + (wm.CurrentMissile.GuidanceMode == GuidanceModes.AAMLoft || wm.CurrentMissile.GuidanceMode == GuidanceModes.Kappa)); + } + else if (wm.mainTGP != null && ModuleTargetingCamera.windowIsOpen && wm.mainTGP.slaveTurrets) + { + slaved = true; + slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(wm.CurrentMissile, wm.mainTGP.targetPointPosition, wm.mainTGP.lockedVessel ? wm.mainTGP.lockedVessel.Velocity() : Vector3.zero, + (wm.CurrentMissile.GuidanceMode == GuidanceModes.AAMLoft || wm.CurrentMissile.GuidanceMode == GuidanceModes.Kappa)); + } + } + if (slaved) + { + AimToTarget(slavedTargetPosition); + } + else + { + if (wm && wm.guardMode) + { + return; + } + if (manuallyControlled && vessel.isActiveVessel) + { + MouseAim(); + } + } + } + + public void AimToTarget(Vector3 targetPosition, bool pitch = true, bool yaw = true) + { + AimInDirection(targetPosition - referenceTransform.position); + } + + public void AimInDirection(Vector3 targetDirection) + { + if ((Servo && !yawTransform) || (Hinge && !pitchTransform)) + { + return; + } + if (!bottomTransform) return; + yawNormal = yawTransform.up; + Vector3 yawComponent = targetDirection.ProjectOnPlanePreNormalized(yawNormal); + Vector3 pitchComponent = targetDirection.ProjectOnPlane(Vector3.Cross(yawComponent, yawNormal)); + //float currentYaw = Hinge ? 0 : Servo ? Servo.currentAngle : 0; //currentAngle for whatever reason only updates when the PAW is open. WTF, KSP. + float currentYaw = Servo ? VectorUtils.SignedAngleDP(bottomTransform.forward, yawTransform.forward, bottomTransform.right) : 0; + float yawError = VectorUtils.SignedAngleDP( + referenceTransform.forward.ProjectOnPlanePreNormalized(yawNormal), + yawComponent, + Vector3.Cross(yawNormal, referenceTransform.forward)); + float targetYawAngle = (currentYaw + yawError).ToAngle(); + // clamp target yaw in a non-wobbly way + if (fullRotation) + { + if (Mathf.Abs(targetYawAngle) > 180) + { + var nonWobblyWay = Vector3.Dot(yawTransform.parent.right, targetDirection + referenceTransform.position - yawTransform.position); + //if (float.IsNaN(nonWobblyWay)) return; + targetYawAngle = 180 * Math.Sign(nonWobblyWay); + } + } + else + { + targetYawAngle = Mathf.Clamp(targetYawAngle, minYaw, maxYaw); // clamp yaw + } + if (!fullRotation && Mathf.Abs(currentYaw - targetYawAngle) >= 180) + { + //if (float.IsNaN(currentYaw)) return; + targetYawAngle = currentYaw - (Math.Sign(currentYaw) * 179); + } + if (Servo) + { + Servo.targetAngle = targetYawAngle; + if (Servo.inverted) Servo.targetAngle *= -1; + //Debug.Log($"[BDArmory.ModuleCustomTurret] CurrYaw: {currentYaw}; YawError: {yawError}; Servo target Angle {Servo.targetAngle}"); + } + if (Hinge) + { + yawNormal = Hinge.mainAxis switch + { + "X" => bottomTransform.forward, + "Z" => -bottomTransform.right, + _ => bottomTransform.forward + }; + pitchForward = Hinge.mainAxis switch + { + "X" => pitchTransform.up, + "Z" => pitchTransform.forward, + _ => -pitchTransform.right + }; + //float pitchError = (float)VectorUtils.SignedAngleDP(pitchComponent, yawNormal, Hinge.mainAxis == "X" ? pitchTransform.right : pitchTransform.forward) - (float)VectorUtils.SignedAngleDP(referenceTransform.forward, yawNormal, Hinge.mainAxis == "X" ? pitchTransform.right : pitchTransform.forward); + float pitchError = (float)Vector3d.Angle(pitchComponent, yawNormal) - (float)Vector3d.Angle(referenceTransform.forward, yawNormal); + //float currentPitch = Hinge ? Hinge.currentAngle : Servo ? 0 : 0; + //float currentPitch = VectorUtils.SignedAngleDP(baseForward, pitchForward, Hinge.mainAxis == "X" ? pitchTransform.right : pitchTransform.forward); + //SignedAngle switches sign a couple of frames every sec or so. + float currentPitch = 90 - (float)Vector3d.Angle(pitchForward, yawNormal); + float targetPitchAngle = (currentPitch - pitchError).ToAngle(); + targetPitchAngle = Mathf.Clamp(targetPitchAngle, minPitch, maxPitch); // clamp pitch + //Debug.Log($"[BDArmory.ModuleCustomTurret] PitchError: {pitchError}; CurrPitch: {currentPitch}; Target Pitch Angle: {targetPitchAngle}"); + Hinge.targetAngle = targetPitchAngle; + } + } + + const int mouseAimLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); + void MouseAim() + { + Vector3 targetPosition; + float maxTargetingRange = 5000; + //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, mouseAimLayerMask)) + { + targetPosition = hit.point; + + //aim through self vessel if occluding mouseray + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (p && p.vessel && p.vessel == vessel) + { + targetPosition = ray.direction * maxTargetingRange + FlightCamera.fetch.mainCamera.transform.position; + } + } + else + { + targetPosition = (ray.direction * (maxTargetingRange + (FlightCamera.fetch.Distance * 0.75f))) + + FlightCamera.fetch.mainCamera.transform.position; + } + AimToTarget(targetPosition); + } + + public bool ReturnTurret() + { + manuallyControlled = false; + if ((Servo && !yawTransform) || (Hinge && !pitchTransform)) + { + return false; + } + + if (!(Hinge || Servo)) + return true; + + if (Servo) Servo.targetAngle = 0; + if (Hinge) Hinge.targetAngle = 0; + + return true; + } + + public void SetReferenceTransform(Transform t) + { + referenceTransform = t; + } + + void OnGUI() + { + if (HighLogic.LoadedSceneIsEditor && BDArmorySetup.showWeaponAlignment) + { + if ((Servo && !yawTransform) || (Hinge && !pitchTransform)) return; + if (Servo) + { + Vector3 fwdPos = referenceTransform.position + (5 * referenceTransform.forward); + GUIUtils.DrawLineBetweenWorldPositions(referenceTransform.position, fwdPos, 4, Color.blue); + } + /* + Vector3 upPos = referenceTransform.position + (5 * referenceTransform.up); + GUIUtils.DrawLineBetweenWorldPositions(referenceTransform.position, upPos, 4, Color.green); + + Vector3 rightPos = referenceTransform.position + (5 * referenceTransform.right); + GUIUtils.DrawLineBetweenWorldPositions(referenceTransform.position, rightPos, 4, Color.red); + */ + Vector3 yawNrm = yawTransform.position + (5 * yawTransform.up); + if (Hinge) + { + if (Hinge.mainAxis == "X") + { + yawNrm = pitchTransform.position + (10 * pitchTransform.forward); + Vector3 forPos = referenceTransform.position + (5 * referenceTransform.up); + GUIUtils.DrawLineBetweenWorldPositions(referenceTransform.position, forPos, 4, Color.blue); + } + if (Hinge.mainAxis == "Z") + { + yawNrm = pitchTransform.position + (10 * pitchTransform.right); + Vector3 forPos = referenceTransform.position + (5 * -referenceTransform.up); + GUIUtils.DrawLineBetweenWorldPositions(referenceTransform.position, forPos, 4, Color.blue); + } + if (Hinge.mainAxis == "Y") + { + Vector3 forPos = referenceTransform.position + (5 * referenceTransform.forward); + GUIUtils.DrawLineBetweenWorldPositions(referenceTransform.position, forPos, 4, Color.blue); + } + //GUIUtils.DrawLineBetweenWorldPositions(bottomTransform.position, referenceTransform.position + (1 * baseFor), 10, Color.cyan); + } + GUIUtils.DrawLineBetweenWorldPositions(yawTransform.position, yawNrm, 4, Color.green); + } + } + + } +} \ No newline at end of file diff --git a/BDArmory/Modules/ModuleTurret.cs b/BDArmory/WeaponMounts/ModuleTurret.cs similarity index 54% rename from BDArmory/Modules/ModuleTurret.cs rename to BDArmory/WeaponMounts/ModuleTurret.cs index 4c115a29c..b66d0d9dc 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 { @@ -33,6 +35,11 @@ public class ModuleTurret : PartModule UI_FloatRange(minValue = 1f, maxValue = 60f, stepIncrement = 1f, scene = UI_Scene.All)] public float yawRange; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_YawStandbyAngle"), + UI_FloatRange(minValue = -90f, maxValue = 90f, stepIncrement = 0.5f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.None)] + public float yawStandbyAngle = 0; + Quaternion standbyLocalRotation;// = Quaternion.identity; + [KSPField(isPersistant = true)] public float minPitchLimit = 400; [KSPField(isPersistant = true)] public float maxPitchLimit = 400; [KSPField(isPersistant = true)] public float yawRangeLimit = 400; @@ -59,29 +66,32 @@ public override void OnStart(StartState state) { base.OnStart(state); - SetupTweakables(); - pitchTransform = part.FindModelTransform(pitchTransformName); yawTransform = part.FindModelTransform(yawTransformName); if (!pitchTransform) { - Debug.LogWarning(part.partInfo.title + " has no pitchTransform"); + Debug.LogWarning("[BDArmory.ModuleTurret]: " + part.partInfo.title + " has no pitchTransform"); } if (!yawTransform) { - Debug.LogWarning(part.partInfo.title + " has no yawTransform"); + Debug.LogWarning("[BDArmory.ModuleTurret]: " + part.partInfo.title + " has no yawTransform"); } if (!referenceTransform) { - SetReferenceTransform(pitchTransform); + if (pitchTransform) + SetReferenceTransform(pitchTransform); + else + SetReferenceTransform(yawTransform); } + SetupTweakables(); + if (!string.IsNullOrEmpty(audioPath) && (yawSpeedDPS != 0 || pitchSpeedDPS != 0)) { - soundClip = GameDatabase.Instance.GetAudioClip(audioPath); + soundClip = SoundUtils.GetAudioClip(audioPath); audioSource = gameObject.AddComponent(); audioSource.clip = soundClip; @@ -125,7 +135,7 @@ void FixedUpdate() } Vector3 tDir = yawTransform.parent.InverseTransformDirection(pitchTransform.forward); - float angle = Vector3.Angle(tDir, lastTurretDirection); + float angle = VectorUtils.Angle(tDir, lastTurretDirection); float rate = Mathf.Clamp01((angle / Time.fixedDeltaTime) / maxAudioRotRate); lastTurretDirection = tDir; @@ -155,6 +165,11 @@ void Update() } } + void OnDestroy() + { + GameEvents.onEditorPartPlaced.Remove(OnEditorPartPlaced); + } + public void AimToTarget(Vector3 targetPosition, bool pitch = true, bool yaw = true) { AimInDirection(targetPosition - referenceTransform.position, pitch, yaw); @@ -167,16 +182,18 @@ public void AimInDirection(Vector3 targetDirection, bool pitch = true, bool yaw return; } + if (!(pitch || yaw)) + return; + 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); @@ -184,13 +201,11 @@ public void AimInDirection(Vector3 targetDirection, bool pitch = true, bool yaw // clamp target yaw in a non-wobbly way if (Mathf.Abs(targetYawAngle) > yawRange / 2) { - var nonWooblyWay = Vector3.Dot(yawTransform.parent.right, - targetDirection + referenceTransform.position - yawTransform.position); - if (float.IsNaN(nonWooblyWay)) return; - - targetYawAngle = yawRange / 2 * Math.Sign(nonWooblyWay); + var nonWobblyWay = Vector3.Dot(yawTransform.parent.right, targetDirection + referenceTransform.position - yawTransform.position); + //if (float.IsNaN(nonWobblyWay)) return; + targetYawAngle = yawRange / 2 * Math.Sign(nonWobblyWay); } - + float pitchError = (float)Vector3d.Angle(pitchComponent, yawNormal) - (float)Vector3d.Angle(referenceTransform.forward, yawNormal); float currentPitch = -pitchTransform.localEulerAngles.x.ToAngle(); // from current rotation transform @@ -198,9 +213,6 @@ public void AimInDirection(Vector3 targetDirection, bool pitch = true, bool yaw float pitchOffset = Mathf.Abs(targetPitchAngle - currentPitch); targetPitchAngle = Mathf.Clamp(targetPitchAngle, minPitch, maxPitch); // clamp pitch - float linPitchMult = yawOffset > 0 ? Mathf.Clamp01((pitchOffset / yawOffset) * (yawSpeedDPS / pitchSpeedDPS)) : 1; - float linYawMult = pitchOffset > 0 ? Mathf.Clamp01((yawOffset / pitchOffset) * (pitchSpeedDPS / yawSpeedDPS)) : 1; - float yawSpeed; float pitchSpeed; if (smoothRotation) @@ -214,38 +226,42 @@ public void AimInDirection(Vector3 targetDirection, bool pitch = true, bool yaw pitchSpeed = pitchSpeedDPS * deltaTime; } - yawSpeed *= linYawMult; - pitchSpeed *= linPitchMult; - if (yawRange < 360 && Mathf.Abs(currentYaw - targetYawAngle) >= 180) { - if (float.IsNaN(currentYaw)) - { - return; - } - + //if (float.IsNaN(currentYaw)) return; targetYawAngle = currentYaw - (Math.Sign(currentYaw) * 179); } + if (yaw) - yawTransform.localRotation = Quaternion.RotateTowards(yawTransform.localRotation, - Quaternion.Euler(0, targetYawAngle, 0), yawSpeed); + { + float linYawMult = pitch && pitchOffset > 0 ? Mathf.Clamp01((yawOffset / pitchOffset) * (pitchSpeedDPS / yawSpeedDPS)) : 1; + yawTransform.localRotation = Quaternion.RotateTowards(yawTransform.localRotation, Quaternion.Euler(0, targetYawAngle, 0), yawSpeed * linYawMult); + } if (pitch) - pitchTransform.localRotation = Quaternion.RotateTowards(pitchTransform.localRotation, - Quaternion.Euler(-targetPitchAngle, 0, 0), pitchSpeed); + { + float linPitchMult = yaw && yawOffset > 0 ? Mathf.Clamp01((pitchOffset / yawOffset) * (yawSpeedDPS / pitchSpeedDPS)) : 1; + pitchTransform.localRotation = Quaternion.RotateTowards(pitchTransform.localRotation, Quaternion.Euler(-targetPitchAngle, 0, 0), pitchSpeed * linPitchMult); + } } - public bool ReturnTurret() + public float Pitch => -pitchTransform.localEulerAngles.x.ToAngle(); + public float Yaw => yawTransform.localEulerAngles.y.ToAngle(); + + public bool ReturnTurret(bool pitch = true, bool yaw = true) { if (!yawTransform) { return false; } + if (!(pitch || yaw)) + return true; + float deltaTime = Time.fixedDeltaTime; - float yawOffset = Vector3.Angle(yawTransform.forward, yawTransform.parent.forward); - float pitchOffset = Vector3.Angle(pitchTransform.forward, yawTransform.forward); + float yawOffset = Quaternion.Angle(yawTransform.localRotation, standbyLocalRotation); + float pitchOffset = VectorUtils.Angle(pitchTransform.forward, yawTransform.forward); float yawSpeed; float pitchSpeed; @@ -261,34 +277,30 @@ public bool ReturnTurret() pitchSpeed = pitchSpeedDPS * deltaTime; } - float linPitchMult = yawOffset > 0 ? Mathf.Clamp01((pitchOffset / yawOffset) * (yawSpeedDPS / pitchSpeedDPS)) : 1; - float linYawMult = pitchOffset > 0 ? Mathf.Clamp01((yawOffset / pitchOffset) * (pitchSpeedDPS / yawSpeedDPS)) : 1; - - yawSpeed *= linYawMult; - pitchSpeed *= linPitchMult; - - yawTransform.localRotation = Quaternion.RotateTowards(yawTransform.localRotation, Quaternion.identity, - yawSpeed); - pitchTransform.localRotation = Quaternion.RotateTowards(pitchTransform.localRotation, Quaternion.identity, - pitchSpeed); - - if (yawTransform.localRotation == Quaternion.identity && pitchTransform.localRotation == Quaternion.identity) + if (yaw) { - return true; + float linYawMult = pitch && pitchOffset > 0 ? Mathf.Clamp01((yawOffset / pitchOffset) * (pitchSpeedDPS / yawSpeedDPS)) : 1; + yawTransform.localRotation = Quaternion.RotateTowards(yawTransform.localRotation, standbyLocalRotation, yawSpeed * linYawMult); + } + if (pitch) + { + float linPitchMult = yaw && yawOffset > 0 ? Mathf.Clamp01((pitchOffset / yawOffset) * (yawSpeedDPS / pitchSpeedDPS)) : 1; + pitchTransform.localRotation = Quaternion.RotateTowards(pitchTransform.localRotation, Quaternion.identity, pitchSpeed * linPitchMult); } - return false; + + return (yawTransform.localRotation == standbyLocalRotation || !yaw) && (pitchTransform.localRotation == Quaternion.identity || !pitch); } - public bool TargetInRange(Vector3 targetPosition, float thresholdDegrees, float maxDistance) + public bool TargetInRange(Vector3 targetPosition, float maxDistance, float thresholdDegrees = 0) { - if (!pitchTransform) - { - return false; - } - bool withinView = Vector3.Angle(targetPosition - pitchTransform.position, pitchTransform.forward) < - thresholdDegrees; - bool withinDistance = (targetPosition - pitchTransform.position).sqrMagnitude < maxDistance * maxDistance; - return (withinView && withinDistance); + if (!referenceTransform) return false; + Vector3 vectorToTarget = targetPosition - referenceTransform.position; + if (vectorToTarget.sqrMagnitude > maxDistance * maxDistance) return false; + + float angleYaw = VectorUtils.Angle(vectorToTarget.ProjectOnPlanePreNormalized(referenceTransform.up), referenceTransform.forward); + float signedAnglePitch = 90 - VectorUtils.Angle(referenceTransform.up, vectorToTarget); + bool withinView = thresholdDegrees > 0 ? VectorUtils.Angle(vectorToTarget, referenceTransform.forward) < thresholdDegrees : (signedAnglePitch > minPitch && signedAnglePitch < maxPitch && angleYaw < yawRange / 2); + return withinView; } public void SetReferenceTransform(Transform t) @@ -309,6 +321,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) @@ -321,6 +335,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) @@ -344,6 +360,102 @@ 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 + + yawRangeEd.onFieldChanged = SetupStandbyLocalRotation; + SetupStandbyLocalRotation(); + } + void SetupStandbyLocalRotation(BaseField field = null, object obj = null) + { + UI_FloatRange yawStandbyAngleEd = (UI_FloatRange)Fields["yawStandbyAngle"].uiControlEditor; + yawStandbyAngleEd.minValue = -yawRange / 2f; + yawStandbyAngleEd.maxValue = yawRange / 2f; + yawStandbyAngle = Mathf.Clamp(yawStandbyAngle, yawStandbyAngleEd.minValue, yawStandbyAngleEd.maxValue); + yawStandbyAngleEd.onFieldChanged = OnStandbyAngleChanged; + GameEvents.onEditorPartPlaced.Add(OnEditorPartPlaced); + SetStandbyAngle(); + } + + void OnEditorPartPlaced(Part p = null) { if (p == part) OnStandbyAngleChanged(); } + + void OnStandbyAngleChanged(BaseField field = null, object obj = null) + { + SetStandbyAngle(); + foreach (Part symmetryPart in part.symmetryCounterparts) + { + ModuleTurret symmetryTurret = symmetryPart.FindModuleImplementing(); + if (part.symMethod == SymmetryMethod.Mirror) + { + symmetryTurret.yawStandbyAngle = -yawStandbyAngle; + } + else + { + symmetryTurret.yawStandbyAngle = yawStandbyAngle; + } + + symmetryTurret.SetStandbyAngle(); + } } + + void SetStandbyAngle() + { + standbyLocalRotation = Quaternion.AngleAxis(yawStandbyAngle, Vector3.up); + if (yawTransform != null) yawTransform.localRotation = standbyLocalRotation; + } + } + 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/Weapons/BDCustomWarhead.cs b/BDArmory/Weapons/BDCustomWarhead.cs new file mode 100644 index 000000000..430a9657c --- /dev/null +++ b/BDArmory/Weapons/BDCustomWarhead.cs @@ -0,0 +1,128 @@ +using KSP.Localization; +using System.Linq; +using UnityEngine; + +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Bullets; +using static BDArmory.Bullets.PooledBullet; + +namespace BDArmory.Weapons +{ + public class BDCustomWarhead : BDWarheadBase + { + [KSPField] + public string warheadType = "def"; + public string warheadReportingName; + public BulletInfo _warheadType; + + [KSPField] + public string explModelPath = "BDArmory/Models/explosion/explosion"; + + [KSPField] + public string explSoundPath = "BDArmory/Sounds/explode1"; + + [KSPField] + public string smokeTexturePath = ""; + + [KSPField] + public string bulletTexturePath = "BDArmory/Textures/bullet"; + + [KSPField] + public float maxDeviation = -1f; + + public void ParseWarheadType() + { + _warheadType = BulletInfo.bullets[warheadType]; + if (_warheadType.DisplayName != "Default Bullet") + warheadReportingName = _warheadType.DisplayName; + else + warheadReportingName = _warheadType.name; + } + + private void FireProjectile(float detRange = -1, float detTime = -1) + { + SourceInfo sourceInfo = new SourceInfo(vessel, Team.Name, part, transform.position); + GraphicsInfo graphicsInfo = new GraphicsInfo(bulletTexturePath, _warheadType.projectileColorC, _warheadType.startColorC, + _warheadType.caliber / 300, _warheadType.caliber / 750, 0, 1.75f, 2.65f, smokeTexturePath, explModelPath, explSoundPath); + NukeInfo nukeInfo = new NukeInfo(); // Will inherit parent part's models on enable + float currentSpeed = (float)vessel.Velocity().magnitude; + + if (_warheadType.tntMass > 0 || _warheadType.beehive) + { + detRange = detRange < 0 ? detonationRange : detRange; + detTime = detTime < 0 ? detonationRange / (currentSpeed + _warheadType.bulletVelocity) : detTime; + } + if (maxDeviation < 0) maxDeviation = _warheadType.subProjectileDispersion; + FireBullet(_warheadType, _warheadType.projectileCount, sourceInfo, graphicsInfo, nukeInfo, + true, _warheadType.projectileTTL + (detTime < 0.0f ? 0.0f : detTime), TimeWarp.fixedDeltaTime, detRange, detTime, + false, null, null, false, 1f, 1f, + true, currentSpeed, 0f, transform.up, true, maxDeviation); + + } + + protected override void WarheadSpecificSetup() + { + ParseWarheadType(); + } + protected override void WarheadSpecificUISetup() + { + + } + + public override void DetonateIfPossible() + { + if (!hasDetonated && Armed) + { + hasDetonated = true; + + if (fuseFailureRate > 0f) + if (Random.Range(0f, 1f) < fuseFailureRate) + fuseFailed = true; + + if (!fuseFailed) + { + direction = 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 blasts for missiels at present anyway, so not a bit deal RN) + //var sourceWeapon = part.FindModuleImplementing(); + FireProjectile(); + + //////////////////////////////////////////////////// + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDCustomWarhead]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} (Team:{Team.Name}) detonating with a {warheadType} warhead"); + part.explode(); + } + else + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDCustomWarhead]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} explosive fuse failed!"); + } + } + + protected override void Detonate() + { + if (!hasDetonated && Armed) + { + hasDetonated = true; + + if (fuseFailureRate > 0f) + if (Random.Range(0f, 1f) < fuseFailureRate) + fuseFailed = true; + + if (!fuseFailed) + { + direction = part.partTransform.forward; + FireProjectile(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDCustomWarhead]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} detonating with a {warheadType} warhead"); + ///////////////////////// + + part.Destroy(); + part.explode(); + } + else + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDCustomWarhead]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} explosive fuse failed!"); + } + } + } +} diff --git a/BDArmory/Weapons/BDExplosivePart.cs b/BDArmory/Weapons/BDExplosivePart.cs new file mode 100644 index 000000000..5f48bc299 --- /dev/null +++ b/BDArmory/Weapons/BDExplosivePart.cs @@ -0,0 +1,172 @@ +using KSP.Localization; +using System.Linq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; +using BDArmory.Competition; + +namespace BDArmory.Weapons +{ + public class BDExplosivePart : BDWarheadBase + { + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_TNTMass"),//TNT mass equivalent + UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] + public float tntMass = 1; + + [KSPField(isPersistant = false, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_BlastRadius"),//Blast Radius + UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] + public float blastRadius = 10; + + [KSPField] + public string warheadType = "standard"; + public string warheadReportingName; + public ExplosionFx.WarheadTypes _warheadType = ExplosionFx.WarheadTypes.Standard; + + [KSPField] + public float caliber = 120; + + [KSPField] + public float apMod = 1; + + [KSPField] + public string explModelPath = "BDArmory/Models/explosion/explosion"; + + [KSPField] + public string explSoundPath = "BDArmory/Sounds/explode1"; + + private double previousMass = -1; + + protected override void WarheadSpecificSetup() + { + CalculateBlast(); + ParseWarheadType(); + } + + protected override void WarheadSpecificUISetup() + { + SetInitialDetonationDistance(); + } + + public void Update() + { + if (HighLogic.LoadedSceneIsEditor) + { + OnUpdateEditor(); + } + } + + private void OnUpdateEditor() + { + CalculateBlast(); + } + + private void CalculateBlast() + { + if (part.Resources.Contains("HighExplosive")) + { + if (part.Resources["HighExplosive"].amount == previousMass) return; + + tntMass = (float)(part.Resources["HighExplosive"].amount * part.Resources["HighExplosive"].info.density * 1000) * 1.5f; + part.explosionPotential = tntMass / 10f; + previousMass = part.Resources["HighExplosive"].amount; + } + + blastRadius = BlastPhysicsUtils.CalculateBlastRange(tntMass); + } + public void ParseWarheadType() + { + warheadType = warheadType.ToLower(); + switch (warheadType) //make sure this is a valid entry + { + case "continuousrod": + warheadReportingName = "Continuous Rod"; + _warheadType = ExplosionFx.WarheadTypes.ContinuousRod; + break; + case "shapedcharge": + warheadReportingName = "Shaped Charge"; + _warheadType = ExplosionFx.WarheadTypes.ShapedCharge; + break; + default: + warheadType = "standard"; + warheadReportingName = "Standard"; + _warheadType = ExplosionFx.WarheadTypes.Standard; + break; + } + } + + public override void DetonateIfPossible() + { + if (!hasDetonated && Armed) + { + hasDetonated = true; + + if (fuseFailureRate > 0f) + if (Random.Range(0f, 1f) < fuseFailureRate) + fuseFailed = true; + + if (!fuseFailed) + { + direction = _warheadType == ExplosionFx.WarheadTypes.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 blasts 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, SourceVesselName, Team.Name, sourceWeapon != null ? sourceWeapon.GetShortName() : null, direction, -1, false, _warheadType == ExplosionFx.WarheadTypes.Standard ? part.mass : 0, -1, 1, _warheadType, null, apMod); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDExplosivePart]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} (Team:{Team.Name}) detonating with a {_warheadType} warhead"); + part.explode(); + } + else + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDExplosivePart]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} explosive fuse failed!"); + } + } + + protected override void Detonate() + { + if (!hasDetonated && Armed) + { + hasDetonated = true; + + if (fuseFailureRate > 0f) + if (Random.Range(0f, 1f) < fuseFailureRate) + fuseFailed = true; + + if (!fuseFailed) + { + direction = _warheadType == ExplosionFx.WarheadTypes.Standard ? default : part.partTransform.forward; + var sourceWeapon = part.FindModuleImplementing(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDExplosivePart]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} detonating with a {_warheadType} warhead"); + ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, caliber, part, SourceVesselName, Team.Name, sourceWeapon != null ? sourceWeapon.GetShortName() : null, direction, -1, false, _warheadType == ExplosionFx.WarheadTypes.Standard ? part.mass : 0, -1, 1, _warheadType, null, apMod); + + part.Destroy(); + part.explode(); + } + else + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDExplosivePart]: {part} ({(uint)(part.GetInstanceID())}) from {SourceVesselName} explosive fuse failed!"); + } + } + + public float GetBlastRadius() + { + CalculateBlast(); + return blastRadius; + } + protected void SetInitialDetonationDistance() + { + if (this.detonationRange == -1) + { + if (tntMass != 0) + { + detonationRange = (BlastPhysicsUtils.CalculateBlastRange(tntMass) * 0.66f); + } + } + } + } +} diff --git a/BDArmory/Weapons/BDModuleNuke.cs b/BDArmory/Weapons/BDModuleNuke.cs new file mode 100644 index 000000000..146129d49 --- /dev/null +++ b/BDArmory/Weapons/BDModuleNuke.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections; +using System.Text; +using UnityEngine; + +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; +using System.Collections.Generic; + +namespace BDArmory.Weapons +{ + class BDModuleNuke : PartModule + { + //[KSPField(isPersistant = true, guiActive = true, guiName = "WARNING: Reactor Safeties:", guiActiveEditor = false), UI_Label(affectSymCounterparts = UI_Scene.All, scene = UI_Scene.All)]//Weapon Name + //public string status = "OFFLINE"; + + //[KSPField(isPersistant = true, guiActive = true, guiName = "Coolant Remaining", guiActiveEditor = false), UI_Label(scene = UI_Scene.All)] + //public double fuelleft = 0; + + public static string defaultflashModelPath = "BDArmory/Models/explosion/nuke/nukeFlash"; + [KSPField] + public string flashModelPath = defaultflashModelPath; + + public static string defaultShockModelPath = "BDArmory/Models/explosion/nuke/nukeShock"; + [KSPField] + public string shockModelPath = defaultShockModelPath; + + public static string defaultBlastModelPath = "BDArmory/Models/explosion/nuke/nukeBlast"; + [KSPField] + public string blastModelPath = defaultBlastModelPath; + + public static string defaultPlumeModelPath = "BDArmory/Models/explosion/nuke/nukePlume"; + [KSPField] + public string plumeModelPath = defaultPlumeModelPath; + + public static string defaultDebrisModelPath = "BDArmory/Models/explosion/nuke/nukeScatter"; + [KSPField] + public string debrisModelPath = defaultDebrisModelPath; + + public static string defaultBlastSoundPath = "BDArmory/Models/explosion/nuke/nukeBoom"; + [KSPField] + public string blastSoundPath = defaultBlastSoundPath; + + [KSPField(isPersistant = true)] + public float thermalRadius = 750; + + [KSPField(isPersistant = true)] + public float yield = 0.05f; + + [KSPField(isPersistant = true)] + public float fluence = 0.05f; + + [KSPField(isPersistant = true)] + public bool isEMP = false; + + [KSPField(isPersistant = true)] + public bool engineCore = false; + + [KSPField(isPersistant = true)] + public bool fuelCheck = false; + + [KSPField(isPersistant = true)] + public float meltDownDuration = 2.5f; + + private bool hasDetonated = false; + private bool goingCritical = false; + public string Sourcevessel; + + [KSPField(isPersistant = true)] + public string reportingName = "Reactor Containment Failure"; + + MissileLauncher missile; + public MissileLauncher Launcher + { + get + { + if (missile) return missile; + missile = part.FindModuleImplementing(); + return missile; + } + } + ModuleEngines thisEngine; + public ModuleEngines engineCoreEngine + { + get + { + if (hasCheckedEngineCore || thisEngine) return thisEngine; + thisEngine = part.FindModuleImplementing(); + hasCheckedEngineCore = true; + return thisEngine; + } + } + bool hasCheckedEngineCore = false; // Only check once, it's not going to change. + public void Start() + { + if (HighLogic.LoadedSceneIsFlight) + { + if (engineCore) + { + if (part.vessel.rootPart != part) + { + if (engineCoreEngine != null) + { + engineCoreEngine.allowShutdown = false; + } + part.force_activate(); + } + } + else + { + //Fields["status"].guiActive = false; + // Fields["fuelleft"].guiActive = false; + //Fields["status"].guiActiveEditor = false; + // Fields["fuelleft"].guiActiveEditor = false; + } + Sourcevessel = part.vessel.GetName(); + + if (engineCore) part.OnJustAboutToBeDestroyed += Detonate; + GameEvents.onVesselPartCountChanged.Add(CheckAttached); + GameEvents.onVesselCreate.Add(CheckAttached); + } + } + + public void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight) + { + if (BDACompetitionMode.Instance.competitionIsActive) //only begin checking engine state after comp start + { + if (engineCore && (!goingCritical && !hasDetonated)) + { + bool engineOut = true; + if (part.vessel.rootPart == part) + { + foreach (var e in VesselModuleRegistry.GetModuleEngines(vessel)) + { + if (e != null && !e.flameout && e.vessel == part.vessel && e.thrustPercentage > 0) + { + engineOut = false; //we have a functioning engine, hold off on detonation + break; + } + } + } + else + { + engineOut = false; + if (engineCoreEngine != null) + { + if (engineCoreEngine.vessel.GetName() != Sourcevessel || engineCoreEngine.flameout) + { + engineOut = true; + } + + if (!engineCoreEngine.isEnabled || !engineCoreEngine.EngineIgnited) //this is getting tripped by multimode engines toggling from wet/dry + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is Off, detonating"); + Detonate(); //nuke engine off after comp start, detonate. + } + if (engineCoreEngine.thrustPercentage < 100) + { + if (part.Modules.GetModule().Hitpoints == part.Modules.GetModule().GetMaxHitpoints()) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is manually thrust limited, detonating"); + Detonate(); //nuke engine throttle limit modified after comp start and it wasn't battle damage, detonate. + } + } + } + } + + if (engineOut) + { + if (!hasDetonated && !goingCritical) + { + 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 + + } + } + } + } + } + } + + void CheckAttached(Vessel v) + { + if (v != vessel || hasDetonated || goingCritical || !engineCore) return; + VesselModuleRegistry.OnVesselModified(v); + if (v.ActiveController().WM == null) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: Nuclear engine on " + Sourcevessel + " has become detached."); + goingCritical = true; + StartCoroutine(DelayedDetonation(0.5f)); + } + } + + IEnumerator DelayedDetonation(float delay) + { + 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 WaitForSecondsFixed(0.5f); + if (part.vessel.rootPart == part) //double check to ensure vessel is legitimately out of fuel, and not from poorly timed drop tanks/ions running out of Ec but not Xe, etc + { + bool engineOut = true; + { + foreach (var e in VesselModuleRegistry.GetModuleEngines(vessel)) + { + if (e != null && !e.flameout && e.vessel == part.vessel && e.thrustPercentage > 0) + { + engineOut = false; //we have a functioning engine, hold off on detonation + break; + } + } + } + if (!engineOut) //oops, we do still have working engines; abort! + { + goingCritical = false; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDModuleNuke]: engines on " + Sourcevessel + " still have fuel, aborting detonation"); + yield break; + } + if (BDACompetitionMode.Instance.competitionIsActive) + { + string msg = $"{vessel.GetName()} is out of fuel!"; + BDACompetitionMode.Instance.competitionStatus.Add(msg); + } + } + yield return new WaitForSecondsFixed(delay - 0.5f); + + if (!hasDetonated && part != null) Detonate(); + } + + public void OnDestroy() + { + GameEvents.onVesselPartCountChanged.Remove(CheckAttached); + GameEvents.onVesselCreate.Remove(CheckAttached); + } + + public void Detonate() + { + if (hasDetonated || FlightGlobals.currentMainBody == null || VesselSpawnerStatus.vesselsSpawning) // Don't trigger on scene changes or during spawning. + { + return; + } + if (Launcher != null && + (Launcher.MissileState == MissileBase.MissileStates.Idle || Launcher.MissileState == MissileBase.MissileStates.Drop)) + { + return; + } + 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, Launcher != null ? ExplosionSourceType.Missile : ExplosionSourceType.BattleDamage, Sourcevessel, reportingName, 0, thermalRadius, yield, fluence, isEMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", "", nukePart: part); + hasDetonated = true; + if (part.vessel != null) // Already in the process of being destroyed. + part.Destroy(); + } + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + if (engineCore) + { + output.Append(Environment.NewLine); + output.AppendLine($"Reactor Core"); + output.AppendLine($"An unstable reactor core that will detonate if the engine is disabled"); + output.AppendLine($"Yield: {yield}"); + output.AppendLine($"Generates EMP: {isEMP}"); + } + else + { + output.AppendLine($"Nuclear Warhead"); + output.AppendLine($"Yield: {yield}"); + output.AppendLine($"Generates EMP: {isEMP}"); + } + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Modules/BDExplosivePart.cs b/BDArmory/Weapons/BDWarheadBase.cs similarity index 52% rename from BDArmory/Modules/BDExplosivePart.cs rename to BDArmory/Weapons/BDWarheadBase.cs index e57d80d11..0d05db79d 100644 --- a/BDArmory/Modules/BDExplosivePart.cs +++ b/BDArmory/Weapons/BDWarheadBase.cs @@ -1,36 +1,42 @@ -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Control; -using BDArmory.FX; 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; +using BDArmory.Competition; + +namespace BDArmory.Weapons { - public class BDExplosivePart : PartModule + public abstract class BDWarheadBase : PartModule { - float distanceFromStart = 500; - public Vessel sourcevessel; + protected float distanceFromStart = 500; - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_TNTMass"),//TNT mass equivalent - UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] - public float tntMass = 1; + public Vessel sourcevessel + { + get { return _sourceVessel; } + set { _sourceVessel = value; SourceVesselName = _sourceVessel != null ? _sourceVessel.vesselName : null; } + } + protected Vessel _sourceVessel; + public string SourceVesselName { get; protected set; } - [KSPField(isPersistant = false, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_BlastRadius"),//Blast Radius - UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] - public float blastRadius = 10; + public BDTeam Team { get; set; } = BDTeam.Get("Neutral"); - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ProximityFuzeRadius"), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Proximity Fuze Radius + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ProximityTriggerDistance"), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Proximity Fuze Radius public float detonationRange = -1f; // give ability to set proximity range [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DetonateAtMinimumDistance"), UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] // Detonate At Minumum Distance public bool detonateAtMinimumDistance = false; - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_Status")]//Status + [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() @@ -39,16 +45,16 @@ 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" } } - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "Targeting Logic")]//Status + [KSPField(guiActive = false, guiActiveEditor = false, guiName = "Targeting Logic")]//Status public string guiIFFString = "Ignore Allies"; //PartWindow buttons @@ -59,12 +65,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" } } @@ -84,21 +90,15 @@ public void ToggleProx() Fields["detonationRange"].guiActiveEditor = false; Fields["detonationRange"].guiActive = false; } - Misc.Misc.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } - [KSPField] - public string explModelPath = "BDArmory/Models/explosion/explosion"; - - [KSPField] - public string explSoundPath = "BDArmory/Sounds/explode1"; - [KSPAction("Arm")] 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")] @@ -113,21 +113,24 @@ public void DetonateEvent() Detonate(); } - public bool Armed { get; set; } = true; + [KSPField(isPersistant = true)] + public bool Armed = true; public bool Shaped { get; set; } = false; public bool isMissile = true; [KSPField(isPersistant = true)] public bool IFF_On = true; - private float updateTimer = 0; + protected float updateTimer = 0; [KSPField(isPersistant = true)] public bool manualOverride = false; - private double previousMass = -1; - public bool hasDetonated; + public bool fuseFailed = false; + protected Collider[] proximityHitColliders = new Collider[100]; + + public Vector3 direction; public override void OnStart(StartState state) { @@ -137,19 +140,19 @@ public override void OnStart(StartState state) part.OnJustAboutToBeDestroyed += DetonateIfPossible; part.force_activate(); sourcevessel = vessel; - using (List.Enumerator MF = vessel.FindPartModulesImplementing().GetEnumerator()) - while (MF.MoveNext()) // grab the vessel the Weapon manager is on at start - { - if (MF.Current == null) continue; - sourcevessel = MF.Current.vessel; - break; - } + if (part == null) return; + var MF = vessel.ActiveController().WM; + if (MF != null) + { + sourcevessel = MF.vessel; + } } if (part.FindModuleImplementing() == null) { isMissile = false; } GuiSetup(); + /* if (BDArmorySettings.ADVANCED_EDIT) { //Fields["tntMass"].guiActiveEditor = true; @@ -158,9 +161,17 @@ public override void OnStart(StartState state) //((UI_FloatRange)Fields["tntMass"].uiControlEditor).maxValue = 3000f; //((UI_FloatRange)Fields["tntMass"].uiControlEditor).stepIncrement = 5f; } + */ + WarheadSpecificSetup(); + if (HighLogic.LoadedSceneIsFlight) + GameEvents.onGameSceneSwitchRequested.Add(HandleSceneChange); + } - CalculateBlast(); + protected void OnDestroy() + { + GameEvents.onGameSceneSwitchRequested.Remove(HandleSceneChange); } + protected abstract void WarheadSpecificSetup(); public void GuiSetup() { @@ -176,6 +187,26 @@ public void GuiSetup() Fields["guiStatusString"].guiActive = true; Fields["guiIFFString"].guiActiveEditor = true; Fields["guiIFFString"].guiActive = true; + if (Armed) + { + guiStatusString = "ARMED"; + Events["Toggle"].guiName = StringUtils.Localize("Disarm Warhead"); + } + else + { + guiStatusString = "Safe"; + Events["Toggle"].guiName = StringUtils.Localize("Arm Warhead"); + } + if (IFF_On) + { + guiIFFString = "Ignore Allies"; + Events["ToggleIFF"].guiName = StringUtils.Localize("Disable IFF"); + } + else + { + guiIFFString = "Indescriminate"; + Events["ToggleIFF"].guiName = StringUtils.Localize("Enable IFF"); + } if (manualOverride) { Fields["detonationRange"].guiActiveEditor = true; @@ -186,7 +217,7 @@ public void GuiSetup() Fields["detonationRange"].guiActiveEditor = false; Fields["detonationRange"].guiActive = false; } - SetInitialDetonationDistance(); + WarheadSpecificUISetup(); } else { @@ -202,54 +233,43 @@ public void GuiSetup() Fields["guiIFFString"].guiActive = false; Fields["detonationRange"].guiActiveEditor = false; Fields["detonationRange"].guiActive = false; + Fields["detonateAtMinimumDistance"].guiActiveEditor = false; + Fields["detonateAtMinimumDistance"].guiActive = false; } - Misc.Misc.RefreshAssociatedWindows(part); - } - - public void Update() - { - if (HighLogic.LoadedSceneIsEditor) - { - OnUpdateEditor(); - } - if (hasDetonated) - { - this.part.explode(); - } + GUIUtils.RefreshAssociatedWindows(part); } + protected abstract void WarheadSpecificUISetup(); 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 (vessel.ActiveController().WM == null) { - if (vessel.FindPartModulesImplementing().Count <= 0) // doing it this way to avoid having to calcualte part trees in case of multiple MMG missiles on a vessel + 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(); + } } } } @@ -257,80 +277,37 @@ public override void OnFixedUpdate() private void GetTeamID() { - IFFID = sourcevessel.FindPartModuleImplementing()?.teamString; + var weaponManager = sourcevessel != null ? sourcevessel.ActiveController().WM : null; + IFFID = weaponManager != null ? weaponManager.teamString : null; } - private void OnUpdateEditor() - { - CalculateBlast(); - } - - private void CalculateBlast() - { - if (part.Resources.Contains("HighExplosive")) - { - if (part.Resources["HighExplosive"].amount == previousMass) return; - - tntMass = (float)(part.Resources["HighExplosive"].amount * part.Resources["HighExplosive"].info.density * 1000) * 1.5f; - part.explosionPotential = tntMass / 10f; - previousMass = part.Resources["HighExplosive"].amount; - } + public abstract void DetonateIfPossible(); - blastRadius = BlastPhysicsUtils.CalculateBlastRange(tntMass); - } + protected abstract void Detonate(); - public void DetonateIfPossible() + public void HandleSceneChange(GameEvents.FromToAction fromTo) { - if (!hasDetonated && Armed) - { - Vector3 direction = default(Vector3); - - if (Shaped) - { - direction = (part.transform.position + part.rb.velocity * Time.deltaTime).normalized; - } - ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, 0, part, null, direction); - hasDetonated = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + sourcevessel?.vesselName + " detonating."); - } - } - - private void Detonate() - { - if (!hasDetonated && Armed) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + sourcevessel?.vesselName + " detonating."); - ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, 0, part); - hasDetonated = true; - part.Destroy(); - } + if (fromTo.from == GameScenes.FLIGHT) + { hasDetonated = true; } // Don't trigger explosions on scene changes. } - public float GetBlastRadius() - { - CalculateBlast(); - return blastRadius; - } - protected void SetInitialDetonationDistance() - { - if (this.detonationRange == -1) - { - if (tntMass != 0) - { - detonationRange = (BlastPhysicsUtils.CalculateBlastRange(tntMass) * 0.66f); - } - } - } private bool Checkproximity(float distanceFromStart) { bool detonate = false; - if (distanceFromStart < blastRadius) + if (distanceFromStart < detonationRange) { return detonate = false; } - using (var hitsEnu = Physics.OverlapSphere(transform.position, detonationRange, 557057).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()) { @@ -338,10 +315,12 @@ private bool Checkproximity(float distanceFromStart) Part partHit = hitsEnu.Current.GetComponentInParent(); if (partHit == null || partHit.vessel == null) continue; - if (partHit?.vessel == vessel || partHit?.vessel == sourcevessel) continue; - if (partHit?.vessel.vesselType == VesselType.Debris) continue; - if (sourcevessel != null && partHit.vessel.vesselName.Contains(sourcevessel.vesselName)) continue; - if (IFF_On && partHit.vessel.FindPartModuleImplementing()?.teamString == IFFID) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.vessel == vessel || partHit.vessel == sourcevessel) continue; + if (partHit.vessel.vesselType == VesselType.Debris) continue; + if (!string.IsNullOrEmpty(SourceVesselName) && partHit.vessel.vesselName.Contains(SourceVesselName)) continue; + var weaponManager = partHit.vessel.ActiveController().WM; + if (IFF_On && (weaponManager == null || weaponManager.teamString == IFFID)) continue; if (detonateAtMinimumDistance) { var distance = Vector3.Distance(partHit.transform.position + partHit.CoMOffset, transform.position); @@ -351,7 +330,7 @@ private bool Checkproximity(float distanceFromStart) return detonate = false; } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("Proxifuze triggered by " + partHit.partName + " from " + partHit.vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDWarheadBase]: Proxifuze triggered by {partHit.partName} from {partHit.vessel.vesselName}"); return detonate = true; } } diff --git a/BDArmory/Modules/ClusterBomb.cs b/BDArmory/Weapons/ClusterBomb.cs similarity index 80% rename from BDArmory/Modules/ClusterBomb.cs rename to BDArmory/Weapons/ClusterBomb.cs index d210600d2..6bb807500 100644 --- a/BDArmory/Modules/ClusterBomb.cs +++ b/BDArmory/Weapons/ClusterBomb.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; -using BDArmory.Core.Extension; -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 { - List submunitions; + public List submunitions; List fairings; MissileLauncher missileLauncher; @@ -82,6 +84,7 @@ public override void OnStart(StartState state) public override void OnFixedUpdate() { + base.OnFixedUpdate(); if (missileLauncher != null && missileLauncher.HasFired && missileLauncher.TimeIndex > deployDelay && !deployed && AltitudeTrigger()) @@ -92,7 +95,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; @@ -118,7 +121,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(); @@ -129,6 +132,7 @@ void DeploySubmunitions() subScript.blastRadius = missileLauncher.GetBlastRadius(); subScript.subExplModelPath = subExplModelPath; subScript.subExplSoundPath = subExplSoundPath; + subScript.sourceVesselName = missileLauncher.SourceVessel.vesselName; sub.Current.AddComponent(); } @@ -139,7 +143,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(); @@ -169,12 +173,14 @@ public class Submunition : MonoBehaviour public float blastHeat; public string subExplModelPath; public string subExplSoundPath; + public string sourceVesselName; Vector3 currPosition; Vector3 prevPosition; float startTime; Rigidbody rb; + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); void Start() { @@ -188,7 +194,7 @@ void OnCollisionEnter(Collision col) { ContactPoint contact = col.contacts[0]; Vector3 pos = contact.point; - ExplosionFx.CreateExplosion(pos, blastForce, subExplModelPath, subExplSoundPath, ExplosionSourceType.Missile); + ExplosionFx.CreateExplosion(pos, blastForce, subExplModelPath, subExplSoundPath, ExplosionSourceType.Missile, 0, null, sourceVesselName, null, null, default, -1, false, rb.mass * 1000, Hitpart: col.gameObject.GetComponentInParent()); } void FixedUpdate() @@ -202,10 +208,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; @@ -213,22 +219,22 @@ void FixedUpdate() Ray ray = new Ray(prevPosition, currPosition - prevPosition); RaycastHit hit; - if (Physics.Raycast(ray, out hit, dist, 9076737)) + if (Physics.Raycast(ray, out hit, dist, explosionLayerMask)) { Part hitPart = null; try { hitPart = hit.collider.gameObject.GetComponentInParent(); } - catch (NullReferenceException) + catch (NullReferenceException e) { - Debug.Log("[BDArmory]:NullReferenceException for Submunition Hit"); + Debug.LogWarning("[BDArmory.ClusterBomb]:NullReferenceException for Submunition Hit: " + e.Message); return; } if (hitPart != null || CheckBuildingHit(hit)) { - Detonate(hit.point); + Detonate(hit.point, hitPart); } else if (hitPart == null) { @@ -244,9 +250,9 @@ void FixedUpdate() } } - void Detonate(Vector3 pos) + void Detonate(Vector3 pos, Part hitPart = null) { - ExplosionFx.CreateExplosion(pos, blastForce, subExplModelPath, subExplSoundPath, ExplosionSourceType.Missile); + ExplosionFx.CreateExplosion(pos, blastForce, subExplModelPath, subExplSoundPath, ExplosionSourceType.Missile, 0, null, sourceVesselName, null, null, default, -1, false, rb.mass * 1000, Hitpart: hitPart, sourceVelocity: rb.velocity + BDKrakensbane.FrameVelocityV3f); Destroy(gameObject); } @@ -257,7 +263,10 @@ private bool CheckBuildingHit(RaycastHit hit) { building = hit.collider.gameObject.GetComponentUpwards(); } - catch (Exception) { } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.ClusterBomb]: Exception thrown in CheckBuildingHit: " + e.Message + "\n" + e.StackTrace); + } if (building != null && building.IsIntact) { @@ -276,6 +285,7 @@ public class ClusterBombFairing : MonoBehaviour float startTime; Rigidbody rb; + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); void Start() { @@ -290,17 +300,17 @@ 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; float dist = (currPosition - prevPosition).magnitude; Ray ray = new Ray(prevPosition, currPosition - prevPosition); RaycastHit hit; - if (Physics.Raycast(ray, out hit, dist, 9076737)) + if (Physics.Raycast(ray, out hit, dist, explosionLayerMask)) { Destroy(gameObject); } diff --git a/BDArmory/Modules/EngageableWeapon.cs b/BDArmory/Weapons/EngageableWeapon.cs similarity index 52% rename from BDArmory/Modules/EngageableWeapon.cs rename to BDArmory/Weapons/EngageableWeapon.cs index d9e8ab8ad..b2005211d 100644 --- a/BDArmory/Modules/EngageableWeapon.cs +++ b/BDArmory/Weapons/EngageableWeapon.cs @@ -1,5 +1,9 @@ -using KSP.Localization; -namespace BDArmory.Modules +using BDArmory.Extensions; +using BDArmory.Services; +using BDArmory.Utils; +using UnityEngine; + +namespace BDArmory.Weapons { public abstract class EngageableWeapon : PartModule, IEngageService { @@ -8,11 +12,11 @@ public abstract class EngageableWeapon : PartModule, IEngageService // Weapon usage settings [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EngageRangeMin"),//Engage Range Min - UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 100f, scene = UI_Scene.Editor)] + UI_FloatPowerRange(minValue = 0f, maxValue = 5000f, power = 2, sigFig = 2, scene = UI_Scene.Editor)] public float engageRangeMin; [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EngageRangeMax"),//Engage Range Max - UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 100f, scene = UI_Scene.Editor)] + UI_FloatPowerRange(minValue = 0f, maxValue = 5000f, power = 2, sigFig = 2, scene = UI_Scene.Editor)] public float engageRangeMax; [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EngageAir"),//Engage Air @@ -31,6 +35,10 @@ public abstract class EngageableWeapon : PartModule, IEngageService UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true")]//false--true public bool engageSLW = true; + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_weaponChannel"), + UI_FloatRange(minValue = 0, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float weaponChannel = 0; // weaponChannel telling a weaponManager which weapons it may use + [KSPEvent(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DisableEngageOptions", active = true)]//Disable Engage Options public void ToggleEngageOptions() { @@ -38,11 +46,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; @@ -58,9 +66,27 @@ public void ToggleEngageOptions() Fields["engageSLW"].guiActive = engageEnabled; Fields["engageSLW"].guiActiveEditor = engageEnabled; - Misc.Misc.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); + } + public void HideEngageOptions() + { + Events["ToggleEngageOptions"].guiActive = false; + Events["ToggleEngageOptions"].guiActiveEditor = false; + Fields["engageRangeMin"].guiActive = true; + Fields["engageRangeMin"].guiActiveEditor = true; + Fields["engageRangeMax"].guiActive = true; + Fields["engageRangeMax"].guiActiveEditor = true; + Fields["engageAir"].guiActive = false; + Fields["engageAir"].guiActiveEditor = false; + Fields["engageMissile"].guiActive = false; + Fields["engageMissile"].guiActiveEditor = false; + Fields["engageGround"].guiActive = false; + Fields["engageGround"].guiActiveEditor = false; + Fields["engageSLW"].guiActive = false; + Fields["engageSLW"].guiActiveEditor = false; + + GUIUtils.RefreshAssociatedWindows(part); } - public void OnRangeUpdated(BaseField field, object obj) { // ensure max >= min @@ -68,18 +94,46 @@ public void OnRangeUpdated(BaseField field, object obj) engageRangeMax = engageRangeMin; } + void OnEngageOptionsChanged(BaseField field, object obj) + { + var wm = vessel.ActiveController().WM; + 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; - rangeMin.minValue = min; - rangeMin.maxValue = max; - rangeMin.stepIncrement = (max - min) / 100f; + min = Mathf.Max(min, 1f); // Avoid 0 min range for now. FIXME Remove these if the special value of 0 gets added to UI_FloatSemiLogRange. + max = Mathf.Max(max, 1f); // Avoid 0 max range for now. + + var rangeMin = (UI_FloatPowerRange)Fields["engageRangeMin"].uiControlEditor; + rangeMin.UpdateLimits(min, max); rangeMin.onFieldChanged = OnRangeUpdated; - UI_FloatRange rangeMax = (UI_FloatRange)Fields["engageRangeMax"].uiControlEditor; - rangeMax.minValue = min; - rangeMax.maxValue = max; - rangeMax.stepIncrement = (max - min) / 100f; + var rangeMax = (UI_FloatPowerRange)Fields["engageRangeMax"].uiControlEditor; + rangeMax.UpdateLimits(min, max); rangeMax.onFieldChanged = OnRangeUpdated; if ((engageRangeMin == 0) && (engageRangeMax == 0)) @@ -128,5 +182,10 @@ public string GetShortName() { return shortName; } + + public float GetWeaponChannel() + { + return weaponChannel; + } } } diff --git a/BDArmory/Misc/IBDWeapon.cs b/BDArmory/Weapons/IBDWeapon.cs similarity index 69% rename from BDArmory/Misc/IBDWeapon.cs rename to BDArmory/Weapons/IBDWeapon.cs index 44ea295e0..8bdfc4a4f 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,10 +8,20 @@ public interface IBDWeapon string GetSubLabel(); + float GetEngageRange(); + + float GetEngageFOV(); + string GetMissileType(); + string GetPartName(); + + float GetWeaponChannel(); + Part GetPart(); + ModuleWeapon GetWeaponModule(); + // extensions for feature_engagementenvelope } diff --git a/BDArmory/Modules/BDMMLauncher.cs b/BDArmory/Weapons/Missiles/BDMMLauncher.cs similarity index 60% rename from BDArmory/Modules/BDMMLauncher.cs rename to BDArmory/Weapons/Missiles/BDMMLauncher.cs index e7ba91c33..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 { @@ -17,15 +20,15 @@ public void Fire() part.decouple(0); - foreach (BDModularGuidance bdmm in vessel.FindPartModulesImplementing()) + foreach (BDModularGuidance bdmm in VesselModuleRegistry.GetModules(vessel)) { bdmm.HasFired = true; //bdmm.target = target; } - foreach (BDExplosivePart bde in vessel.FindPartModulesImplementing()) - { - //bde.target = target; - } + // foreach (BDExplosivePart bde in VesselModuleRegistry.GetModules(vessel)) + // { + // //bde.target = target; + // } } } } diff --git a/BDArmory/Weapons/Missiles/BDModularGuidance.cs b/BDArmory/Weapons/Missiles/BDModularGuidance.cs new file mode 100644 index 000000000..eb3f0529b --- /dev/null +++ b/BDArmory/Weapons/Missiles/BDModularGuidance.cs @@ -0,0 +1,2223 @@ +using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Guidances; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.WeaponMounts; +using KSP.UI.Screens; +using System; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; + +namespace BDArmory.Weapons.Missiles +{ + public class BDModularGuidance : MissileBase + { + private bool _missileIgnited; + private int _nextStage = 1; + + private PartModule _targetDecoupler; + + private readonly Vessel _targetVessel = new(); + + private Transform _velocityTransform; + + public Vessel LegacyTargetVessel; + + MissileFire WeaponManager // WM on the modular missile once it's detached, otherwise the WM on the parent vessel. + { + get + { + if (!_noWM && (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel)) + { + if (vessel && vessel.loaded) + { + _weaponManager = vessel.ActiveController().WM; + _noWM = _weaponManager == null; + } + else _weaponManager = null; + } + return _weaponManager; + } + } + MissileFire _weaponManager; + bool _noWM = false; // If no WM is found the first time, don't check again. + + private readonly List _vesselParts = []; + + #region KSP FIELDS + + [KSPField] + public string ForwardTransform = "ForwardNegative"; + + [KSPField] + public string UpTransform = "RightPositive"; + + [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 WeaponName; + + // priority transferred to MissileBase + + [KSPField(isPersistant = false, guiActive = true, guiName = "#LOC_BDArmory_GuidanceType", guiActiveEditor = true)]//Guidance Type + public string GuidanceLabel = "AGM/STS"; + + [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_TargetingMode", guiActiveEditor = true), UI_Label(affectSymCounterparts = UI_Scene.All, scene = UI_Scene.All)]//Targeting Mode + private string _targetingLabel = TargetingModes.Radar.ToString(); + + [KSPField(isPersistant = true)] + public int GuidanceIndex = 2; + + [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_AI_SteerLimiter"), UI_FloatRange(minValue = .1f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Steer Limiter + public float MaxSteer = 1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StagesNumber"), UI_FloatRange(minValue = 1f, maxValue = 9f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Stages Number + public float StagesNumber = 1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StageToTriggerOnProximity"), UI_FloatRange(minValue = 0f, maxValue = 6f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Stage to Trigger On Proximity + public float StageToTriggerOnProximity = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerDamping"), UI_FloatRange(minValue = 0f, maxValue = 20f, stepIncrement = .05f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Steer Damping + public float SteerDamping = 5; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_SteerPower"), UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = .1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Steer Factor + public float SteerMult = 10; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_RollCorrection"), UI_Toggle(controlEnabled = true, enabledText = "#LOC_BDArmory_RollCorrection_enabledText", disabledText = "#LOC_BDArmory_RollCorrection_disabledText", scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Roll Correction--Roll enabled--Roll disabled + public bool RollCorrection = false; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_TimeBetweenStages"),//Time Between Stages + UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.5f, scene = UI_Scene.Editor)] + public float timeBetweenStages = 1f; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MinSpeedGuidance"),//Min Speed before guidance + UI_FloatRange(minValue = 0f, maxValue = 1000f, stepIncrement = 50f, scene = UI_Scene.Editor)] + public float MinSpeedGuidance = 200f; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_AI_MaxSpeed"),//Max guided speed (orbital only) + UI_FloatRange(minValue = 200f, maxValue = 10000f, stepIncrement = 100f, scene = UI_Scene.Editor)] + public float MaxSpeed = 2000f; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ClearanceRadius", advancedTweakable = true),//Clearance radius + UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.05f, scene = UI_Scene.Editor)] + public float clearanceRadius = 0.14f; + + public override float ClearanceRadius => clearanceRadius; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ClearanceLength", advancedTweakable = true),//Clearance length + UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.05f, scene = UI_Scene.Editor)] + public float clearanceLength = 0.14f; + + public override float ClearanceLength => clearanceLength; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissileCMRange"), UI_FloatRange(minValue = 0, maxValue = 10000f, stepIncrement = 500f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]// Missile Countermeasure Range + public float MissileCMRange = -1f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissileCMInterval"), UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.05f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]// Missile Countermeasure Interval + public float MissileCMInterval = 1f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissileIFF"), UI_Toggle(controlEnabled = true, enabledText = "#LOC_BDArmory_MissileIFF_enabledText", disabledText = "#LOC_BDArmory_MissileIFF_disabledText", scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Roll Correction--Roll enabled--Roll disabled + public bool HasIFF = true; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_terminalHomingRange"), + UI_FloatRange(minValue = 500f, maxValue = 20000f, stepIncrement = 100f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float TerminalHomingRange = 3000; + + private Vector3 initialMissileRollPlane; + private Vector3 initialMissileForward; + + // Orbital Guidance vars + private Vector3 rcsVector = Vector3.zero; + private Vector3 rcsVectorLerped = Vector3.zero; + private bool missileTarget = false; + private List rcsThrusters; + private List engines; + + private bool _minSpeedAchieved = false; + private double lastRollAngle; + private double angularVelocity; + + public float warheadYield = 0; + public float thrust = 0; + public float mass = 0.1f; + #endregion KSP FIELDS + + public TransformAxisVectors ForwardTransformAxis { get; set; } + public TransformAxisVectors UpTransformAxis { get; set; } + + public float Mass => (float)vessel.totalMass; + + public enum TransformAxisVectors + { + UpPositive, + UpNegative, + ForwardPositive, + ForwardNegative, + RightPositive, + RightNegative + } + + private void RefreshGuidanceMode() + { + switch (GuidanceIndex) + { + case 1: + GuidanceMode = GuidanceModes.AAMPure; + GuidanceLabel = "AAM"; + break; + + case 2: + GuidanceMode = GuidanceModes.AGM; + GuidanceLabel = "AGM/STS"; + break; + + case 3: + GuidanceMode = GuidanceModes.Cruise; + GuidanceLabel = "Cruise"; + break; + + case 4: + 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; + + case 7: + GuidanceMode = GuidanceModes.Orbital; + GuidanceLabel = "Orbital"; + break; + case 8: + GuidanceMode = GuidanceModes.AAMLoft; + GuidanceLabel = "AAM Loft"; + break; + } + + if (Fields["CruiseAltitude"] != null) + { + CruiseAltitudeRange(); + Fields["CruiseAltitude"].guiActive = GuidanceMode == GuidanceModes.Cruise; + Fields["CruiseAltitude"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; + Fields["CruiseSpeed"].guiActive = GuidanceMode == GuidanceModes.Cruise; + Fields["CruiseSpeed"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; + Events["CruiseAltitudeRange"].guiActive = GuidanceMode == GuidanceModes.Cruise; + Events["CruiseAltitudeRange"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; + Fields["CruisePredictionTime"].guiActiveEditor = GuidanceMode == GuidanceModes.Cruise; + } + + if (Fields["BallisticOverShootFactor"] != null) + { + Fields["BallisticOverShootFactor"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; + Fields["BallisticOverShootFactor"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; + Fields["BallisticAngle"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; + Fields["BallisticAngle"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; + } + if (Fields["SoftAscent"] != null) + { + Fields["SoftAscent"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; + Fields["SoftAscent"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; + } + + 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"].guiActiveEditor = true; + Fields["LoftRangeOverride"].guiActiveEditor = true; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = true; + Fields["LoftMinAltitude"].guiActiveEditor = true; + //Fields["terminalHomingRange"].guiActive = true; + //Fields["terminalHomingRange"].guiActiveEditor = true; + + if (!GameSettings.ADVANCED_TWEAKABLES) + { + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActiveEditor = false; + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + //Fields["LoftAltComp"].guiActive = false; + //Fields["LoftAltComp"].guiActiveEditor = false; + } + else + { + Fields["LoftAngle"].guiActiveEditor = true; + Fields["LoftTermAngle"].guiActiveEditor = true; + Fields["LoftRangeFac"].guiActiveEditor = true; + Fields["LoftVelComp"].guiActiveEditor = true; + Fields["LoftVertVelComp"].guiActiveEditor = true; + //Fields["LoftAltComp"].guiActive = true; + //Fields["LoftAltComp"].guiActiveEditor = true; + } + + if (!BDArmorySettings.DEBUG_MISSILES) + { + Fields["LoftMaxAltitude"].guiActive = false; + Fields["LoftRangeOverride"].guiActive = false; + Fields["LoftAltitudeAdvMax"].guiActive = false; + Fields["LoftMinAltitude"].guiActive = false; + Fields["LoftAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActive = false; + Fields["LoftRangeFac"].guiActive = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActive = false; + } + else + { + Fields["LoftMaxAltitude"].guiActive = true; + Fields["LoftRangeOverride"].guiActive = true; + Fields["LoftAltitudeAdvMax"].guiActive = true; + Fields["LoftMinAltitude"].guiActive = true; + Fields["LoftAngle"].guiActive = true; + Fields["LoftTermAngle"].guiActive = true; + Fields["LoftRangeFac"].guiActive = true; + Fields["LoftVelComp"].guiActive = true; + Fields["LoftVertVelComp"].guiActive = true; + } + } + + if (!terminalHoming && GuidanceMode != GuidanceModes.AAMLoft) //GuidanceMode != GuidanceModes.AAMHybrid && GuidanceMode != GuidanceModes.AAMLoft) + { + Fields["terminalHomingRange"].guiActive = false; + Fields["TerminalHomingRange"].guiActiveEditor = false; + } + else + { + if (!BDArmorySettings.DEBUG_MISSILES) + Fields["TerminalHomingRange"].guiActive = false; + else + Fields["TerminalHomingRange"].guiActive = true; + + Fields["terminalHomingRange"].guiActive = true; + Fields["TerminalHomingRange"].guiActiveEditor = true; + } + + if (GuidanceMode != GuidanceModes.Orbital) + { + Fields["MaxSpeed"].guiActive = false; + Fields["SteerMult"].guiActive = true; + Fields["SteerDamping"].guiActive = true; + Fields["MaxSteer"].guiActive = true; + Fields["RollCorrection"].guiActive = true; + Fields["MaxSpeed"].guiActiveEditor = false; + Fields["SteerMult"].guiActiveEditor = true; + Fields["SteerDamping"].guiActiveEditor = true; + Fields["MaxSteer"].guiActiveEditor = true; + Fields["RollCorrection"].guiActiveEditor = true; + } + else + { + Fields["MaxSpeed"].guiActive = true; + Fields["SteerMult"].guiActive = false; + Fields["SteerDamping"].guiActive = false; + Fields["MaxSteer"].guiActive = false; + Fields["RollCorrection"].guiActive = false; + Fields["MaxSpeed"].guiActiveEditor = true; + Fields["SteerMult"].guiActiveEditor = false; + Fields["SteerDamping"].guiActiveEditor = false; + Fields["MaxSteer"].guiActiveEditor = false; + Fields["RollCorrection"].guiActiveEditor = false; + } + + GUIUtils.RefreshAssociatedWindows(part); + } + + public override void OnFixedUpdate() + { + base.OnFixedUpdate(); + + if (!HighLogic.LoadedSceneIsFlight) return; + + if (HasFired && !HasExploded) + { + UpdateGuidance(); + CheckDetonationState(true); + CheckDetonationDistance(); + CheckDelayedFired(); + CheckNextStage(); + CheckCountermeasureDistance(); + + if (isTimed && TimeIndex > detonationTime) + { + AutoDestruction(); + } + } + + if (HasExploded && StageToTriggerOnProximity == 0) + { + AutoDestruction(); + } + } + + protected override void InitializeCountermeasures() + { + List ECM = VesselModuleRegistry.GetModules(vessel); + foreach (ModuleECMJammer jammer in ECM) + { + jammer.EnableJammer(); + CMenabled = true; + } + + missileCM = VesselModuleRegistry.GetModules(vessel); + missileCM.Sort((a, b) => b.priority.CompareTo(a.priority)); // Sort from highest to lowest priority + missileCMTime = Time.time; + int currPriority = 0; + foreach (CMDropper dropper in missileCM) + { + if (dropper.cmType == CMDropper.CountermeasureTypes.Chaff) + dropper.UpdateVCI(); + dropper.SetupAudio(); + if (currPriority <= dropper.Priority) + { + if (dropper.DropCM()) + { + currPriority = dropper.Priority; + } + } + CMenabled = true; + } + } + + protected override void DropCountermeasures() + { + int currPriority = 0; + bool invalidCMs = false; + foreach (CMDropper dropper in missileCM) + { + if (dropper.vessel == vessel) + { + if (currPriority <= dropper.Priority) + { + if (dropper.DropCM()) + currPriority = dropper.Priority; + } + } + else + invalidCMs = true; + } + + if (invalidCMs) + missileCM.RemoveAll(dropper => dropper.vessel != vessel); + } + + public override void OnAwake() + { + base.OnAwake(); + SetPersistantFields(); // Adjust persistency of various fields before they get loaded. + } + + void Update() + { + if (!HighLogic.LoadedSceneIsFlight) return; + + if (!HasFired) + CheckDetonationState(true); + } + + private void CheckNextStage() + { + if (ShouldExecuteNextStage()) + { + if (!nextStageCountdownStart) + { + nextStageCountdownStart = true; + stageCutOfftime = Time.time; + } + else + { + if ((Time.time - stageCutOfftime) >= timeBetweenStages) + { + ExecuteNextStage(); + nextStageCountdownStart = false; + } + } + } + } + + public bool nextStageCountdownStart { get; set; } = false; + + public float stageCutOfftime { get; set; } = 0f; + + private void CheckDelayedFired() + { + if (_missileIgnited) return; + if (TimeIndex > dropTime) + { + MissileIgnition(); + } + } + + private void DisableRecursiveFlow(List children) + { + List.Enumerator child = children.GetEnumerator(); + while (child.MoveNext()) + { + if (child.Current == null) continue; + mass += child.Current.mass; + if (child.Current.isEngine()) thrust += 1; + DisablingExplosives(child.Current); + + IEnumerator resource = child.Current.Resources.GetEnumerator(); + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + if (resource.Current.flowState) + { + resource.Current.flowState = false; + } + } + resource.Dispose(); + + if (child.Current.children.Count > 0) + { + DisableRecursiveFlow(child.Current.children); + } + if (!_vesselParts.Contains(child.Current)) _vesselParts.Add(child.Current); + } + child.Dispose(); + } + + private void EnableResourceFlow(List children) + { + List.Enumerator child = children.GetEnumerator(); + while (child.MoveNext()) + { + if (child.Current == null) continue; + + SetupExplosive(child.Current); + var tnt = part.FindModuleImplementing(); + if (tnt) + { + tnt.Team = Team; + tnt.sourcevessel = SourceVessel; + } + IEnumerator resource = child.Current.Resources.GetEnumerator(); + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + if (!resource.Current.flowState) + { + resource.Current.flowState = true; + } + } + resource.Dispose(); + if (child.Current.children.Count > 0) + { + EnableResourceFlow(child.Current.children); + } + } + child.Dispose(); + } + + private void DisableResourcesFlow() + { + if (_targetDecoupler != null) + { + if (_targetDecoupler.part.children.Count == 0) return; + _vesselParts.Clear(); + DisableRecursiveFlow(_targetDecoupler.part.children); + } + } + + private void MissileIgnition() + { + EnableResourceFlow(_vesselParts); + GameObject velocityObject = new GameObject("velObject"); + velocityObject.transform.position = vessel.transform.position; + velocityObject.transform.parent = vessel.transform; + _velocityTransform = velocityObject.transform; + + MissileState = MissileStates.Boost; + + ExecuteNextStage(); + + MissileState = MissileStates.Cruise; + + _missileIgnited = true; + RadarWarningReceiver.WarnMissileLaunch(MissileReferenceTransform.position, GetForwardTransform(), TargetingMode == TargetingModes.Radar); + } + + private bool ShouldExecuteNextStage() + { + if (!_missileIgnited) return false; + if (TimeIndex < 1) return false; + + // Replaced Linq expression... + using (List.Enumerator parts = vessel.parts.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null || !IsEngine(parts.Current)) continue; + if (EngineIgnitedAndHasFuel(parts.Current)) + { + return false; + } + } + + //If the next stage is greater than the number defined of stages the missile is done + if (_nextStage > StagesNumber) + { + MissileState = MissileStates.PostThrust; + return false; + } + + return true; + } + + public bool IsEngine(Part p, bool returnThrust = false) + { + using (List.Enumerator m = p.Modules.GetEnumerator()) + while (m.MoveNext()) + { + if (m.Current == null) continue; + if (m.Current is ModuleEngines) + { + if (!returnThrust) return true; + else thrust += p.FindModuleImplementing().maxThrust; + } + } + return false; + } + + public static bool EngineIgnitedAndHasFuel(Part p) + { + using List.Enumerator m = p.Modules.GetEnumerator(); + while (m.MoveNext()) + { + PartModule pm = m.Current; + ModuleEngines eng = pm as ModuleEngines; + if (eng == null) continue; + if (eng.EngineIgnited && (!eng.getFlameoutState || eng.flameoutBar == 0 || eng.status == "Nominal")) + return true; + } + return false; + } + + public override void OnStart(StartState state) + { + base.OnStart(state); + SetupsFields(); + + if (string.IsNullOrEmpty(GetShortName())) + { + shortName = "Unnamed"; + } + + part.force_activate(); + RefreshGuidanceMode(); + + UpdateTargetingMode((TargetingModes)Enum.Parse(typeof(TargetingModes), _targetingLabel)); + + _targetDecoupler = FindFirstDecoupler(part.parent, null); + thrust = 0; + mass = 0; + DisableResourcesFlow(); + + weaponClass = WeaponClasses.Missile; + WeaponName = GetShortName(); + if (HighLogic.LoadedSceneIsFlight && customTurretID > 0) + { + missileName = shortName; + using (var servo = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (servo.MoveNext()) + { + if (servo.Current == null) continue; + if ((int)servo.Current.turretID != (int)customTurretID) continue; + customTurret.Add(servo.Current); + servo.Current.SetReferenceTransform(MissileReferenceTransform); //confirm this is pointing in the right direction + } + if (customTurret.Count == 0) customTurretID = 0; + } + if (HighLogic.LoadedSceneIsEditor) + { + GameEvents.onEditorPartPlaced.Add(OnEditorPartPlaced); + FindTurretInParents(part); + } + activeRadarRange = ActiveRadarRange; + chaffEffectivity = ChaffEffectivity; + missileCMRange = MissileCMRange; + missileCMInterval = MissileCMInterval; + hasIFF = HasIFF; + terminalHomingRange = TerminalHomingRange; + //TODO: BDModularGuidance should be configurable? + heatThreshold = 50; + lockedSensorFOV = 5; + radarLOAL = true; + + if (missileFireAngle < 0 && maxOffBoresight < 180) + { + UI_FloatRange mFA = (UI_FloatRange)Fields["missileFireAngle"].uiControlEditor; + mFA.maxValue = maxOffBoresight * 0.75f; + //mFA.stepIncrement = mFA.maxValue / 100; + missileFireAngle = maxOffBoresight * 0.75f; + } + + // fill lockedSensorFOVBias with default values if not set by part config: + if ((TargetingMode == TargetingModes.Heat || TargetingModeTerminal == TargetingModes.Heat) && heatThreshold > 0 && lockedSensorFOVBias.minTime == float.MaxValue) + { + 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.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.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))); + } + } + + // fill lockedSensorVelocityBias with default values if not set by part config: + if ((TargetingMode == TargetingModes.Heat || TargetingModeTerminal == TargetingModes.Heat) && heatThreshold > 0) + { + bool defaultVelocityBias = false; + if (lockedSensorVelocityBias.minTime == float.MaxValue) + { + lockedSensorVelocityBias.Add(0f, 1f); + lockedSensorVelocityBias.Add(180f, 1f); + defaultVelocityBias = true; + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.BDModularGuidance]: OnStart missile {shortName}: setting default lockedSensorVelocityBias curve to:"); + Debug.Log("key = 0 1"); + Debug.Log("key = 180 1"); + } + } + + if (lockedSensorVelocityMagnitudeBias.minTime == float.MaxValue) + { + lockedSensorVelocityMagnitudeBias.Add(1f, 1f); + if (defaultVelocityBias) + lockedSensorVelocityMagnitudeBias.Add(0f, 1f); + else + lockedSensorVelocityMagnitudeBias.Add(0f, 0f); + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default lockedSensorVelocityMagnitudeBias curve to:"); + Debug.Log("key = 1 1"); + if (defaultVelocityBias) + Debug.Log("key = 0 1"); + else + Debug.Log("key = 0 0"); + } + } + } + + // fill activeRadarLockTrackCurve with default values if not set by part config: + if ((TargetingMode == TargetingModes.Radar || TargetingModeTerminal == TargetingModes.Radar) && activeRadarRange > 0 && activeRadarLockTrackCurve.minTime == float.MaxValue) + { + activeRadarLockTrackCurve.Add(0f, 0f); + activeRadarLockTrackCurve.Add(activeRadarRange, RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); // TODO: tune & balance constants! + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDModularGuidance]: OnStart missile {shortName}: setting default locktrackcurve with maxrange/minrcs: {activeRadarLockTrackCurve.maxTime} / {RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS}"); + } + + var explosiveParts = VesselModuleRegistry.GetModules(vessel); + if (explosiveParts != null) + { + foreach (var explosivePart in explosiveParts) + { + if (warheadYield < explosivePart.blastRadius) warheadYield = explosivePart.blastRadius; + } + } + } + + private void SetupsFields() + { + Events["HideUI"].active = false; + Events["ShowUI"].active = true; + + if (isTimed) + { + Fields["detonationTime"].guiActive = true; + Fields["detonationTime"].guiActiveEditor = true; + } + else + { + Fields["detonationTime"].guiActive = false; + Fields["detonationTime"].guiActiveEditor = false; + } + + Fields["terminalHomingRange"].guiActiveEditor = false; + + Fields["LoftMaxAltitude"].uiControlEditor = (UI_FloatRange)Fields["LoftMaxAltitude"].uiControlFlight; + Fields["LoftRangeOverride"].uiControlEditor = (UI_FloatRange)Fields["LoftRangeOverride"].uiControlFlight; + Fields["LoftAltitudeAdvMax"].uiControlEditor = (UI_FloatRange)Fields["LoftAltitudeAdvMax"].uiControlFlight; + Fields["LoftMinAltitude"].uiControlEditor = (UI_FloatRange)Fields["LoftMinAltitude"].uiControlFlight; + Fields["LoftAngle"].uiControlEditor = (UI_FloatRange)Fields["LoftAngle"].uiControlFlight; + Fields["LoftTermAngle"].uiControlEditor = (UI_FloatRange)Fields["LoftTermAngle"].uiControlFlight; + Fields["LoftRangeFac"].uiControlEditor = (UI_FloatRange)Fields["LoftRangeFac"].uiControlFlight; + Fields["LoftVelComp"].uiControlEditor = (UI_FloatRange)Fields["LoftVelComp"].uiControlFlight; + Fields["LoftVertVelComp"].uiControlEditor = (UI_FloatRange)Fields["LoftVertVelComp"].uiControlFlight; + + if (HighLogic.LoadedSceneIsEditor) + { + WeaponNameWindow.OnActionGroupEditorOpened.Add(OnActionGroupEditorOpened); + WeaponNameWindow.OnActionGroupEditorClosed.Add(OnActionGroupEditorClosed); + Fields["CruiseAltitude"].guiActiveEditor = true; + Fields["CruiseSpeed"].guiActiveEditor = false; + Events["SwitchTargetingMode"].guiActiveEditor = true; + Events["SwitchGuidanceMode"].guiActiveEditor = true; + } + else + { + Fields["CruiseAltitude"].guiActiveEditor = false; + Fields["CruiseSpeed"].guiActiveEditor = false; + Events["SwitchTargetingMode"].guiActiveEditor = false; + Events["SwitchGuidanceMode"].guiActiveEditor = false; + SetMissileTransform(); + } + + UI_FloatRange staticMin = (UI_FloatRange)Fields["minStaticLaunchRange"].uiControlEditor; + UI_FloatRange staticMax = (UI_FloatRange)Fields["maxStaticLaunchRange"].uiControlEditor; + UI_FloatRange radarMax = (UI_FloatRange)Fields["ActiveRadarRange"].uiControlEditor; + + staticMin.onFieldChanged += OnStaticRangeUpdated; + staticMax.onFieldChanged += OnStaticRangeUpdated; + staticMax.maxValue = BDArmorySettings.MAX_ENGAGEMENT_RANGE; + staticMax.stepIncrement = BDArmorySettings.MAX_ENGAGEMENT_RANGE / 100; + radarMax.maxValue = BDArmorySettings.MAX_ENGAGEMENT_RANGE; + radarMax.stepIncrement = BDArmorySettings.MAX_ENGAGEMENT_RANGE / 100; + + UI_FloatRange stageOnProximity = (UI_FloatRange)Fields["StageToTriggerOnProximity"].uiControlEditor; + stageOnProximity.onFieldChanged = OnStageOnProximity; + + OnStageOnProximity(Fields["StageToTriggerOnProximity"], null); + InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); + } + + private void SetPersistantFields() + { + Fields["LoftMaxAltitude"].isPersistant = true; + Fields["LoftRangeOverride"].isPersistant = true; + Fields["LoftAltitudeAdvMax"].isPersistant = true; + Fields["LoftMinAltitude"].isPersistant = true; + Fields["LoftAngle"].isPersistant = true; + Fields["LoftTermAngle"].isPersistant = true; + Fields["LoftRangeFac"].isPersistant = true; + Fields["LoftVelComp"].isPersistant = true; + Fields["LoftVertVelComp"].isPersistant = true; + } + + private void OnStageOnProximity(BaseField baseField, object o) + { + UI_FloatRange detonationDistance = (UI_FloatRange)Fields["DetonationDistance"].uiControlEditor; + + if (StageToTriggerOnProximity != 0) + { + detonationDistance = (UI_FloatRange)Fields["DetonationDistance"].uiControlEditor; + + detonationDistance.maxValue = 8000; + + detonationDistance.stepIncrement = 50; + } + else + { + detonationDistance.maxValue = 100; + + detonationDistance.stepIncrement = 1; + } + } + + private void OnStaticRangeUpdated(BaseField baseField, object o) + { + InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); + } + + private void UpdateTargetingMode(TargetingModes newTargetingMode) + { + if (newTargetingMode == TargetingModes.Radar) + { + Fields["ActiveRadarRange"].guiActive = true; + Fields["ActiveRadarRange"].guiActiveEditor = true; + } + else + { + Fields["ActiveRadarRange"].guiActive = false; + Fields["ActiveRadarRange"].guiActiveEditor = false; + } + TargetingMode = newTargetingMode; + _targetingLabel = newTargetingMode.ToString(); + + GUIUtils.RefreshAssociatedWindows(part); + } + + void OnEditorPartPlaced(Part p) + { + if (p = part) FindTurretInParents(part); + } + private void FindTurretInParents(Part p) + { + if (p == null) + { + Fields["customTurretID"].guiActiveEditor = false; + return; + } + var turret = p.FindModuleImplementing(); + if (turret != null) + { + Fields["customTurretID"].guiActiveEditor = true; + return; + } + FindTurretInParents(p.parent); + } + + private void OnDestroy() + { + if (vessel) vessel.OnFlyByWire -= GuidanceSteer; + WeaponNameWindow.OnActionGroupEditorOpened.Remove(OnActionGroupEditorOpened); + WeaponNameWindow.OnActionGroupEditorClosed.Remove(OnActionGroupEditorClosed); + GameEvents.onPartDie.Remove(PartDie); + GameEvents.onEditorPartPlaced.Remove(OnEditorPartPlaced); + if (_velocityTransform != null) { Destroy(_velocityTransform.gameObject); } + } + + private void SetMissileTransform() + { + MissileReferenceTransform = part.transform; + ForwardTransformAxis = (TransformAxisVectors)Enum.Parse(typeof(TransformAxisVectors), ForwardTransform); + UpTransformAxis = (TransformAxisVectors)Enum.Parse(typeof(TransformAxisVectors), UpTransform); + } + + void UpdateGuidance() + { + if (guidanceActive) + { + switch (TargetingMode) + { + case TargetingModes.None: + if (_targetVessel != null) + { + TargetPosition = _targetVessel.CurrentCoM; + TargetVelocity = _targetVessel.Velocity(); + TargetAcceleration = _targetVessel.acceleration; + } + break; + + case TargetingModes.Radar: + UpdateRadarTarget(); + break; + + case TargetingModes.Heat: + UpdateHeatTarget(); + break; + + case TargetingModes.Laser: + UpdateLaserTarget(); + break; + + case TargetingModes.Gps: + UpdateGPSTarget(); + break; + + case TargetingModes.AntiRad: + UpdateAntiRadiationTarget(); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private Vector3 AAMGuidance() + { + Vector3 aamTarget; + if (TargetAcquired) + { + float timeToImpact; + float gLimit; + + if (GuidanceIndex == 6) // Augmented Pro-Nav + aamTarget = MissileGuidance.GetAPNTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, 3f, out timeToImpact, out gLimit); + else if (GuidanceIndex == 5) // Pro-Nav + aamTarget = MissileGuidance.GetPNTarget(TargetPosition, TargetVelocity, vessel, 3f, out timeToImpact, out gLimit); + else if (GuidanceIndex == 8) // Loft + { + 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) && VectorUtils.Angle(TargetPosition - vessel.CoM, vessel.upAxis) > Mathf.Min(LoftAngle, 90f - LoftAngle) && ((vessel.altitude - targetAlt <= LoftAltitudeAdvMax) || (TargetPosition - vessel.CoM).sqrMagnitude > (LoftRangeOverride * LoftRangeOverride))) loftState = LoftStates.Boost; + else loftState = LoftStates.Terminal; + } + float currgLimit = -1; + aamTarget = MissileGuidance.GetAirToAirLoftTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, targetAlt, LoftMaxAltitude, LoftRangeFac, LoftVertVelComp, LoftVelComp, LoftAngle, LoftTermAngle, terminalHomingRange, 20f, 0.05f, ref loftState, out float currTimeToImpact, out currgLimit, out float rangeToTarget, homingModeTerminal, 3); + + float fac = (1 - (rangeToTarget - terminalHomingRange - 100f) / Mathf.Clamp(terminalHomingRange * 4f, 5000f, 25000f)); + + timeToImpact = currTimeToImpact; + } + else // AAM Lead + aamTarget = MissileGuidance.GetAirToAirTargetModular(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact); + TimeToImpact = timeToImpact; + + if (VectorUtils.Angle(aamTarget - vessel.CoM, vessel.transform.forward) > maxOffBoresight * 0.75f) + { + 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); + } + else + { + aamTarget = vessel.CoM + (20 * vessel.Velocity()); + } + + return aamTarget; + } + + private Vector3 AGMGuidance() + { + if (TargetingMode != TargetingModes.Gps) + { + if (TargetAcquired) + { + //lose lock if seeker reaches gimbal limit + float targetViewAngle = VectorUtils.Angle(vessel.transform.forward, TargetPosition - vessel.CoM); + + if (targetViewAngle > maxOffBoresight) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.BDModularGuidance]: AGM Missile guidance failed - target out of view"); + guidanceActive = false; + } + } + else + { + if (TargetingMode == TargetingModes.Laser) + { + //keep going straight until found laser point + TargetPosition = laserStartPosition + (20000 * startDirection); + } + } + } + Vector3 agmTarget = MissileGuidance.GetAirToGroundTarget(TargetPosition, TargetVelocity, vessel, 1.85f); + return agmTarget; + } + + private Vector3 CruiseGuidance() + { + if (_guidance == null) + { + _guidance = new CruiseGuidance(this); + } + + return _guidance.GetDirection(this, TargetPosition, TargetVelocity); + } + + #region Orbital Modular Missile Guidance + // Code contained within this region is adapted from Hatbat, Spartwo and MiffedStarfish's Kerbal Combat Systems Mod https://github.com/Halbann/StockCombatAI/tree/dev/Source/KerbalCombatSystems. + // Code is distributed under CC-BY-SA 4.0: https://creativecommons.org/licenses/by-sa/4.0/ + private Vector3 OrbitalGuidance() + { + Vector3 orbitalTarget; + Vector3 forwardDir = GetForwardTransform(); + if (TargetAcquired) + { + float timeToImpact; + + // Target information update is one frame behind on vessel.OnFlyByWire, so compensate here + Vector3 targetAcceleration = TargetAcceleration; + Vector3 targetVelocity = TargetVelocity + Time.fixedDeltaTime * targetAcceleration; + Vector3 targetPosition = TargetPosition + TimeWarp.fixedDeltaTime * targetVelocity; + + Vector3 targetVector = targetPosition - vessel.CoM; + Vector3 relVel = vessel.Velocity() - targetVelocity; + + Vector3 relVelNrm = relVel.normalized; + Vector3 interceptVector; + float relVelmag = relVel.magnitude; + + // Calculate max accel + Vector3 propulsionVector = vessel.transform.InverseTransformDirection(-GetFireVector(engines, rcsThrusters, -forwardDir)); + float maxThrust = propulsionVector.magnitude; + float maxAcceleration = maxThrust / vessel.GetTotalMass(); + + if (targetVessel != null) + missileTarget = targetVessel.isMissile; + + if (!missileTarget) + { + timeToImpact = BDAMath.SolveTime(targetVector.magnitude, maxAcceleration, Vector3.Dot(relVel, targetVector.normalized)); + Vector3 lead = -timeToImpact * relVelmag * relVelNrm; + interceptVector = (targetPosition + lead) - vessel.CoM; + } + else + { + Vector3 acceleration = forwardDir * maxAcceleration; + + relVel = targetVelocity - vessel.Velocity(); + timeToImpact = AIUtils.TimeToCPA(targetVector, relVel, targetAcceleration - acceleration, 30); + interceptVector = AIUtils.PredictPosition(targetVector, relVel, targetAcceleration - 0.5f * acceleration, timeToImpact); + + if (Vector3.Dot(interceptVector, targetVector) < 0) + interceptVector = targetVector; + } + + orbitalTarget = interceptVector.normalized; + + orbitalTarget = VacuumClearanceManeuver(orbitalTarget, vessel.CoM, rcsThrusters.Any(), engines.Any()); + if (vacuumClearanceState == VacuumClearanceStates.Cleared) + { + float accuracy = Vector3.Dot(orbitalTarget, relVelNrm); + float shutoffDistanceSqr = missileTarget ? 9 : 100; + if (targetVector.sqrMagnitude < shutoffDistanceSqr || (!engines.Any() && !rcsThrusters.Any()) && accuracy < 0.99f) + { + guidanceActive = false; + return forwardDir; + } + + bool drift = accuracy > 0.999999f + && (Vector3.Dot(relVel, orbitalTarget) > MaxSpeed || missileTarget); + + Throttle = drift ? 0 : 1; + } + + // Set RCS direction + if (!(vacuumClearanceState == VacuumClearanceStates.Clearing || (TimeIndex < dropTime + Mathf.Min(0.5f, BDAMath.SolveTime(10f, maxAcceleration))))) // Don't use RCS immediately after launch or when clearing a vessel to avoid running into VLS/SourceVessel + { + if (vacuumClearanceState == VacuumClearanceStates.Turning && SourceVessel) // Clear away from launching vessel + { + Vector3 relP = (vessel.CoM - SourceVessel.CoM).normalized; + relVel = relP + (vessel.Velocity() - SourceVessel.Velocity()).normalized.ProjectOnPlanePreNormalized(relP); + relVel = 100f * relVel.ProjectOnPlane(targetPosition - vessel.CoM); + } + else // Kill relative velocity to target + relVel = vessel.Velocity() - targetVelocity; + rcsVector = -Vector3.ProjectOnPlane(relVel, forwardDir); + } + } + else + { + orbitalTarget = vessel.CoM + forwardDir; + } + DrawDebugLine(vessel.CoM, vessel.CoM + 1000 * orbitalTarget.normalized); + return orbitalTarget; + } + + private void UpdateOrbitalStage() + { + // Update list of engines/thrusters + engines = VesselModuleRegistry.GetModuleEngines(vessel); + rcsThrusters = VesselModuleRegistry.GetModules(vessel); + + // Set up clearance maneuver + vacuumClearanceState = (engines.Any() && vessel.InVacuum()) ? VacuumClearanceStates.Clearing : VacuumClearanceStates.Cleared; + + // Get a probe core and align its reference transform with the propulsion vector. + ModuleCommand commander = VesselModuleRegistry.GetModuleCommand(vessel); + if (commander != null) + { + commander.MakeReference(); + Vector3 propulsionVector = -GetFireVector(engines, rcsThrusters, -vessel.ReferenceTransform.up); + if (propulsionVector != null) + AlignReference(commander, propulsionVector.normalized); + } + } + + // Create and set a new control point for a command module (commander) pointing along a world space vector (direction). + // Uses: controlling missiles from the the average engine direction to allow for mistaken/unconventional probe core orientation. + private static void AlignReference(ModuleCommand commander, Vector3 direction) + { + ControlPoint dynamic = commander.GetControlPoint("dynamic"); + // Check for an already existing dynamic control point + if (dynamic == null) + { + // Create a new transform named dynamic. + GameObject tc = new GameObject("dynamic"); + Transform transform = tc.transform; + transform.SetParent(commander.transform); + transform.position = commander.transform.position; + + // Create a new control point with the transform. + dynamic = new ControlPoint("dynamic", "Dynamic", transform, Vector3.zero); + + // Add the control point to the command module and set it as active. + commander.controlPoints.Add("dynamic", dynamic); + } + + commander.SetControlPoint("dynamic"); + + Vector3 referenceRoll = commander.part.GetReferenceTransform().forward; + Vector3 roll = referenceRoll != direction.normalized ? referenceRoll : commander.transform.forward; + + // Orient the control point towards direction (finger) with perpendicular as the up vector (thumb). + Vector3 perpendicular = Vector3.ProjectOnPlane(roll, direction.normalized); + dynamic.transform.rotation = Quaternion.LookRotation(perpendicular, direction.normalized); // VAB orientation. + } + + private static Vector3 GetFireVector(List engines, List RCS = null, Vector3 thrustVector = default(Vector3)) + { + // Place linears first to establish a direction, not currently needed + if (engines?.Any() == true) + { + // If there are engines we can override any potential provided vector + thrustVector = GetMeanVector(engines.First()); + foreach (ModuleEngines engine in engines.Skip(1)) + { + thrustVector += GetMeanVector(engine); + } + if (RCS?.Any() == true) + { + // If there are engines we can add RCS on top + foreach (ModuleRCS thruster in RCS) + { + thrustVector += GetRCSVector(thruster, thrustVector); + } + } + } + else if (RCS?.Any() == true) + { + // If there are no engines we have to plot the RCS along the provided vector + Vector3 rcsVector = GetRCSVector(RCS.First(), thrustVector); + foreach (ModuleRCSFX thruster in RCS.Skip(1)) + { + rcsVector += GetRCSVector(thruster, thrustVector); + } + //replace thrustVector with RCS Vector + thrustVector = rcsVector; + } + + return thrustVector; + } + + private static Vector3 GetRCSVector(ModuleRCS thruster, Vector3 thrustVector) + { + //method to get the thrust vector of a specified rcs thruster + Vector3 meanVector = Vector3.zero; + if (!thruster) return meanVector; + + foreach (Transform thrusterTransform in thruster.thrusterTransforms) + { + if (!thrusterTransform) continue; + Vector3 pos = thruster.thrusterPower * thrusterTransform.up; + // rcs will fire if thrust goes in the forward direction by any degree, this is reduced with angle offset + float rcsThrustDot = Vector3.Dot(thrustVector.normalized, pos.normalized); + if (rcsThrustDot > 0) + meanVector += rcsThrustDot * pos; + } + + return meanVector; + } + + private static Vector3 GetMeanVector(ModuleEngines thruster) + { + //method to get the thrust vector of a specified engine + Vector3 meanVector = Vector3.zero; + if (!thruster) return meanVector; + + foreach (Transform thrusterTransform in thruster.thrustTransforms) + { + if (!thrusterTransform) continue; + Vector3 pos = thrusterTransform.forward; + meanVector += pos; + } + + //get vector and set length to the thruster power + meanVector = (thruster.MaxThrustOutputVac(true) * meanVector.normalized); + return meanVector; + } + #endregion + + private void CheckMiss(Vector3 targetPosition) + { + if (HasMissed) return; + if (MissileState != MissileStates.PostThrust) return; + if (GuidanceMode == GuidanceModes.Orbital) return; + // if I'm to close to my vessel avoid explosion + if ((vessel.CoM - SourceVessel.CoM).sqrMagnitude < 16 * DetonationDistanceSqr) return; + // 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 (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Missile CheckMiss showed miss for {vessel.vesselName} ({SourceVessel}) with target at {targetPosition - vessel.CoM:G3}"); + + // var AI = vessel.ActiveController().AI; // Get the AI if the missile has one. + // if (AI != null) + // { + // ResetMissile(); + // AI.ActivatePilot(); + // return; + // } + + HasMissed = true; + guidanceActive = false; + isTimed = true; + detonationTime = TimeIndex + 1.5f; + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && vessel.isActiveVessel) LoadedVesselSwitcher.Instance.TriggerSwitchVessel(); + } + + private void ResetMissile() + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Resetting missile {vessel.vesselName}"); + heatTarget = TargetSignatureData.noTarget; + vrd = null; + radarTarget = TargetSignatureData.noTarget; + HasFired = false; + StagesNumber = 1; + _nextStage = 1; + TargetAcquired = false; + TimeFired = -1; + _missileIgnited = false; + lockFailTimer = -1; + guidanceActive = false; + HasMissed = false; + HasExploded = false; + DetonationDistanceState = DetonationDistanceStates.Cruising; + BDATargetManager.FiredMissiles.Remove(this); + MissileState = MissileStates.Idle; + if (FiredByWM != null && FiredByWM.guardFiringMissile) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: disabling target lock for {vessel.vesselName}"); + FiredByWM.guardFiringMissile = false; // Disable target lock. + } + } + + private void CheckMiss() + { + if (HasMissed) return; + bool noProgress = MissileState == MissileStates.PostThrust && + ((Vector3.Dot(vessel.Velocity() - TargetVelocity, TargetPosition - vessel.transform.position) < 0) || + (vessel.LandedOrSplashed || vessel.Velocity().sqrMagnitude < GetKinematicSpeed() * GetKinematicSpeed())); + if (noProgress) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Missile CheckMiss showed miss for {vessel.vesselName}"); + + // var AI = vessel.ActiveController().AI; // Get the AI if the missile has one. + // if (AI != null) + // { + // ResetMissile(); + // AI.ActivatePilot(); + // return; + // } + + HasMissed = true; + guidanceActive = false; + isTimed = true; + detonationTime = TimeIndex + 1.5f; + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && vessel.isActiveVessel) LoadedVesselSwitcher.Instance.TriggerSwitchVessel(); + } + } + + + public void GuidanceSteer(FlightCtrlState s) + { + if (!vessel || !vessel.loaded || vessel.packed) return; + FloatingOriginCorrection(); + debugString.Length = 0; + if (guidanceActive && MissileReferenceTransform != null && _velocityTransform != null) + { + if (FiredByWM != null && !FiredByWM.guardFiringMissile) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: enabling target lock for {vessel.vesselName}"); + FiredByWM.guardFiringMissile = true; // Enable target lock. + } + + if (vessel.Velocity().magnitude < MinSpeedGuidance) + { + if (!_minSpeedAchieved) + { + s.mainThrottle = 1; + return; + } + } + else + { + _minSpeedAchieved = true; + } + + Vector3 newTargetPosition = new Vector3(); + switch (GuidanceIndex) + { + case 1: + newTargetPosition = AAMGuidance(); + break; + case 2: + newTargetPosition = AGMGuidance(); + break; + case 3: + newTargetPosition = CruiseGuidance(); + break; + case 4: + newTargetPosition = BallisticGuidance(); + break; + case 5: + newTargetPosition = AAMGuidance(); + break; + case 6: + newTargetPosition = AAMGuidance(); + break; + case 7: + newTargetPosition = OrbitalGuidance(); + break; + case 8: + newTargetPosition = AAMGuidance(); + break; + } + CheckMiss(newTargetPosition); + + if (GuidanceMode != GuidanceModes.Orbital) + { + //Updating aero surfaces + if (TimeIndex > dropTime + 0.5f) + { + + _velocityTransform.rotation = Quaternion.LookRotation(vessel.Velocity(), -vessel.transform.forward); + Vector3 targetDirection = _velocityTransform.InverseTransformPoint(newTargetPosition).normalized; + targetDirection = Vector3.RotateTowards(Vector3.forward, targetDirection, 15 * Mathf.Deg2Rad, 0); + + Vector3 localAngVel = vessel.angularVelocity; + float steerYaw = SteerMult * targetDirection.x - SteerDamping * -localAngVel.z; + float steerPitch = SteerMult * targetDirection.y - SteerDamping * -localAngVel.x; + + s.yaw = Mathf.Clamp(steerYaw, -MaxSteer, MaxSteer); + s.pitch = Mathf.Clamp(steerPitch, -MaxSteer, MaxSteer); + + if (RollCorrection) + { + SetRoll(); + s.roll = Roll; + } + + } + s.mainThrottle = Throttle; + } + else // Orbital guidance + { + if (TimeIndex > dropTime) + { + // Update RCS + if (rcsVector != Vector3.zero) + { + float rcsPower = 20; + + if (rcsVectorLerped == Vector3.zero) + rcsVectorLerped = rcsVector; + + float rcsLerpMag = rcsVectorLerped.magnitude; + + rcsVectorLerped = Vector3.Lerp(rcsVectorLerped, rcsVector, 5f * Time.fixedDeltaTime * Mathf.Clamp01(rcsLerpMag / rcsPower)); + float rcsThrottle = Mathf.Lerp(0, 1.732f, Mathf.InverseLerp(0, rcsPower, rcsLerpMag)); + Vector3 rcsThrust = rcsVectorLerped.normalized * rcsThrottle; + + Vector3 up = -vessel.ReferenceTransform.forward; + Vector3 forward = -vessel.ReferenceTransform.up; + Vector3 right = Vector3.Cross(up, forward); + + s.X = Mathf.Clamp(Vector3.Dot(rcsThrust, right), -1, 1); + s.Y = Mathf.Clamp(Vector3.Dot(rcsThrust, up), -1, 1); + s.Z = Mathf.Clamp(Vector3.Dot(rcsThrust, forward), -1, 1); + } + + // Position error + Vector3 attitude = newTargetPosition; + float error = VectorUtils.Angle(vessel.ReferenceTransform.up, attitude); + + // Update throttle if we have finished clearing maneuever + if (vacuumClearanceState == VacuumClearanceStates.Cleared) + { + float alignmentToleranceforBurn = missileTarget ? 60 : 20; + bool facingDesiredRotation = error < alignmentToleranceforBurn; + float throttleActual = facingDesiredRotation ? Throttle : 0; + s.mainThrottle = throttleActual; + } + else + s.mainThrottle = Throttle; + + // Update SAS + if (attitude == Vector3.zero) return; + + if (vessel.ActionGroups[KSPActionGroup.SAS]) + vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, false); + + var ap = vessel.Autopilot; + if (ap == null) return; + + // The offline SAS must not be on stability assist. Normal seems to work on most probes. + if (ap.Mode != VesselAutopilot.AutopilotMode.Normal) + ap.SetMode(VesselAutopilot.AutopilotMode.Normal); + + ap.SAS.SetTargetOrientation(attitude, false); + } + } + } + CheckMiss(); + } + + private void SetRoll() + { + Vector3 up = vessel.up; + Vector3 right = vessel.transform.right; + + var currentAngle = Vector3.SignedAngle(right, up, Vector3.Cross(right, up)) - 90f; + + angularVelocity = currentAngle - lastRollAngle; + //angularAcceleration = angularVelocity - lasAngularVelocity; + + var futureAngle = currentAngle + angularVelocity / Time.fixedDeltaTime * 1f; + + if (futureAngle > 0.5f || currentAngle > 0.5f) + { + Roll = Mathf.Clamp(Roll - 0.001f, -1f, 0f); + } + else if (futureAngle < -0.5f || currentAngle < -0.5f) + { + Roll = Mathf.Clamp(Roll + 0.001f, 0, 1f); + } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugString.AppendLine($"Roll angle: {currentAngle}"); + debugString.AppendLine($"future Roll angle: {futureAngle}"); + debugString.AppendLine($"Roll value: {Roll}"); + } + lastRollAngle = currentAngle; + //lasAngularVelocity = angularVelocity; + } + + public float Roll { get; set; } + + private Vector3 BallisticGuidance() + { + return CalculateAGMBallisticGuidance(this, TargetPosition); + } + + private void UpdateMenus(bool visible) + { + Events["HideUI"].active = visible; + Events["ShowUI"].active = !visible; + } + + private void OnActionGroupEditorOpened() + { + Events["HideUI"].active = false; + Events["ShowUI"].active = false; + } + + private void OnActionGroupEditorClosed() + { + Events["HideUI"].active = false; + Events["ShowUI"].active = true; + } + + /// + /// Recursive method to find the top decoupler that should be used to jettison the missile. + /// + /// + /// + /// + public static PartModule FindFirstDecoupler(Part parent, PartModule last) + { + if (parent == null || !parent) return last; + + PartModule newModuleDecouple = parent.FindModuleImplementing(); + if (newModuleDecouple == null) + { + newModuleDecouple = parent.FindModuleImplementing(); + } + if (newModuleDecouple != null && newModuleDecouple) + { + return FindFirstDecoupler(parent.parent, newModuleDecouple); + } + return FindFirstDecoupler(parent.parent, last); + } + + /// + /// This method will execute the next ActionGroup. Due to StageManager is designed to work with an active vessel + /// And a missile is not an active vessel. I had to use a different way to handle stages, and action groups work perfectly! + /// + public void ExecuteNextStage() + { + 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)); + + if (MissileState > MissileStates.Drop) // Past the drop stage, auto-enable some things if the player forgot. + { + if (StagesNumber == 1) // Auto-enable engines for single stage missiles. + { + if (SpawnUtils.CountActiveEngines(vessel) < 1) + SpawnUtils.ActivateAllEngines(vessel, true, false); + } + var warheads = VesselModuleRegistry.GetModules(vessel); + if (!warheads.Any(warhead => warhead.Armed)) // Auto-arm warheads if none are armed. + { + foreach (var warhead in warheads) + warhead.ArmAG(null); + } + } + + _nextStage++; + + if (GuidanceMode == GuidanceModes.Orbital) + UpdateOrbitalStage(); + + vessel.OnFlyByWire -= GuidanceSteer; // Remove possibly pre-existing callback. + vessel.OnFlyByWire += GuidanceSteer; + + //todo: find a way to fly by wire vessel decoupled + } + + protected override void OnGUI() + { + base.OnGUI(); + if (HighLogic.LoadedSceneIsFlight) + { + drawLabels(); + } + } + + #region KSP ACTIONS + + [KSPAction("Fire Missile")] + public void AgFire(KSPActionParam param) + { + FireMissile(); + } + + /// + /// Reset the missile if it has a pilot AI. + /// + // [KSPAction("Reset Missile")] + // public void AGReset(KSPActionParam param) + // { + // var AI = vessel.ActiveController().AI; // Get the AI if the missile has one. + // if (AI != null) + // { + // ResetMissile(); + // AI.ActivatePilot(); + // } + // } + + #endregion KSP ACTIONS + + #region KSP EVENTS + + [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_FireMissile", active = true)]//Fire Missile + public void GuiFire() + { + FireMissile(); + } + + [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_FireMissile", active = true)]//Fire Missile + public override void FireMissile() + { + if (HasFired) return; + + FiredByWM = WeaponManager; // Generally, the parent plane's WM, but may also be the WM on the modular missile if the missile has reset. + if (FiredByWM != null && targetVessel == null) + FiredByWM.SendTargetDataToMissile(this, null); + + GameEvents.onPartDie.Add(PartDie); + SourceVessel = vessel; + SetTargeting(); + Jettison(); + + BDATargetManager.FiredMissiles.Add(this); + + if (FiredByWM != null) + { + Team = FiredByWM.Team; + FiredByWM.UpdateMissilesAway(targetVessel, this); + } + AddTargetInfoToVessel(); // Wait until we've assigned the team before adding target info. + IncreaseTolerance(); + + if (radarTarget.exists && radarTarget.lockedByRadar && radarTarget.lockedByRadar.vessel != SourceVessel) + { + MissileFire datalinkwpm = radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateMissilesAway(targetVessel, this, false); + } + + initialMissileRollPlane = -vessel.transform.up; + initialMissileForward = vessel.transform.forward; + vessel.vesselName = GetShortName(); + vessel.vesselType = VesselType.Plane; + + if (!vessel.ActionGroups[KSPActionGroup.SAS]) + { + vessel.ActionGroups.ToggleGroup(KSPActionGroup.SAS); + } + + TimeFired = Time.time; + guidanceActive = true; + MissileState = MissileStates.Drop; + + GUIUtils.RefreshAssociatedWindows(part); + + HasFired = true; + DetonationDistanceState = DetonationDistanceStates.NotSafe; + if (vessel.InNearVacuum()) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + } + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && SourceVessel.isActiveVessel) LoadedVesselSwitcher.Instance.ForceSwitchVessel(vessel); + if (FiredByWM != null) + FiredByWM.UpdateList(); + } + + private void IncreaseTolerance() + { + foreach (var vesselPart in vessel.parts) + { + vesselPart.crashTolerance = 99; + vesselPart.breakingForce = 99; + vesselPart.breakingTorque = 99; + } + } + + private void SetTargeting() + { + startDirection = GetForwardTransform(); + SetLaserTargeting(); + SetAntiRadTargeting(); + } + + void OnDisable() + { + if (TargetingMode == TargetingModes.AntiRad) + { + RadarWarningReceiver.OnRadarPing -= ReceiveRadarPing; + } + } + + public Vector3 StartDirection { get; set; } + + [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_GuidanceMode", active = true)]//Guidance Mode + public void SwitchGuidanceMode() + { + GuidanceIndex++; + if (GuidanceIndex > 8) + { + GuidanceIndex = 1; + } + + RefreshGuidanceMode(); + } + + [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetingMode", active = true)]//Targeting Mode + public void SwitchTargetingMode() + { + string[] targetingModes = Enum.GetNames(typeof(TargetingModes)); + + int currentIndex = targetingModes.IndexOf(TargetingMode.ToString()); + + if (currentIndex < targetingModes.Length - 1) + { + UpdateTargetingMode((TargetingModes)Enum.Parse(typeof(TargetingModes), targetingModes[currentIndex + 1])); + } + else + { + UpdateTargetingMode((TargetingModes)Enum.Parse(typeof(TargetingModes), targetingModes[0])); + } + } + + [KSPEvent(guiActive = true, guiActiveEditor = false, active = true, guiName = "#LOC_BDArmory_Jettison")]//Jettison + public override void Jettison() + { + if (_targetDecoupler == null || !_targetDecoupler || _targetDecoupler is not IStageSeparator) return; + + ModuleDecouple decouple = _targetDecoupler as ModuleDecouple; + if (decouple != null) + { + decouple.ejectionForce *= 5; + decouple.Decouple(); + } + else + { + ((ModuleAnchoredDecoupler)_targetDecoupler).ejectionForce *= 5; + ((ModuleAnchoredDecoupler)_targetDecoupler).Decouple(); + } + + var weaponManager = WeaponManager; // If there's a WM on the MMG, update its weapons list, then disable it. + if (weaponManager != null) + { + weaponManager.UpdateList(); + if (weaponManager.guardMode) weaponManager.ToggleGuardMode(); + } + var AI = vessel.ActiveController().AI; // Get the AI if the missile has one and deactivate it. The MMG is in control. + if (AI != null) AI.DeactivatePilot(); + } + + public override float GetBlastRadius() + { + if (VesselModuleRegistry.GetModuleCount(vessel) > 0) + { + return VesselModuleRegistry.GetModules(vessel).Max(x => x.blastRadius); + } + else + { + return 5; + } + } + + protected override void PartDie(Part p) + { + if (p != part) return; + AutoDestruction(); + BDATargetManager.FiredMissiles.Remove(this); + GameEvents.onPartDie.Remove(PartDie); + Destroy(this); // If this is the active vessel, then KSP doesn't destroy it until we switch away, but we want to get rid of the MissileBase straight away. + } + + private void AutoDestruction() + { + var parts = vessel.Parts.ToArray(); + for (int i = parts.Length - 1; i >= 0; i--) + { + if (parts[i] != null) + parts[i].explode(); + } + + parts = null; + } + + 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) + { + vessel.ActionGroups.ToggleGroup((KSPActionGroup)Enum.Parse(typeof(KSPActionGroup), "Custom0" + (int)StageToTriggerOnProximity)); + HasExploded = true; + } + else + { + var explosiveParts = VesselModuleRegistry.GetModules(vessel); + if (explosiveParts != null) + { + foreach (var explosivePart in explosiveParts) + { if (!explosivePart.manualOverride) explosivePart.DetonateIfPossible(); } + if (explosiveParts.Any(explosivePart => explosivePart.hasDetonated)) + { + HasExploded = true; + AutoDestruction(); + } + } + var NukeParts = VesselModuleRegistry.GetModules(vessel); + if (NukeParts != null) + { + foreach (var nukePart in NukeParts) + { + nukePart.Detonate(); + AutoDestruction(); + } + } + if (explosiveParts == null && NukeParts == null) //kinetic 'detonation' + { + Vector3 relVel = TargetVelocity != Vector3.zero ? vessel.Velocity() - TargetVelocity : vessel.Velocity() - BDKrakensbane.FrameVelocityV3f; + Ray ray = new(transform.position, relVel); + if (Physics.Raycast(ray, out RaycastHit hit, 500f, (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels))) + { + ExplosionFx.CreateExplosion(hit.point, 0.5f * (1000f * vessel.GetTotalMass()) * relVel.sqrMagnitude / 4184000f, "BDArmory/Models/explosion/explosion", "BDArmory/Sounds/explode1", ExplosionSourceType.Missile, 1000f * vessel.GetRadius(), part, SourceVesselName, Team.Name, GetShortName(), ray.direction, -1, false, part.mass, -1, 1, ExplosionFx.WarheadTypes.Kinetic, null, 1.2f, sourceVelocity: vessel.Velocity()); + } + } + } + } + + public override Vector3 GetForwardTransform() + { + return GetTransform(ForwardTransformAxis); + } + + public override float GetKinematicTime() + { + if (!_missileIgnited) return -1f; + + float missileKinematicTime = (float)vessel.VesselDeltaV.TotalBurnTime; + if (!vessel.InVacuum()) + { + float drag = vessel.parts.Sum(x => x.dragScalar); + float speed = (float)vessel.srfSpeed; + float mass = (float)vessel.totalMass; + float dragTerm = 0.008f * mass * drag * 0.5f * (float)vessel.atmDensity; + float minSpeed = GetKinematicSpeed(); + if (speed > minSpeed) + missileKinematicTime += mass / (minSpeed * dragTerm) - mass / (speed * dragTerm); ; // Add time for missile to slow down to min speed + } + + return missileKinematicTime; + } + + public override float GetKinematicSpeed() + { + return vessel.InVacuum() ? 0f : Mathf.Max(MinSpeedGuidance, 100f); + } + + public Vector3 GetTransform(TransformAxisVectors transformAxis) + { + switch (transformAxis) + { + case TransformAxisVectors.UpPositive: + return MissileReferenceTransform.up; + + case TransformAxisVectors.UpNegative: + return -MissileReferenceTransform.up; + + case TransformAxisVectors.ForwardPositive: + return MissileReferenceTransform.forward; + + case TransformAxisVectors.ForwardNegative: + return -MissileReferenceTransform.forward; + + case TransformAxisVectors.RightNegative: + return -MissileReferenceTransform.right; + + case TransformAxisVectors.RightPositive: + return MissileReferenceTransform.right; + + default: + return MissileReferenceTransform.forward; + } + } + + [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_HideUI", active = false)]//Hide Weapon Name UI + public void HideUI() + { + WeaponNameWindow.HideGUI(); + UpdateMenus(false); + } + + [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_ShowUI", active = false)]//Set Weapon Name UI + public void ShowUI() + { + WeaponNameWindow.ShowGUI(this); + UpdateMenus(true); + } + + void OnCollisionEnter(Collision col) + { + base.CollisionEnter(col); + } + + #endregion KSP EVENTS + } + + #region UI + + [KSPAddon(KSPAddon.Startup.EditorAny, false)] + public class WeaponNameWindow : MonoBehaviour + { + internal static EventVoid OnActionGroupEditorOpened = new EventVoid("OnActionGroupEditorOpened"); + internal static EventVoid OnActionGroupEditorClosed = new EventVoid("OnActionGroupEditorClosed"); + + private static GUIStyle unchanged; + private static GUIStyle changed; + private static GUIStyle greyed; + private static GUIStyle overfull; + + private static WeaponNameWindow instance; + private static Vector3 mousePos = Vector3.zero; + + private bool ActionGroupMode; + + private Rect guiWindowRect = new Rect(0, 0, 0, 0); + + private BDModularGuidance missile_module; + + [KSPField] public int offsetGUIPos = -1; + + 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 + + private bool styleSetup; + + private string txtName = string.Empty; + + public static void HideGUI() + { + if (instance != null && instance.missile_module != null) + { + instance.missile_module.WeaponName = instance.missile_module.shortName; + instance.missile_module = null; + instance.UpdateGUIState(); + } + EditorLogic editor = EditorLogic.fetch; + if (editor != null) + editor.Unlock("BD_MN_GUILock"); + } + + public static void ShowGUI(BDModularGuidance missile_module) + { + if (instance != null) + { + instance.missile_module = missile_module; + instance.UpdateGUIState(); + } + } + + private void UpdateGUIState() + { + enabled = missile_module != null; + EditorLogic editor = EditorLogic.fetch; + if (!enabled && editor != null) + editor.Unlock("BD_MN_GUILock"); + } + + private IEnumerator CheckActionGroupEditor() + { + while (EditorLogic.fetch == null) + { + yield return null; + } + EditorLogic editor = EditorLogic.fetch; + while (EditorLogic.fetch != null) + { + if (editor.editorScreen == EditorScreen.Actions) + { + if (!ActionGroupMode) + { + HideGUI(); + OnActionGroupEditorOpened.Fire(); + } + EditorActionGroups age = EditorActionGroups.Instance; + if (missile_module && !age.GetSelectedParts().Contains(missile_module.part)) + { + HideGUI(); + } + ActionGroupMode = true; + } + else + { + if (ActionGroupMode) + { + HideGUI(); + OnActionGroupEditorClosed.Fire(); + } + ActionGroupMode = false; + } + yield return null; + } + } + + private void Awake() + { + enabled = false; + instance = this; + } + + void Start() + { + StartCoroutine(CheckActionGroupEditor()); + } + + private void OnDestroy() + { + instance = null; + } + + public void OnGUI() + { + if (!styleSetup) + { + styleSetup = true; + Styles.InitStyles(); + } + + EditorLogic editor = EditorLogic.fetch; + if (!HighLogic.LoadedSceneIsEditor || !editor) + { + return; + } + bool cursorInGUI = false; // nicked the locking code from Ferram + mousePos = Input.mousePosition; //Mouse location; based on Kerbal Engineer Redux code + mousePos.y = Screen.height - mousePos.y; + + int posMult = 0; + if (offsetGUIPos != -1) + { + posMult = offsetGUIPos; + } + if (ActionGroupMode) + { + if (guiWindowRect.width == 0) + { + guiWindowRect = new Rect(430 * posMult, 365, 438, 50); + } + new Rect(guiWindowRect.xMin + 440, mousePos.y - 5, 300, 20); + } + else + { + if (guiWindowRect.width == 0) + { + //guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, (Screen.height - 365)); + guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, 50); + } + new Rect(guiWindowRect.xMin - (230 - 8), mousePos.y - 5, 220, 20); + } + cursorInGUI = guiWindowRect.Contains(mousePos); + if (cursorInGUI) + { + editor.Lock(false, false, false, "BD_MN_GUILock"); + //if (EditorTooltip.Instance != null) + // EditorTooltip.Instance.HideToolTip(); + } + else + { + editor.Unlock("BD_MN_GUILock"); + } + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, guiWindowRect.position); + guiWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), guiWindowRect, GUIWindow, "Weapon Name GUI", Styles.styleEditorPanel); + } + + public void GUIWindow(int windowID) + { + InitializeStyles(); + + GUILayout.BeginVertical(); + GUILayout.Space(20); + + GUILayout.BeginHorizontal(); + + GUILayout.Label("Weapon Name: "); + + txtName = GUILayout.TextField(txtName); + + if (GUILayout.Button("Save & Close")) + { + missile_module.WeaponName = txtName; + missile_module.shortName = txtName; + instance.missile_module.HideUI(); + } + + GUILayout.EndHorizontal(); + + scrollPos = GUILayout.BeginScrollView(scrollPos); + + GUILayout.EndScrollView(); + + GUILayout.EndVertical(); + + GUI.DragWindow(); + GUIUtils.RepositionWindow(ref guiWindowRect); + } + + private static void InitializeStyles() + { + if (unchanged == null) + { + if (GUI.skin == null) + { + unchanged = new GUIStyle(); + changed = new GUIStyle(); + greyed = new GUIStyle(); + overfull = new GUIStyle(); + } + else + { + unchanged = new GUIStyle(GUI.skin.textField); + changed = new GUIStyle(GUI.skin.textField); + greyed = new GUIStyle(GUI.skin.textField); + overfull = new GUIStyle(GUI.skin.label); + } + + unchanged.normal.textColor = Color.white; + unchanged.active.textColor = Color.white; + unchanged.focused.textColor = Color.white; + unchanged.hover.textColor = Color.white; + + changed.normal.textColor = Color.yellow; + changed.active.textColor = Color.yellow; + changed.focused.textColor = Color.yellow; + changed.hover.textColor = Color.yellow; + + greyed.normal.textColor = Color.gray; + + overfull.normal.textColor = Color.red; + } + } + } + + internal class Styles + { + // Base styles + public static GUIStyle styleEditorTooltip; + public static GUIStyle styleEditorPanel; + + /// + /// This one sets up the styles we use + /// + internal static void InitStyles() + { + styleEditorTooltip = new GUIStyle(); + styleEditorTooltip.name = "Tooltip"; + styleEditorTooltip.fontSize = 12; + styleEditorTooltip.normal.textColor = new Color32(207, 207, 207, 255); + styleEditorTooltip.stretchHeight = true; + styleEditorTooltip.wordWrap = true; + styleEditorTooltip.normal.background = CreateColorPixel(new Color32(7, 54, 66, 200)); + styleEditorTooltip.border = new RectOffset(3, 3, 3, 3); + styleEditorTooltip.padding = new RectOffset(4, 4, 6, 4); + styleEditorTooltip.alignment = TextAnchor.MiddleLeft; + + styleEditorPanel = new GUIStyle(); + styleEditorPanel.normal.background = CreateColorPixel(new Color32(7, 54, 66, 200)); + styleEditorPanel.border = new RectOffset(27, 27, 27, 27); + styleEditorPanel.padding = new RectOffset(10, 10, 10, 10); + styleEditorPanel.normal.textColor = new Color32(147, 161, 161, 255); + styleEditorPanel.fontSize = 12; + } + + /// + /// Creates a 1x1 texture + /// + /// Color of the texture + /// + internal static Texture2D CreateColorPixel(Color32 Background) + { + Texture2D retTex = new Texture2D(1, 1); + retTex.SetPixel(0, 0, Background); + retTex.Apply(); + return retTex; + } + } + + #endregion UI +} diff --git a/BDArmory/Weapons/Missiles/MissileBase.cs b/BDArmory/Weapons/Missiles/MissileBase.cs new file mode 100644 index 000000000..987e225ac --- /dev/null +++ b/BDArmory/Weapons/Missiles/MissileBase.cs @@ -0,0 +1,2022 @@ +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Guidances; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace BDArmory.Weapons.Missiles +{ + public abstract class MissileBase : EngageableWeapon, IBDWeapon + { + // High Speed missile fix + /// ////////////////////////////////// + [KSPField(isPersistant = true)] + public float DetonationOffset = 0.1f; + + [KSPField(isPersistant = true)] + public bool autoDetCalc = false; + /// ////////////////////////////////// + + protected WeaponClasses weaponClass; + + public WeaponClasses GetWeaponClass() + { + return weaponClass; + } + + ModuleWeapon weap = null; + public ModuleWeapon GetWeaponModule() + { + return weap; + } + + public string GetMissileType() + { + return missileType; + } + + public float GetEngageFOV() + { + return missileFireAngle; + } + + 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"; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxStaticLaunchRange"), UI_FloatRange(minValue = 5000f, maxValue = 50000f, stepIncrement = 1000f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Max Static Launch Range + public float maxStaticLaunchRange = 5000; + + [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; + + public virtual float ClearanceRadius => 0.14f; + + public virtual float ClearanceLength => 0.14f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxOffBoresight"),//Max Off Boresight + UI_FloatRange(minValue = 0f, maxValue = 180f, stepIncrement = 5f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)] + public float maxOffBoresight = 180; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DetonationDistanceOverride"), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Detonation distance override + public float DetonationDistance = -1; + public float DetonationDistanceSqr => DetonationDistance > 0 ? DetonationDistance * DetonationDistance : -1; // Account for the -1 special value when checking against Sqr distance. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DetonateAtMinimumDistance"), // Detonate At Minumum Distance + UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public bool DetonateAtMinimumDistance = false; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_UseStaticMaxLaunchRange", advancedTweakable = true), // Use Static Max Launch Range + UI_Toggle(disabledText = "#LOC_BDArmory_dynamic", enabledText = "#LOC_BDArmory_static", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public bool UseStaticMaxLaunchRange = false; + + //[KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "SLW Offset"), UI_FloatRange(minValue = -1000f, maxValue = 0f, stepIncrement = 100f, affectSymCounterparts = UI_Scene.All)] + public float SLWOffset = 0; + + public float getSWLWOffset + { + get + { + return SLWOffset; + } + } + + [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 + + public float GpsUpdateMax = -1f; + + [KSPField] + public float lockedSensorFOV = 2.5f; + + [KSPField] + public FloatCurve lockedSensorFOVBias = new FloatCurve(); // weighting of targets and flares from center (0) to edge of FOV (lockedSensorFOV) + + [KSPField] + public FloatCurve lockedSensorVelocityBias = new FloatCurve(); // weighting of targets and flares from angular velocity angle of prior target and new target aligned (0) to opposite (180) + + [KSPField] + public FloatCurve lockedSensorVelocityMagnitudeBias = new FloatCurve(); // weighting of targets and flares from angular velocity magnitude of prior target and new target, normalized by the prior target angular velocity magnitude + + [KSPField] + public float lockedSensorMinAngularVelocity = 5; // minimum target/flare angular velocity detectable, in deg/s, used primarily for dealing with divide-by-zero issues in MagnitudeBias + + [KSPField] + public float heatThreshold = 150; + + [KSPField] + 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 float flareEffectivity = 1f; // Modifies how the missile targeting is affected by flares, 1 is fully affected (normal behavior), lower values mean less affected (0 is ignores flares), 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 targetCoM = false; //if true, IR missile targets the center of a craft, false IR missile targets hottest part + + [KSPField] + public bool isTimed = false; + + [KSPField] + 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] + public bool hasIFF = true; + + [KSPField] + public float missileCMRange = -1f; + + [KSPField] + public float missileCMInterval = -1f; + + protected List missileCM; + + protected float missileCMTime = -1f; + + protected bool CMenabled = false; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_DropTime"),//Drop Time + UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.1f, scene = UI_Scene.Editor)] + public float dropTime = 0.5f; + + [KSPField(isPersistant = true, advancedTweakable = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringAngle"),//Firing Angle + UI_FloatRange(minValue = 1f, maxValue = 90, stepIncrement = 1f, scene = UI_Scene.Editor)] + public float missileFireAngle = -1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_InCargoBay"),//In Cargo Bay: + UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true", affectSymCounterparts = UI_Scene.All)]//False--True + public bool inCargoBay = false; + + // This previously was in both MissileLauncher and BDModular Guidance, transferred it here + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_FiringPriority"), + UI_FloatRange(minValue = 0, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float priority = 0; //per-weapon priority selection override + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_InCustomCargoBay"), // In custom/modded "cargo bay" + UI_ChooseOption( + options = new string[] { + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16" + }, + display = new string[] { + "Disabled", + "AG1", + "AG2", + "AG3", + "AG4", + "AG5", + "AG6", + "AG7", + "AG8", + "AG9", + "AG10", + "Lights", + "RCS", + "SAS", + "Brakes", + "Abort", + "Gear" + } + )] + public string customBayGroup = "0"; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_DetonationTime"),//Detonation Time + UI_FloatRange(minValue = 2f, maxValue = 30f, stepIncrement = 0.5f, scene = UI_Scene.Editor)] + public float detonationTime = 2; + + [KSPField] + public float activeRadarRange = 6000; + + [Obsolete("Use activeRadarLockTrackCurve!")] + [KSPField] + public float activeRadarMinThresh = 140; + + [KSPField] + public FloatCurve activeRadarLockTrackCurve = new FloatCurve(); // floatcurve to define min/max range and lockable radar cross section + + [KSPField] + public FloatCurve activeRadarVelocityGate = new FloatCurve(); + + [KSPField] + public float activeRadarVelocityFilter = 50f; + + [KSPField] + public FloatCurve activeRadarRangeGate = new FloatCurve(); + + [KSPField] + public float activeRadarRangeFilter = 2000f; + + [KSPField] + public float activeRadarMinTrackSCR = 1f; + + [KSPField] + public bool activeRadarCanNotch = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BallisticOvershootFactor"),//Ballistic Overshoot factor + UI_FloatRange(minValue = 0.5f, maxValue = 1.5f, stepIncrement = 0.01f, scene = UI_Scene.Editor)] + public float BallisticOverShootFactor = 0.7f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BallisticAnglePath"),//Ballistic Angle path + UI_FloatRange(minValue = 5f, maxValue = 60f, stepIncrement = 5f, scene = UI_Scene.Editor)] + public float BallisticAngle = 45.0f; + + [KSPField] + public float inertialDrift = 0.05f; //meters/sec + + private Vector3 driftSeed = Vector3.zero; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CruiseAltitude"), UI_FloatRange(minValue = 5f, maxValue = 500f, stepIncrement = 5f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Cruise Altitude + public float CruiseAltitude = 500; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Missile_CruiseSpeed"), UI_FloatRange(minValue = 100f, maxValue = 6000f, stepIncrement = 50f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Cruise speed + public float CruiseSpeed = 300; + + [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 = false, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_CruisePopup"), + UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true", scene = UI_Scene.All)] + public bool CruisePopup = false; // Cruise Guidance Popup Attack + + [KSPField] + public float CruisePopupAngle = 45f; // Cruise Guidance Popup Attack Angle + + [KSPField] + public float CruisePopupAltitude = 250; // Cruise Guidance Popup Attack Altitude + + [KSPField] + public float CruisePopupRange = 2000; // Cruise Guidance Popup Attack Range + + [KSPField] + public float WeaveVerticalG = 2f; // Weave Guidance Vertical g + + [KSPField] + public float WeaveHorizontalG = 2f; // Weave Guidance Horizontal g + + [KSPField] + public float WeaveFrequency = 0.05f; // Weave Guidance Frequency (Hz) + + [KSPField] + public float WeaveTerminalAngle = 25f; // Weave Guidance Terminal Angle (deg, down) + + [KSPField] + public float WeaveFactor = 1f; // Weave Guidance Factor (higher means more weave, lower means less) + + [KSPField] + public bool WeaveUseAGMDescentRatio = false; // Weave Guidance will use agmDescentRatio as a floor + + [KSPField] + public Vector3 WeaveRandomRange = new Vector3(0.5f, 0.5f, 0.01f); // Weave Guidance horz/vert g and freq randomization range (only affects if horz/vert != 0) + + protected float WeaveOffset = -1f; + + protected Vector3 WeaveStart = Vector3.zero; + + protected float WeaveAlt = -1f; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftMaxAltitude"),//Loft Max Altitude + UI_FloatRange(minValue = 5000f, maxValue = 30000f, stepIncrement = 100f, scene = UI_Scene.Flight, affectSymCounterparts = UI_Scene.All)] + public float LoftMaxAltitude = 16000; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftRangeOverride"),//Loft Altitude Difference + UI_FloatRange(minValue = 500f, maxValue = 25000f, stepIncrement = 100f, scene = UI_Scene.Flight, affectSymCounterparts = UI_Scene.All)] + public float LoftRangeOverride = 15000; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftAltitudeAdvMax"),//Loft Maximum Altitude Advantage + UI_FloatRange(minValue = 500f, maxValue = 5000f, stepIncrement = 100f, scene = UI_Scene.Flight, affectSymCounterparts = UI_Scene.All)] + public float LoftAltitudeAdvMax = 3000; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftMinAltitude"),//Loft Maximum Altitude Advantage + UI_FloatRange(minValue = 0f, maxValue = 10000f, stepIncrement = 100f, scene = UI_Scene.Flight, affectSymCounterparts = UI_Scene.All)] + public float LoftMinAltitude = 2000; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftAngle"),//Loft Angle + UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 0.5f, scene = UI_Scene.Flight, affectSymCounterparts = UI_Scene.All)] + public float LoftAngle = 45; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftTermAngle"),//Loft Termination Angle + UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 0.5f, scene = UI_Scene.Flight, affectSymCounterparts = UI_Scene.All)] + public float LoftTermAngle = 20; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftRangeFac"),//Loft Range Factor + UI_FloatRange(minValue = 0.1f, maxValue = 5.0f, stepIncrement = 0.01f, scene = UI_Scene.Flight)] + public float LoftRangeFac = 0.5f; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftVelComp"),//Loft Velocity Compensation (Horizontal) + UI_FloatRange(minValue = -2.0f, maxValue = 2.0f, stepIncrement = 0.01f, scene = UI_Scene.Flight)] + public float LoftVelComp = -0.5f; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftVertVelComp"),//Loft Velocity Compensation (Vertical) + UI_FloatRange(minValue = -2.0f, maxValue = 2.0f, stepIncrement = 0.01f, scene = UI_Scene.Flight)] + 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 = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_terminalHomingRange")]//Terminal Homing Range + public float terminalHomingRange = 3000; + + [KSPField(advancedTweakable = false, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_TurretID"),//Custom Turret ID +UI_FloatRange(minValue = 0f, maxValue = 20f, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float customTurretID = 0; + + [KSPField] + public bool terminalHoming = false; + + [KSPField] + public float kappaAngle = 45; // Kappa Guidance Vertical Shaping Angle + + [KSPField] + public float missileRadarCrossSection = RadarUtils.RCS_MISSILES; // radar cross section of this missile for detection purposes + + public enum MissileStates { Idle, Drop, Boost, Cruise, PostThrust } + + public enum DetonationDistanceStates { NotSafe, Cruising, CheckingProximity, Detonate } + + public enum TargetingModes { None, Radar, Heat, Laser, Gps, AntiRad, Inertial } + + public MissileStates MissileState { get; set; } = MissileStates.Idle; + + public DetonationDistanceStates DetonationDistanceState { get; set; } = DetonationDistanceStates.NotSafe; + + public enum GuidanceModes { None, AAMLead, AAMPure, AGM, AGMBallistic, Cruise, Weave, STS, Bomb, Orbital, BeamRiding, SLW, PN, APN, AAMLoft, Kappa, CLOS, CLOSThreePoint, CLOSLead } + + public GuidanceModes GuidanceMode; + + public enum WarheadTypes { Kinetic, Standard, ContinuousRod, Custom, CustomStandard, CustomContinuous, EMP, Nuke, Legacy, Launcher } + + public WarheadTypes warheadType = WarheadTypes.Kinetic; + public bool HasFired { get; set; } = false; + public MissileFire FiredByWM { get; set; } // The WM that fired this missile. + + public bool launched = false; + + public BDTeam Team { get; set; } = BDTeam.Get("Neutral"); + + public bool HasMissed { get; set; } = false; + + public Vector3 TargetPosition { get; set; } = Vector3.zero; + + public Vector3 TargetVelocity { get; set; } = Vector3.zero; + + public Vector3 TargetAcceleration { get; set; } = Vector3.zero; + + public float TimeIndex => Time.time - TimeFired; + + public TargetingModes TargetingMode { get; set; } + + public TargetingModes TargetingModeTerminal { get; set; } + + public GuidanceModes homingModeTerminal { get; set; } + + public bool terminalHomingActive = false; + + public float TimeToImpact { get; set; } + + public enum LoftStates { Boost, Midcourse, Terminal } + + public LoftStates loftState = LoftStates.Boost; + + public bool TargetAcquired { get; set; } + + public bool ActiveRadar { get; set; } + + // Boolean, used to determine whether or not to update the missile's TargetInfo, should be set + // to true whenever a change is to be made to the missile's RCS, like ActiveRadar or radarLOALSearching + public bool updateRadarCS = false; + + public Vessel SourceVessel + { + get { return _sourceVessel; } + set + { + _sourceVessel = value; + SourceVesselName = SourceVessel != null ? SourceVessel.vesselName : ""; + } + } + Vessel _sourceVessel = null; + + public bool HasExploded { get; set; } = false; + + public bool FuseFailed { get; set; } = false; + + public bool HasDied { get; set; } = false; + + public int clusterbomb { get; set; } = 1; + + protected IGuidance _guidance; + + public enum VacuumClearanceStates { Clearing, Turning, Cleared } + public VacuumClearanceStates vacuumClearanceState = VacuumClearanceStates.Cleared; + private readonly string[] VacuumClearanceStatesString = new string[3] { "Clearing", "Turning", "Cleared" }; + + private double _lastVerticalSpeed; + private double _lastHorizontalSpeed; + private int gpsUpdateCounter = 0; + + public double HorizontalAcceleration + { + get + { + var result = (vessel.horizontalSrfSpeed - _lastHorizontalSpeed); + _lastHorizontalSpeed = vessel.horizontalSrfSpeed; + return result; + + } + } + + public double VerticalAcceleration + { + get + { + var result = (vessel.horizontalSrfSpeed - _lastHorizontalSpeed); + _lastVerticalSpeed = vessel.verticalSpeed; + return result; + } + } + + + + public float Throttle + { + get + { + return _throttle; + } + + set + { + _throttle = Mathf.Clamp01(value); + } + } + + public float TimeFired = -1; + + protected float lockFailTimer = -1; + + public TargetInfo targetVessel + { + get + { + if (_targetVessel != null && _targetVessel.Vessel == null) _targetVessel = null; // The vessel could die before _targetVessel gets cleared otherwise. + return _targetVessel; + } + set + { + _targetVessel = value; + if (_targetVessel != null && _targetVessel.Vessel != null) + TargetVesselName = _targetVessel.Vessel.vesselName; + } + } + TargetInfo _targetVessel; + + public Transform MissileReferenceTransform; + + protected ModuleTargetingCamera targetingPod; + + public List customTurret = new List(); + + //laser stuff + public ModuleTargetingCamera lockedCamera; + protected Vector3 lastLaserPoint; + protected Vector3 laserStartPosition; + protected Vector3 startDirection; + + //GPS stuff + public Vector3d targetGPSCoords; + public float lastPingTime; + //heat stuff + public TargetSignatureData heatTarget; + private TargetSignatureData predictedHeatTarget; + + //radar stuff + public VesselRadarData vrd; + public TargetSignatureData radarTarget; + protected TargetSignatureData[] scannedTargets; + private LineRenderer LR; + + //INS stuff + public Vector3d TargetINSCoords { get; set; } = Vector3d.zero; + public float TimeOfLastINS = 0; + public float INStimetogo = 0; + + private int snapshotTicker; + private int locksCount = 0; + private float _radarFailTimer = 0; + + [KSPField] public float radarTimeout = -1; + [KSPField] public float seekerTimeout = 5; + [KSPField] public float terminalSeekerTimeout = -1; + private float lastRWRPing = 0; + public RadarWarningReceiver.RWRThreatTypes[] antiradTargets; + public bool radarLOALSearching { get; protected set; } = false; + private bool hasLostLock = false; + protected bool checkMiss = false; + public StringBuilder debugString = new StringBuilder(); + + private float _throttle = 1f; + + 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.railAmmo; + } + } + public bool isMMG = false; + + public override void OnAwake() + { + base.OnAwake(); + var MMG = GetPart().FindModuleImplementing(); + if (MMG != null) + { + hasAmmo = false; + isMMG = true; + } + } + + public void GetMissileCount() // could stick this in GetSublabel, but that gets called every frame by BDArmorySetup? + { + missilecount = 0; + 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 is null) continue; + if (craftPart.Current.GetPartName() != missilePartName) continue; + if (craftPart.Current.weaponChannel > weaponChannel) continue; + if (craftPart.Current.engageRangeMax != engageRangeMax) continue; + if (craftPart.Current.missileFireAngle != missileFireAngle) continue; + missilecount += craftPart.Current.AmmoCount; + } + if (hasAmmo) missilecount += (int)reloadableRail.magazineAmmo; + } + + public string GetSubLabel() + { + return Sublabel = $"Guidance: {Enum.GetName(typeof(TargetingModes), TargetingMode)}; Max Range: {Mathf.Round(engageRangeMax / 100) / 10} km; Boresight: {(missileFireAngle > 0 ? missileFireAngle : "360")}°; Remaining: {missilecount}"; + } + + public Part GetPart() + { + return part; + } + + public abstract void FireMissile(); + + public abstract void Jettison(); + + public abstract float GetBlastRadius(); + + protected abstract void PartDie(Part p); + + protected void DisablingExplosives(Part p) + { + if (p == null) return; + + List tntList = part.FindModulesImplementing(); + foreach (BDWarheadBase tnt in tntList) + { + if (tnt) + tnt.Armed = false; + } + + var emp = p.FindModuleImplementing(); + if (emp != null) emp.Armed = false; + } + + protected void SetupExplosive(Part p) + { + if (p == null) return; + + List tntList = part.FindModulesImplementing(); + foreach (BDWarheadBase tnt in tntList) + { + tnt.Armed = true; + tnt.detonateAtMinimumDistance = DetonateAtMinimumDistance; + } + + var emp = p.FindModuleImplementing(); + if (emp != null) emp.Armed = true; + } + + public abstract void Detonate(); + + public abstract Vector3 GetForwardTransform(); + + public abstract float GetKinematicTime(); + + public abstract float GetKinematicSpeed(); + + protected void AddTargetInfoToVessel() + { + TargetInfo info = vessel.gameObject.AddComponent(); + info.Team = Team; + info.isMissile = true; + info.MissileBaseModule = this; + updateRadarCS = true; + } + + [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_GPSTarget", active = true, name = "GPSTarget")]//GPS Target + public void assignGPSTarget() + { + if (HighLogic.LoadedSceneIsFlight) + PickGPSTarget(); + } + + [KSPField(isPersistant = true)] + public bool gpsSet = false; + + [KSPField(isPersistant = true)] + public Vector3 assignedGPSCoords; + + [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_GPSTarget")]//GPS Target + public string gpsTargetName = "Unknown"; // Can't have an empty name as it breaks KSP's flightstate autosave. + + void PickGPSTarget() + { + gpsSet = true; + Fields["gpsTargetName"].guiActive = true; + var weaponManager = vessel.ActiveController().WM; + if (weaponManager) + { + gpsTargetName = weaponManager.designatedGPSInfo.name; + assignedGPSCoords = weaponManager.designatedGPSCoords; + } + } + + public Vector3d UpdateGPSTarget() + { + Vector3 gpsTargetCoords_; + + if (gpsSet && assignedGPSCoords != null) + { + gpsTargetCoords_ = assignedGPSCoords; + } + else + { + gpsTargetCoords_ = targetGPSCoords; + if (targetVessel && HasFired && (gpsUpdates >= 0f)) + { + TargetSignatureData t = TargetSignatureData.noTarget; + TargetPosition = Vector3.zero; + UpdateLaserTarget(); //available cam for new GPS coords? + //Debug.Log($"[MissileBase] GPS vrd: {vrd != null}; vrd lock: {vrd && vrd.locked}"); + if (vrd && vrd.locked)//no cam; available radar lock? + { + List possibleTargets = vrd.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == targetVessel.Vessel) + t = possibleTargets[i]; + } + if (t.exists) TargetPosition = t.position; + //Debug.Log($"[MissileBase] GPS targetPosition is{TargetPosition.x:F2}, {TargetPosition.x:F2}, {TargetPosition.z:F2}"); + } + if (TargetPosition != Vector3.zero) + { + float distanceToTargetSqr = (vessel.transform.position - gpsTargetCoords_).sqrMagnitude; + float jamDistance = RadarUtils.GetVesselECMJammingDistance(targetVessel.Vessel); //does the target have a jammer, and is the missile within the jammed AoE + if (jamDistance * jamDistance < distanceToTargetSqr) //outside/no area of interference, can receive GPS signal + { + //if (FiredByWM != null && FiredByWM.CanSeeTarget(targetVessel, false)) + + if (gpsUpdates == 0) // Constant updates + { + gpsTargetCoords_ = VectorUtils.WorldPositionToGeoCoords(TargetPosition, targetVessel.Vessel.mainBody); + targetGPSCoords = gpsTargetCoords_; + } + else // Update every gpsUpdates seconds + { + float updateCount = TimeIndex / gpsUpdates; + if (updateCount > gpsUpdateCounter) + { + gpsUpdateCounter++; + gpsTargetCoords_ = VectorUtils.WorldPositionToGeoCoords(TargetPosition, targetVessel.Vessel.mainBody); + targetGPSCoords = gpsTargetCoords_; + } + } + } + } + //else + // In theory if the jammer knew the GPS receiver channel/encryption, could transmit false coords using a more powerful signal to override out the originals... + //currently just cuts off updates and ordnance heads to last valid coords. Instead have jammer Strength come into play and have it be a jStrength * 4prDist^2 check that slows update + //frequency/increases the RNG threshold to make the GPS update? + } + } + + if (TargetAcquired) + { + TargetPosition = VectorUtils.GetWorldSurfacePostion(gpsTargetCoords_, vessel.mainBody); + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + } + else + { + guidanceActive = false; + } + + return gpsTargetCoords_; + } + + protected void UpdateHeatTarget() + { + + if (lockFailTimer > seekerTimeout) + { + targetVessel = null; + TargetAcquired = false; + predictedHeatTarget.exists = false; + predictedHeatTarget.signalStrength = 0; //have this instead set to originalHeatTarget missile had on initial lock? + return; + } + + if (heatTarget.exists && lockFailTimer < 0) + { + lockFailTimer = 0; + predictedHeatTarget = heatTarget; + } + if (lockFailTimer >= 0) + { + // Decide where to point seeker + Ray lookRay; + if (predictedHeatTarget.exists) // We have an active target we've been seeking, or a prior target that went stale + { + lookRay = new Ray(transform.position, predictedHeatTarget.position - transform.position); + } + else if (heatTarget.exists) // We have a new active target and no prior target + { + lookRay = new Ray(transform.position, heatTarget.position + (heatTarget.velocity * Time.fixedDeltaTime) - transform.position); + } + else // No target, look straight ahead + { + lookRay = new Ray(transform.position, MissileReferenceTransform.forward); + } + + // Prevent seeker from looking past maxOffBoresight + float offBoresightAngle = VectorUtils.Angle(GetForwardTransform(), lookRay.direction); + if (offBoresightAngle > maxOffBoresight) + lookRay = new Ray(lookRay.origin, Vector3.RotateTowards(lookRay.direction, GetForwardTransform(), (offBoresightAngle - maxOffBoresight) * Mathf.Deg2Rad, 0)); + + DrawDebugLine(lookRay.origin, lookRay.origin + lookRay.direction * 10000, predictedHeatTarget.exists ? Color.magenta : heatTarget.exists ? Color.yellow : Color.blue); + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[MissileBase] offboresightAngle {offBoresightAngle > maxOffBoresight}; lockFailtimer: {lockFailTimer}; heatTarget? {heatTarget.exists}; predictedheattaret? {predictedHeatTarget.exists}; heatTarget vessel {(heatTarget.exists && heatTarget.vessel != null ? heatTarget.vessel.name : "null")}"); + // Update heat target + if (activeRadarRange < 0) + heatTarget = BDATargetManager.GetAcousticTarget(SourceVessel, vessel, lookRay, predictedHeatTarget, lockedSensorFOV * 0.5f, heatThreshold, targetCoM, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity, + FiredByWM, targetVessel, IFF: hasIFF); + else + heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, lookRay, predictedHeatTarget, lockedSensorFOV * 0.5f, heatThreshold, frontAspectHeatModifier, uncagedLock, targetCoM, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity, FiredByWM, targetVessel, IFF: hasIFF); + + // heatTarget.vessel == null should account for flares and decoys, but out of an abundance of caution + // I've added a .isDecoy flag to TargetSignatureData, might be useful for other purposes too + if (heatTarget.exists && (heatTarget.isDecoy || CheckTargetEngagementEnvelope(heatTarget.targetInfo))) + { + TargetAcquired = true; + TargetPosition = heatTarget.position; + TargetVelocity = heatTarget.velocity; + TargetAcceleration = heatTarget.acceleration; + //targetVessel = heatTarget.targetInfo; + lockFailTimer = 0; + + // Update target information + // if (heatTarget.vessel != predictedHeatTarget.vessel) Debug.LogError($"[IR DEBUG] Switching targets from {predictedHeatTarget.vessel.vesselName} to {heatTarget.vessel.vesselName}"); + predictedHeatTarget = heatTarget; + } + else + { + lockFailTimer += Time.fixedDeltaTime; + } + + // Update predicted values based on target information + if (predictedHeatTarget.exists) + { + float currentFactor = (1400 * 1400) / Mathf.Clamp((predictedHeatTarget.position - transform.position).sqrMagnitude, 90000, 36000000); + Vector3 currVel = vessel.Velocity(); + predictedHeatTarget.position = predictedHeatTarget.position + predictedHeatTarget.velocity * Time.fixedDeltaTime; + predictedHeatTarget.velocity = predictedHeatTarget.velocity + predictedHeatTarget.acceleration * Time.fixedDeltaTime; + float futureFactor = (1400 * 1400) / Mathf.Clamp((predictedHeatTarget.position - (transform.position + (currVel * Time.fixedDeltaTime))).sqrMagnitude, 90000, 36000000); + predictedHeatTarget.signalStrength *= futureFactor / currentFactor; + } + + } + } + + protected void SetAntiRadTargeting() + { + if (TargetingMode == TargetingModes.AntiRad && TargetAcquired) + { + RadarWarningReceiver.OnRadarPing += ReceiveRadarPing; + } + } + + protected void SetLaserTargeting() + { + if (TargetingMode == TargetingModes.Laser) + { + laserStartPosition = MissileReferenceTransform.position; + if (lockedCamera) + { + TargetAcquired = true; + TargetPosition = lastLaserPoint = lockedCamera.groundTargetPosition; + targetingPod = lockedCamera; + } + } + } + + protected void UpdateLaserTarget() + { + if (TargetAcquired) + { + bool isCLOS = GuidanceMode == GuidanceModes.CLOS || GuidanceMode == GuidanceModes.CLOSThreePoint || GuidanceMode == GuidanceModes.CLOSLead; + if (lockedCamera && !lockedCamera.gimbalLimitReached && lockedCamera.groundStabilized && lockedCamera.surfaceDetected) //active laser target + { + TargetPosition = lockedCamera.groundTargetPosition; + TargetVelocity = isCLOS ? Vector3.zero : (TargetPosition - lastLaserPoint) / Time.fixedDeltaTime; + TargetAcceleration = Vector3.zero; + lastLaserPoint = TargetPosition; + lockFailTimer = 0f; + + if (GuidanceMode == GuidanceModes.BeamRiding && TimeIndex > 0.25f && Vector3.Dot(GetForwardTransform(), vessel.CoM - lockedCamera.transform.position) < 0) + { + TargetAcquired = false; + lockedCamera = null; + } + } + else //lost active laser target, home on last known position + { + Ray smokeRay = new Ray(vessel.CoM, isCLOS ? lockedCamera.transform.position : (lastLaserPoint - vessel.CoM)); + if (CMSmoke.RaycastSmoke(smokeRay)) + { + float angle = VectorUtils.FullRangePerlinNoise(0.75f * Time.time, 10) * BDArmorySettings.SMOKE_DEFLECTION_FACTOR; + TargetPosition = isCLOS ? + VectorUtils.RotatePointAround(lockedCamera.targetPointPosition, smokeRay.origin, vessel.up, angle) : + VectorUtils.RotatePointAround(lastLaserPoint, vessel.CoM, vessel.up, angle); + lastLaserPoint = TargetPosition; + lockFailTimer = 0f; + } + else + { + TargetPosition = lastLaserPoint; + lockFailTimer += Time.fixedDeltaTime; + if (lockFailTimer > seekerTimeout) + TargetAcquired = false; + } + + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + } + } + else + { + ModuleTargetingCamera foundCam = null; + bool parentOnly = GuidanceMode == GuidanceModes.BeamRiding || GuidanceMode == GuidanceModes.CLOS || GuidanceMode == GuidanceModes.CLOSThreePoint || GuidanceMode == GuidanceModes.CLOSLead; + foundCam = BDATargetManager.GetLaserTarget(this, parentOnly, Team); + float threshold = Mathf.Max(targetVessel ? targetVessel.Vessel.GetRadius() : 20, 20); + if (foundCam != null && foundCam.cameraEnabled && foundCam.groundStabilized && BDATargetManager.CanSeePosition(foundCam.groundTargetPosition, vessel.transform.position, MissileReferenceTransform.position, threshold)) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Laser guided missileBase actively found laser point. Enabling guidance."); + lockedCamera = foundCam; + TargetAcquired = true; + SetLaserTargeting(); + } + } + } + + protected void UpdateRadarTarget() + { + TargetAcquired = false; + + if (radarTarget.exists) + { + Vector3 vectorToTarget = radarTarget.predictedPosition - transform.position; + float angleToTarget = VectorUtils.Angle(vectorToTarget, GetForwardTransform()); + // locked-on before launch, passive radar guidance or waiting till in active radar range: + if (!ActiveRadar && (vectorToTarget.sqrMagnitude > (activeRadarRange * activeRadarRange) || angleToTarget > maxOffBoresight * 0.75f)) + { + if (vrd && vrd.locked) + { + TargetSignatureData t = TargetSignatureData.noTarget; + if (canRelock && hasLostLock) + { + if (vrd.locked) + { + t = vrd.lockedTargetData.targetData; //SARH is passive, and guided towards whatever is currently painted by FCS radar + //Debug.Log($"[MML RADAR DEBUG] missile switched target to {t.vessel.GetName()}"); + } + } + else + { + List possibleTargets = vrd.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; 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; + //if (weaponClass == WeaponClasses.SLW) //Radar/Active Sonar guidance would be vulnerable to chaff/various acoustic CMs that function basically like chaff, so commenting this out + //{ + // TargetPosition = radarTarget.predictedPosition; + //} + //else + TargetPosition = radarTarget.predictedPositionWithChaffFactor(!ActiveRadar ? vrd.lockedTargetData.detectedByRadar.radarChaffClutterFactor : chaffEffectivity); + TargetVelocity = radarTarget.velocity; + TargetAcceleration = radarTarget.acceleration; + targetVessel = t.targetInfo; //reset targetvessel in case of canRelock getting a new target + _radarFailTimer = 0; + return; + } + else + { + if (_radarFailTimer > 0f) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed. Parent radar lost target."); + radarTarget = TargetSignatureData.noTarget; + targetVessel = null; + return; + } + else + { + if (_radarFailTimer == 0) + { + 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; + radarTarget.position = radarTarget.predictedPosition; + //if (weaponClass == WeaponClasses.SLW) + // TargetPosition = radarTarget.predictedPosition; + //else + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); + TargetVelocity = radarTarget.velocity; + TargetAcceleration = Vector3.zero; + TargetAcquired = true; + } + } + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed. Out of range and no data feed."); + radarTarget = TargetSignatureData.noTarget; + targetVessel = null; + return; + } + } + else //onboard radar is on, or off but in range + { + // active radar with target locked: + vrd = null; + if (angleToTarget > maxOffBoresight && TimeIndex > 3) //Give non-SARH VLS-launched missiles a 3sec grace period after launch to tip over towards target first before checking boresight + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. Target is out of active seeker gimbal limits."); + radarTarget = TargetSignatureData.noTarget; + targetVessel = null; + return; + } + else + { + if (scannedTargets == null) scannedTargets = new TargetSignatureData[BDATargetManager.LoadedVessels.Count]; + TargetSignatureData.ResetTSDArray(ref scannedTargets); + Ray ray = new Ray(transform.position, vectorToTarget); + bool pingRWR = Time.time - lastRWRPing > 0.4f; + if (pingRWR) lastRWRPing = Time.time; + bool radarSnapshot = (snapshotTicker > 10); + if (radarSnapshot) + { + snapshotTicker = 0; + } + else + { + snapshotTicker++; + } + + //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 ? 250000f : 1600; // 500 * 500 : 40 * 40; + + if (radarLOAL && radarLOALSearching && !radarSnapshot) + { + //only scan on snapshot interval + TargetAcquired = true; + } + else + { + for (int i = 0; i < scannedTargets.Length; i++) + { + if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - radarTarget.predictedPosition).sqrMagnitude < sqrThresh) + { + //re-check engagement envelope, only lock appropriate targets + if (CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo) && (!hasIFF || !Team.IsFriendly(scannedTargets[i].Team))) + { + radarTarget = scannedTargets[i]; + TargetAcquired = true; + radarLOALSearching = false; + //if (weaponClass == WeaponClasses.SLW) + // TargetPosition = radarTarget.predictedPosition + (radarTarget.velocity * Time.fixedDeltaTime); + //else + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); + + TargetVelocity = radarTarget.velocity; + TargetAcceleration = radarTarget.acceleration; + _radarFailTimer = 0; + if (!ActiveRadar && Time.time - TimeFired > 1) + { + if (locksCount == 0) + { + if (weaponClass == WeaponClasses.SLW) + RadarWarningReceiver.PingRWR(ray, lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); + else + RadarWarningReceiver.PingRWR(ray, lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); + 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.DEBUG_MISSILES) + { + Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. Radar missileBase reached max re-lock attempts."); + } + } + locksCount++; + } + ActiveRadar = true; + updateRadarCS = true; + return; + } + } + //if (!scannedTargets[i].exists) + // if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase][Radar Active]: Target: {i} doesn't exist!."); + //if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - radarTarget.predictedPosition).sqrMagnitude >= sqrThresh) + // if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase][Radar Active]: Target: {i} too far from target loc!."); + } + } + + if (radarLOAL) + { + // Lost track of target, but we can re-acquire set radarLOALSearching = true and try to re-acquire using existing target information + if (!radarLOALSearching) + { + radarLOALSearching = true; + updateRadarCS = true; + startDirection = GetForwardTransform(); + } + TargetAcquired = true; + + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); + TargetVelocity = radarTarget.velocity; + TargetAcceleration = Vector3.zero; + ActiveRadar = false; + _radarFailTimer = 0; + radarTarget = TargetSignatureData.noTarget; + } + else + { + // Lost track of target and unable to re-acquire + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. No target locked."); + radarTarget = TargetSignatureData.noTarget; + targetVessel = null; + radarLOALSearching = false; + radarLOAL = false; + TargetAcquired = false; + ActiveRadar = false; + updateRadarCS = true; + } + } + } + } + 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: + + if (scannedTargets == null) scannedTargets = new TargetSignatureData[BDATargetManager.LoadedVessels.Count]; + TargetSignatureData.ResetTSDArray(ref scannedTargets); + Vector3 forward = GetForwardTransform(); + Ray ray = new Ray(transform.position, forward); + bool pingRWR = Time.time - lastRWRPing > 0.4f; + if (pingRWR) lastRWRPing = Time.time; + bool radarSnapshot = (snapshotTicker > 5); + if (radarSnapshot) + { + snapshotTicker = 0; + } + else + { + snapshotTicker++; + } + + //RadarUtils.UpdateRadarLock(ray, lockedSensorFOV * 3, activeRadarMinThresh * 2, ref scannedTargets, 0.4f, pingRWR, RadarWarningReceiver.RWRThreatTypes.MissileLock, radarSnapshot); + RadarUtils.RadarUpdateMissileLock(ray, maxOffBoresight, ref scannedTargets, 0.4f, this); + + //float sqrThresh = targetVessel != null ? 1000000 : 90000f; // 1000 * 1000 : 300 * 300; Expand threshold if no target to search for, grab first available target + + float smallestAngle = maxOffBoresight; + float smallestDist = float.PositiveInfinity; + float currDist = 0; + float currAngle = 0; + TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + bool useSoughtTarget = radarTarget.exists || targetVessel != null; + Vector3 soughtTarget; + if (useSoughtTarget) + soughtTarget = radarTarget.exists ? radarTarget.predictedPosition : targetVessel.Vessel.CoM; + else + soughtTarget = vessel.CoM; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase][Radar LOAL]: Active radar found: {scannedTargets.Length} targets; radarTarget?{radarTarget.exists}; tgtVessel? {targetVessel != null}"); + for (int i = 0; i < scannedTargets.Length; i++) + { + float tempDist = -1f; + if (scannedTargets[i].exists && !useSoughtTarget || (tempDist = (scannedTargets[i].predictedPosition - soughtTarget).sqrMagnitude) < 1000000f) + { + //re-check engagement envelope, only lock appropriate targets + if (CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo)) + { + if (hasIFF && Team.IsFriendly(scannedTargets[i].targetInfo.Team)) continue;//Don't lock friendlies + + if (!useSoughtTarget) + { + (tempDist, Vector3 currDir) = (scannedTargets[i].predictedPosition - soughtTarget).MagNorm(); + currAngle = Mathf.Rad2Deg * Mathf.Acos(Vector3.Dot(currDir, forward)); + if (currAngle > (smallestAngle + 5f)) continue; // Look for the smallest angle, give 5 degrees of wiggle room. + // Look for closest target to the missile + currDist = tempDist; + } + else + // Look for closest target to the previous target location + currDist = tempDist; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase][Radar LOAL]: Target: {scannedTargets[i].vessel.name} has {(targetVessel == null ? "currDist" : "currSqrDist")}: {currDist}."); + + if (currDist < smallestDist) + { + if (!useSoughtTarget && currAngle < smallestAngle) + smallestAngle = currAngle; + smallestDist = currDist; + lockedTarget = scannedTargets[i]; + ActiveRadar = true; + updateRadarCS = true; + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase][Radar LOAL]: Target: {scannedTargets[i].vessel.name} selected."); + } + //return; + } + } + //if (!scannedTargets[i].exists) + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase][Radar LOAL]: Target: {i} doesn't exist!."); + //if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - soughtTarget).sqrMagnitude >= 1000000f) + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.missileBase][Radar LOAL]: Target: {i} too far from target loc!."); + } + + if (lockedTarget.exists) + { + radarTarget = lockedTarget; + targetVessel = radarTarget.targetInfo; + TargetAcquired = true; + radarLOALSearching = false; + //if (weaponClass == WeaponClasses.SLW) + // TargetPosition = radarTarget.predictedPosition + (radarTarget.velocity * Time.fixedDeltaTime); + //else + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); + TargetVelocity = radarTarget.velocity; + TargetAcceleration = radarTarget.acceleration; + + if (!ActiveRadar && Time.time - TimeFired > 1) + { + if (weaponClass == WeaponClasses.SLW) + RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); + else + RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); + + //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 * 5000); + TargetVelocity = vessel.Velocity(); // Set the relative target velocity to 0. + TargetAcceleration = Vector3.zero; + if (!radarLOALSearching) + { + radarLOALSearching = true; + updateRadarCS = true; + startDirection = GetForwardTransform(); + } + _radarFailTimer += Time.fixedDeltaTime; + if (_radarFailTimer > seekerTimeout) + { + 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; + updateRadarCS = true; + } + return; + } + } + + if (!radarTarget.exists) + { + if (_radarFailTimer < seekerTimeout) + { + if (radarLOAL) + { + if (!radarLOALSearching) + { + radarLOALSearching = true; + updateRadarCS = true; + TargetAcquired = true; + startDirection = GetForwardTransform(); + } + } + else + { + _radarFailTimer += Time.fixedDeltaTime; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase]: No assigned radar target. Awaiting timeout({seekerTimeout - _radarFailTimer}).... "); + } + } + 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) || + (ti.isUnderwater && engageSLW); + } + + protected void ReceiveRadarPing(Vessel v, Vector3 source, RadarWarningReceiver.RWRThreatTypes type, float persistTime) + { + if (TargetingMode == TargetingModes.AntiRad && TargetAcquired && v == vessel) + { + + if (!antiradTargets.Contains(type)) return; //Type check, so a different RWRType ping doesn't decoy the ARM. multiple radar sources on the same frequency within boresight will canse missile to pingpong between them, if sufficiently close to each other. + //if (targetVessel != null) //filter on a per-vessel basis? Technically speaking, as a passive sensor, ARH would have no way of distinguishing a specific vessel to focus on, and ping filtering would need to be based on distance from previous ping(s) + //{ + // if ((VectorUtils.WorldPositionToGeoCoords(source, vessel.mainBody) - VectorUtils.WorldPositionToGeoCoords(targetVessel.Vessel.CoM, vessel.mainBody)).sqrMagnitude > Mathf.Max(400, 0.013f * (!vessel.InVacuum() ? (float)targetVessel.Vessel.srf_velocity.sqrMagnitude : (float)targetVessel.Vessel.obt_velocity.sqrMagnitude)) return; + //} + if (Time.time - lastPingTime < persistTime) return; //if multiple radar sources in boresite, filter the ones we don't want. How does the misisle know this interval? + // presumably, the launching craft has held the intended target in boresight fo a few seconds before launch, and the missile can log the charateristics of the intended radar source pre-launch. + //persistTime is a bit shorter than actual radar sweep interval, so there's some margin built in as ping times shift slightly due to maneuvering changing relative location on radar scope. + //Technically, ARH should probably be able to log signal *strength* as well as type and frequency to help filter out extraneous radar sources to prevent getting decoyed by a different source... + + // Ping was close to the previous target position and is within the boresight of the missile. + //this needs to look at previous ping, and filter ping that's closest to previous position. Easy, if they all happened at the same time. If they're coming in one at a time... + //var staticLaunchThresholdSqr = maxStaticLaunchRange * maxStaticLaunchRange / 16f; //this is a huge threshold. 7.5km for the stock HARM. shouldn't it instead be something akin to MissileFire.GPSDistanceCheck? Only have it grab pings that are within ~20m of previous ping? + var pingDistanceThreshold = (BodyUtils.GetRadarAltitudeAtPos(source) < 20 ? 50 : 350) * Mathf.Max(1, persistTime); //ground stuff likely not going to move > 50m/s, air stuff... min 350m should be sufficient? Anything with slow ping time will get extended threshold, and faster stuff will be pinging more than 1/s + pingDistanceThreshold *= pingDistanceThreshold; + //if ((source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).sqrMagnitude < staticLaunchThresholdSqr && VectorUtils.Angle(source - transform.position, GetForwardTransform()) < maxOffBoresight) + + //should this be predicted ping pos instead of last ping pos? targetGPSCoords + (lastPingCoords - targetGPSCoords) * (lastPingCoords - targetGPSCoords).distance? + if ((source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).sqrMagnitude < pingDistanceThreshold && VectorUtils.Angle(source - transform.position, GetForwardTransform()) < maxOffBoresight) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase]: Radar ping! Adjusting target position by {(source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).magnitude} to {TargetPosition}, ping type {type} from vessel {v.vesselName}"); + TargetAcquired = true; + TargetPosition = source; + targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + lockFailTimer = 0; + lastPingTime = Time.time; + } + } + } + + protected void UpdateAntiRadiationTarget() + { + if (FlightGlobals.ready && TargetAcquired) + { + if (lockFailTimer < 0) + { + lockFailTimer = 0; + } + lockFailTimer += Time.fixedDeltaTime; + if (lockFailTimer > seekerTimeout) + { + TargetAcquired = false; + } + } + if (targetGPSCoords != Vector3d.zero) + { + TargetPosition = VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody); + if (BDArmorySettings.DEBUG_LINES) + DrawDebugLine(vessel.CoM, TargetPosition, Color.blue); + } + else + { + if (BDArmorySettings.DEBUG_LINES) + DrawDebugLine(vessel.CoM, vessel.CoM + MissileReferenceTransform.forward * 10000, Color.grey); + } + } + private bool setInertialTarget = false; + + public Vector3d UpdateInertialTarget() + { + Vector3 TargetCoords_; + Vector3 TargetLead; + bool detectedByRadar = false; + bool activeDatalink = false; + if (!setInertialTarget) + { + //driftSeed = new Vector3(UnityEngine.Random.Range(-1, 1) * inertialDrift, UnityEngine.Random.Range(-1, 1) * inertialDrift, UnityEngine.Random.Range(-1, 1) * inertialDrift); + driftSeed = UnityEngine.Random.insideUnitSphere * inertialDrift; + setInertialTarget = true; + if (gpsUpdates >= 0) + { + if (gpsUpdates > GpsUpdateMax) GpsUpdateMax = gpsUpdates; + } + } + TargetCoords_ = targetGPSCoords; + + if (lockFailTimer > seekerTimeout) + { + targetVessel = null; + TargetAcquired = false; + guidanceActive = false; + return TargetCoords_; + } + if (targetVessel && lockFailTimer < 0) + { + lockFailTimer = 0; + } + if (targetVessel && HasFired) + { + if (gpsUpdates >= 0f) + { + TargetSignatureData INStarget = TargetSignatureData.noTarget; + bool radarLocked = false; + if (FiredByWM != null && FiredByWM.vesselRadarData) + { + if (FiredByWM._radarsEnabled || weaponClass == WeaponClasses.SLW && FiredByWM._sonarsEnabled) + (INStarget, radarLocked) = FiredByWM.vesselRadarData.detectedRadarTargetLock(targetVessel.Vessel, FiredByWM); //is the target tracked by radar or ISRT? + if (INStarget.exists) + detectedByRadar = true; + else + if (FiredByWM._irstsEnabled) INStarget = FiredByWM.vesselRadarData.activeIRTarget(targetVessel.Vessel, FiredByWM); + } + if (INStarget.exists) + { + activeDatalink = true; + float distanceToTargetSqr = (SourceVessel.CoM - targetVessel.Vessel.CoM).sqrMagnitude; //sourceVessel radar tracking garbled? + float distanceToJammerSqr = (vessel.CoM - targetVessel.Vessel.CoM).sqrMagnitude; //missile datalink jammed? + float jamDistance = RadarUtils.GetVesselECMJammingDistance(targetVessel.Vessel); //is the target jamming? + if ((!detectedByRadar || jamDistance * jamDistance < distanceToTargetSqr) && jamDistance * jamDistance < distanceToJammerSqr) + { + if (gpsUpdates == 0 && (detectedByRadar && radarLocked)) // Constant updates + { + TargetINSCoords = VectorUtils.WorldPositionToGeoCoords(targetVessel.Vessel.CoM, targetVessel.Vessel.mainBody); + TimeOfLastINS = TimeIndex; + TargetLead = MissileGuidance.GetAirToAirFireSolution(this, targetVessel.Vessel, out INStimetogo); + if (detectedByRadar) TargetLead += (INStarget.predictedPositionWithChaffFactor(chaffEffectivity) - INStarget.position); + TargetCoords_ = VectorUtils.WorldPositionToGeoCoords(TargetLead, targetVessel.Vessel.mainBody); + targetGPSCoords = TargetCoords_; + } + else //clamp updates to radar/IRST track speed + { + float updateCount = TimeIndex / GpsUpdateMax; + if (updateCount > gpsUpdateCounter) + { + gpsUpdateCounter++; + TargetINSCoords = VectorUtils.WorldPositionToGeoCoords(targetVessel.Vessel.CoM, targetVessel.Vessel.mainBody); + TimeOfLastINS = TimeIndex; + TargetLead = MissileGuidance.GetAirToAirFireSolution(this, targetVessel.Vessel, out INStimetogo); + if (detectedByRadar) TargetLead += (INStarget.predictedPositionWithChaffFactor(chaffEffectivity) - INStarget.position); + TargetCoords_ = VectorUtils.WorldPositionToGeoCoords(TargetLead, targetVessel.Vessel.mainBody); + targetGPSCoords = TargetCoords_; + } + } + } + } + else activeDatalink = false; + } + else + { + TargetAcquired = true; + if (targetVessel != null) + { + float angleToTarget = VectorUtils.Angle(targetVessel.Vessel.CoM - transform.position, GetForwardTransform()); + + if (angleToTarget > maxOffBoresight * 1.1f) + { + TargetAcquired = false; //technically non-datalink missiles shouldn't know when they've 'Missed' if they're just fired at a point a target is predicted to be at + //but a pilot seeing their missile obviously missing would then fire another, so this frees up the AI to do that if MissilesAway = MaxMissilesPerTarget. + } + } + } + } + if (TargetAcquired) + { + TargetPosition = VectorUtils.GetWorldSurfacePostion(TargetCoords_, vessel.mainBody); + if (activeDatalink) + { + TargetINSCoords = VectorUtils.WorldPositionToGeoCoords(VectorUtils.GetWorldSurfacePostion(TargetINSCoords, vessel.mainBody) + driftSeed * TimeIndex, vessel.mainBody); + lockFailTimer = 0; + } + else if (gpsUpdates >= 0) + lockFailTimer += Time.fixedDeltaTime; + } + else + lockFailTimer += Time.fixedDeltaTime; + //Debug.Log($"[INSDebug] lockfailTimer {lockFailTimer.ToString("0.00")}; datalink {activeDatalink}"); + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + TargetPosition += driftSeed * TimeIndex; + return TargetCoords_; + } + + public Vector3 VacuumClearanceManeuver(Vector3 orbitalTarget, Vector3 CoM, bool hasRCS = true, bool vacuumSteerable = true) + { + // In vacuum, gradually turn towards target shortly after launch to minimize wasted delta-V + // During this maneuver, check that we have cleared any obstacles before throttling up + if (SourceVessel == null || !vacuumSteerable || !vessel.InVacuum()) + { + vacuumClearanceState = VacuumClearanceStates.Cleared; + Throttle = 1f; + return orbitalTarget; + } + + if (vacuumSteerable && (vessel.InVacuum())) + { + float dotTol; + float clearedDotTol = hasRCS ? 0.98f : 0.7f; + Vector3 toSource = CoM - SourceVessel.CoM; + Ray toTarget = new Ray(CoM, orbitalTarget); + float sourceRadius = SourceVessel.GetRadius(); + switch (vacuumClearanceState) + { + case VacuumClearanceStates.Clearing: // We are launching, stay on course + { + dotTol = 0.98f; + if (!isMMG) + { + if (Physics.Raycast(toTarget, out RaycastHit hit, toSource.sqrMagnitude, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Wheels))) + { + Part p = hit.collider.gameObject.GetComponentInParent(); + if (p != null && hit.distance > 10f) + vacuumClearanceState = VacuumClearanceStates.Turning; + } + else // No obstacles in the way + { + vacuumClearanceState = VacuumClearanceStates.Cleared; + Throttle = 1f; + dotTol = clearedDotTol; + } + } + else + { + if (!VectorUtils.CheckClearOfSphere(toTarget, SourceVessel.CoM, sourceRadius)) + { + if ((toSource.sqrMagnitude) >= (sourceRadius + 10f) * (sourceRadius + 10f)) + vacuumClearanceState = VacuumClearanceStates.Turning; + } + else // No obstacles in the way + { + vacuumClearanceState = VacuumClearanceStates.Cleared; + Throttle = 1f; + dotTol = clearedDotTol; + } + } + if (vacuumClearanceState == VacuumClearanceStates.Clearing) // Adjust throttle if still clearing + { + orbitalTarget = CoM + 100f * GetForwardTransform(); + if (isMMG || !hasRCS) + { + float t = toSource.sqrMagnitude / ((sourceRadius + 5) * (sourceRadius + 5)); + Throttle = Mathf.Lerp(0.5f, 0f, t); // Use throttle kick for MMG or missiles without RCS + } + else + Throttle = 0f; + } + } + break; + case VacuumClearanceStates.Turning: // It is now safe to turn towards target and burn to maneuver away from SourceVessel + { + dotTol = 0.98f; + if (isMMG || !hasRCS) // For MMGs or missiles without RCS use throttle for turning + { + Throttle = 0.5f; + dotTol = 0.5f; + } + bool cleared = VectorUtils.CheckClearOfSphere(toTarget, SourceVessel.CoM, sourceRadius); + cleared = isMMG ? cleared : cleared && !Physics.Raycast(toTarget, out RaycastHit hit, toSource.sqrMagnitude, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Wheels)); + if ((Vector3.Dot((orbitalTarget - CoM).normalized, GetForwardTransform()) >= dotTol) && cleared) + { + vacuumClearanceState = VacuumClearanceStates.Cleared; + dotTol = clearedDotTol; + Throttle = 1f; + } + } + break; + default: // VacuumClearanceStates.Cleared, We are engaging target + { + dotTol = clearedDotTol; + Throttle = 1f; + } + break; + } + + // Rotate towards target if necessary + if (Vector3.Dot((orbitalTarget - CoM).normalized, GetForwardTransform()) < dotTol) + Throttle = 0; + } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + debugString.AppendLine($"Clearance State: {VacuumClearanceStatesString[(int)vacuumClearanceState]}"); + return orbitalTarget; + } + + public void DrawDebugLine(Vector3 start, Vector3 end, Color color = default(Color)) + { + if (BDArmorySettings.DEBUG_LINES) + { + if (!gameObject.GetComponent()) + { + LR = gameObject.AddComponent(); + LR.material = new Material(Shader.Find("KSP/Emissive/Diffuse")); + LR.material.SetColor("_EmissiveColor", color); + } + else + { + 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.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Target detected inside sphere - detonating"); + + Detonate(); + } + } + + protected void CheckCountermeasureDistance() + { + if (missileCMRange > 0) + { + if (CMenabled) + { + if (missileCMInterval > 0 && (Time.time - missileCMTime) > missileCMInterval) + { + missileCMTime = Time.time; + + DropCountermeasures(); + } + } + else if ((TargetPosition - vessel.CoM).sqrMagnitude < missileCMRange * missileCMRange) + { + InitializeCountermeasures(); + } + } + } + + protected abstract void InitializeCountermeasures(); + + protected abstract void DropCountermeasures(); + + protected Vector3 CalculateAGMBallisticGuidance(MissileBase missile, Vector3 targetPosition) + { + if (this._guidance == null) + { + _guidance = new BallisticGuidance(); + } + + return _guidance.GetDirection(this, targetPosition, Vector3.zero); + } + + protected void drawLabels() + { + if (vessel == null || !HasFired || !vessel.isActiveVessel) return; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + GUI.Label(new Rect(200, Screen.height - 300, 600, 300), $"{this.shortName}\n{debugString}"); + } + } + + public float GetTntMass() + { + return VesselModuleRegistry.GetModules(vessel).Max(x => x.tntMass); + } + + public void CheckDetonationState(bool separateWarheads = false) + { + //Guard clauses + //if (!TargetAcquired) return; + var targetDistancePerFrame = Time.fixedDeltaTime * (TargetVelocity - BDKrakensbane.FrameVelocityV3f) + 0.5f * Time.fixedDeltaTime * Time.fixedDeltaTime * TargetAcceleration; + var missileDistancePerFrame = Time.fixedDeltaTime * (vessel.Velocity() - BDKrakensbane.FrameVelocityV3f) + 0.5f * Time.fixedDeltaTime * Time.fixedDeltaTime * vessel.acceleration_immediate; + + var futureTargetPosition = (TargetPosition + targetDistancePerFrame); + var futureMissilePosition = (vessel.CoM + missileDistancePerFrame); + + float relativeSpeed = (float)(TargetVelocity - vessel.Velocity()).magnitude * Time.fixedDeltaTime; // relativeSpeed is actually a distance! + + switch (DetonationDistanceState) + { + case DetonationDistanceStates.NotSafe: + { + //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()) + { + while (hitsEnu.MoveNext()) + { + 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. + { + //We found a hit to the vessel + return; + } + } + 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; + 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 (!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 : DetonationDistanceSqr)) + { + //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 (Vector3.Distance(futureMissilePosition, futureTargetPosition) > this.DetonationDistance) return; + if ((futureMissilePosition - futureTargetPosition).sqrMagnitude > DetonationDistanceSqr) return; + + DetonationDistanceState = DetonationDistanceStates.CheckingProximity; + } + break; + } + + case DetonationDistanceStates.CheckingProximity: + { + if (!TargetAcquired) return; + if (DetonationDistance == 0) + { + if (weaponClass == WeaponClasses.Bomb) return; + + if (TimeIndex > 1f) + { + Ray rayFuturePosition = new Ray(vessel.CoM, missileDistancePerFrame); + //float dist = Time.fixedDeltaTime * (float)vessel.Velocity().magnitude; + var hitCount = Physics.RaycastNonAlloc(rayFuturePosition, proximityHits, relativeSpeed, 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. + { + proximityHits = Physics.RaycastAll(rayFuturePosition, relativeSpeed, layerMask); + hitCount = proximityHits.Length; + } + if (hitCount > 0) + { + Array.Sort(proximityHits, 0, hitCount, RaycastHitComparer.raycastHitComparer); + + using (var hitsEnu = proximityHits.Take(hitCount).GetEnumerator()) + { + while (hitsEnu.MoveNext()) + { + 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, set transform.position to hit point (moves immediately, but doesn't update .CoM fields, etc) + vessel.SetPosition(hit.point - 0.1f * rayFuturePosition.direction); + DetonationDistanceState = DetonationDistanceStates.Detonate; + Detonate(); + return; + } + } + catch (Exception e) + { + // ignored + Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonationState: " + e.Message + "\n" + e.StackTrace); + } + } + } + } + else if (TargetAcquired && targetVessel != null && targetVessel.Vessel != null) + { + // For very high speed intercepts when missiles may phase through small vessels/missiles within a frame + Vector3 relPos = TargetPosition - vessel.CoM; + Vector3 relVel = TargetVelocity - vessel.Velocity(); + bool approaching = Vector3.Dot(relPos, relVel) < 0f; + float targetRad = heatTarget.exists && heatTarget.vessel == null ? 1 : targetVessel.Vessel.GetRadius(); + // if target is a flare, return 1, else standard vessel radius + float selfRad = vessel.GetRadius(); + float sepRad = 1.7321f * (targetRad + selfRad); + + if (approaching && relVel.sqrMagnitude * Time.fixedDeltaTime * Time.fixedDeltaTime > sepRad * sepRad) + { + bool shouldDetonate = false; + Ray ray = new(vessel.CoM, -relVel); + ray.origin += selfRad * ray.direction; // Start at the tip of the missile (assuming it's pointing roughly prograde in the relVel direction and is longest on that axis). + if (Physics.Raycast(ray, out RaycastHit hit, relativeSpeed, (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels))) // Hit! + { + vessel.SetPosition(hit.point - 0.1f * ray.direction); // Slightly back so that shaped charge explosives hit properly. + shouldDetonate = true; + } + else // Not hitting, just getting close, check for reaching CPA. + { + Vector3 relAccel = TargetAcceleration - vessel.acceleration_immediate; + float cpaTime = AIUtils.TimeToCPA(relPos, relVel, relAccel, Time.fixedDeltaTime); + + if (cpaTime > 0f && cpaTime < Time.fixedDeltaTime) + { + // Set relative position to the same as at CPA point, but relative to the target's current position. This avoids having to move the target and wait an additional frame. + vessel.SetPosition(TargetPosition - AIUtils.PredictPosition(relPos, relVel, relAccel, cpaTime)); + shouldDetonate = true; + } + } + if (shouldDetonate) + { + Detonate(); + if (targetVessel.isMissile && targetVessel.MissileBaseModule) + targetVessel.MissileBaseModule.Detonate(); // The above approach is only about ~50% effective against missiles, so just tell the other missile to detonate just in case + return; + } + } + } + } + } + else + { + float optimalDistance = (float)(Math.Max(DetonationDistance, relativeSpeed)); + Vector3 targetPoint = (warheadType == WarheadTypes.ContinuousRod ? vessel.CoM - VectorUtils.GetUpDirection(TargetPosition) * (GetBlastRadius() > 0f ? Mathf.Min(GetBlastRadius() / 3f, DetonationDistance / 3f) : 5f) : vessel.CoM); + var hitCount = Physics.OverlapSphereNonAlloc(targetPoint, optimalDistance, proximityHitColliders, layerMask); + if (hitCount == proximityHitColliders.Length) + { + proximityHitColliders = Physics.OverlapSphere(targetPoint, optimalDistance, layerMask); + hitCount = proximityHitColliders.Length; + } + using (var hitsEnu = proximityHitColliders.Take(hitCount).GetEnumerator()) + { + while (hitsEnu.MoveNext()) + { + 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) continue; // Ignore source vessel + if (partHit.IsMissile() && partHit.GetComponent().SourceVessel == SourceVessel) continue; // Ignore other missiles fired by same vessel + if (partHit.vessel.vesselType == VesselType.Debris) continue; // Ignore debris + + 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) + { + 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) + { + // ignored + Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); + } + } + } + } + break; + } + } + + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileBase]: DetonationDistanceState = : {DetonationDistanceState}"); + } + } + + protected void SetInitialDetonationDistance() + { + if (this.DetonationDistance == -1) + { + if (GuidanceMode == GuidanceModes.AAMLead || GuidanceMode == GuidanceModes.AAMPure || GuidanceMode == GuidanceModes.PN || GuidanceMode == GuidanceModes.APN || GuidanceMode == GuidanceModes.AAMLoft || GuidanceMode == GuidanceModes.Kappa || GuidanceMode == GuidanceModes.CLOSThreePoint || GuidanceMode == GuidanceModes.CLOSLead) //|| GuidanceMode == GuidanceModes.AAMHybrid) + { + DetonationDistance = GetBlastRadius() * 0.25f; + } + else + { + //DetonationDistance = GetBlastRadius() * 0.05f; + DetonationDistance = 0f; + } + } + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileBase]: DetonationDistance = : {DetonationDistance}"); + } + } + + protected void CollisionEnter(Collision col) + { + if (TimeIndex > 2 && HasFired && col.collider.gameObject.GetComponentInParent().GetFireFX()) + { + ContactPoint contact = col.contacts[0]; + Vector3 pos = contact.point; + BulletHitFX.AttachFlames(pos, col.collider.gameObject.GetComponentInParent()); + } + + if (HasExploded || !HasFired) return; + + if (DetonationDistanceState != DetonationDistanceStates.CheckingProximity) return; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Missile Collided - Triggering Detonation"); + Detonate(); + } + + [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ChangetoLowAltitudeRange", active = true)]//Change to Low Altitude Range + public void CruiseAltitudeRange() + { + if (Events["CruiseAltitudeRange"].guiName == "Change to Low Altitude Range") + { + Events["CruiseAltitudeRange"].guiName = "Change to High Altitude Range"; + + UI_FloatRange cruiseAltitudeField = (UI_FloatRange)Fields["CruiseAltitude"].uiControlEditor; + cruiseAltitudeField.maxValue = 500f; + cruiseAltitudeField.minValue = 5f; + cruiseAltitudeField.stepIncrement = 5f; + } + else + { + Events["CruiseAltitudeRange"].guiName = "Change to Low Altitude Range"; + UI_FloatRange cruiseAltitudField = (UI_FloatRange)Fields["CruiseAltitude"].uiControlEditor; + cruiseAltitudField.maxValue = 25000f; + cruiseAltitudField.minValue = 500; + cruiseAltitudField.stepIncrement = 500f; + } + this.part.RefreshAssociatedWindows(); + } + } +} diff --git a/BDArmory/Weapons/Missiles/MissileDummy.cs b/BDArmory/Weapons/Missiles/MissileDummy.cs new file mode 100644 index 000000000..2b1e6d798 --- /dev/null +++ b/BDArmory/Weapons/Missiles/MissileDummy.cs @@ -0,0 +1,92 @@ +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 float missileDia = 0.2f; + 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..9bc6b9689 --- /dev/null +++ b/BDArmory/Weapons/Missiles/MissileLaunchParams.cs @@ -0,0 +1,127 @@ +using UnityEngine; + +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).normalized; + if (maxAngleOffTarget >= 0) { launcherVelocity = Vector3.RotateTowards(vectorToTarget, launcherVelocity, maxAngleOffTarget * Mathf.Deg2Rad, 0) * launcherVelocity.magnitude; } + + bool surfaceLaunch = missile.vessel.LandedOrSplashed; + float minLaunchRange = Mathf.Max(missile.minStaticLaunchRange, missile.GetEngagementRangeMin()); + float maxLaunchRange = missile.GetEngagementRangeMax(); + if (unguidedGuidedMissile) maxLaunchRange /= 10; + + // For missiles in space, bypass DLZ calc and just return static ranges + if (missile.vessel.InNearVacuum()) + return new MissileLaunchParams(Mathf.Clamp(minLaunchRange, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE), Mathf.Clamp(maxLaunchRange, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE)); + + + float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)missile.vessel.orbit.referenceBody.GeeASL; // Set gravity for calculations; + float missileActiveTime = GetMissileActiveTime(missile, surfaceLaunch); + float rangeAddMax = 0; + float relSpeed; + float missileMaxRangeTime = 8; //placeholder value for MMGs + // 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(); + Vector3 missileFwd = missile.GetForwardTransform(); + if (maxAngleOffTarget >= 0) { missileFwd = Vector3.RotateTowards(vectorToTarget, missileFwd, maxAngleOffTarget * Mathf.Deg2Rad, 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.currMaxTorque, 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 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 = VectorUtils.Angle(missileFwd, futureRelPosition); + float arcLength = Mathf.Deg2Rad * targetAngle * missileTurnRadius; + + // Add additional range term for the missile to manuever to target at missileActiveTime + minLaunchRange = Mathf.Max(arcLength, minLaunchRange); + } + missileMaxRangeTime = Mathf.Min(Vector3.Distance(targetPosition, launcherPosition), missile.maxStaticLaunchRange) / ml.optimumAirspeed; + } + //For missiles with static max launch range enabled, use static max range, but grab min range adjusted by predicted missile kinematics + if (missile.UseStaticMaxLaunchRange || missile.vessel.InNearVacuum()) + return new MissileLaunchParams(Mathf.Clamp(minLaunchRange, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE), Mathf.Clamp(maxLaunchRange, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE)); + + // 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, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE); + return new MissileLaunchParams(min, max); + } + public static float GetMissileActiveTime(MissileBase missile, bool surfaceLaunch) + { + float missileActiveTime = surfaceLaunch ? 0f : missile.dropTime; + if (missile.GetComponent() == null) + { + 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 += BDAMath.Sqrt(2 * blastRadius / maxMissileAccel); + } + return Mathf.Clamp(missileActiveTime, 0f, missile.dropTime + 2f); // Clamp at (drop time + 2s) + } + } +} diff --git a/BDArmory/Weapons/Missiles/MissileLauncher.cs b/BDArmory/Weapons/Missiles/MissileLauncher.cs new file mode 100644 index 000000000..89c86a70f --- /dev/null +++ b/BDArmory/Weapons/Missiles/MissileLauncher.cs @@ -0,0 +1,4601 @@ +using BDArmory.Bullets; +using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Guidances; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + + +namespace BDArmory.Weapons.Missiles +{ + public class MissileLauncher : MissileBase, IPartMassModifier + { + public Coroutine reloadRoutine; + bool reloadInProgress = false; + Coroutine reloadableMissile; + #region Variable Declarations + + [KSPField] + public string homingType = "AAM"; + + [KSPField] + public float guidanceDelay = -1; + + [KSPField] + public float pronavGain = 3f; + + [KSPField] + public float gLimit = -1; + + [KSPField] + public float gMargin = -1; + + [KSPField] + public string targetingType = "none"; + + [KSPField] + public string antiradTargetTypes = "0,5"; + + 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; + + #region Effects + + // Classic FX + + [KSPField] + public string exhaustPrefabPath; + + [KSPField] + public string boostExhaustPrefabPath; + + [KSPField] + public string boostExhaustTransformName; + + #endregion + + #region Aero + + [KSPField] + public bool aero = false; + + [KSPField] + public string liftArea = "0.015"; + private float[] parsedLiftArea; + public float currLiftArea = 0.015f; + + [KSPField] + public string dragArea = "-1"; // Optional parameter to specify separate drag reference area, otherwise defaults to liftArea + private float[] parsedDragArea; + public float currDragArea = -1f; + + [KSPField] + public string steerMult = "0.5"; + private float[] parsedSteerMult; + public float currSteerMult = 0.5f; + + [KSPField] + public float torqueRampUp = 30f; + Vector3 aeroTorque = Vector3.zero; + float controlAuthority; + float finalMaxTorque; + + [KSPField] + public float aeroSteerDamping = 0; + + [KSPField] + public string maxTorqueAero = "0"; + private float[] parsedMaxTorqueAero; + public float currMaxTorqueAero = 0f; + + #endregion Aero + + [KSPField] + public string maxTorque = "90"; + private float[] parsedMaxTorque; + public float currMaxTorque = 90; + + [KSPField] + public float thrust = 30; + + [KSPField] + public float cruiseThrust = 3; + + [KSPField] + public float boostTime = 2.2f; + + [KSPField] + public float cruiseTime = 45; + + [KSPField] + public float cruiseDelay = 0; + + [KSPField] + public float cruiseRangeTrigger = -1; + + [KSPField] + public float maxAoA = 35; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_Direction"),//Direction: + UI_Toggle(disabledText = "#LOC_BDArmory_Direction_disabledText", enabledText = "#LOC_BDArmory_Direction_enabledText")]//Lateral--Forward + public bool decoupleForward = false; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_DecoupleSpeed"),//Decouple Speed + UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.Editor)] + public float decoupleSpeed = 0; + + [KSPField] + public float clearanceRadius = 0.14f; + + public override float ClearanceRadius => clearanceRadius; + + [KSPField] + public float clearanceLength = 0.14f; + + public override float ClearanceLength => clearanceLength; + + [KSPField] + public float optimumAirspeed = 220; + + [KSPField] + public FloatCurve pronavGainCurve = new FloatCurve(); + + [KSPField] + public float blastRadius = -1; + + [KSPField] + public float blastPower = 0; // Depreciated, support for legacy missiles only + + [KSPField] + public float blastHeat = -1; + + [KSPField] + public float maxTurnRateDPS = 20; + + [KSPField] + public bool proxyDetonate = true; + + [KSPField] + public string audioClipPath = string.Empty; + + AudioClip thrustAudio; + + [KSPField] + public string boostClipPath = string.Empty; + + AudioClip boostAudio; + + [KSPField] + public bool isSeismicCharge = false; + + [KSPField] + public float rndAngVel = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MaxAltitude"),//Max Altitude + UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 10f, scene = UI_Scene.All)] + public float maxAltitude = 0f; + + [KSPField] + public string rotationTransformName = string.Empty; + Transform rotationTransform; + + [KSPField] + public string terminalGuidanceType = ""; + + [KSPField] + public bool dumbTerminalGuidance = true; + + [KSPField] + public float terminalGuidanceDistance = 0.0f; + + private bool terminalGuidanceActive; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TerminalGuidance"), UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true")]//Terminal Guidance: false true + public bool terminalGuidanceShouldActivate = true; + + [KSPField] + public string explModelPath = "BDArmory/Models/explosion/explosion"; + + public string explSoundPath = "BDArmory/Sounds/explode1"; + + //weapon specifications + // priority transferred to MissileBase + + [KSPField] + public bool spoolEngine = false; + + [KSPField] + public bool hasRCS = false; + + [KSPField] + public float rcsThrust = 1; + float rcsRVelThreshold = 0.13f; + KSPParticleEmitter upRCS; + KSPParticleEmitter downRCS; + KSPParticleEmitter leftRCS; + KSPParticleEmitter rightRCS; + List forwardRCS; + float rcsAudioMinInterval = 0.2f; + + private AudioSource audioSource; + public AudioSource sfAudioSource; + List pEmitters; + List gaplessEmitters; + + //float cmTimer; + + //deploy animation + [KSPField] + public string deployAnimationName = ""; + + [KSPField] + public bool deployedLiftInCruise = true; + + [KSPField] + public float deployedDrag = 0.02f; + + [KSPField] + public float deployTime = 0.2f; + + [KSPField] + public string cruiseAnimationName = ""; + + [KSPField] + public float cruiseDeployTime = 0.2f; + + [KSPField] + public string flightAnimationName = ""; + + [KSPField] + public bool OneShotAnim = true; + + [KSPField] + public bool useSimpleDrag = false; + + public bool useSimpleDragTemp = false; + + [KSPField] + public float simpleDrag = 0.02f; + + [KSPField] + public float simpleStableTorque = 5; + + [KSPField] + public Vector3 simpleCoD = new Vector3(0, 0, -1); + + [KSPField] + public float agmDescentRatio = 1.45f; + + float currentThrust; + + public bool deployed; + //public float deployedTime; + + AnimationState[] deployStates; + + AnimationState[] cruiseStates; + + AnimationState[] animStates; + + bool hasPlayedFlyby; + + float debugTurnRate; + + List boosters; + + List fairings; + + [KSPField] + public bool decoupleBoosters = false; + bool boostersDecoupled = false; + + [KSPField] + public float boosterDecoupleSpeed = 5; + + [KSPField] + public float boosterMass = 0; // The booster mass (dry mass if using fuel, wet otherwise) + + //Fuel Weight variables + [KSPField] + public float boosterFuelMass = 0; // The mass of the booster fuel (separate from the booster mass) + + [KSPField] + public float cruiseFuelMass = 0; // The mass of the cruise fuel + + [KSPField] + public bool useFuel = false; + + Transform vesselReferenceTransform; + + [KSPField] + public string boostTransformName = string.Empty; + List boostEmitters; + List boostGaplessEmitters; + + [KSPField] + public string fairingTransformName = string.Empty; + + [KSPField] + public bool torpedo = false; + + [KSPField] + public float waterImpactTolerance = 25; + + //ballistic options + [KSPField] + public bool indirect = false; //unused + + [KSPField] + public bool vacuumSteerable = true; + + // Loft Options + [KSPField] + public string terminalHomingType = "pronav"; + + [KSPField] + public float LoftTermRange = -1; + + [KSPField] + public float maneuvergLimit = 20; + float invManeuvergLimit; + + 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 - (boostersDecoupled ? boosterMass : 0); + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.CONSTANTLY; + + private float burnRate = 0; + private float burnedFuelMass = 0; + public float maxCruiseSpeed = 300f; + public bool canCruisePopup = false; + public bool canDetMinDist = false; + + private int cruiseTerminationFrames = 0; + + public bool SetupComplete => StartSetupComplete; + public int[] torqueBounds = [-1, 7]; + public float[] torqueAoABounds = [-1f, -1f, -1f]; + public SmoothingF smoothedAoA; + #endregion Variable Declarations + + [KSPAction("Fire Missile")] + public void AGFire(KSPActionParam param) + { + GuiFire(); + } + + [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_FireMissile", active = true)]//Fire Missile + public void GuiFire() + { + var weaponManager = vessel.ActiveController().WM; + if (weaponManager != null) weaponManager.SendTargetDataToMissile(this, null); + if (missileTurret) + { + missileTurret.FireMissile(this, null); + } + else if (rotaryRail) + { + rotaryRail.FireMissile(this, null); + } + else if (deployableRail) + { + deployableRail.FireMissile(this, null); + } + else + { + FireMissile(); + } + if (weaponManager != null) weaponManager.UpdateList(); + } + + [KSPEvent(guiActive = true, guiActiveEditor = false, active = true, guiName = "#LOC_BDArmory_Jettison")]//Jettison + public override void Jettison() + { + if (missileTurret) return; + if (multiLauncher && !multiLauncher.permitJettison) return; + var weaponManager = vessel.ActiveController().WM; + part.decouple(0); + if (weaponManager != null) weaponManager.UpdateList(); + } + + [KSPAction("Jettison")] + public void AGJettsion(KSPActionParam param) + { + Jettison(); + } + + void ParseWeaponClass() + { + missileType = missileType.ToLower(); + if (missileType == "bomb") + { + weaponClass = WeaponClasses.Bomb; + } + else if (missileType == "torpedo" || missileType == "depthcharge") + { + weaponClass = WeaponClasses.SLW; + } + else + { + weaponClass = WeaponClasses.Missile; + } + } + + public override void OnStart(StartState state) + { + //base.OnStart(state); + + 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 (boosterMass + boosterFuelMass + cruiseFuelMass > initialMass * 0.95f) + { + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, boosterMass: {boosterMass} + 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) + { + var totalBoosterMass = Mathf.Clamp(boosterMass + boosterFuelMass, 0, initialMass * 0.4f); // Scale total booster mass + fuel to 40% of missile. + boosterMass = boosterMass / (boosterMass + boosterFuelMass) * totalBoosterMass; + boosterFuelMass = totalBoosterMass - boosterMass; + cruiseFuelMass = Mathf.Clamp(cruiseFuelMass, 0, initialMass * 0.4f); + } + else + { + var totalBoosterMass = Mathf.Clamp(boosterMass + boosterFuelMass, 0, initialMass * 0.8f); // Scale total booster mass + fuel to 80% of missile. + boosterMass = boosterMass / (boosterMass + boosterFuelMass) * totalBoosterMass; + boosterFuelMass = totalBoosterMass - boosterMass; + } + } + else + { + boosterMass = 0; // Fuel-less boosters aren't sensible when requiring fuel. + 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; + } + gaplessEmitters = new List(); + pEmitters = new List(); + boostEmitters = new List(); + boostGaplessEmitters = new List(); + if (hasRCS) forwardRCS = new List(); + + Fields["maxOffBoresight"].guiActive = false; + Fields["maxOffBoresight"].guiActiveEditor = false; + + Fields["maxStaticLaunchRange"].guiActive = false; + Fields["maxStaticLaunchRange"].guiActiveEditor = false; + Fields["minStaticLaunchRange"].guiActive = false; + Fields["minStaticLaunchRange"].guiActiveEditor = false; + + ParseLiftDragSteerTorque(); + + MissileGuidance.setupTorqueAoALimit(this, currLiftArea, currDragArea); + + loftState = LoftStates.Boost; + TimeToImpact = float.PositiveInfinity; + WeaveOffset = -1f; + terminalHomingActive = false; + + if (radarTimeout >= 0) + { + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, radarTimeout is deprecated, please use seekerTimeout instead."); + seekerTimeout = radarTimeout; + radarTimeout = -1; + } + + if (LoftTermRange > 0) + { + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, LoftTermRange is deprecated, please use terminalHomingRange instead."); + terminalHomingRange = LoftTermRange; + LoftTermRange = -1; + } + // extension for feature_engagementenvelope + + using (var pEemitter = part.FindModelComponents().GetEnumerator()) + while (pEemitter.MoveNext()) + { + if (pEemitter.Current == null) continue; + EffectBehaviour.AddParticleEmitter(pEemitter.Current); + pEemitter.Current.emit = false; + } + + if (HighLogic.LoadedSceneIsFlight) + { + 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"); + if (!MissileReferenceTransform) + { + 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()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + AttachExhaustPrefab(exhaustPrefabPath, this, t.Current); + } + } + + if (!string.IsNullOrEmpty(boostExhaustPrefabPath) && !string.IsNullOrEmpty(boostExhaustTransformName)) + { + using (var t = part.FindModelTransforms(boostExhaustTransformName).AsEnumerable().GetEnumerator()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + AttachExhaustPrefab(boostExhaustPrefabPath, this, t.Current); + } + } + + boosters = new List(); + if (!string.IsNullOrEmpty(boostTransformName)) + { + using (var t = part.FindModelTransforms(boostTransformName).AsEnumerable().GetEnumerator()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + boosters.Add(t.Current.gameObject); + using (var be = t.Current.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (be.MoveNext()) + { + if (be.Current == null) continue; + if (be.Current.useWorldSpace) + { + var existingBE = be.Current.GetComponent(); + if (existingBE) + { + existingBE.emit = false; + be.Current.emit = false; + continue; + } + BDAGaplessParticleEmitter ge = be.Current.gameObject.AddComponent(); + ge.part = part; + ge.emit = false; + boostGaplessEmitters.Add(ge); + } + else + { + if (!boostEmitters.Contains(be.Current)) + { + boostEmitters.Add(be.Current); + } + EffectBehaviour.AddParticleEmitter(be.Current); + } + } + } + } + + 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.FindModelComponents().AsEnumerable().GetEnumerator()) + while (pEmitter.MoveNext()) + { + if (pEmitter.Current == null) continue; + var existingGE = pEmitter.Current.GetComponent(); + if (existingGE || boostEmitters.Contains(pEmitter.Current)) + { + if (existingGE) existingGE.emit = false; + continue; + } + + if (pEmitter.Current.useWorldSpace) + { + BDAGaplessParticleEmitter gaplessEmitter = pEmitter.Current.gameObject.AddComponent(); + gaplessEmitter.part = part; + gaplessEmitter.emit = false; + gaplessEmitters.Add(gaplessEmitter); + } + else + { + if (pEmitter.Current.transform.name != boostTransformName) + { + pEmitters.Add(pEmitter.Current); + } + else + { + boostEmitters.Add(pEmitter.Current); + } + EffectBehaviour.AddParticleEmitter(pEmitter.Current); + } + } + + using (IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 0; + } + + //cmTimer = Time.time; + + using (var pe = pEmitters.GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + if (hasRCS) + { + if (pe.Current.gameObject.name == "rcsUp") upRCS = pe.Current; + else if (pe.Current.gameObject.name == "rcsDown") downRCS = pe.Current; + else if (pe.Current.gameObject.name == "rcsLeft") leftRCS = pe.Current; + else if (pe.Current.gameObject.name == "rcsRight") rightRCS = pe.Current; + else if (pe.Current.gameObject.name.Contains("rcsForward")) forwardRCS.Add(pe.Current); + } + + if (!pe.Current.gameObject.name.Contains("rcs") && !pe.Current.useWorldSpace) + { + //pe.Current.sizeGrow = 99999; + } + } + + if (rotationTransformName != string.Empty) + { + rotationTransform = part.FindModelTransform(rotationTransformName); + } + + if (hasRCS) + { + SetupRCS(); + KillRCS(); + } + SetupAudio(); + var missileSpawner = part.FindModuleImplementing(); + if (missileSpawner != null) + { + reloadableRail = missileSpawner; + hasAmmo = true; + } + if (customTurretID > 0) + { + using (var servo = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (servo.MoveNext()) + { + if (servo.Current == null) continue; + if ((int)servo.Current.turretID != (int)customTurretID) continue; + customTurret.Add(servo.Current); + servo.Current.SetReferenceTransform(MissileReferenceTransform); + } + if (customTurret.Count == 0) customTurretID = 0; + } + } + if (HighLogic.LoadedSceneIsEditor) + { + GameEvents.onEditorPartPlaced.Add(OnEditorPartPlaced); + FindTurretInParents(part); + } + if (deployAnimationName != "") + { + deployStates = GUIUtils.SetUpAnimation(deployAnimationName, part); + } + else + { + deployedDrag = simpleDrag; + } + if (cruiseAnimationName != "") + { + cruiseStates = GUIUtils.SetUpAnimation(cruiseAnimationName, part); + } + if (flightAnimationName != "") + { + animStates = GUIUtils.SetUpAnimation(flightAnimationName, part); + } + + warheadType = WarheadTypes.Kinetic; // Default to Kinetic if no appropriate modules are found. + foreach (var partModule in part.Modules) + { + if (partModule == null) continue; + switch (partModule.moduleName) + { + case "BDExplosivePart": + ((BDExplosivePart)partModule).ParseWarheadType(); + if (((BDExplosivePart)partModule).warheadReportingName == "Continuous Rod") + if (warheadType == WarheadTypes.Custom) + warheadType = WarheadTypes.CustomContinuous; + else + warheadType = WarheadTypes.ContinuousRod; + else + if (warheadType == WarheadTypes.Custom) + warheadType = WarheadTypes.CustomStandard; + else + warheadType = WarheadTypes.Standard; + continue; //EMPs sometimes have BDExplosivePart modules for FX, so keep going + case "BDCustomWarhead": + if (warheadType == WarheadTypes.ContinuousRod) + warheadType = WarheadTypes.CustomContinuous; + else if (warheadType == WarheadTypes.Standard) + warheadType = WarheadTypes.CustomStandard; + else + warheadType = WarheadTypes.Custom; + continue; + case "ClusterBomb": + clusterbomb = ((ClusterBomb)partModule).submunitions.Count; + break; //CBs destroy the part on deployment, doesn't support other modules, break + case "MultiMissileLauncher": + if (!String.IsNullOrEmpty(((MultiMissileLauncher)partModule).subMunitionName)) + { + //shouldn't have both MML and ClusterBomb/BDExplosivepart/ModuleEMP/BDModuleNuke on the same part; explosive would be on the submunition .cfg + //so instead need a check if the MML comes with a default ordnance, and see what it is to inherit stats. + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) continue; + if (parts.Current.partPrefab.partInfo.name != ((MultiMissileLauncher)partModule).subMunitionName) continue; + foreach (var subModule in parts.Current.partPrefab.Modules) + { + if (subModule == null) continue; + switch (subModule.moduleName) + { + case "BDExplosivePart": + ((BDExplosivePart)subModule).ParseWarheadType(); + if (((BDExplosivePart)subModule).warheadReportingName == "Continuous Rod") + if (warheadType == WarheadTypes.Custom) + warheadType = WarheadTypes.CustomContinuous; + else + warheadType = WarheadTypes.ContinuousRod; + else + if (warheadType == WarheadTypes.Custom) + warheadType = WarheadTypes.CustomStandard; + else + warheadType = WarheadTypes.Standard; + continue; //EMPs sometimes have BDExplosivePart modules for FX, so keep going + case "BDCustomWarhead": + if (warheadType == WarheadTypes.ContinuousRod) + warheadType = WarheadTypes.CustomContinuous; + else if (warheadType == WarheadTypes.Standard) + warheadType = WarheadTypes.CustomStandard; + else + warheadType = WarheadTypes.Custom; + continue; + case "ClusterBomb": + clusterbomb = ((ClusterBomb)subModule).submunitions.Count; //No bomb check, since I guess you could have a missile with a clusterbomb module, for some reason...? + if (clusterbomb > 1) clusterbomb *= (int)((MultiMissileLauncher)partModule).salvoSize; + break; + case "ModuleEMP": + warheadType = WarheadTypes.EMP; + StandOffDistance = ((ModuleEMP)subModule).proximity; + break; + case "BDModuleNuke": + warheadType = WarheadTypes.Nuke; + StandOffDistance = BDAMath.Sqrt(((BDModuleNuke)subModule).yield) * 500; + break; + } + } + } + } + else + { + if (warheadType == WarheadTypes.Kinetic) warheadType = WarheadTypes.Launcher; //empty MultiMissile Launcher + } + break; //MMLs don't support other modules, break + case "ModuleEMP": + warheadType = WarheadTypes.EMP; + StandOffDistance = ((ModuleEMP)partModule).proximity; + break; + case "BDModuleNuke": + warheadType = WarheadTypes.Nuke; + StandOffDistance = BDAMath.Sqrt(((BDModuleNuke)partModule).yield) * 500; + break; + default: + continue; + } + break; // Break if a valid module is found. + } + if (warheadType == WarheadTypes.Kinetic && blastPower > 0) warheadType = WarheadTypes.Legacy; + + // Get maxOffboresight here because MMLs won't have the correct value for this if this is done in SetFields() + string maxOffboresightString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "MissileLauncher", "maxOffBoresight"); + if (!string.IsNullOrEmpty(maxOffboresightString)) // Use the default value from the MM patch. + { + try + { + maxOffBoresight = float.Parse(maxOffboresightString); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: setting maxOffBoresight of {part} on {(HighLogic.LoadedSceneIsFlight ? part.vessel.vesselName : EditorLogic.fetch.ship.shipName)} to {maxOffBoresight}"); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.MissileLauncher]: Failed to parse maxOffBoresight configNode ({maxOffboresightString}): {e.Message}\n{e.StackTrace}"); + } + } + + SetFields(); + smoothedAoA = new SmoothingF(Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime * 10f)); // Half-life of 0.1s. + StartSetupComplete = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher] Start() setup complete"); + } + + public void SetFields(bool checkBaseConfig = true) + { + ParseWeaponClass(); + ParseModes(); + InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); + if (proxyDetonate) + SetInitialDetonationDistance(); + else + DetonationDistance = 0f; + 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 + invManeuvergLimit = 1f / maneuvergLimit; + // MMLs **shouldn't** be checking the base config, hence checkBaseConfig being a thing + MissileLauncher baseConfig = checkBaseConfig ? part.partInfo.partPrefab.FindModuleImplementing() : null; + + if (checkBaseConfig && baseConfig) + { + canDetMinDist = baseConfig.DetonateAtMinimumDistance; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: setting canDetMinDist of {part} on {(HighLogic.LoadedSceneIsFlight ? part.vessel.vesselName : EditorLogic.fetch.ship.shipName)} to {canDetMinDist}"); + } + if (isTimed) + { + Fields["detonationTime"].guiActive = true; + Fields["detonationTime"].guiActiveEditor = true; + } + else + { + Fields["detonationTime"].guiActive = false; + Fields["detonationTime"].guiActiveEditor = false; + } + if (GuidanceMode != GuidanceModes.Cruise && (!terminalHoming || homingModeTerminal != GuidanceModes.Cruise)) + { + CruiseAltitudeRange(); + Fields["CruiseAltitude"].guiActive = false; + Fields["CruiseAltitude"].guiActiveEditor = false; + Fields["CruiseSpeed"].guiActive = false; + Fields["CruiseSpeed"].guiActiveEditor = false; + Events["CruiseAltitudeRange"].guiActive = false; + Events["CruiseAltitudeRange"].guiActiveEditor = false; + Fields["CruisePredictionTime"].guiActiveEditor = false; + Fields["CruisePopup"].guiActive = false; + Fields["CruisePopup"].guiActiveEditor = false; + } + else + { + /*string baseConfigParamString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "MissileLauncher", "CruiseSpeed"); + if (!string.IsNullOrEmpty(baseConfigParamString)) // Use the default value from the MM patch. + { + try + { + maxCruiseSpeed = float.Parse(baseConfigParamString); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: setting maxCruiseSpeed of " + part + " on " + part.vessel.vesselName + " to " + maxCruiseSpeed); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.MissileLauncher]: Failed to parse maxCruiseSpeed configNode: " + e.Message); + } + }*/ + if (checkBaseConfig && baseConfig) + { + maxCruiseSpeed = baseConfig.CruiseSpeed; + canCruisePopup = baseConfig.CruisePopup; + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileLauncher]: setting maxCruiseSpeed of {part} on {(HighLogic.LoadedSceneIsFlight ? part.vessel.vesselName : EditorLogic.fetch.ship.shipName)} to {maxCruiseSpeed}"); + Debug.Log($"[BDArmory.MissileLauncher]: setting canCruisePopup of {part} on {(HighLogic.LoadedSceneIsFlight ? part.vessel.vesselName : EditorLogic.fetch.ship.shipName)} to {canCruisePopup}"); + } + } + UI_FloatRange CruiseSpeedRange = (UI_FloatRange)Fields["CruiseSpeed"].uiControlEditor; + CruiseSpeedRange.maxValue = maxCruiseSpeed; + CruiseSpeedRange.stepIncrement = Mathf.Clamp((maxCruiseSpeed - 100f) * 0.1f, 5f, 50f); + 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 (canCruisePopup) + { + Fields["CruisePopup"].guiActive = true; + Fields["CruisePopup"].guiActiveEditor = true; + } + else + { + Fields["CruisePopup"].guiActive = false; + Fields["CruisePopup"].guiActiveEditor = false; + } + } + + 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; + Fields["BallisticOverShootFactor"].guiActiveEditor = false; + 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") || weaponClass == WeaponClasses.SLW) + { + Fields["dropTime"].guiActive = false; + Fields["dropTime"].guiActiveEditor = false; + if (torpedo) dropTime = 0; + } + else + { + Fields["dropTime"].guiActive = true; + Fields["dropTime"].guiActiveEditor = true; + } + + // Moved mFA setting here instead of OnStart() to account for the need for this to be set for MMLs as well + if (maxOffBoresight < 180 && missileType.ToLower() == "missile" || missileType.ToLower() == "torpedo") + { + UI_FloatRange mFA = (UI_FloatRange)Fields["missileFireAngle"].uiControlEditor; + mFA.maxValue = maxOffBoresight * 0.75f; + //mFA.stepIncrement = mFA.maxValue / 100; + if (missileFireAngle < 0) + missileFireAngle = maxOffBoresight * 0.75f; + else + missileFireAngle = Mathf.Min(missileFireAngle, maxOffBoresight * 0.75f); + } + + if (TargetingModeTerminal != TargetingModes.None) + { + Fields["terminalGuidanceShouldActivate"].guiName += terminalGuidanceType; + } + else + { + Fields["terminalGuidanceShouldActivate"].guiActive = false; + Fields["terminalGuidanceShouldActivate"].guiActiveEditor = false; + terminalGuidanceShouldActivate = false; + } + + if (GuidanceMode != GuidanceModes.AAMLoft && GuidanceMode != GuidanceModes.Kappa) + { + Fields["LoftMaxAltitude"].guiActive = false; + Fields["LoftMaxAltitude"].guiActiveEditor = false; + Fields["LoftRangeOverride"].guiActive = false; + Fields["LoftRangeOverride"].guiActiveEditor = false; + Fields["LoftAngle"].guiActive = false; + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + } + else + { + + Fields["LoftMaxAltitude"].guiActiveEditor = true; + Fields["LoftRangeOverride"].guiActiveEditor = true; + + if (!GameSettings.ADVANCED_TWEAKABLES) + { + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + } + else + { + Fields["LoftAngle"].guiActiveEditor = true; + Fields["LoftTermAngle"].guiActiveEditor = true; + } + + if (!BDArmorySettings.DEBUG_MISSILES) + { + Fields["LoftMaxAltitude"].guiActive = false; + Fields["LoftRangeOverride"].guiActive = false; + Fields["LoftAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActive = false; + + } + else + { + Fields["LoftMaxAltitude"].guiActive = true; + Fields["LoftRangeOverride"].guiActive = true; + Fields["LoftAngle"].guiActive = true; + Fields["LoftTermAngle"].guiActive = true; + } + } + + if (GuidanceMode != GuidanceModes.AAMLoft) + { + Fields["LoftMinAltitude"].guiActive = false; + Fields["LoftMinAltitude"].guiActiveEditor = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + Fields["LoftAltitudeAdvMax"].guiActive = false; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActive = false; + Fields["LoftRangeFac"].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["LoftMinAltitude"].guiActiveEditor = true; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = true; + //Fields["terminalHomingRange"].guiActive = true; + //Fields["terminalHomingRange"].guiActiveEditor = true; + + if (!GameSettings.ADVANCED_TWEAKABLES) + { + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActiveEditor = false; + //Fields["LoftAltComp"].guiActive = false; + //Fields["LoftAltComp"].guiActiveEditor = false; + } + else + { + Fields["LoftVelComp"].guiActiveEditor = true; + Fields["LoftVertVelComp"].guiActiveEditor = true; + Fields["LoftRangeFac"].guiActiveEditor = true; + //Fields["LoftAltComp"].guiActive = true; + //Fields["LoftAltComp"].guiActiveEditor = true; + } + + if (!BDArmorySettings.DEBUG_MISSILES) + { + Fields["LoftMinAltitude"].guiActive = false; + Fields["LoftAltitudeAdvMax"].guiActive = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActive = false; + Fields["LoftRangeFac"].guiActive = false; + } + else + { + Fields["LoftMinAltitude"].guiActive = true; + Fields["LoftAltitudeAdvMax"].guiActive = true; + Fields["LoftVelComp"].guiActive = true; + Fields["LoftVertVelComp"].guiActive = true; + Fields["LoftRangeFac"].guiActive = 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; + } + + // fill lockedSensorFOVBias with default values if not set by part config: + if ((TargetingMode == TargetingModes.Heat || TargetingModeTerminal == TargetingModes.Heat) && heatThreshold > 0 && lockedSensorFOVBias.minTime == float.MaxValue) + { + 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.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.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))); + } + } + + // fill lockedSensorVelocityBias with default values if not set by part config: + if ((TargetingMode == TargetingModes.Heat || TargetingModeTerminal == TargetingModes.Heat) && heatThreshold > 0) + { + bool defaultVelocityBias = false; + if (lockedSensorVelocityBias.minTime == float.MaxValue) + { + lockedSensorVelocityBias.Add(0f, 1f); + lockedSensorVelocityBias.Add(180f, 1f); + defaultVelocityBias = true; + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default lockedSensorVelocityBias curve to:"); + Debug.Log("key = 0 1"); + Debug.Log("key = 180 1"); + } + } + if (lockedSensorVelocityMagnitudeBias.minTime == float.MaxValue) + { + lockedSensorVelocityMagnitudeBias.Add(1f, 1f); + if (defaultVelocityBias) + lockedSensorVelocityMagnitudeBias.Add(0f, 1f); + else + lockedSensorVelocityMagnitudeBias.Add(0f, 0f); + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default lockedSensorVelocityMagnitudeBias curve to:"); + Debug.Log("key = 1 1"); + if (defaultVelocityBias) + Debug.Log("key = 0 1"); + else + Debug.Log("key = 0 0"); + } + } + } + + // fill activeRadarLockTrackCurve, activeRadarVelocityGate and activeRadarRangeGate with default values if not set by part config: + if ((TargetingMode == TargetingModes.Radar || TargetingModeTerminal == TargetingModes.Radar) && activeRadarRange > 0) + { + if (activeRadarLockTrackCurve.minTime == float.MaxValue) + { + activeRadarLockTrackCurve.Add(0f, 0f); + activeRadarLockTrackCurve.Add(activeRadarRange, RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); // TODO: tune & balance constants! + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default locktrackcurve with maxrange/minrcs: {activeRadarLockTrackCurve.maxTime}/{RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS}"); + } + + if (activeRadarVelocityGate.minTime == float.MaxValue) + { + activeRadarVelocityGate.Add(0f, RadarUtils.MISSILE_DEFAULT_GATE_RCS); + activeRadarVelocityGate.Add(activeRadarVelocityFilter, 1f); // TODO: tune & balance constants! + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default activeRadarVelocityGate with maxfilter: {activeRadarLockTrackCurve.maxTime}"); + } + else if (activeRadarVelocityFilter < activeRadarVelocityGate.maxTime) + { + activeRadarVelocityFilter = activeRadarVelocityGate.maxTime; + } + + + if (activeRadarRangeGate.minTime == float.MaxValue) + { + activeRadarRangeGate.Add(0f, 1f); + activeRadarRangeGate.Add(activeRadarRangeFilter, 0f); // TODO: tune & balance constants! + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default activeRadarRangeGate with maxfilter/minrcs: {activeRadarRangeGate.maxTime}/{RadarUtils.MISSILE_DEFAULT_GATE_RCS}"); + } + else if(activeRadarRangeFilter < activeRadarRangeGate.maxTime) + { + activeRadarRangeFilter = activeRadarRangeGate.maxTime; + } + } + + // Don't show detonation distance settings for kinetic warheads + if (warheadType == WarheadTypes.Kinetic || !proxyDetonate) + { + Fields["DetonationDistance"].guiActive = false; + Fields["DetonationDistance"].guiActiveEditor = false; + Fields["DetonateAtMinimumDistance"].guiActive = false; + Fields["DetonateAtMinimumDistance"].guiActiveEditor = false; + } + else if (!canDetMinDist) + { + Fields["DetonateAtMinimumDistance"].guiActive = false; + Fields["DetonateAtMinimumDistance"].guiActiveEditor = false; + } + ParseAntiRadTargetTypes(); + GUIUtils.RefreshAssociatedWindows(part); + } + + /// + /// This method will convert the blastPower to a tnt mass equivalent + /// + private void FromBlastPowerToTNTMass() + { + blastPower = BlastPhysicsUtils.CalculateExplosiveMass(blastRadius); + } + + void OnCollisionEnter(Collision col) + { + base.CollisionEnter(col); + } + + void SetupAudio() + { + if (audioSource == null) + { + audioSource = gameObject.AddComponent(); + audioSource.minDistance = 1; + audioSource.maxDistance = 1000; + audioSource.loop = true; + audioSource.pitch = 1f; + audioSource.priority = 255; + audioSource.spatialBlend = 1; + } + + if (audioClipPath != string.Empty) + { + audioSource.clip = SoundUtils.GetAudioClip(audioClipPath); + } + + if (sfAudioSource == null) + { + sfAudioSource = gameObject.AddComponent(); + sfAudioSource.minDistance = 1; + sfAudioSource.maxDistance = 2000; + sfAudioSource.dopplerLevel = 0; + sfAudioSource.priority = 230; + sfAudioSource.spatialBlend = 1; + } + + if (audioClipPath != string.Empty) + { + thrustAudio = SoundUtils.GetAudioClip(audioClipPath); + } + + if (boostClipPath != string.Empty) + { + boostAudio = SoundUtils.GetAudioClip(boostClipPath); + } + + UpdateVolume(); + BDArmorySetup.OnVolumeChange -= UpdateVolume; // Remove it if it's already there. (Doesn't matter if it isn't.) + BDArmorySetup.OnVolumeChange += UpdateVolume; + } + + void UpdateVolume() + { + if (audioSource) + { + audioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; + } + if (sfAudioSource) + { + sfAudioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; + } + } + + void OnDestroy() + { + //Debug.Log("{TorpDebug] torpedo crash tolerance: " + part.crashTolerance); + DetachExhaustPrefabs(); + KillRCS(); + if (upRCS) EffectBehaviour.RemoveParticleEmitter(upRCS); + if (downRCS) EffectBehaviour.RemoveParticleEmitter(downRCS); + if (leftRCS) EffectBehaviour.RemoveParticleEmitter(leftRCS); + if (rightRCS) EffectBehaviour.RemoveParticleEmitter(rightRCS); + if (forwardRCS != null) + foreach (var pe in forwardRCS) + if (pe) EffectBehaviour.RemoveParticleEmitter(pe); + 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 (boostGaplessEmitters 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 bgpe in boostGaplessEmitters) + if (bgpe is not null) Destroy(bgpe); + if (boostEmitters != null) + foreach (var pe in boostEmitters) + if (pe) EffectBehaviour.RemoveParticleEmitter(pe); + BDArmorySetup.OnVolumeChange -= UpdateVolume; + GameEvents.onPartDie.Remove(PartDie); + GameEvents.onEditorPartPlaced.Remove(OnEditorPartPlaced); + if (vesselReferenceTransform != null && vesselReferenceTransform.gameObject != null) + { + Destroy(vesselReferenceTransform.gameObject); + } + } + void OnEditorPartPlaced(Part p) + { + if (p = part) FindTurretInParents(part); + } + private void FindTurretInParents(Part p) + { + if (p == null) + { + Fields["customTurretID"].guiActiveEditor = false; + return; + } + var turret = p.FindModuleImplementing(); + if (turret != null) + { + Fields["customTurretID"].guiActiveEditor = true; + return; + } + FindTurretInParents(p.parent); + } + + public override float GetBlastRadius() + { + if (blastRadius >= 0) { return blastRadius; } + else + { + 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 (warheadType == WarheadTypes.Kinetic) + { + blastRadius = 0f; + return 0f; + } + else + { + if (part.FindModuleImplementing() != null) + { + List tntList = part.FindModulesImplementing(); + foreach (BDExplosivePart tnt in tntList) + { + float tempBlastRadius = tnt.GetBlastRadius(); + blastRadius = tempBlastRadius > blastRadius ? tempBlastRadius : blastRadius; + } + return blastRadius; + } + else if (part.FindModuleImplementing() != null) + { + blastRadius = BlastPhysicsUtils.CalculateBlastRange(part.FindModuleImplementing().tntMass); + return blastRadius; + } + else + { + blastRadius = 150; + return blastRadius; + } + } + } + } + + public override void FireMissile() + { + if (HasFired || launched) return; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: Missile launch initiated! {vessel.vesselName}"); + + if (SourceVessel == null) + { + SourceVessel = vessel; + } + FiredByWM = SourceVessel.ActiveController().WM; + if (FiredByWM != null) Team = FiredByWM.Team; + + if (multiLauncher) + { + if (multiLauncher.isMultiLauncher) + { + //multiLauncher.rippleRPM = FiredWM.rippleRPM; + //if (FiredWM.rippleRPM > 0) multiLauncher.rippleRPM = FiredWM.rippleRPM; + multiLauncher.Team = Team; + launched = true; + if (reloadableRail && reloadableRail.ammoCount >= 1 || BDArmorySettings.INFINITE_ORDINANCE) + { + if (FiredByWM) + FiredByWM.UpdateQueuedLaunches(targetVessel, this, true); + if (radarTarget.exists && radarTarget.lockedByRadar && radarTarget.lockedByRadar.vessel != SourceVessel) + { + MissileFire datalinkwpm = radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateQueuedLaunches(targetVessel, this, true, false); + } + multiLauncher.fireMissile(); + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: firing Multilauncher! {vessel.vesselName}; {multiLauncher.subMunitionName}"); + } + else //isClusterMissile + { + if (reloadableRail && (reloadableRail.maxAmmo > 1 && (reloadableRail.ammoCount >= 1 || BDArmorySettings.INFINITE_ORDINANCE))) //clustermissile with reload module + { + if (reloadableMissile == null) + { + if (FiredByWM) + FiredByWM.UpdateQueuedLaunches(targetVessel, this, true); + if (radarTarget.exists && radarTarget.lockedByRadar && radarTarget.lockedByRadar.vessel != SourceVessel) + { + MissileFire datalinkwpm = radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateQueuedLaunches(targetVessel, this, true, false); + } + reloadableMissile = StartCoroutine(FireReloadableMissile()); + } + launched = true; + } + else //standard non-reloadable missile + { + multiLauncher.missileSpawner.MissileName = multiLauncher.subMunitionName; + multiLauncher.missileSpawner.UpdateMissileValues(); + DetonationDistance = multiLauncher.clusterMissileTriggerDist; + blastRadius = multiLauncher.clusterMissileTriggerDist; + multiLauncher.isLaunchedClusterMissile = true; + TimeFired = Time.time; + part.decouple(0); + part.Unpack(); + 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 + MissileLaunch(); + BDATargetManager.FiredMissiles.Add(this); + if (FiredByWM != null) + { + FiredByWM.heatTarget = TargetSignatureData.noTarget; + GpsUpdateMax = FiredByWM.GpsUpdateMax; + FiredByWM.UpdateMissilesAway(targetVessel, this); + } + + if (radarTarget.exists && radarTarget.lockedByRadar && radarTarget.lockedByRadar.vessel != SourceVessel) + { + MissileFire datalinkwpm = radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateMissilesAway(targetVessel, this, false); + } + + launched = true; + } + } + } + else + { + if (reloadableRail && (reloadableRail.ammoCount >= 1 || BDArmorySettings.INFINITE_ORDINANCE)) + { + if (reloadableMissile == null) + { + if (FiredByWM) + FiredByWM.UpdateQueuedLaunches(targetVessel, this, true); + if (radarTarget.exists && radarTarget.lockedByRadar && radarTarget.lockedByRadar.vessel != SourceVessel) + { + MissileFire datalinkwpm = radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateQueuedLaunches(targetVessel, this, true, false); + } + reloadableMissile = StartCoroutine(FireReloadableMissile()); + } + launched = true; + } + else + { + TimeFired = Time.time; + part.decouple(0); + part.Unpack(); + 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 + MissileLaunch(); + BDATargetManager.FiredMissiles.Add(this); + if (FiredByWM != null) + { + FiredByWM.heatTarget = TargetSignatureData.noTarget; + GpsUpdateMax = FiredByWM.GpsUpdateMax; + FiredByWM.UpdateMissilesAway(targetVessel, this); + } + + if (radarTarget.exists && radarTarget.lockedByRadar && radarTarget.lockedByRadar.vessel != SourceVessel) + { + MissileFire datalinkwpm = radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateMissilesAway(targetVessel, this, false); + } + + launched = true; + } + } + } + IEnumerator FireReloadableMissile() + { + var firedByWM = SourceVessel.ActiveController().WM; + var sourceVessel = SourceVessel; + part.partTransform.localScale = Vector3.zero; + part.ShieldedFromAirstream = true; + part.crashTolerance = 100; + if (!reloadableRail.SpawnMissile(MissileReferenceTransform)) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning($"[BDArmory.MissileLauncher]: Failed to spawn a missile in {reloadableRail} on {vessel.vesselName}"); + yield break; + } + 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 is null || ml.gameObject is null || !ml.gameObject.activeInHierarchy) + { + if (ml is not null) Destroy(ml); // The gameObject is gone, make sure the module goes too. + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error while spawning missile with {part.name}, MissileLauncher was null!"); + yield break; + } + + FiredByWM = firedByWM; + ml.launched = true; + ml.SourceVessel = sourceVessel; + ml.GuidanceMode = GuidanceMode; + //FiredByWM.SendTargetDataToMissile(ml); + ml.TimeFired = Time.time; + 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 causing this to quit? + ml.BallisticAngle = BallisticAngle; + } + if (GuidanceMode == GuidanceModes.Cruise) + { + ml.CruiseAltitude = CruiseAltitude; + ml.CruiseSpeed = CruiseSpeed; + ml.CruisePredictionTime = CruisePredictionTime; + } + + if (BDArmorySettings.DEBUG_MISSILES) + { + 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.loftState = LoftStates.Boost; + ml.TimeToImpact = float.PositiveInfinity; + } + /*if (GuidanceMode == GuidanceModes.AAMHybrid) + ml.pronavGain = pronavGain;*/ + + if (GuidanceMode == GuidanceModes.Kappa) + { + ml.kappaAngle = kappaAngle; + ml.LoftAngle = LoftAngle; + ml.LoftTermAngle = LoftTermAngle; + ml.LoftMaxAltitude = LoftMaxAltitude; + ml.LoftRangeFac = LoftRangeFac; + ml.LoftVertVelComp = LoftVertVelComp; + ml.LoftRangeOverride = LoftRangeOverride; + ml.loftState = LoftStates.Boost; + } + } + + 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 (BDArmorySettings.DEBUG_MISSILES) + { + 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.loftState = LoftStates.Boost; + ml.TimeToImpact = float.PositiveInfinity; + } + + if (homingModeTerminal == GuidanceModes.Kappa) + { + ml.kappaAngle = kappaAngle; + ml.LoftAngle = LoftAngle; + ml.LoftTermAngle = LoftTermAngle; + ml.LoftMaxAltitude = LoftMaxAltitude; + ml.LoftRangeFac = LoftRangeFac; + ml.LoftVertVelComp = LoftVertVelComp; + ml.LoftRangeOverride = LoftRangeOverride; + ml.loftState = LoftStates.Boost; + } + } + } + + ml.decoupleForward = decoupleForward; + ml.decoupleSpeed = decoupleSpeed; + if (GuidanceMode == GuidanceModes.AGM) + ml.maxAltitude = maxAltitude; + ml.terminalGuidanceShouldActivate = terminalGuidanceShouldActivate; + ml.guidanceActive = true; + + BDATargetManager.FiredMissiles.Add(ml); + if (FiredByWM != null) + { + ml.Team = FiredByWM.Team; + FiredByWM.SendTargetDataToMissile(ml, targetVessel != null ? targetVessel.Vessel : null, true, new MissileFire.TargetData(targetGPSCoords, TimeOfLastINS, INStimetogo), true); + FiredByWM.heatTarget = TargetSignatureData.noTarget; + ml.GpsUpdateMax = FiredByWM.GpsUpdateMax; + FiredByWM.UpdateQueuedLaunches(targetVessel, ml, false); + FiredByWM.UpdateMissilesAway(targetVessel, ml); + } + + if (ml.radarTarget.exists && ml.radarTarget.lockedByRadar && ml.radarTarget.lockedByRadar.vessel != ml.SourceVessel) + { + MissileFire datalinkwpm = ml.radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + { + datalinkwpm.UpdateQueuedLaunches(targetVessel, ml, false, false); + datalinkwpm.UpdateMissilesAway(targetVessel, ml, false); + } + } + + 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.railAmmo < 1 && 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() + { + // if (gameObject is null || !gameObject.activeInHierarchy) { Debug.LogError($"[BDArmory.MissileLauncher]: Trying to fire non-existent missile {missileName} {(reloadableRail != null ? " (reloadable)" : "")} on {SourceVesselName} at {TargetVesselName}!"); return; } + HasFired = true; + try // FIXME Remove this once the fix is sufficiently tested. + { + GameEvents.onPartDie.Add(PartDie); + + if (GetComponentInChildren()) + { + BDArmorySetup.numberOfParticleEmitters++; + } + + if (sfAudioSource == null) SetupAudio(); + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/deployClick")); + //SourceVessel = vessel; + + //TARGETING + 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.force_activate(); + part.gTolerance = 999; + vessel.situation = Vessel.Situations.FLYING; + part.rb.isKinematic = false; + part.bodyLiftMultiplier = 0; + part.dragModel = Part.DragModel.NONE; + + //add target info to vessel + AddTargetInfoToVessel(); + StartCoroutine(DecoupleRoutine()); + if (BDArmorySettings.DEBUG_MISSILES) shortName = $"{SourceVessel.GetName()}'s {GetShortName()}"; + vessel.vesselName = GetShortName(); + vessel.vesselType = VesselType.Probe; + //setting ref transform for navball + GameObject refObject = new GameObject(); + refObject.transform.rotation = Quaternion.LookRotation(-transform.up, transform.forward); + refObject.transform.parent = transform; + part.SetReferenceTransform(refObject.transform); + vessel.SetReferenceTransform(part); + vesselReferenceTransform = refObject.transform; + DetonationDistanceState = DetonationDistanceStates.NotSafe; + MissileState = MissileStates.Drop; + part.crashTolerance = torpedo ? waterImpactTolerance : 9999; //to combat stresses of launch, missiles generate a lot of G Force + part.explosionPotential = 0; // Minimise the default part explosion FX that sometimes gets offset from the main explosion. + vacuumClearanceState = (GuidanceMode == GuidanceModes.Orbital && vacuumSteerable && part.atmDensity <= 0.001f && missileTurret == null) ? // vessel.InVacuum() not updated, will return 0, so use part.atmDensity check + VacuumClearanceStates.Clearing : VacuumClearanceStates.Cleared; // Set up clearance check if missile is vacuumSteerable, and is in space, and was not launched from a turret + + CruiseSpeed = Mathf.Min(CruiseSpeed, maxCruiseSpeed); + CruisePopup = canCruisePopup && CruisePopup; + if (!proxyDetonate) + { + DetonationDistance = 0f; + DetonateAtMinimumDistance = false; + } + else + DetonateAtMinimumDistance = canDetMinDist && DetonateAtMinimumDistance; + + StartCoroutine(MissileRoutine()); + List tntList = part.FindModulesImplementing(); + foreach (BDWarheadBase tnt in tntList) + { + tnt.Team = Team; + tnt.sourcevessel = SourceVessel; + } + 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 + "\n" + e.StackTrace); + 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 targetVessel?: " + (targetVessel == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG targetVessel: " + 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() + { + bool redployTurret = false; + //TODO: missile reload SFX support? + MissileTurret turret = multiLauncher ? multiLauncher.turret : missileTurret; + if ((turret != null) && (turret.deployBlocksReload && turret.hasDeployAnimation)) //change this to an AnimatedReload bool for deploy/reloadAnim support + { + turret.isReloading = true; + redployTurret = true; + turret.ReturnTurret(); + yield return new WaitUntilFixed(() => !turret.isDeployed()); + } + + reloadableRail.loadOrdnance(multiLauncher ? multiLauncher.launchTubes : 1); + if (reloadableRail.railAmmo > 0 || BDArmorySettings.INFINITE_ORDINANCE) + { + if (vessel.isActiveVessel) gauge.UpdateReloadMeter(reloadTimer); + reloadInProgress = true; + yield return new WaitForSecondsFixed(reloadableRail.reloadTime); + reloadInProgress = false; + 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; + if (redployTurret && turret) + { + turret.isReloading = false; + } + } + + IEnumerator DecoupleRoutine() + { + yield return new WaitForFixedUpdate(); + + if (rndAngVel > 0) + { + part.rb.angularVelocity += UnityEngine.Random.insideUnitSphere.normalized * rndAngVel; + } + + 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 + { + part.rb.velocity += decoupleSpeed * -part.transform.up; + } + } + + /// + /// Fires the missileBase on target vessel. Used by AI currently. + /// + /// V. + public void FireMissileOnTarget(Vessel v) + { + if (!HasFired) + { + targetVessel = v.gameObject.GetComponent(); + FireMissile(); + } + } + + void OnDisable() + { + if (TargetingMode == TargetingModes.AntiRad) + { + RadarWarningReceiver.OnRadarPing -= ReceiveRadarPing; + } + } + + void Update() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (!vessel.isActiveVessel) return; + if (reloadableRail) + { + if (launched && reloadInProgress) + { + reloadTimer += TimeWarp.deltaTime; + gauge.UpdateReloadMeter(Mathf.Clamp01(reloadTimer / reloadableRail.reloadTime)); + } + } + if (multiLauncher && heatTimer > 0) + { + heatTimer -= TimeWarp.deltaTime; + gauge.UpdateHeatMeter(Mathf.Clamp01(heatTimer / multiLauncher.launcherCooldown)); + } + } + + public override void OnFixedUpdate() + { + base.OnFixedUpdate(); + + if (!HighLogic.LoadedSceneIsFlight) return; + + FloatingOriginCorrection(); + + try // FIXME Remove this once the fix is sufficiently tested. + { + debugString.Length = 0; + + if (HasFired && !HasExploded && part != null) + { + part.rb.isKinematic = false; + AntiSpin(); + //simpleDrag + if (useSimpleDrag || useSimpleDragTemp) + { + SimpleDrag(); + } + + //flybyaudio + float mCamDistanceSqr = (FlightCamera.fetch.mainCamera.transform.position - vessel.CoM).sqrMagnitude; + float mCamRelVSqr = (float)(FlightGlobals.ActiveVessel.Velocity() - vessel.Velocity()).sqrMagnitude; + if (!hasPlayedFlyby + && FlightGlobals.ActiveVessel != vessel + && FlightGlobals.ActiveVessel != SourceVessel + && mCamDistanceSqr < 400 * 400 && mCamRelVSqr > 300 * 300 + && mCamRelVSqr < 800 * 800 + && VectorUtils.Angle(vessel.Velocity(), FlightGlobals.ActiveVessel.CoM - vessel.CoM) < 60) + { + if (sfAudioSource == null) SetupAudio(); + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/missileFlyby")); + hasPlayedFlyby = true; + } + if (vessel.isActiveVessel) + { + audioSource.dopplerLevel = 0; + } + else + { + audioSource.dopplerLevel = 1f; + } + + UpdateThrustForces(); + UpdateGuidance(); + CheckDetonationState(); // this needs to be after UpdateGuidance() + CheckDetonationDistance(); + CheckCountermeasureDistance(); + + //RaycastCollisions(); + + //Timed detonation + if (isTimed && TimeIndex > detonationTime) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher] missile timed out; self-destructing!"); + Detonate(); + } + //debugString.AppendLine($"crashTol: {part.crashTolerance}; collider: {part.collider.enabled}; usingSimpleDrag: {(useSimpleDrag && useSimpleDragTemp)}; drag: {part.angularDrag.ToString("0.00")}"); + } + } + catch (Exception e) + { + 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 (OldInfAmmo != BDArmorySettings.INFINITE_ORDINANCE) + { + if (reloadableRail.railAmmo < 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; + } + } + } + + protected override void InitializeCountermeasures() + { + var ECM = part.FindModuleImplementing(); + if (ECM != null) + { + ECM.EnableJammer(); + CMenabled = true; + } + + missileCM = part.FindModulesImplementing(); + missileCM.Sort((a, b) => b.priority.CompareTo(a.priority)); // Sort from highest to lowest priority + missileCMTime = Time.time; + int currPriority = 0; + foreach (CMDropper dropper in missileCM) + { + if (dropper.cmType == CMDropper.CountermeasureTypes.Chaff) + dropper.UpdateVCI(); + dropper.SetupAudio(); + if (currPriority <= dropper.Priority) + { + if (dropper.DropCM()) + { + currPriority = dropper.Priority; + } + } + CMenabled = true; + } + } + + protected override void DropCountermeasures() + { + int currPriority = 0; + foreach (CMDropper dropper in missileCM) + { + if (currPriority <= dropper.Priority) + { + if (dropper.DropCM()) + currPriority = dropper.Priority; + } + } + } + + private void CheckMiss() + { + if (weaponClass == WeaponClasses.Bomb) return; + float sqrDist = (float)((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (vessel.CoM + (vessel.Velocity() * Time.fixedDeltaTime))).sqrMagnitude; + bool targetBehindMissile = !TargetAcquired || (!(MissileState != MissileStates.PostThrust && hasRCS) && Vector3.Dot(TargetPosition - vessel.CoM, transform.forward) < 0f); // Target is not acquired or we are behind it and not an RCS missile + if (sqrDist < 160000 || MissileState == MissileStates.PostThrust || (targetBehindMissile && sqrDist > 1000000)) //missile has come within 400m, is post thrust, or > 1km behind target + { + checkMiss = true; + } + if (maxAltitude != 0f) + { + if (vessel.altitude >= maxAltitude) checkMiss = true; + } + + //kill guidance if missileBase has missed + if (!HasMissed && checkMiss) + { + Vector3 tgtVel = TargetVelocity == Vector3.zero && targetVessel != null ? targetVessel.Vessel.Velocity() : TargetVelocity; + bool noProgress = MissileState == MissileStates.PostThrust && ((Vector3.Dot(vessel.Velocity() - tgtVel, TargetPosition - vessel.CoM) < 0) || + (!vessel.InVacuum() && vessel.srfSpeed < GetKinematicSpeed() && weaponClass == WeaponClasses.Missile)); + bool pastGracePeriod = TimeIndex > ((MissileState == MissileStates.PostThrust ? 1 : optimumAirspeed / vessel.speed) * ((vessel.LandedOrSplashed ? 0f : dropTime) + guidanceDelay + Mathf.Clamp(maxTurnRateDPS / 15f, 1, 8))); //180f / maxTurnRateDPS); + if ((pastGracePeriod && targetBehindMissile) || noProgress) // Check that we're not moving away from the target after a grace period + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: Missile has missed({(noProgress ? "no progress" : !TargetAcquired ? "no target" : "past target")})!"); + + if (vessel.altitude >= maxAltitude && maxAltitude != 0f) + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: CheckMiss trigged by MaxAltitude"); + + HasMissed = true; + guidanceActive = false; + + MissileLauncher launcher = this as MissileLauncher; + if (launcher != null) + { + if (launcher.hasRCS) launcher.KillRCS(); + } + + 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() + { + if (guidanceActive && guidanceFailureRatePerFrame > 0f) + if (UnityEngine.Random.Range(0f, 1f) < guidanceFailureRatePerFrame) + { + guidanceActive = false; + BDATargetManager.FiredMissiles.Remove(this); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Missile Guidance Failed!"); + } + + if (guidanceActive) + { + switch (TargetingMode) + { + case TargetingModes.Heat: + UpdateHeatTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + if (heatTarget.vessel) + debugGuidanceTarget = $"{heatTarget.vessel.GetName()} {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.GetName()} {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; + case TargetingModes.Inertial: + UpdateInertialTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugGuidanceTarget = $"TgtPos: {TargetPosition}; Drift: {(TargetPosition - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).ToString()}"; + } + break; + default: + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + TargetPosition = transform.position + (startDirection * 500); + debugGuidanceTarget = TargetPosition.ToString(); + } + break; + } + + UpdateTerminalGuidance(); + } + + if (MissileState != MissileStates.Idle && MissileState != MissileStates.Drop) //guidance + { + //guidance and attitude stabilisation scales to atmospheric density. //use part.atmDensity + float atmosMultiplier = Mathf.Clamp01(2.5f * (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(vessel.CoM), FlightGlobals.getExternalTemperature(vessel.CoM), FlightGlobals.currentMainBody)); + + if (vessel.srfSpeed < optimumAirspeed) + { + float optimumSpeedFactor = (float)vessel.srfSpeed / (2 * optimumAirspeed); + controlAuthority = Mathf.Clamp01(atmosMultiplier * (-Mathf.Abs(2 * optimumSpeedFactor - 1) + 1)); + } + else + { + controlAuthority = Mathf.Clamp01(atmosMultiplier); + } + + if (vacuumSteerable) + { + controlAuthority = 1; + } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) debugString.AppendLine($"controlAuthority: {controlAuthority}"); + + if (guidanceActive) + { + WarnTarget(); + if (TimeIndex - dropTime > guidanceDelay) + { + //if (targetVessel && targetVessel.loaded) + //{ + // Vector3 targetCoMPos = targetVessel.CoM; + // TargetPosition = targetCoMPos + targetVessel.Velocity() * Time.fixedDeltaTime; + //} + + // Increase turn rate gradually after launch, unless vacuum steerable in space + float turnRateDPS = maxTurnRateDPS; + if (!((vacuumSteerable && vessel.InVacuum()) || boostTime == 0f)) + turnRateDPS = Mathf.Clamp(((TimeIndex - dropTime) / boostTime) * maxTurnRateDPS * 25f, 0, maxTurnRateDPS); + if (!hasRCS) + { + turnRateDPS *= controlAuthority; + } + + //decrease turn rate after thrust cuts out + if (MissileState == MissileStates.PostThrust) + { + var clampedTurnRate = Mathf.Clamp(maxTurnRateDPS - ((cruiseRangeTrigger < 0 ? (TimeIndex - dropTime - boostTime - cruiseDelay - cruiseTime) : (Time.time - cruiseStartTime - cruiseTime)) * 0.45f), + 1, maxTurnRateDPS); + turnRateDPS = clampedTurnRate; + + if (!vacuumSteerable) + { + turnRateDPS *= atmosMultiplier; + } + + if (hasRCS) + { + turnRateDPS = 0; + } + } + + if (hasRCS) + { + if (turnRateDPS > 0) + { + DoRCS(); + } + else + { + KillRCS(); + } + } + debugTurnRate = turnRateDPS; + + finalMaxTorque = Mathf.Clamp((TimeIndex - dropTime) * torqueRampUp, 0, currMaxTorque); //ramp up torque + + if (terminalHoming && !terminalHomingActive) + { + if (Vector3.SqrMagnitude(TargetPosition - vessel.CoM) < terminalHomingRange * terminalHomingRange) + { + GuidanceMode = homingModeTerminal; + terminalHomingActive = true; + Throttle = 1f; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: Terminal with {GuidanceMode}"); + } + } + switch (GuidanceMode) + { + case GuidanceModes.AAMLead: + case GuidanceModes.APN: + case GuidanceModes.PN: + case GuidanceModes.AAMLoft: + case GuidanceModes.AAMPure: + case GuidanceModes.Kappa: + //GuidanceModes.AAMHybrid: + AAMGuidance(); + break; + case GuidanceModes.AGM: + AGMGuidance(); + break; + case GuidanceModes.AGMBallistic: + AGMBallisticGuidance(); + break; + case GuidanceModes.BeamRiding: + BeamRideGuidance(); + break; + case GuidanceModes.CLOS: + case GuidanceModes.CLOSThreePoint: + case GuidanceModes.CLOSLead: + CLOSGuidance(); + break; + case GuidanceModes.Orbital: //nee GuidanceModes.RCS + OrbitalGuidance(turnRateDPS); + break; + case GuidanceModes.Cruise: + CruiseGuidance(); + break; + case GuidanceModes.Weave: + AAMGuidance(); + break; + case GuidanceModes.SLW: + SLWGuidance(); + break; + case GuidanceModes.None: + DoAero(TargetPosition); + CheckMiss(); + break; + } + } + else + aeroTorque = MissileGuidance.DoAeroForces(this, TargetPosition, currLiftArea, currDragArea, .25f, aeroTorque, currMaxTorque, currMaxTorqueAero, 0.1f, MissileGuidance.DefaultLiftCurve, MissileGuidance.DefaultDragCurve); + } + else + { + CheckMiss(); + if (aero) + { + aeroTorque = MissileGuidance.DoAeroForces(this, TargetPosition, currLiftArea, currDragArea, .25f, aeroTorque, currMaxTorque, currMaxTorqueAero, 0.1f, MissileGuidance.DefaultLiftCurve, MissileGuidance.DefaultDragCurve); + } + } + + if (aero && aeroSteerDamping > 0f) + { + part.rb.AddRelativeTorque(-aeroSteerDamping * part.transform.InverseTransformDirection(part.rb.angularVelocity)); + } + + if (hasRCS && !guidanceActive) + { + KillRCS(); + } + } + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + if (guidanceActive) debugString.AppendLine($"Missile target={debugGuidanceTarget}. seekerTimeout={lockFailTimer}/{seekerTimeout}."); + else debugString.AppendLine("Guidance inactive"); + + debugString.AppendLine("Source vessel=" + (SourceVessel != null ? SourceVessel.GetName() : "null")); + + debugString.AppendLine("Target vessel=" + ((targetVessel != null && targetVessel.Vessel != null) ? targetVessel.Vessel.GetName() : "null")); + + if (!(BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES)) return; + var distance = (TargetPosition - vessel.CoM).magnitude; + debugString.AppendLine($"Target distance: {(distance > 1000 ? $" {distance / 1000:F1} km" : $" {distance:F0} m")}, closing speed: {Vector3.Dot(vessel.Velocity() - TargetVelocity, GetForwardTransform()):F1} m/s"); + } + } + + // feature_engagementenvelope: terminal guidance mode for cruise missiles + private void UpdateTerminalGuidance() + { + Vector3 tempTargetPos = TargetPosition; + + bool scanOverride = false; + + if (TargetingMode == TargetingModes.Inertial && TimeOfLastINS > 0) + { + float deltaT = TimeIndex - TimeOfLastINS; + tempTargetPos = VectorUtils.GetWorldSurfacePostion(TargetINSCoords, vessel.mainBody); + if (deltaT > GpsUpdateMax) + { + deltaT /= INStimetogo; + + tempTargetPos = new Vector3((1f - deltaT) * tempTargetPos.x + deltaT * TargetPosition.x, (1f - deltaT) * tempTargetPos.y + deltaT * TargetPosition.y, (1f - deltaT) * tempTargetPos.z + deltaT * TargetPosition.z); + } + } + + if (!TargetAcquired && targetVessel == null) + scanOverride = true; // Allow missiles to go to their terminal guidance when dumbfired + + // check if guidance mode should be changed for terminal phase + float distanceSqr = (tempTargetPos - vessel.CoM).sqrMagnitude; + + if (terminalGuidanceShouldActivate && !terminalGuidanceActive && (TargetingModeTerminal != TargetingModes.None) && (scanOverride || (distanceSqr < terminalGuidanceDistance * terminalGuidanceDistance))) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: missile {GetPartName()} updating targeting mode: {terminalGuidanceType}"); + + TargetAcquired = false; + + switch (TargetingModeTerminal) + { + case TargetingModes.Heat: + // gets ground heat targets and after locking one, disallows the lock to break to another target + + if (activeRadarRange < 0 && torpedo) + heatTarget = BDATargetManager.GetAcousticTarget(SourceVessel, vessel, new Ray(vessel.CoM, tempTargetPos - vessel.CoM), TargetSignatureData.noTarget, lockedSensorFOV * 0.5f, heatThreshold, targetCoM, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity, + FiredByWM, targetVessel, IFF: hasIFF); + else + heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, new Ray(vessel.CoM, tempTargetPos - vessel.CoM), TargetSignatureData.noTarget, lockedSensorFOV * 0.5f, heatThreshold, frontAspectHeatModifier, uncagedLock, targetCoM, lockedSensorFOVBias, lockedSensorVelocityBias, lockedSensorVelocityMagnitudeBias, lockedSensorMinAngularVelocity, FiredByWM, targetVessel, IFF: hasIFF); + if (heatTarget.exists && CheckTargetEngagementEnvelope(heatTarget.targetInfo)) + { + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: {(activeRadarRange < 0 && torpedo ? "Acoustic" : "Heat")} target acquired! Position: {heatTarget.position}, {(activeRadarRange < 0 && torpedo ? "Noise" : "Heat")}score: {heatTarget.signalStrength}"); + } + TargetAcquired = true; + TargetPosition = heatTarget.position; + TargetVelocity = heatTarget.velocity; + TargetAcceleration = heatTarget.acceleration; + //targetVessel = heatTarget.targetInfo; will mess with AI MissilesAway and potentially result in ripplefired IR missiles against an enemy actively flaring and decoying heaters. + lockFailTimer = -1; // ensures proper entry into UpdateHeatTarget() + + // Disable terminal guidance and switch to regular heat guidance for next update + terminalGuidanceShouldActivate = false; + TargetingMode = TargetingModes.Heat; + terminalGuidanceActive = true; + + // Adjust heat score based on distance missile will travel in the next update + if (!torpedo && heatTarget.signalStrength > 0) + { + float currentFactor = (1400 * 1400) / Mathf.Clamp((heatTarget.position - vessel.CoM).sqrMagnitude, 90000, 36000000); + Vector3 currVel = vessel.Velocity(); + heatTarget.position = heatTarget.position + heatTarget.velocity * Time.fixedDeltaTime; + heatTarget.velocity = heatTarget.velocity + heatTarget.acceleration * Time.fixedDeltaTime; + float futureFactor = (1400 * 1400) / Mathf.Clamp((heatTarget.position - (vessel.CoM + (currVel * Time.fixedDeltaTime))).sqrMagnitude, 90000, 36000000); + heatTarget.signalStrength *= futureFactor / currentFactor; + } + } + else + { + if (!dumbTerminalGuidance) + { + TargetAcquired = true; + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + //continue towards primary guidance targetPosition until heat lock acquired + } + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Missile heatseeker could not acquire a target lock, reverting to default guidance."); + } + } + break; + + case TargetingModes.Radar: + + // pretend we have an active radar seeker for ground targets: + //TargetSignatureData[] scannedTargets = new TargetSignatureData[5]; + if (scannedTargets == null) scannedTargets = new TargetSignatureData[BDATargetManager.LoadedVessels.Count]; + TargetSignatureData.ResetTSDArray(ref scannedTargets); + Ray ray = new Ray(vessel.CoM, GetForwardTransform()); + + // Missile's radar has gone active + ActiveRadar = true; + updateRadarCS = true; + + //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 = terminalGuidanceDistance * terminalGuidanceDistance * 2.25f; // (terminalGuidanceDistance * 1.5f)^2 + + //float smallestAngle = maxOffBoresight; + //TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + + float currDist = float.PositiveInfinity; + float prevDist = float.PositiveInfinity; + int lockIndex = -1; + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: Active radar found: {scannedTargets.Length} targets."); + + for (int i = 0; i < scannedTargets.Length; i++) + { + if (scannedTargets[i].exists && (!hasIFF || !Team.IsFriendly(scannedTargets[i].Team))) + { + currDist = (scannedTargets[i].predictedPosition - tempTargetPos).sqrMagnitude; + + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: Target: {scannedTargets[i].vessel.name} has currDist: {currDist}."); + + //re-check engagement envelope, only lock appropriate targets + if (currDist < sqrThresh && currDist < prevDist && CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo)) + { + prevDist = currDist; + + lockIndex = i; + } + } + //if (!scannedTargets[i].exists) + // if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: Target: {i} doesn't exist!."); + //if (scannedTargets[i].exists && Team.IsFriendly(scannedTargets[i].Team)) + // if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: Target: {scannedTargets[i].vessel.name} is friendly, continuing."); + + } + + if (lockIndex >= 0) + { + radarTarget = scannedTargets[lockIndex]; + TargetAcquired = true; + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); + TargetVelocity = radarTarget.velocity; + TargetAcceleration = radarTarget.acceleration; + targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); + + if (weaponClass == WeaponClasses.SLW) + RadarWarningReceiver.PingRWR(new Ray(vessel.CoM, radarTarget.predictedPosition - vessel.CoM), 45, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); + else + RadarWarningReceiver.PingRWR(new Ray(vessel.CoM, radarTarget.predictedPosition - vessel.CoM), 45, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); + + 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}"); + terminalGuidanceActive = true; + } + else + { + TargetAcquired = true; + TargetPosition = VectorUtils.GetWorldSurfacePostion(UpdateGPSTarget(), vessel.mainBody); //putting back the GPS target if no radar target found + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); //tgtPos/tgtGPS should really be not set here, so the last valid postion/coords are used, in case of non-GPS primary guidance + radarTarget = TargetSignatureData.noTarget; + if (activeRadarRange > 0f && radarLOAL) + radarLOALSearching = true; + if (dumbTerminalGuidance) + terminalGuidanceActive = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Missile radar could not acquire a target lock - Defaulting to GPS Target"); + } + break; + + case TargetingModes.Laser: + // not very useful, currently unsupported! + break; + + case TargetingModes.Gps: + // from gps to gps -> no actions need to be done! + break; + case TargetingModes.Inertial: + // Not sure *why* you'd use this for TerminalGuideance, but ok... + TargetAcquired = true; + if (targetVessel != null) TargetPosition = VectorUtils.GetWorldSurfacePostion(MissileGuidance.GetAirToAirFireSolution(this, targetVessel.Vessel.CoM, TargetVelocity), vessel.mainBody); + TargetVelocity = Vector3.zero; + TargetAcceleration = Vector3.zero; + terminalGuidanceActive = true; + break; + + case TargetingModes.AntiRad: + 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.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Antiradiation mode set! Waiting for radar signals..."); + terminalGuidanceActive = true; + break; + } + if (dumbTerminalGuidance || terminalGuidanceActive) + { + // De-activate active radar if it's active + if (TargetingMode == TargetingModes.Radar && TargetingModeTerminal != TargetingModes.Radar) + { + ActiveRadar = false; + radarLOALSearching = false; + updateRadarCS = true; + } + + TargetingMode = TargetingModeTerminal; + if (terminalSeekerTimeout >= 0) + seekerTimeout = terminalSeekerTimeout; + terminalGuidanceActive = true; + terminalGuidanceShouldActivate = false; + } + } + } + + void UpdateThrustForces() + { + if (MissileState == MissileStates.PostThrust) return; + if (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(vessel.CoM) > 0) return; //#710, no torp thrust out of water + if (currentThrust * Throttle > 0) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugString.AppendLine($"Missile thrust= {currentThrust * Throttle:F3} kN"); + debugString.AppendLine($"Missile mass= {part.mass * 1000f:F1} kg"); + } + part.rb.AddRelativeForce(currentThrust * Throttle * Vector3.forward); + } + } + + 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; + } + + if (deployStates != null) StartCoroutine(DeployAnimRoutine()); + yield return new WaitForSecondsFixed(dropTime); + if (animStates != null) StartCoroutine(FlightAnimRoutine()); + yield return StartCoroutine(BoostRoutine()); + + yield return new WaitForSecondsFixed(cruiseDelay); + if (cruiseRangeTrigger > 0) + yield return new WaitUntilFixed(checkCruiseRangeTrigger); + + if (cruiseStates != null) StartCoroutine(CruiseAnimRoutine()); + yield return StartCoroutine(CruiseRoutine()); + } + + bool checkCruiseRangeTrigger() + { + float sqrRange = (TargetPosition - part.rb.position).sqrMagnitude; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: Check cruise range trigger range: {BDAMath.Sqrt(sqrRange)}"); + + if (sqrRange < cruiseRangeTrigger * cruiseRangeTrigger || (!vessel.InVacuum() && vessel.Velocity().sqrMagnitude < optimumAirspeed * optimumAirspeed * 0.5625f)) + { + if (cruiseTerminationFrames < 5) + { + cruiseTerminationFrames++; + return false; + } + + cruiseTerminationFrames = 0; + return true; + } + + cruiseTerminationFrames = 0; + return false; + } + + IEnumerator DeployAnimRoutine() + { + yield return new WaitForSecondsFixed(deployTime); + if (deployStates == null) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning("[BDArmory.MissileLauncher]: deployStates was null, aborting AnimRoutine."); + yield break; + } + + if (!string.IsNullOrEmpty(deployAnimationName)) + { + deployed = true; + + applyDeployedLiftDrag(); + MissileGuidance.setupTorqueAoALimit(this, currLiftArea, currDragArea); + + using (var anim = deployStates.AsEnumerable().GetEnumerator()) + while (anim.MoveNext()) + { + if (anim.Current == null) continue; + anim.Current.enabled = true; + anim.Current.speed = 1; + } + } + } + + private void applyDeployedLiftDrag(bool cruise = false) + { + int index = cruise ? 3 : 2; + // Apply the deltas + if (parsedLiftArea[index] > 0f) + { + // If lift area delta + currLiftArea += parsedLiftArea[index]; + // Then check drag area delta + // if drag area delta exists, then + // apply it, otherwise just apply + // lift area delta + if (parsedDragArea[index] > 0f) + currDragArea += parsedDragArea[index]; + else + currDragArea += parsedLiftArea[index]; + } + else if (parsedDragArea[index] > 0f) + // If drag area delta, apply it + currDragArea += parsedDragArea[index]; + + // Apply any maxTorqueAero delta + if (parsedMaxTorqueAero[index] > 0f) + currMaxTorqueAero += parsedMaxTorqueAero[index]; + } + + IEnumerator CruiseAnimRoutine() + { + yield return new WaitForSecondsFixed(cruiseDeployTime); + if (cruiseStates == null) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning("[BDArmory.MissileLauncher]: deployStates was null, aborting AnimRoutine."); + yield break; + } + + if (!string.IsNullOrEmpty(cruiseAnimationName)) + { + deployed = true; + + applyDeployedLiftDrag(true); + MissileGuidance.setupTorqueAoALimit(this, currLiftArea, currDragArea); + + using (var anim = cruiseStates.AsEnumerable().GetEnumerator()) + while (anim.MoveNext()) + { + if (anim.Current == null) continue; + anim.Current.enabled = true; + anim.Current.speed = 1; + } + } + } + IEnumerator FlightAnimRoutine() + { + if (animStates == null) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning("[BDArmory.MissileLauncher]: animStates was null, aborting AnimRoutine."); + yield break; + } + + if (!string.IsNullOrEmpty(flightAnimationName)) + { + using (var anim = animStates.AsEnumerable().GetEnumerator()) + while (anim.MoveNext()) + { + if (anim.Current == null) continue; + anim.Current.enabled = true; + if (!OneShotAnim) + { + anim.Current.wrapMode = WrapMode.Loop; + } + anim.Current.speed = 1; + } + } + } + IEnumerator updateCrashTolerance() + { + yield return new WaitForSecondsFixed(0.5f); //wait half sec after boost motor fires, then set crashTolerance to 1. Torps have already waited until splashdown before this is called. + part.crashTolerance = 1; + if (useSimpleDragTemp) + { + yield return new WaitForSecondsFixed((clearanceLength * 1.2f) / 2); + part.dragModel = Part.DragModel.DEFAULT; + useSimpleDragTemp = false; + } + var childColliders = part.GetComponentsInChildren(includeInactive: false); + foreach (var col in childColliders) + col.enabled = true; + + } + IEnumerator BoostRoutine() + { + if (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(vessel.CoM) > 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... + } + if (useFuel) + { + burnRate = boostTime > 0 ? boosterFuelMass / boostTime * Time.fixedDeltaTime : 0; + burnedFuelMass = 0f; + } + + StartBoost(); + StartCoroutine(updateCrashTolerance()); + + var wait = new WaitForFixedUpdate(); + float boostStartTime = Time.time; + while (Time.time - boostStartTime < boostTime || (useFuel && burnedFuelMass < boosterFuelMass)) + { + //light, sound & particle fx + //sound + if (!BDArmorySetup.GameIsPaused) + { + if (!audioSource.isPlaying) + { + audioSource.Play(); + } + } + else if (audioSource.isPlaying) + { + audioSource.Stop(); + } + + //thrust + if (useFuel && burnRate > 0) + { + //if (boosterFuelMass - burnedFuelMass < burnRate * Throttle) + //{ + // Throttle = (boosterFuelMass - burnedFuelMass) / burnRate; + // burnedFuelMass = boosterFuelMass; + //} + //else + //{ + burnedFuelMass = Mathf.Min(burnedFuelMass + Throttle * burnRate, boosterFuelMass); // Impulse conservation code was showing issues + //} + } + + audioSource.volume = Throttle; + + //particleFx + using (var emitter = boostEmitters.GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + //if (!hasRCS) + //{ + // emitter.Current.sizeGrow = Mathf.Lerp(emitter.Current.sizeGrow, 0, 20 * Time.deltaTime); + //} + if (Throttle == 0 || thrust == 0) + emitter.Current.emit = false; + else + emitter.Current.emit = true; + } + + using (var gpe = boostGaplessEmitters.GetEnumerator()) + while (gpe.MoveNext()) + { + if (gpe.Current == null) continue; + if ((!vessel.InVacuum() && Throttle > 0) && weaponClass != WeaponClasses.SLW || (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(vessel.CoM) < 0)) //#710 + { + if (Throttle == 0 || thrust == 0) + gpe.Current.emit = false; + else + { + gpe.Current.emit = true; + gpe.Current.pEmitter.worldVelocity = 2 * ParticleTurbulence.flareTurbulence; + } + } + else + { + gpe.Current.emit = false; + } + } + + if (spoolEngine) + { + currentThrust = Mathf.MoveTowards(currentThrust, thrust, thrust / 10); + } + + yield return wait; + } + EndBoost(); + } + + void StartBoost() + { + MissileState = MissileStates.Boost; + + if (audioSource == null || sfAudioSource == null) SetupAudio(); + if (boostAudio) + { + audioSource.clip = boostAudio; + } + else if (thrustAudio) + { + audioSource.clip = thrustAudio; + } + audioSource.volume = Throttle; + + if (BDArmorySettings.LightFX) + { + using (var light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 1.5f; + } + } + + if (!spoolEngine) + { + currentThrust = thrust; + } + + if (string.IsNullOrEmpty(boostTransformName)) + { + boostEmitters = pEmitters; + if (hasRCS && rcsTransforms != null) boostEmitters.RemoveAll(pe => rcsTransforms.Contains(pe)); + if (hasRCS && forwardRCS.Any()) + foreach (var pe in forwardRCS) + if (!boostEmitters.Contains(pe)) boostEmitters.Add(pe); + boostGaplessEmitters = gaplessEmitters; + } + + using (var emitter = boostEmitters.GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + emitter.Current.emit = true; + } + + if (!(thrust > 0)) return; + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/launch")); + RadarWarningReceiver.WarnMissileLaunch(vessel.CoM, transform.forward, TargetingMode == TargetingModes.Radar); + } + + void EndBoost() + { + using (var emitter = boostEmitters.GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + emitter.Current.emit = false; + } + + using (var gEmitter = boostGaplessEmitters.GetEnumerator()) + while (gEmitter.MoveNext()) + { + if (gEmitter.Current == null) continue; + gEmitter.Current.emit = false; + } + + if (useFuel) burnedFuelMass = boosterFuelMass; + + if (cruiseRangeTrigger > 0 || cruiseDelay > 0) + { + // Use the coast value until *after* the cruise stage starts + if (parsedMaxTorque[3] >= 0) + currMaxTorque = parsedMaxTorque[3]; + } + else + { + // Directly set the cruise value + if (parsedMaxTorque[1] >= 0) + currMaxTorque = parsedMaxTorque[1]; + } + + if (parsedSteerMult[1] >= 0) + currSteerMult = parsedSteerMult[1]; + + if (decoupleBoosters) + { + // We only apply any lift/drag area changes if parsedLiftArea[1] is valid + if (parsedLiftArea[1] >= 0f) + { + currLiftArea = parsedLiftArea[1]; + currDragArea = parsedDragArea[1]; + + if (currDragArea < 0) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: decoupleBoosters missile {shortName}: setting default dragArea to liftArea {currLiftArea}:"); + currDragArea = currLiftArea; + } + + if (currMaxTorqueAero >= 0) + currMaxTorqueAero = parsedMaxTorqueAero[1]; + + if (deployed && deployedLiftInCruise) + applyDeployedLiftDrag(); + + MissileGuidance.setupTorqueAoALimit(this, currLiftArea, currDragArea); + } + + boostersDecoupled = true; + using (var booster = boosters.GetEnumerator()) + while (booster.MoveNext()) + { + if (booster.Current == null) continue; + booster.Current.AddComponent().DecoupleBooster(part.rb.velocity, boosterDecoupleSpeed); + } + } + + if (cruiseDelay > 0 || cruiseRangeTrigger > 0) + { + currentThrust = 0; + } + } + + float cruiseStartTime = -1f; + + IEnumerator CruiseRoutine() + { + float massToBurn = 0; + if (useFuel) + { + burnRate = cruiseTime > 0 ? cruiseFuelMass / cruiseTime * Time.fixedDeltaTime : 0; + massToBurn = boosterFuelMass + cruiseFuelMass; + } + StartCruise(); + var wait = new WaitForFixedUpdate(); + cruiseStartTime = Time.time; + while (Time.time - cruiseStartTime < cruiseTime || (useFuel && burnedFuelMass < massToBurn)) + { + if (!BDArmorySetup.GameIsPaused) + { + if (!audioSource.isPlaying || audioSource.clip != thrustAudio) + { + audioSource.clip = thrustAudio; + audioSource.Play(); + } + } + else if (audioSource.isPlaying) + { + audioSource.Stop(); + } + + //Thrust + if (useFuel && burnRate > 0) + { + //if (massToBurn - burnedFuelMass < burnRate * Throttle) + //{ + // Throttle = (massToBurn - burnedFuelMass) / burnRate; + // burnedFuelMass = massToBurn; + //} + //else + //{ + burnedFuelMass = Mathf.Min(burnedFuelMass + Throttle * burnRate, massToBurn); // Other code was causing issues + //} + } + + audioSource.volume = Throttle; + + //particleFx + using (var emitter = pEmitters.GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + /* + if (!hasRCS) + { + emitter.Current.sizeGrow = Mathf.Lerp(emitter.Current.sizeGrow, 0, 20 * Time.deltaTime); //uh, why? this turns reasonable missileFX into giant doom plumes + } + emitter.Current.maxSize = Mathf.Clamp01(Throttle / Mathf.Clamp((float)vessel.atmDensity, 0.2f, 1f)); + */ + if (weaponClass != WeaponClasses.SLW || (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(vessel.CoM) < 0)) //#710 + { + if (Throttle == 0 || cruiseThrust == 0) + emitter.Current.emit = false; + else + emitter.Current.emit = true; + } + else + { + emitter.Current.emit = false; // #710, shut down thrust FX for torps out of water + } + } + + using (var gpe = gaplessEmitters.GetEnumerator()) + while (gpe.MoveNext()) + { + if (gpe.Current == null) continue; + if (weaponClass != WeaponClasses.SLW || (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(vessel.CoM) < 0)) //#710 + { + if (Throttle == 0 || cruiseThrust == 0) + gpe.Current.emit = false; + else + { + //gpe.Current.pEmitter.maxSize = Mathf.Clamp01(Throttle / Mathf.Clamp((float)vessel.atmDensity, 0.2f, 1f)); + gpe.Current.emit = true; + gpe.Current.pEmitter.worldVelocity = 2 * ParticleTurbulence.flareTurbulence; + } + } + else + { + gpe.Current.emit = false; + } + } + + if (spoolEngine) + { + currentThrust = Mathf.MoveTowards(currentThrust, cruiseThrust, cruiseThrust / 10); + } + + yield return wait; + } + + EndCruise(); + } + + void StartCruise() + { + MissileState = MissileStates.Cruise; + + if (audioSource == null) SetupAudio(); + if (thrustAudio) + { + audioSource.clip = thrustAudio; + } + + currentThrust = spoolEngine ? 0 : cruiseThrust; + + // Set the cruise value + if (parsedMaxTorque[1] >= 0) + currMaxTorque = parsedMaxTorque[1]; + + using (var pEmitter = pEmitters.GetEnumerator()) + while (pEmitter.MoveNext()) + { + if (pEmitter.Current == null) continue; + EffectBehaviour.AddParticleEmitter(pEmitter.Current); + pEmitter.Current.emit = true; + } + + using (var gEmitter = gaplessEmitters.GetEnumerator()) + while (gEmitter.MoveNext()) + { + if (gEmitter.Current == null) continue; + EffectBehaviour.AddParticleEmitter(gEmitter.Current.pEmitter); + gEmitter.Current.emit = true; + } + + if (!hasRCS) return; + foreach (var pe in forwardRCS) + pe.emit = false; + audioSource.Stop(); + } + + void EndCruise() + { + MissileState = MissileStates.PostThrust; + + currentThrust = 0f; + + if (useFuel) burnedFuelMass = cruiseFuelMass + boosterFuelMass; + + // If we specify a post-thrust maxTorque (I.E. TVC) + if (parsedMaxTorque[2] >= 0) + currMaxTorque = parsedMaxTorque[2]; + + using (IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 0; + } + + StartCoroutine(FadeOutAudio()); + StartCoroutine(FadeOutEmitters()); + } + + IEnumerator FadeOutAudio() + { + if (thrustAudio && audioSource.isPlaying) + { + while (audioSource.volume > 0 || audioSource.pitch > 0) + { + audioSource.volume = Mathf.Lerp(audioSource.volume, 0, 5 * Time.deltaTime); + audioSource.pitch = Mathf.Lerp(audioSource.pitch, 0, 5 * Time.deltaTime); + yield return null; + } + } + } + + IEnumerator FadeOutEmitters() + { + float fadeoutStartTime = Time.time; + while (Time.time - fadeoutStartTime < 5) + { + /* + using (var pe = pEmitters.GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + pe.Current.maxEmission = Mathf.FloorToInt(pe.Current.maxEmission * 0.8f); + pe.Current.minEmission = Mathf.FloorToInt(pe.Current.minEmission * 0.8f); + } + */ + using (var gpe = gaplessEmitters.GetEnumerator()) + while (gpe.MoveNext()) + { + if (gpe.Current == null) continue; + //gpe.Current.pEmitter.maxSize = Mathf.MoveTowards(gpe.Current.pEmitter.maxSize, 0, 0.005f); + //gpe.Current.pEmitter.minSize = Mathf.MoveTowards(gpe.Current.pEmitter.minSize, 0, 0.008f); + gpe.Current.pEmitter.worldVelocity = ParticleTurbulence.Turbulence; + } + yield return new WaitForFixedUpdate(); + } + + yield return new WaitForFixedUpdate(); + using (var pe2 = pEmitters.GetEnumerator()) + while (pe2.MoveNext()) + { + if (pe2.Current == null) continue; + pe2.Current.emit = false; + } + + using (var gpe2 = gaplessEmitters.GetEnumerator()) + while (gpe2.MoveNext()) + { + if (gpe2.Current == null) continue; + gpe2.Current.emit = false; + } + } + + [KSPField] + public float beamCorrectionFactor; + + [KSPField] + public float beamCorrectionDamping; + + [KSPField] + public float beamLeadFactor = 0.5f; + + Ray previousBeam; + + void BeamRideGuidance() + { + if (!targetingPod) + { + guidanceActive = false; + return; + } + + if (RadarUtils.TerrainCheck(targetingPod.cameraParentTransform.position, vessel.CoM)) + { + guidanceActive = false; + return; + } + Ray laserBeam = new Ray(targetingPod.cameraParentTransform.position + (targetingPod.vessel.Velocity() * Time.fixedDeltaTime), targetingPod.targetPointPosition - targetingPod.cameraParentTransform.position); + Vector3 target = MissileGuidance.GetBeamRideTarget(laserBeam, vessel.CoM, vessel.Velocity(), beamCorrectionFactor, beamCorrectionDamping, (TimeIndex > 0.25f ? previousBeam : laserBeam)); + previousBeam = laserBeam; + DrawDebugLine(vessel.CoM, target); + DoAero(target); + } + + void CLOSGuidance() + { + Vector3 target; + float currgLimit = -1f; + + if (TargetAcquired || TargetingMode == TargetingModes.Laser) + { + Vector3 sensorPos; + Vector3 sensorVel; + Vector3 targetVel; + + if (TargetingMode == TargetingModes.Laser) + { + if (!targetingPod) + { + guidanceActive = false; + return; + } + sensorVel = targetingPod.vessel.Velocity(); + sensorPos = targetingPod.cameraParentTransform.position + (sensorVel * Time.fixedDeltaTime); + if (targetingPod.lockedVessel) + targetVel = targetingPod.lockedVessel.Velocity(); + else + { + MissileFire weaponManagerTemp; + if (targetingPod.radarLock && (weaponManagerTemp = targetingPod.WeaponManager) != null && weaponManagerTemp.vesselRadarData && weaponManagerTemp.vesselRadarData.locked) + targetVel = weaponManagerTemp.vesselRadarData.lockedTargetData.targetData.velocity; + else + targetVel = Vector3.zero; + } + } + else if (TargetingMode == TargetingModes.Radar) + { + if (!radarTarget.exists || !radarTarget.lockedByRadar) + { + guidanceActive = false; + return; + } + sensorVel = radarTarget.lockedByRadar.vessel.Velocity(); + sensorPos = radarTarget.lockedByRadar.transform.position + (sensorVel * Time.fixedDeltaTime); + targetVel = radarTarget.velocity; + } + else + { + guidanceActive = false; + return; + } + + + if (RadarUtils.TerrainCheck(sensorPos, vessel.CoM, vessel.mainBody)) + { + guidanceActive = false; + return; + } + + /*//proxy detonation + var distThreshold = 0.5f * GetBlastRadius(); + if (proxyDetonate && !DetonateAtMinimumDistance && (!heatTarget.exists || heatTarget.vessel) && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (vessel.CoM)).sqrMagnitude < distThreshold * distThreshold) + { + //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 + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher] ProxiDetonate triggered"); + Detonate(); + }*/ + + float tempPronavGain = pronavGain > 0 ? pronavGain : pronavGainCurve.Evaluate(Vector3.Distance(TargetPosition, vessel.CoM)); + + switch (GuidanceMode) + { + case GuidanceModes.CLOS: + target = MissileGuidance.GetCLOSTarget(sensorPos, vessel.CoM, vessel.Velocity(), TargetPosition, targetVel, beamCorrectionFactor, tempPronavGain, out currgLimit); + break; + case GuidanceModes.CLOSThreePoint: + target = MissileGuidance.GetThreePointTarget(sensorPos, sensorVel, vessel.CoM, vessel.Velocity(), TargetPosition, targetVel, beamCorrectionFactor, tempPronavGain, out currgLimit); + break; + case GuidanceModes.CLOSLead: + target = MissileGuidance.GetCLOSLeadTarget(sensorPos, sensorVel, vessel.CoM, vessel.Velocity(), TargetPosition, targetVel, beamCorrectionFactor, tempPronavGain, beamLeadFactor, out currgLimit, this); + break; + + default: + target = MissileGuidance.GetCLOSTarget(sensorPos, vessel.CoM, vessel.Velocity(), TargetPosition, targetVel, beamCorrectionFactor, tempPronavGain, out currgLimit); + break; + } + + if (!(GuidanceMode == GuidanceModes.CLOSLead)) DrawDebugLine(sensorPos, TargetPosition); + } + else + { + target = vessel.CoM + (2000f * vessel.Velocity().normalized); + } + + if (TimeIndex > dropTime + 0.25f) + { + DoAero(target, currgLimit); + CheckMiss(); + } + } + + void CruiseGuidance() + { + if (this._guidance == null) + { + this._guidance = new CruiseGuidance(this, invManeuvergLimit); + } + + Vector3 cruiseTarget = TargetPosition; + + if (FlightGlobals.currentMainBody.ocean && targetVessel != null) + { + if (targetVessel.Vessel.radarAltitude < 0) + cruiseTarget = cruiseTarget - targetVessel.Vessel.up * targetVessel.Vessel.radarAltitude; + } + + cruiseTarget = this._guidance.GetDirection(this, cruiseTarget, TargetVelocity); + + Vector3 upDirection = vessel.upAxis; + + //axial rotation + if (rotationTransform) + { + Quaternion originalRotation = transform.rotation; + Quaternion originalRTrotation = rotationTransform.rotation; + transform.rotation = Quaternion.LookRotation(transform.forward, upDirection); + rotationTransform.rotation = originalRTrotation; + Vector3 lookUpDirection = (cruiseTarget - vessel.CoM).ProjectOnPlanePreNormalized(transform.forward) * 100; + lookUpDirection = transform.InverseTransformPoint(lookUpDirection + vessel.CoM); + + lookUpDirection = new Vector3(lookUpDirection.x, 0, 0); + lookUpDirection += 10 * Vector3.up; + + rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, Quaternion.LookRotation(Vector3.forward, lookUpDirection), 0.04f); + Quaternion finalRotation = rotationTransform.rotation; + transform.rotation = originalRotation; + rotationTransform.rotation = finalRotation; + + vesselReferenceTransform.rotation = Quaternion.LookRotation(-rotationTransform.up, rotationTransform.forward); + } + DoAero(cruiseTarget); + CheckMiss(); + } + + void AAMGuidance() + { + Vector3 aamTarget = TargetPosition; + float currgLimit = -1f; + float currAoALimit = -1f; + + if (TargetAcquired) + { + if (warheadType == WarheadTypes.ContinuousRod) //Have CR missiles target slightly above target to ensure craft caught in planar blast AOE + { + // If target is above, the we offset below, if target is below, we offset above + TargetPosition += vessel.up * (Mathf.Sign(Vector3.Dot(vessel.CoM - TargetPosition, vessel.up)) * (blastRadius > 0f ? Mathf.Min(blastRadius / 3f, DetonationDistance / 3f) : 5f)); + } + DrawDebugLine(vessel.CoM + (part.rb.velocity * Time.fixedDeltaTime), TargetPosition); + + float timeToImpact; + switch (GuidanceMode) + { + case GuidanceModes.APN: + { + float tempPronavGain = pronavGain > 0 ? pronavGain : pronavGainCurve.Evaluate(Vector3.Distance(TargetPosition, vessel.CoM)); + + aamTarget = MissileGuidance.GetAPNTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, tempPronavGain, out timeToImpact, out currgLimit); + TimeToImpact = timeToImpact; + break; + } + + case GuidanceModes.PN: // Pro-Nav + { + float tempPronavGain = pronavGain > 0 ? pronavGain : pronavGainCurve.Evaluate(Vector3.Distance(TargetPosition, vessel.CoM)); + + aamTarget = MissileGuidance.GetPNTarget(TargetPosition, TargetVelocity, vessel, tempPronavGain, out timeToImpact, out currgLimit); + TimeToImpact = timeToImpact; + break; + } + case 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() && (SourceVessel.Landed || vessel.altitude >= LoftMinAltitude) && VectorUtils.Angle(TargetPosition - vessel.CoM, vessel.upAxis) > Mathf.Min(LoftAngle, 90f - LoftAngle) && ((vessel.altitude - targetAlt <= LoftAltitudeAdvMax) || (TargetPosition - vessel.CoM).sqrMagnitude > (LoftRangeOverride * LoftRangeOverride))) loftState = LoftStates.Boost; + else loftState = LoftStates.Terminal; + } + + float tempPronavGain = pronavGain > 0 ? pronavGain : pronavGainCurve.Evaluate(Vector3.Distance(TargetPosition, vessel.CoM)); + + //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, maneuvergLimit, invManeuvergLimit, ref loftState, out float currTimeToImpact, out currgLimit, out float rangeToTarget, homingModeTerminal, tempPronavGain, optimumAirspeed); + + //float fac = (1 - (rangeToTarget - terminalHomingRange - 100f) / Mathf.Clamp(terminalHomingRange * 4f, 5000f, 25000f)); + + //if (loftState > LoftStates.Boost) + // maxAoA = Mathf.Clamp(initMaxAoA * fac, 4f, initMaxAoA); + if (loftState == LoftStates.Midcourse) + currAoALimit = 30f; + + 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}]"); + break; + } + case GuidanceModes.AAMPure: + { + TimeToImpact = Vector3.Distance(TargetPosition, vessel.CoM) / Mathf.Max((float)vessel.srfSpeed, optimumAirspeed); + aamTarget = TargetPosition; + break; + } + /* Case GuidanceModes.AAMHybrid: +{ + aamTarget = MissileGuidance.GetAirToAirHybridTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, terminalHomingRange, out timeToImpact, homingModeTerminal, pronavGain, optimumAirspeed); + TimeToImpact = timeToImpact; + break; + } + */ + case GuidanceModes.AAMLead: + { + aamTarget = MissileGuidance.GetAirToAirTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact, optimumAirspeed); + TimeToImpact = timeToImpact; + break; + } + + case GuidanceModes.Kappa: + { + aamTarget = MissileGuidance.GetKappaTarget(TargetPosition, TargetVelocity, this, MissileState == MissileStates.PostThrust ? 0f : currentThrust * Throttle, kappaAngle, LoftRangeFac, LoftVertVelComp, FlightGlobals.getAltitudeAtPos(TargetPosition), terminalHomingRange, LoftAngle, LoftTermAngle, LoftRangeOverride, LoftMaxAltitude, out timeToImpact, out currgLimit, ref loftState); + TimeToImpact = timeToImpact; + break; + } + + case GuidanceModes.Weave: + { + aamTarget = MissileGuidance.GetWeaveTarget(TargetPosition, TargetVelocity, vessel, ref WeaveVerticalG, ref WeaveHorizontalG, WeaveRandomRange, ref WeaveFrequency, WeaveTerminalAngle, WeaveFactor, WeaveUseAGMDescentRatio, agmDescentRatio, maneuvergLimit, ref WeaveOffset, ref WeaveStart, ref WeaveAlt, out timeToImpact, out currgLimit); + TimeToImpact = timeToImpact; + break; + } + } + + if (VectorUtils.Angle(aamTarget - vessel.CoM, transform.forward) > maxOffBoresight * 0.75f) + { + aamTarget = TargetPosition; + } + + /*//proxy detonation + var distThreshold = 0.5f * GetBlastRadius(); + if (proxyDetonate && !DetonateAtMinimumDistance && (!heatTarget.exists || heatTarget.vessel) && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (vessel.CoM)).sqrMagnitude < distThreshold * distThreshold) + { + //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 + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher] ProxiDetonate triggered"); + Detonate(); + }*/ + } + else + { + aamTarget = vessel.CoM + (2000f * vessel.Velocity().normalized); + } + + if (TimeIndex > dropTime + 0.25f) + { + DoAero(aamTarget, currgLimit, currAoALimit); + CheckMiss(); + } + + } + + void AGMGuidance() + { + if (TargetingMode != TargetingModes.Gps) + { + if (TargetAcquired) + { + //lose lock if seeker reaches gimbal limit + float targetViewAngle = VectorUtils.Angle(transform.forward, TargetPosition - vessel.CoM); + + if (targetViewAngle > maxOffBoresight) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: AGM Missile guidance failed - target out of view"); + guidanceActive = false; + } + CheckMiss(); + } + else + { + if (TargetingMode == TargetingModes.Laser) + { + //keep going straight until found laser point + TargetPosition = laserStartPosition + (20000 * startDirection); + } + } + } + + Vector3 targetPosTemp = TargetPosition; + + if (FlightGlobals.currentMainBody.ocean && targetVessel != null) + { + if (targetVessel.Vessel.radarAltitude < 0) + targetPosTemp = targetPosTemp - targetVessel.Vessel.up * targetVessel.Vessel.radarAltitude; + } + + Vector3 agmTarget = MissileGuidance.GetAirToGroundTarget(targetPosTemp, TargetVelocity, vessel, agmDescentRatio); + DoAero(agmTarget); + } + + void SLWGuidance() + { + Vector3 SLWTarget; + float runningDepth = torpedo ? Mathf.Min(-3, (float)FlightGlobals.getAltitudeAtPos(TargetPosition)) : (float)vessel.radarAltitude; + Vector3 upDir = (vessel.transform.position - vessel.mainBody.transform.position).normalized; + if (TargetAcquired) + { + //DrawDebugLine(transform.position + (part.rb.velocity * Time.fixedDeltaTime), TargetPosition); + float timeToImpact; + SLWTarget = MissileGuidance.GetAirToAirTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact, optimumAirspeed); + if (!torpedo) runningDepth = Mathf.Max(timeToImpact / 5, 0.1f) * 10; + if (VectorUtils.Angle(SLWTarget - vessel.CoM, transform.forward) > maxOffBoresight * 0.75f) + { + SLWTarget = TargetPosition; + } + if (!torpedo) SLWTarget = SLWTarget - targetVessel.Vessel.up * targetVessel.Vessel.radarAltitude; + float longitudinalOffset = 0; + if (longitudinalOffset == 0) longitudinalOffset = targetVessel.Vessel.GetRadius() * 0.75f * UnityEngine.Random.Range(-1, 1); + SLWTarget += targetVessel.Vessel.vesselTransform.up * longitudinalOffset; + SLWTarget = vessel.CoM + (SLWTarget - vessel.CoM).normalized * 100; + SLWTarget = SLWTarget - ((float)FlightGlobals.getAltitudeAtPos(SLWTarget) * upDir) + upDir * runningDepth; + TimeToImpact = timeToImpact; + + /*//proxy detonation + var distThreshold = 0.5f * GetBlastRadius(); + if (proxyDetonate && !DetonateAtMinimumDistance && (!heatTarget.exists || heatTarget.vessel) && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (vessel.CoM)).sqrMagnitude < distThreshold * distThreshold) + { + Detonate(); //ends up the same as part.Destroy, except it doesn't trip the hasDied flag for clustermissiles + }*/ + } + else + { + SLWTarget = TargetPosition; //head to last known contact and then begin circling + SLWTarget = vessel.CoM + (SLWTarget - vessel.CoM.normalized) * 100; + SLWTarget = (SLWTarget - ((float)FlightGlobals.getAltitudeAtPos(SLWTarget) * upDir)) + upDir * runningDepth; + } + DrawDebugLine(vessel.CoM, SLWTarget, Color.blue); + //allow inverse contRod-style target offset for srf targets for 'under-the-keel' proximity detonation? or at least not having the torps have a target alt of 0 (and thus be vulnerable to surface PD?) + if (TimeIndex > dropTime + 0.25f) + { + DoAero(SLWTarget); + } + + CheckMiss(); + } + + void DoAero(Vector3 targetPosition, float currgLimit = -1f, float currAoALimit = -1f) + { + if (gLimit > 0f) + { + if (currgLimit < 0f) + currgLimit = gLimit; + else + { + currgLimit = Mathf.Min(currgLimit, gLimit); + currgLimit += Mathf.Min(0.15f * currgLimit, 2f); + } + } + else + currgLimit = -1f; + + if (currAoALimit < 0f) + currAoALimit = maxAoA; + else + currAoALimit = Mathf.Min(currAoALimit, maxAoA); + + if (currgLimit > 0f) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) debugString.AppendLine($"commanded g: {currgLimit:F5}"); + currAoALimit = MissileGuidance.getGLimit(this, MissileState == MissileStates.PostThrust ? 0f : currentThrust * Throttle, currgLimit, gMargin, currAoALimit); + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: maxAoA: {maxAoA}, currAoALimit: {currAoALimit}, currgLimit: {currgLimit}"); + } + + aeroTorque = MissileGuidance.DoAeroForces(this, targetPosition, currLiftArea, currDragArea, controlAuthority * currSteerMult, aeroTorque, finalMaxTorque, currMaxTorqueAero, currAoALimit, MissileGuidance.DefaultLiftCurve, MissileGuidance.DefaultDragCurve); + } + + void AGMBallisticGuidance() + { + DoAero(CalculateAGMBallisticGuidance(this, TargetPosition)); + } + + void OrbitalGuidance(float turnRateDPS) + { + Vector3 orbitalTarget; + if (TargetAcquired) + { + float guidance_thrust = currentThrust; + if (currentThrust == 0 && cruiseDelay > 0 && (TimeIndex > dropTime + boostTime) && (TimeIndex < dropTime + boostTime + cruiseDelay)) // If in the cruiseDelay, fake thrust to avoid discontinuities in the guidance + guidance_thrust = Mathf.Lerp(thrust, cruiseThrust, (TimeIndex - (dropTime + boostTime)) / cruiseDelay); + + if (!hasRCS) // Use thrust to kill relative velocity + { + Vector3 targetVector = TargetPosition - vessel.CoM; + Vector3 acceleration = guidance_thrust / part.mass * GetForwardTransform(); + Vector3 relVel = TargetVelocity - vessel.Velocity(); + float timeToImpact = AIUtils.TimeToCPA(targetVector, relVel, TargetAcceleration - acceleration, 30); + orbitalTarget = AIUtils.PredictPosition(targetVector, relVel, TargetAcceleration - 0.5f * acceleration, timeToImpact); + } + else // Use thrust to kill relative velocity early, with RCS for later adjustments + { + Vector3 targetVector = TargetPosition - vessel.CoM; + Vector3 relVel = vessel.Velocity() - TargetVelocity; + Vector3 tvNorm = targetVector.normalized; + float timeToImpact = BDAMath.SolveTime(targetVector.magnitude, guidance_thrust / part.mass, Vector3.Dot(relVel, tvNorm)); + Vector3 lead = -timeToImpact * relVel; + float t = (targetVessel && targetVessel.isMissile) ? Vector3.Dot(targetVector + lead, tvNorm) / (targetVector + lead).magnitude : relVel.sqrMagnitude > 0 ? Vector3.Dot(relVel, tvNorm) / relVel.magnitude : 1; + orbitalTarget = Vector3.Slerp(TargetPosition + lead, TargetPosition, t); + } + + // Clamp target position to max off boresight + float angleToTarget = VectorUtils.Angle(TargetPosition - vessel.CoM, orbitalTarget - vessel.CoM); + if (angleToTarget > maxOffBoresight) + { + orbitalTarget = vessel.CoM + Vector3.RotateTowards(TargetPosition - vessel.CoM, orbitalTarget - vessel.CoM, maxOffBoresight * Mathf.Deg2Rad, 0f); + } + } + else + orbitalTarget = vessel.CoM + (2000f * vessel.Velocity().normalized); + + // In vacuum, with RCS, point towards target shortly after launch to minimize wasted delta-V + // During this maneuver, check that we have cleared any obstacles before throttling up + orbitalTarget = VacuumClearanceManeuver(orbitalTarget, vessel.CoM, hasRCS, vacuumSteerable); + if (Throttle == 0) + turnRateDPS *= 15f; + + // If in atmosphere, apply drag + if (!vessel.InVacuum() && vessel.srfSpeed > 0f) + { + Rigidbody rb = part.rb; + if (rb != null && rb.mass > 0) + { + double airDensity = vessel.atmDensity; + double airSpeed = vessel.srfSpeed; + Vector3d velocity = vessel.Velocity(); + Vector3 CoL = new Vector3(0, 0, -1f); + float AoA = Mathf.Clamp(VectorUtils.Angle(part.transform.forward, velocity), 0, 90); + double dragForce = 0.5 * airDensity * airSpeed * airSpeed * currDragArea * BDArmorySettings.GLOBAL_DRAG_MULTIPLIER * Mathf.Max(MissileGuidance.DefaultDragCurve.Evaluate(AoA), 0f); + rb.AddForceAtPosition((float)dragForce * -velocity.normalized, + part.transform.TransformPoint(part.CoMOffset + CoL)); + } + } + + part.transform.rotation = Quaternion.RotateTowards(part.transform.rotation, Quaternion.LookRotation(orbitalTarget - vessel.CoM, TargetVelocity), turnRateDPS * Time.fixedDeltaTime); + if (TimeIndex > dropTime + 0.25f) + CheckMiss(); + + DrawDebugLine(vessel.CoM + (part.rb.velocity * Time.fixedDeltaTime), orbitalTarget); + } + + public override void Detonate() + { + if (HasExploded || FuseFailed || !HasFired) return; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Detonate Triggered"); + + BDArmorySetup.numberOfParticleEmitters--; + HasExploded = true; + /* + if (targetVessel != null) + { + using (var wpm = VesselModuleRegistry.GetMissileFires(targetVessel).GetEnumerator()) + while (wpm.MoveNext()) + { + if (wpm.Current == null) continue; + wpm.Current.missileIsIncoming = false; //handled by attacked vessel + } + } + */ + if (SourceVessel == null) SourceVessel = vessel; + if (multiLauncher && multiLauncher.isClusterMissile) + { + 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); + } + } + multiLauncher.Team = Team; + multiLauncher.fireMissile(true); + } + } + else + { + if (warheadType == WarheadTypes.Standard || warheadType == WarheadTypes.ContinuousRod || + warheadType == WarheadTypes.Custom || + warheadType == WarheadTypes.CustomStandard || warheadType == WarheadTypes.CustomContinuous) + { + /* + if (warheadType == WarheadTypes.Standard || warheadType == WarheadTypes.ContinuousRod || + warheadType == WarheadTypes.CustomStandard || warheadType == WarheadTypes.CustomContinuous) + { + var tnt = part.FindModuleImplementing(); + tnt.DetonateIfPossible(); + FuseFailed = tnt.fuseFailed; + guidanceActive = false; + if (FuseFailed) + HasExploded = false; + } + + if (warheadType == WarheadTypes.Custom || warheadType == WarheadTypes.CustomStandard || warheadType == WarheadTypes.CustomContinuous) + { + var warhead = part.FindModuleImplementing(); + warhead.DetonateIfPossible(); + FuseFailed = warhead.fuseFailed; + guidanceActive = false; + if (FuseFailed) + HasExploded = false; + } + */ + var tntList = part.FindModulesImplementing(); + foreach (BDWarheadBase tnt in tntList) + { + tnt.DetonateIfPossible(); + FuseFailed = tnt.fuseFailed || FuseFailed; + } + guidanceActive = false; + if (FuseFailed) + HasExploded = false; + } + else if (warheadType == WarheadTypes.Nuke) + { + var U235 = part.FindModuleImplementing(); + U235.Detonate(); + } + else if (warheadType == WarheadTypes.EMP || warheadType == WarheadTypes.Legacy) // EMP/really old legacy missiles using BlastPower + { + Vector3 position = transform.position;//+rigidbody.velocity*Time.fixedDeltaTime; + ExplosionFx.CreateExplosion(position, blastPower, explModelPath, explSoundPath, ExplosionSourceType.Missile, 0, part, SourceVessel.vesselName, Team.Name, GetShortName(), default(Vector3), -1, warheadType == WarheadTypes.EMP, part.mass * 1000); + } + else if (warheadType == WarheadTypes.Kinetic) // Missile will usually just phase through target at high speeds (even with ContinuousCollisions mod), so fake effects using an explosion originating at point of impact + { + Vector3 relVel = TargetVelocity != Vector3.zero ? vessel.Velocity() - TargetVelocity : vessel.Velocity() - BDKrakensbane.FrameVelocityV3f; + Ray ray = new(transform.position, relVel); + if (Physics.Raycast(ray, out RaycastHit hit, 500f, (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels))) + { + ExplosionFx.CreateExplosion(hit.point, 0.5f * (1000f * part.mass) * relVel.sqrMagnitude / 4184000f, explModelPath, explSoundPath, ExplosionSourceType.Missile, 1000f * vessel.GetRadius(), part, SourceVesselName, Team.Name, GetShortName(), ray.direction, -1, false, part.mass, -1, 1, ExplosionFx.WarheadTypes.Kinetic, null, 1.2f, sourceVelocity: vessel.Velocity()); + } + } + 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 + } + } + + using (var e = gaplessEmitters.GetEnumerator()) + while (e.MoveNext()) + { + if (e.Current == null) continue; + e.Current.gameObject.AddComponent(); + e.Current.transform.parent = null; + } + using (IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 0; + } + } + + public void DestroyMissile() + { + part.Destroy(); + part.explode(); + } + + public override Vector3 GetForwardTransform() + { + if (multiLauncher && multiLauncher.overrideReferenceTransform) + return vessel.ReferenceTransform.up; + else + return MissileReferenceTransform.forward; + } + + public override float GetKinematicTime() + { + // Get time at which the missile is traveling at the GetKinematicSpeed() speed + if (!launched) return -1f; + + float missileKinematicTime = boostTime + cruiseTime + cruiseDelay + dropTime - TimeIndex; + if (!vessel.InVacuum()) + { + float speed = currentThrust > 0 ? optimumAirspeed : (float)vessel.srfSpeed; + float minSpeed = GetKinematicSpeed(); + if (speed > minSpeed) + { + float airDensity = (float)vessel.atmDensity; + float dragTerm; + float t; + if (useSimpleDrag) + { + dragTerm = (deployed ? deployedDrag : simpleDrag) * (0.008f * part.mass) * 0.5f * airDensity; + t = part.mass / (minSpeed * dragTerm) - part.mass / (speed * dragTerm); + } + else + { + float AoA = smoothedAoA.Value; + FloatCurve dragCurve = MissileGuidance.DefaultDragCurve; + float dragCd = dragCurve.Evaluate(AoA); + float dragMultiplier = BDArmorySettings.GLOBAL_DRAG_MULTIPLIER; + dragTerm = 0.5f * airDensity * currDragArea * dragMultiplier * dragCd; + float dragTermMinSpeed = 0.5f * airDensity * currDragArea * dragMultiplier * dragCurve.Evaluate(Mathf.Min(30f, maxAoA)); // Max AoA or 29 deg (at kink in drag curve) + t = part.mass / (minSpeed * dragTermMinSpeed) - part.mass / (speed * dragTerm); + } + missileKinematicTime += t; // Add time for missile to slow down to min speed + } + } + + return missileKinematicTime; + } + + public override float GetKinematicSpeed() + { + if (vessel.InVacuum() || weaponClass != WeaponClasses.Missile) return 0f; + + // Get speed at which the missile is only capable of pulling a 2G turn at maxAoA + float Gs = 2f; + + FloatCurve liftCurve = MissileGuidance.DefaultLiftCurve; + float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)vessel.orbit.referenceBody.GeeASL; + float liftMultiplier = BDArmorySettings.GLOBAL_LIFT_MULTIPLIER; + float kinematicSpeed = BDAMath.Sqrt((Gs * part.mass * bodyGravity) / (0.5f * (float)vessel.atmDensity * currLiftArea * liftMultiplier * liftCurve.Evaluate(maxAoA))); + + return Mathf.Min(kinematicSpeed, 0.5f * (float)vessel.speedOfSound); + } + + protected override void PartDie(Part p) + { + if (p != part) return; + HasDied = true; + Detonate(); + BDATargetManager.FiredMissiles.Remove(this); + GameEvents.onPartDie.Remove(PartDie); + Destroy(this); // If this is the active vessel, then KSP doesn't destroy it until we switch away, but we want to get rid of the MissileBase straight away. + } + + public static bool CheckIfMissile(Part p) + { + return p.GetComponent(); + } + + void WarnTarget() + { + if (targetVessel == null) return; + var wpm = targetVessel.Vessel.ActiveController().WM; + if (wpm != null) wpm.MissileWarning(Vector3.Distance(vessel.CoM, targetVessel.position), this); + } + + void SetupRCS() + { + rcsFiredTimes = [0, 0, 0, 0]; + rcsTransforms = [upRCS, leftRCS, rightRCS, downRCS]; + } + + void DoRCS() + { + try + { + if (vacuumClearanceState == VacuumClearanceStates.Clearing || (TimeIndex < dropTime + Mathf.Min(0.5f, BDAMath.SolveTime(10f, currentThrust / part.mass)))) return; // Don't use RCS immediately after launch or when clearing a vessel to avoid running into VLS/SourceVessel + Vector3 relV; + if (vacuumClearanceState == VacuumClearanceStates.Turning && SourceVessel) // Clear away from launching vessel + { + Vector3 relP = (vessel.CoM - SourceVessel.CoM).normalized; + relV = relP + (vessel.Velocity() - SourceVessel.Velocity()).normalized.ProjectOnPlanePreNormalized(relP); + relV = 100f * relV.ProjectOnPlane(TargetPosition - vessel.CoM); + } + else // Kill relative velocity to target + relV = TargetVelocity - vessel.Velocity(); + + // Adjust for gravity if no aero or in near vacuum + if (!aero || vessel.InNearVacuum()) + { + Vector3 toBody = (vessel.CoM - vessel.orbit.referenceBody.position); + float bodyGravity = (float)vessel.orbit.referenceBody.gravParameter / toBody.sqrMagnitude; + relV += -bodyGravity * vessel.up; + } + + for (int i = 0; i < 4; i++) + { + //float giveThrust = Mathf.Clamp(-localRelV.z, 0, rcsThrust); + float giveThrust = Mathf.Clamp(Vector3.Project(relV, rcsTransforms[i].transform.forward).magnitude * -Mathf.Sign(Vector3.Dot(rcsTransforms[i].transform.forward, relV)), 0, rcsThrust); + part.rb.AddForce(-giveThrust * rcsTransforms[i].transform.forward); + + if (giveThrust > rcsRVelThreshold) + { + rcsAudioMinInterval = UnityEngine.Random.Range(0.15f, 0.25f); + if (Time.time - rcsFiredTimes[i] > rcsAudioMinInterval) + { + if (sfAudioSource == null) SetupAudio(); + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/popThrust")); + rcsTransforms[i].emit = true; + rcsFiredTimes[i] = Time.time; + } + } + else + { + rcsTransforms[i].emit = false; + } + + //turn off emit + if (Time.time - rcsFiredTimes[i] > rcsAudioMinInterval * 0.75f) + { + rcsTransforms[i].emit = false; + } + } + } + catch (Exception e) + { + + Debug.LogError("[BDArmory.MissileLauncher]: DEBUG " + e.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.LogWarning("[BDArmory.MissileLauncher]: DEBUG null rcsTransforms[" + i + "]?: " + (rcsTransforms[i] == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG rcsTransforms[" + i + "]: " + 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. + } + } + + public void KillRCS() + { + if (upRCS) upRCS.emit = false; + if (downRCS) downRCS.emit = false; + if (leftRCS) leftRCS.emit = false; + if (rightRCS) rightRCS.emit = false; + } + + protected override void OnGUI() + { + base.OnGUI(); + if (HighLogic.LoadedSceneIsFlight) + { + try + { + drawLabels(); + if (BDArmorySettings.DEBUG_LINES && HasFired) + { + float burnTimeleft = 10 - Mathf.Min(((TimeIndex / (boostTime + cruiseTime)) * 10), 10); + + GUIUtils.DrawLineBetweenWorldPositions(vessel.CoM + MissileReferenceTransform.forward * burnTimeleft, + vessel.CoM + MissileReferenceTransform.forward * 10, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vessel.CoM, + vessel.CoM + MissileReferenceTransform.forward * burnTimeleft, 2, Color.green); + } + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.MissileLauncher]: Exception thrown in OnGUI: " + e.Message + "\n" + e.StackTrace); + } + } + } + + void AntiSpin() + { + part.rb.angularDrag = 0; + part.angularDrag = 0; + Vector3 spin = Vector3.Project(part.rb.angularVelocity, part.rb.transform.forward);// * 8 * Time.fixedDeltaTime; + part.rb.angularVelocity -= spin; + //rigidbody.maxAngularVelocity = 7; + + if (guidanceActive) + { + part.rb.angularVelocity -= 0.6f * part.rb.angularVelocity; + } + else + { + part.rb.angularVelocity -= 0.02f * part.rb.angularVelocity; + } + } + + void SimpleDrag() + { + part.dragModel = Part.DragModel.NONE; + if (part.rb == null || part.rb.mass == 0) return; + //float simSpeedSquared = (float)vessel.Velocity.sqrMagnitude; + float simSpeedSquared = (part.rb.GetPointVelocity(part.transform.TransformPoint(simpleCoD)) + (Vector3)Krakensbane.GetFrameVelocity()).sqrMagnitude; + float drag = deployed ? deployedDrag : simpleDrag; + float dragMagnitude = (0.008f * part.rb.mass) * drag * 0.5f * simSpeedSquared * (float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(vessel.CoM), FlightGlobals.getExternalTemperature(vessel.CoM), FlightGlobals.currentMainBody); + Vector3 dragForce = dragMagnitude * vessel.Velocity().normalized; + part.rb.AddForceAtPosition(-dragForce, transform.TransformPoint(simpleCoD)); + + Vector3 torqueAxis = -Vector3.Cross(vessel.Velocity(), part.transform.forward).normalized; + float AoA = VectorUtils.Angle(part.transform.forward, vessel.Velocity()); + AoA /= 20; + part.rb.AddTorque(AoA * simpleStableTorque * dragMagnitude * torqueAxis); + } + + public void ParseAntiRadTargetTypes() + { + antiradTargets = OtherUtils.ParseEnumArray(antiradTargetTypes); + //Debug.Log($"[BDArmory.MissileLauncher] antiradTargets: {string.Join(", ", antiradTargets)}"); + } + + GuidanceModes ParseHomingType(in string homingType) + { + return homingType switch + { + "aam" => GuidanceModes.AAMLead, + "aamlead" => GuidanceModes.AAMLead, + "aampure" => GuidanceModes.AAMPure, + "aamloft" => GuidanceModes.AAMLoft, + //"aamhybrid" => GuidanceModes.AAMHybrid, // keeping this in case we want to bring it back, it technically is a bit better at handling the handoff than the current method + "agm" => GuidanceModes.AGM, + "agmballistic" => GuidanceModes.AGMBallistic, + "cruise" => GuidanceModes.Cruise, + "weave" => GuidanceModes.Weave, + "sts" => GuidanceModes.STS, // What was this for? it seems to be unused + "rcs" => GuidanceModes.Orbital, + "orbital" => GuidanceModes.Orbital, + "beamriding" => GuidanceModes.BeamRiding, + "slw" => GuidanceModes.SLW, + "pronav" => GuidanceModes.PN, + "augpronav" => GuidanceModes.APN, + "kappa" => GuidanceModes.Kappa, + "clos" => GuidanceModes.CLOS, + "closthree" => GuidanceModes.CLOSThreePoint, + "closlead" => GuidanceModes.CLOSLead, + _ => GuidanceModes.None + }; + } + + TargetingModes ParseTargetingType(in string targetingType) + { + return targetingType switch + { + "radar" => TargetingModes.Radar, + "heat" => TargetingModes.Heat, + "laser" => TargetingModes.Laser, + "gps" => TargetingModes.Gps, + "antirad" => TargetingModes.AntiRad, + "inertial" => TargetingModes.Inertial, + _ => TargetingModes.None + }; + } + + void ParseModes() + { + homingType = homingType.ToLower(); + GuidanceMode = ParseHomingType(homingType); + + targetingType = targetingType.ToLower(); + TargetingMode = ParseTargetingType(targetingType); + + terminalGuidanceType = terminalGuidanceType.ToLower(); + TargetingModeTerminal = ParseTargetingType(terminalGuidanceType); + + terminalHomingType = terminalHomingType.ToLower(); + homingModeTerminal = ParseHomingType(terminalHomingType); + + if (TargetingMode == TargetingModes.Gps) + maxOffBoresight = 180; + + 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 (terminalGuidanceShouldActivate) + { + if (TargetingMode == TargetingModeTerminal) + { + terminalGuidanceShouldActivate = false; + TargetingModeTerminal = TargetingModes.None; + } + + } + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: parsing guidance and homing complete on {part.name}"); + } + + public void ParseLiftDragSteerTorque() + { + // Parse lift area, we have boost, cruise and deploy and cruiseDeploy + // the latter two are deltas, once deployed these are summed to the + // boost and cruise value + parsedLiftArea = ParsePerfParams(liftArea, 4); + // Same thing for drag area + parsedDragArea = ParsePerfParams(dragArea, 4); + + // Pre-set the deploy and cruiseDeploy values to 0 if they're < 0 since + // these will get summed directly to currLiftArea without checking + if (parsedLiftArea[2] < 0f) + parsedLiftArea[2] = 0f; + if (parsedLiftArea[3] < 0f) + parsedLiftArea[3] = 0f; + // Same for drag, except we also set the first value equal to liftArea if it + // is < 0, for convenience. We only modify the cruiseArea if decoupleBoosters is + // true, and we can check the cruise entry of drag area at staging if needed + if (parsedDragArea[0] < 0f) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default dragArea to liftArea {parsedLiftArea[0]}:"); + parsedDragArea[0] = parsedLiftArea[0]; + } + if (parsedDragArea[2] < 0f) + parsedDragArea[2] = 0f; + if (parsedDragArea[3] < 0f) + parsedDragArea[3] = 0f; + + // Parse steerMult, only 2 values here needed, boost and cruise + parsedSteerMult = ParsePerfParams(steerMult, 2); + + // Parse maxTorque, for which there are 4 values, boost, cruise, post-thrust, and coast + // to simulate thrust vectoring + parsedMaxTorque = ParsePerfParams(maxTorque, 4); + + // If the coast value is not set, use the post-thrust value + if (parsedMaxTorque[3] < 0f) + parsedMaxTorque[3] = parsedMaxTorque[2]; + + // Parse maxTorqueAero, for which there are 4 values, boost, cruise and 2 deltas + parsedMaxTorqueAero = ParsePerfParams(maxTorqueAero, 4); + + // Set the curr values + currLiftArea = parsedLiftArea[0]; + currDragArea = parsedDragArea[0]; + currSteerMult = parsedSteerMult[0]; + currMaxTorque = parsedMaxTorque[0]; + // We check currMaxTorqueAero against 0f because this gets used directly without + // any checks in DoAero for efficiency + currMaxTorqueAero = Mathf.Max(parsedMaxTorqueAero[0], 0f); + } + + private static float[] ParsePerfParams(string floatString, int length) + { + string[] floatStrings = floatString.Split(new char[] { ',' }); + float[] floatArray = new float[length]; + // Loop either until the end of floatStrings or length, whichever comes first + int loopLength = (floatStrings.Length < length) ? floatStrings.Length : length; + float temp; + for (int i = 0; i < loopLength; i++) + { + if (float.TryParse(floatStrings[i], out temp)) + floatArray[i] = temp; + else + floatArray[i] = -1f; + } + // If floatStrings is shorter than length + if (loopLength < length) + { + // Then fill the rest of the array with -1f + for (int i = loopLength; i < length; i++) + floatArray[i] = -1f; + } + + return floatArray; + } + + private string GetBrevityCode() + { + //torpedo: determine subtype + if (missileType.ToLower() == "torpedo") + { + if (TargetingMode == TargetingModes.Radar && activeRadarRange > 0) + return "Active Sonar"; + + if (TargetingMode == TargetingModes.Laser || TargetingMode == TargetingModes.Gps) + return "Optical/wireguided"; + + if (TargetingMode == TargetingModes.Heat) + { + if (activeRadarRange <= 0) return "Passive Sonar"; + else return "Heat guided"; + } + + if (TargetingMode == TargetingModes.None) + return "Unguided"; + } + + if (missileType.ToLower() == "bomb") + { + if ((TargetingMode == TargetingModes.Laser) || (TargetingMode == TargetingModes.Gps)) + return "JDAM"; + + if ((TargetingMode == TargetingModes.None)) + return "Unguided"; + } + if (missileType.ToLower() == "launcher") + { + return "Requires Ordnance"; + } + //else: missiles: + + if (TargetingMode == TargetingModes.Radar) + { + //radar: determine subtype + if (activeRadarRange <= 0) + return "SARH"; + if (activeRadarRange > 0 && activeRadarRange < maxStaticLaunchRange) + return "Mixed SARH/F&F"; + if (activeRadarRange >= maxStaticLaunchRange) + return "Fire&Forget"; + } + + if (TargetingMode == TargetingModes.AntiRad) + return "Fire&Forget"; + + if (TargetingMode == TargetingModes.Heat) + return "Fire&Forget"; + + if (TargetingMode == TargetingModes.Laser) + return "SALH"; + + if (TargetingMode == TargetingModes.Gps) + { + return TargetingModeTerminal != TargetingModes.None ? "GPS/Terminal" : "GPS"; + } + if (TargetingMode == TargetingModes.Inertial) + { + return TargetingModeTerminal != TargetingModes.None ? "Inertial/Terminal" : "Inertial"; + } + if (TargetingMode == TargetingModes.None) + { + return TargetingModeTerminal != TargetingModes.None ? "Unguided/Terminal" : "Unguided"; + } + // default: + return "Unguided"; + } + + // RMB info in editor + public override string GetInfo() + { + ParseModes(); + + StringBuilder output = new StringBuilder(); + output.AppendLine($"{missileType.ToUpper()} - {GetBrevityCode()}"); + if (missileType.ToLower() == "launcher") return output.ToString(); //Launcher is empty rail, doesn't have relevant missile stats to display + + output.Append(Environment.NewLine); + output.AppendLine($"Targeting Type: {targetingType.ToLower()}"); + output.AppendLine($"Guidance Mode: {homingType.ToLower()}"); + if (terminalHoming) + { + output.AppendLine($"Terminal Guidance Mode: {terminalHomingType.ToLower()} @ distance: {terminalHomingRange} m"); + } + if (missileRadarCrossSection != RadarUtils.RCS_MISSILES) + { + output.AppendLine($"Detectable cross section: {missileRadarCrossSection} m^2"); + } + 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"); + } + + output.AppendLine($"Max AoA: {maxAoA}"); + + if (gLimit > 0) + output.AppendLine($"G Limit: {gLimit} g"); + + float tempSeekerTimeout = Mathf.Max(seekerTimeout, radarTimeout); + float tempTerminalSeekerTimeout = terminalSeekerTimeout > 0 ? terminalSeekerTimeout : tempSeekerTimeout; + + if (TargetingMode == TargetingModes.Radar) + { + if (activeRadarRange > 0) + { + 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($" - Max Radar Search Time: {tempSeekerTimeout} s"); + } + output.AppendLine($"Max Off Boresight: {maxOffBoresight}"); + output.AppendLine($"Locked FOV: {lockedSensorFOV}"); + output.AppendLine($"Chaff Sensitivity: {chaffEffectivity}"); + } + + if (TargetingMode == TargetingModes.Heat) + { + output.AppendLine($"Uncaged Lock: {uncagedLock}"); + output.AppendLine($"Min Heat threshold: {heatThreshold}"); + output.AppendLine($"Max Off Boresight: {maxOffBoresight}"); + output.AppendLine($"Locked FOV: {lockedSensorFOV}"); + output.AppendLine($"Flare Sensitivity: {flareEffectivity}"); + output.AppendLine($"Seeker Search Time: {tempSeekerTimeout} s"); + } + + if (TargetingMode == TargetingModes.Inertial) + { + output.AppendLine($"Inertial Drift: {inertialDrift} m/s"); + output.AppendLine($"Inertial Guidance Time: {tempSeekerTimeout} s"); + } + + if (TargetingMode == TargetingModes.AntiRad) + output.AppendLine($"Seeker Search Time: {tempSeekerTimeout} s"); + + if (TargetingMode == TargetingModes.Gps || TargetingMode == TargetingModes.None || TargetingMode == TargetingModes.Inertial) + { + output.AppendLine($"Terminal Maneuvering: {terminalGuidanceShouldActivate}"); + if (terminalGuidanceType != "") + { + output.AppendLine($"Terminal Targeting: {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: {tempTerminalSeekerTimeout} s"); + output.AppendLine($"Max Offboresight: {maxOffBoresight}"); + output.AppendLine($"Locked FOV: {lockedSensorFOV}"); + } + + if (TargetingModeTerminal == TargetingModes.Heat) + { + output.AppendLine($"Uncaged Lock: {uncagedLock}"); + output.AppendLine($"Min Heat threshold: {heatThreshold}"); + output.AppendLine($"Max Offboresight: {maxOffBoresight}"); + output.AppendLine($"Locked FOV: {lockedSensorFOV}"); + output.AppendLine($"Seeker Search Time: {tempTerminalSeekerTimeout} s"); + } + + if (TargetingModeTerminal == TargetingModes.Inertial) + { + output.AppendLine($"Inertial Drift: {inertialDrift} m/s"); + output.AppendLine($"Inertial Guidance Time: {tempTerminalSeekerTimeout} s"); + } + + if (TargetingModeTerminal == TargetingModes.AntiRad) + output.AppendLine($"Seeker Search Time: {tempTerminalSeekerTimeout} s"); + } + } + + output.AppendLine($"Warhead:"); + foreach (var partModule in part.Modules) + { + if (partModule == null) continue; + switch (partModule.moduleName) + { + case "MultiMissileLauncher": + { + warheadType = WarheadTypes.Launcher; //Why is this getting set here? warHeadType is already set in onStart() + + if (((MultiMissileLauncher)partModule).isClusterMissile) + { + output.AppendLine($"Cluster Missile:"); + output.AppendLine($"- SubMunition Count: {((MultiMissileLauncher)partModule).salvoSize} "); + } + float tntMass = ((MultiMissileLauncher)partModule).tntMass; + output.AppendLine($"- Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(tntMass), 2)} m"); + output.AppendLine($"- tnt Mass: {tntMass} kg"); + break; //shouldn't have any other module, so break + } + case "BDModuleNuke": + { + warheadType = WarheadTypes.Nuke; + output.AppendLine($"- Nuclear"); + float yield = ((BDModuleNuke)partModule).yield; + float radius = ((BDModuleNuke)partModule).thermalRadius; + float EMPRadius = ((BDModuleNuke)partModule).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: {Math.Round(EMPRadius)} m"); + break; //shouldn't have any other module, so break + } + case "ClusterBomb": + { + warheadType = WarheadTypes.Standard; + //clusterbomb = ((ClusterBomb)partModule).submunitions.Count; //Submunitions list is populated in OnStart(), which runs after getInfo() + output.AppendLine($"Cluster Bomb"); + //output.AppendLine($" - Sub-Munition Count: {clusterbomb} "); //would need adding a submunitions count int to Clusterbomb, and updating relevant .cfgs accordingly + continue; // to grab BDExplosivepart tnt stats + } + case "BDExplosivePart": + { + warheadType = WarheadTypes.Standard; // Also, cts rod. + ((BDExplosivePart)partModule).ParseWarheadType(); + output.AppendLine($"- {((BDExplosivePart)partModule).warheadReportingName} warhead"); + float tntMass = ((BDExplosivePart)partModule).tntMass; + output.AppendLine($" - Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(tntMass), 2)} m"); + output.AppendLine($" - TNT Mass: {tntMass} kg"); + if (((BDExplosivePart)partModule)._warheadType == ExplosionFx.WarheadTypes.ShapedCharge) + output.AppendLine($" - Penetration: {ProjectileUtils.CalculatePenetration(((BDExplosivePart)partModule).caliber > 0 ? ((BDExplosivePart)partModule).caliber * 0.05f : 6f * 0.05f, 5000f, ((BDExplosivePart)partModule).tntMass * 0.0555f, ((BDExplosivePart)partModule).apMod):F2} mm"); + continue; //in case there's also an EMP module + } + case "BDCustomWarhead": + { + warheadType = WarheadTypes.Custom; + //warheadType = WarheadTypes.Standard; // Also, cts rod. + ((BDCustomWarhead)partModule).ParseWarheadType(); + output.AppendLine($"- {((BDCustomWarhead)partModule).warheadReportingName} warhead"); + output.AppendLine($"- Deviation: {Mathf.Tan(Mathf.Deg2Rad * ((BDCustomWarhead)partModule).maxDeviation) * 1000 * (1.285f / 2) * 2:F2} mrad, 80% hit"); + + BulletInfo binfo = ((BDCustomWarhead)partModule)._warheadType; + if (binfo == null) + { + Debug.LogError("[BDArmory.ModuleWeapon]: The requested bullet type (" + ((BDCustomWarhead)partModule).warheadType + ") does not exist."); + output.AppendLine($"Bullet type: {((BDCustomWarhead)partModule).warheadType} - MISSING"); + output.AppendLine(""); + continue; + } + output.AppendLine($"- Mass: {Math.Round(binfo.bulletMass, 2)} kg"); + output.AppendLine($"- Additional velocity: {Math.Round(binfo.bulletVelocity, 2)} m/s"); + //output.AppendLine($"Explosive: {binfo.explosive}"); + if (binfo.projectileCount > 1) + { + output.AppendLine($"- Cannister Warhead"); + output.AppendLine($" - Submunition count: {binfo.projectileCount}"); + } + bool sabotTemp = (((((binfo.bulletMass * 1000) / ((binfo.caliber * binfo.caliber * Mathf.PI / 400f) * 19f) + 1f) * 10f) > binfo.caliber * 4f)) ? true : false; + + output.AppendLine($"- Estimated Penetration: {ProjectileUtils.CalculatePenetration(binfo.caliber, binfo.bulletVelocity + optimumAirspeed, binfo.bulletMass, binfo.apBulletMod, muParam1: sabotTemp ? 0.9470311374f : 0.656060636f, muParam2: sabotTemp ? 1.555757746f : 1.20190930f, muParam3: sabotTemp ? 2.753715499f : 1.77791929f, sabot: sabotTemp):F2} mm"); + if ((binfo.tntMass > 0) && !binfo.nuclear) + { + output.AppendLine($"- Blast:"); + output.AppendLine($" - tnt mass: {Math.Round(binfo.tntMass, 3)} kg"); + output.AppendLine($" - radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(binfo.tntMass), 2)} m"); + if (binfo.fuzeType.ToLower() == "timed" || binfo.fuzeType.ToLower() == "proximity" || binfo.fuzeType.ToLower() == "flak") + { + output.AppendLine($"- Air detonation: True"); + output.AppendLine($" - auto timing: {(binfo.fuzeType.ToLower() != "proximity")}"); + } + else + { + 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) + { + output.AppendLine($"- Nuclear Warhead:"); + output.AppendLine($" - yield: {Math.Round(binfo.tntMass, 3)} kT"); + if (binfo.EMP) + { + output.AppendLine($" - generates EMP"); + } + } + if (binfo.EMP && !binfo.nuclear) + { + output.AppendLine($"- BlueScreen:"); + output.AppendLine($" - EMP buildup per hit:{binfo.caliber * Mathf.Clamp(binfo.bulletMass - binfo.tntMass, 0.1f, 100)}"); + } + if (binfo.impulse != 0) + { + output.AppendLine($"- Concussive:"); + output.AppendLine($" - Impulse to target:{binfo.impulse}"); + } + if (binfo.massMod != 0) + { + output.AppendLine($"- Gravitic:"); + output.AppendLine($" - weight added per hit:{binfo.massMod * 1000} kg"); + } + if (binfo.incendiary) + { + output.AppendLine($"- Incendiary"); + } + if (binfo.beehive) + { + output.AppendLine($"- Beehive Warhead:"); + string[] subMunitionData = binfo.subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + if (subMunitionData.Length < 2 || !int.TryParse(subMunitionData[1], out int count)) count = 1; + BulletInfo sinfo = BulletInfo.bullets[projType]; + output.AppendLine($" - deploys {count}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); + } + continue; //in case there's also an HE module + } + case "ModuleEMP": + { + warheadType = WarheadTypes.EMP; + output.AppendLine($"- Electro-Magnetic Pulse"); + float proximity = ((ModuleEMP)partModule).proximity; + output.AppendLine($" - EMP Blast Radius: {proximity} m"); + continue; //in case a BDExplosivepart is also present + } + default: continue; + } + // Don't break, as some missiles contain multiple warhead types (e.g., Standard + EMP). + } + if (warheadType == WarheadTypes.Kinetic) + { + if (blastPower > 0) + { + warheadType = WarheadTypes.Legacy; + output.AppendLine($"- Legacy Missile"); + output.AppendLine($"- Blast Power: {blastPower}"); + } + else + output.AppendLine($"- Kinetic Impactor"); + } + + return output.ToString(); + } + + #region ExhaustPrefabPooling + static Dictionary exhaustPrefabPool = new Dictionary(); + List exhaustPrefabs = new List(); + + static void AttachExhaustPrefab(string prefabPath, MissileLauncher missileLauncher, Transform exhaustTransform) + { + if (!CreateExhaustPool(prefabPath)) + { + Debug.LogError($"[BDArmory.MissileLauncher]: Failed to get model {prefabPath} for {missileLauncher.part.partInfo.name}. Check that the file exists!"); + return; + } + var exhaustPrefab = exhaustPrefabPool[prefabPath].GetPooledObject(); + exhaustPrefab.SetActive(true); + using (var emitter = exhaustPrefab.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + emitter.Current.emit = false; + } + exhaustPrefab.transform.parent = exhaustTransform; + exhaustPrefab.transform.localPosition = Vector3.zero; + exhaustPrefab.transform.localRotation = Quaternion.identity; + missileLauncher.exhaustPrefabs.Add(exhaustPrefab); + missileLauncher.part.OnJustAboutToDie += missileLauncher.DetachExhaustPrefabs; + missileLauncher.part.OnJustAboutToBeDestroyed += missileLauncher.DetachExhaustPrefabs; + 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 bool CreateExhaustPool(string prefabPath) + { + if (exhaustPrefabPool == null) + { exhaustPrefabPool = new Dictionary(); } + if (!exhaustPrefabPool.ContainsKey(prefabPath) || exhaustPrefabPool[prefabPath] == null || exhaustPrefabPool[prefabPath].poolObject == null) + { + var exhaustPrefabTemplate = GameDatabase.Instance.GetModel(prefabPath); + if (exhaustPrefabTemplate == null) return false; + exhaustPrefabTemplate.SetActive(false); + exhaustPrefabPool[prefabPath] = ObjectPool.CreateObjectPool(exhaustPrefabTemplate, 1, true, true); + } + return true; + } + + void DetachExhaustPrefabs() + { + if (part != null) + { + part.OnJustAboutToDie -= DetachExhaustPrefabs; + part.OnJustAboutToBeDestroyed -= DetachExhaustPrefabs; + } + foreach (var exhaustPrefab in exhaustPrefabs) + { + if (exhaustPrefab == null) continue; + exhaustPrefab.transform.parent = null; + exhaustPrefab.SetActive(false); + 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; + if (decoupleBoosters && boosterMass > 0) mass -= boosterMass; + 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/ModuleMissileMagazine.cs b/BDArmory/Weapons/Missiles/ModuleMissileMagazine.cs new file mode 100644 index 000000000..7a9ea6803 --- /dev/null +++ b/BDArmory/Weapons/Missiles/ModuleMissileMagazine.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.Text; +using BDArmory.Utils; + +namespace BDArmory.Weapons.Missiles +{ + public class ModuleMissileMagazine : PartModule, IPartMassModifier, IPartCostModifier + { + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => Mathf.Max(ammoCount, 0) * missileMass; + + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.FIXED; + public float GetModuleCost(float baseCost, ModifierStagingSituation situation) => Mathf.Max(ammoCount, 0) * missileCost; + public ModifierChangeWhen GetModuleCostChangeWhen() => ModifierChangeWhen.FIXED; + + private float missileMass = 0; + private float missileCost = 0; + + [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_WeaponName", guiActiveEditor = false), UI_Label(affectSymCounterparts = UI_Scene.All, scene = UI_Scene.All)]//Weapon Name + public string loadedMissileName = ""; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_OrdnanceAvailable"),//Ordnance Available +UI_FloatRange(minValue = 1f, maxValue = 4, stepIncrement = 1f, scene = UI_Scene.All)] + public float ammoCount = 1; + + [KSPField(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_OrdnanceAvailable"),//Ordnance Available +UI_ProgressBar(affectSymCounterparts = UI_Scene.None, controlEnabled = false, scene = UI_Scene.Flight, maxValue = 100, minValue = 0, requireFullControl = false)] + public float ammoRemaining = 1; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#autoLOC_8003393"), UI_FloatRange(minValue = 1, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)]//Priority + public float priority = 1; + + [KSPField(isPersistant = true)] + public string MissileName; + + [KSPField] public string RailNode = "rail"; //name of attachnode for VLS MMLs to set missile loadout + + [KSPField] public bool AccountForAmmo = true; + [KSPField] public float maxAmmo = 20; + + [KSPField(isPersistant = true)] + public Vector2 missileScale = Vector2.zero; + + [KSPField] + public string scaleTransformName; + Transform ScaleTransform; + + [KSPField] public bool isRectangularMagazine = true; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorWidth"),// Length +UI_FloatRange(minValue = 1f, maxValue = 4, stepIncrement = 1f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float rowCount = 1; + + FloatCurve cylinderScale = null; + + public void Start() + { + if (cylinderScale == null && !isRectangularMagazine) + { + cylinderScale = new FloatCurve(); //diameter of a circle to fix x uniform smaller circles within its area + cylinderScale.Add(1, 1f); + cylinderScale.Add(2, 2f); + cylinderScale.Add(3, 2.15f); + cylinderScale.Add(4, 2.414f); + cylinderScale.Add(5, 2.7f); + cylinderScale.Add(6, 3f); + cylinderScale.Add(7, 3f); + cylinderScale.Add(8, 3.3f); + cylinderScale.Add(9, 3.6f); + cylinderScale.Add(10, 3.8f); + cylinderScale.Add(11, 3.92f); + cylinderScale.Add(12, 4f); + cylinderScale.Add(13, 4.236f); + cylinderScale.Add(14, 4.33f); + cylinderScale.Add(15, 4.52f); + cylinderScale.Add(16, 4.615f); + cylinderScale.Add(17, 4.792f); + cylinderScale.Add(18, 4.864f); + cylinderScale.Add(19, 4.864f); + cylinderScale.Add(20, 5.122f); + } + if (HighLogic.LoadedSceneIsEditor) + { + if (missileScale == Vector2.zero) missileScale = new Vector2(3, 0.25f); + GameEvents.onEditorShipModified.Add(ShipModified); + if (!isRectangularMagazine) + { + Fields["rowCount"].guiActiveEditor = false; + } + UI_FloatRange Ammo = (UI_FloatRange)Fields["ammoCount"].uiControlEditor; + Ammo.maxValue = maxAmmo; + } + if (HighLogic.LoadedSceneIsFlight) + { + UI_ProgressBar ordnance = (UI_ProgressBar)Fields["ammoRemaining"].uiControlFlight; + ordnance.maxValue = ammoCount; + ammoRemaining = ammoCount; + } + GUIUtils.RefreshAssociatedWindows(part); + StartCoroutine(DelayedStart()); + } + + IEnumerator DelayedStart() + { + yield return new WaitForFixedUpdate(); + + if (AccountForAmmo) + { + if (!String.IsNullOrEmpty(MissileName)) + { + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) + continue; + if (parts.Current.partPrefab.partInfo.name != MissileName) continue; + missileMass = parts.Current.partPrefab.mass; + missileCost = parts.Current.partPrefab.partInfo.cost; + break; + } + } + } + else + { + missileMass = 0; + missileCost = 0; + } + if (string.IsNullOrEmpty(scaleTransformName)) + { + Fields["Scale"].guiActiveEditor = false; + } + else + { + ScaleTransform = part.FindModelTransform(scaleTransformName); + UI_FloatRange scale = (UI_FloatRange)Fields["ammoCount"].uiControlEditor; + scale.onFieldChanged = UpdateScale; + UI_FloatRange rows = (UI_FloatRange)Fields["rowCount"].uiControlEditor; + rows.maxValue = Mathf.CeilToInt(BDAMath.Sqrt(maxAmmo)); + rows.onFieldChanged = UpdateScale; + } + UpdateScaling(missileScale); + } + + public void UpdateScale(BaseField field, object obj) + { + if (ScaleTransform != null) + { + if (isRectangularMagazine) + ScaleTransform.localScale = new Vector3(missileScale.x + 0.05f, missileScale.y * 1.5f * rowCount, missileScale.y * 1.5f * Mathf.CeilToInt(ammoCount / rowCount)); //missile length, missileWidth + else + ScaleTransform.localScale = new Vector3(((missileScale.x + 0.05f) * Mathf.CeilToInt(ammoCount / 20)), missileScale.y * 1.5f * cylinderScale.Evaluate(ammoCount), missileScale.y * 1.5f * cylinderScale.Evaluate(ammoCount)); + //default model scaling is 1x1x1m. Cylinders max diameter at 20 missiles, increase length if mag capacity more + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + var mmm = sym.Current.FindModuleImplementing(); + if (mmm == null) continue; + mmm.missileScale = missileScale; + mmm.rowCount = rowCount; + mmm.UpdateScaling(missileScale); + } + } + } + + public void UpdateScaling(Vector2 scale) + { + //Debug.Log($"[MMM debug] Calling missile mag UpdateScaling, scale({scale.x}, {scale.y})"); + if (ScaleTransform != null) + { + if (isRectangularMagazine) + ScaleTransform.localScale = new Vector3(scale.x + 0.05f, scale.y * 1.5f * rowCount, scale.y * 1.5f * Mathf.CeilToInt(ammoCount / rowCount)); + else + ScaleTransform.localScale = new Vector3(((scale.x + 0.05f) * Mathf.CeilToInt(ammoCount / 20)), scale.y * 1.5f * cylinderScale.Evaluate(ammoCount), scale.y * 1.5f * cylinderScale.Evaluate(ammoCount)); + } + } + private void OnDestroy() + { + GameEvents.onEditorShipModified.Remove(ShipModified); + } + + public void ShipModified(ShipConstruct data) + { + if (part.children.Count > 0) + { + using (List.Enumerator stackNode = part.attachNodes.GetEnumerator()) + while (stackNode.MoveNext()) + { + if (stackNode.Current == null) continue; + 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()) + { + MissileName = missile.name; + float scaleMax = 0f; + float scaleMin = 0f; + var childColliders = missile.GetComponentsInChildren(includeInactive: false); + foreach (var col in childColliders) + { + if (col) + { + scaleMax = Mathf.Max(scaleMax, Mathf.Max(col.bounds.size.x, col.bounds.size.y, col.bounds.size.z)); + scaleMin = Mathf.Max(scaleMin, Mathf.Min(col.bounds.size.x, col.bounds.size.y, col.bounds.size.z)); + } + } + missileScale = new Vector2(scaleMax, scaleMin); + //Debug.Log($"[MissileMagazine] Missile bounds are {missile.collider.bounds.size.x.ToString("0.00")}, {missile.collider.bounds.size.y.ToString("0.00")}, {missile.collider.bounds.size.z.ToString("0.00")}"); + //this will grab missile body dia/length, something something folding fins. But given BDA missiles are IRL scale instead of ~0.7 kerbalscale, including fins would make the mags *really* large + MissileLauncher MLConfig = missile.FindModuleImplementing(); + Fields["loadedMissileName"].guiActive = true; + Fields["loadedMissileName"].guiActiveEditor = true; + loadedMissileName = MLConfig.GetShortName(); + GUIUtils.RefreshAssociatedWindows(part); + missileMass = AccountForAmmo ? missile.partInfo.partPrefab.mass : 0; + missileCost = AccountForAmmo ? missile.partInfo.cost : 0; + EditorLogic.DeletePart(missile); + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + var mmm = sym.Current.FindModuleImplementing(); + if (mmm == null) continue; + mmm.MissileName = MissileName; + } + UpdateScale(null, null); + } + } + } + } + } + } + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + + output.Append(Environment.NewLine); + output.AppendLine($"Missile Magazine"); + output.AppendLine($"Attach a missile to this to load magazine with selected ordnance"); + output.AppendLine($"- Maximum Ordnance: {maxAmmo}"); + output.AppendLine($"- Ammo has Mass/Cost: {AccountForAmmo}"); + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Weapons/Missiles/ModuleMissileRearm.cs b/BDArmory/Weapons/Missiles/ModuleMissileRearm.cs new file mode 100644 index 000000000..cce467915 --- /dev/null +++ b/BDArmory/Weapons/Missiles/ModuleMissileRearm.cs @@ -0,0 +1,345 @@ +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; +using BDArmory.Utils; + +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_OrdnanceAvailable"),//Ordnance Available +UI_FloatRange(minValue = 1f, maxValue = 4, stepIncrement = 1f, scene = UI_Scene.Editor)] + public float railAmmo = 1; //munitions included with/loaded in the launcher (VLS/CLS pods, etc) + public int magazineAmmo = 0; + public int ammoCount => magazineAmmo + (int)railAmmo; + + [KSPField(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_OrdnanceAvailable"),//Ordnance Available +UI_ProgressBar(affectSymCounterparts = UI_Scene.None, controlEnabled = false, scene = UI_Scene.Flight, maxValue = 100, minValue = 0, requireFullControl = false)] + public float ammoRemaining = 1; + + [KSPField(isPersistant = true)] + public string MissileName = "bahaAim120"; + + [KSPField] public float reloadTime = 5f; + [KSPField] public bool AccountForAmmo = true; + [KSPField] public float maxAmmo = -1; + + public List linkedMagazines; + //public float tntmass = 1; + AvailablePart missilePart; + public Part SpawnedMissile; + public bool SpawnMissile(Transform MissileTransform, float offset = 0, bool deductAmmo = true) + { + if (railAmmo >= 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) + { + railAmmo--; + ammoRemaining--; + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.ModuleMissileRearm] spawned " + SpawnedMissile.name + "; ammo remaining: " + railAmmo); + return true; + } + } + } + } + return false; + } + + public void loadOrdnance(int tubesToReload) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.ModuleMissileRearm] reloading {tubesToReload} launchrails, {railAmmo} ordnance in launcher, queuing {tubesToReload - railAmmo} new munitions from magazine"); + if (linkedMagazines.Count > 0 && ammoCount > 0 && railAmmo < tubesToReload) //no/not enough ammo internal to launcher? grab some from magazine + { + int neededReloads = (int)(tubesToReload - railAmmo); + for (int t2r = 0; t2r < neededReloads; t2r++) + { + ModuleMissileMagazine priorityMagazine = null; + float lastPriority = -1; + float lastAmmoQty = -1; + foreach (var mag in linkedMagazines) + { + if (mag == null || mag.ammoCount < 1) continue; // Ignore broken or empty mags. + if (mag.priority < lastPriority) continue; // Ignore lower priority mags. + if (mag.priority > lastPriority) + { + lastAmmoQty = -1; // Reset the ammo quantity, so that the quantity of lower priority mags aren't considered. + lastPriority = mag.priority; + } + if (mag.ammoCount < lastAmmoQty) continue; // Ignore mags with the same priority but less ammo. + lastAmmoQty = mag.ammoCount; + priorityMagazine = mag; + } + if (priorityMagazine != null) + { + priorityMagazine.ammoCount--; //transfer ammo from mag to launcher + priorityMagazine.ammoRemaining--; + railAmmo++; + ammoRemaining++; + using (var mmr = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mmr.MoveNext()) + { + if (mmr.Current == null) continue; + if (mmr.Current.MissileName != MissileName) continue; + mmr.Current.magazineAmmo--; //syncronize magazine count across all launchers using that ammo + } + } + } + } + } + + public override void OnStart(PartModule.StartState state) + { + this.enabled = true; + this.part.force_activate(); + MultiMissileLauncher MML = part.FindModuleImplementing(); + if (MML == null || MML && MML.isClusterMissile) MissileName = part.name; + if (HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight) + StartCoroutine(GetMissileValues(MML)); + //GameEvents.onEditorShipModified.Add(ShipModified); + if (maxAmmo < 0) maxAmmo = railAmmo; + if (maxAmmo == 1) Fields["railAmmo"].guiActiveEditor = false; + else + { + UI_FloatRange Ammo = (UI_FloatRange)Fields["railAmmo"].uiControlEditor; + Ammo.maxValue = maxAmmo; + } + if (HighLogic.LoadedSceneIsFlight) + { + using (var mmm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (mmm.MoveNext()) + { + if (mmm.Current == null) continue; + if (mmm.Current.MissileName != MissileName) continue; + linkedMagazines.Add(mmm.Current); + magazineAmmo += (int)mmm.Current.ammoCount; + } + UI_ProgressBar ordnance = (UI_ProgressBar)Fields["ammoRemaining"].uiControlFlight; + ordnance.maxValue = railAmmo; + ammoRemaining = railAmmo; + } + } + + public void UpdateMissileValues() + { + StartCoroutine(GetMissileValues()); + } + IEnumerator GetMissileValues(MultiMissileLauncher MML = null) + { + 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; + if (BDArmorySettings.DEBUG_MISSILES) 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}"); + missileCost = 0; + missileMass = 0; + yield break; + } + if (AccountForAmmo) + { + missileCost = missilePart.partPrefab.partInfo.cost; + missileMass = missilePart.partPrefab.mass; + } + else + { + missileCost = 0; + missileMass = 0; + } + + if (MML != null && !MML.isClusterMissile) + { + // Parse maxOffboresight here instead of in MML since we're getting the partPrefab here anyways + string maxOffboresightString = ConfigNodeUtils.FindPartModuleConfigNodeValue(missilePart.partPrefab.partInfo.partConfig, "MissileLauncher", "maxOffBoresight"); + if (!string.IsNullOrEmpty(maxOffboresightString)) // Use the default value from the MM patch. + { + try + { + float maxOffboresight = float.Parse(maxOffboresightString); + MML.updateMaxOffBoresight(maxOffboresight); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.ModuleMissileRearm]: setting maxOffBoresight of " + part + " to " + maxOffboresight); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.ModuleMissileRearm]: Failed to parse maxOffBoresight configNode: " + e.Message); + } + } + MML.subMunitionPath = MML.GetMeshurl((UrlDir.UrlConfig)GameDatabase.Instance.root.GetConfig(missilePart.partPrefab.partInfo.partUrl)); + } + } + + 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); + foreach (var col in childColliders) + col.enabled = false; + 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 Ordnance: {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..27c0f0f6a --- /dev/null +++ b/BDArmory/Weapons/Missiles/MultiMissileLauncher.cs @@ -0,0 +1,1587 @@ +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Guidances; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +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 = ""; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_clustermissileTriggerDistance"), UI_FloatRange(minValue = 100f, maxValue = 10000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Detonation distance override + public float clusterMissileTriggerDist = 750; + + Transform[] launchTransforms; + public int launchTubes; + [KSPField(isPersistant = true)] public string subMunitionName; //name of missile in .cfg - e.g. "bahaAim120" + [KSPField(isPersistant = true)] public string subMunitionPath; //model path for missile + public float missileMass = 0.1f; + [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(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_WMWindow_rippleText2"), UI_FloatRange(minValue = 1, maxValue = 10, stepIncrement = 1, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Salvo + public float salvoSize = 1; + [KSPField] public bool setSalvoSize = false; //allow player to edit salvo size + [KSPField] public bool isClusterMissile = false; //cluster submunitions deployed instead of standard detonation? Fold this into warHeadType? + public bool isLaunchedClusterMissile = false; + [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 float deploySpeed = 1; //animation speed + [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; //allow setting eject speed/dir + [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 displayOrdinanceHasColliders = true; //should missile dummies have colliders? (offers somewhat of a performance boost if disabled) + [KSPField] public bool permitJettison = false; //allow jettisoning of missiles for multimissile launchrails and similar + [KSPField] public bool ignoreLauncherColliders = false; //temporarily disable missile colliders to let them clear the launcher, for large-scale VLS or similar. -WARNING- has some effect on missile flight + AnimationState deployState; + public ModuleMissileRearm missileSpawner = null; + MissileLauncher missileLauncher = null; + [KSPField] public bool adjustMissileVOffset = false; //should missile vertical offset dynamically adjust based on missile diameter (for MMLs on adjustable rails, etc) + [KSPField(isPersistant = true)] float attachedMissileDiameter = 0; + MissileFire FiredByWM = null; // Assigned when fired and then not updated even if the parent craft changes their primary WM. + private int tubesFired = 0; + [KSPField(isPersistant = true)] + private bool LoadoutModified = false; + public BDTeam Team = BDTeam.Get("Neutral"); + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorWidth"),// Length + UI_FloatRange(minValue = 0.5f, maxValue = 2, stepIncrement = 0.05f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.Editor)] + public float Scale = 1; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorLength"),// Length + UI_FloatRange(minValue = 0.5f, maxValue = 2, stepIncrement = 0.05f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.Editor)] + public float Length = 1; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_Offset"),// Ordnance Offset + UI_FloatRange(minValue = -1, maxValue = 1, stepIncrement = 0.1f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.Editor)] + public float attachOffset = 0; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_Deploy_Time"),// Deploy Time + UI_FloatRange(minValue = 0, maxValue = 5, stepIncrement = 0.1f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.Editor)] + public float deployTime = 0.5f; + + [KSPField] + public float scaleMax = 2; + + [KSPField] + public float offsetMax = 1; + + [KSPField] + public string lengthTransformName; + Transform LengthTransform; + Vector3 LengthTransformOrigScale; + + [KSPField] + public string scaleTransformName; + Transform ScaleTransform; + Vector3 ScaleTransformOrigScale; + + public MissileTurret turret; + + List targetsAssigned; + + public bool toggleBay = true; + [KSPEvent(guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ToggleAnimation", active = true)]//Disable Engage Options + public void ToggleBay() + { + toggleBay = !toggleBay; + + if (toggleBay == false) + { + Events["ToggleBay"].guiName = StringUtils.Localize("#autoLOC_502069");//"Open" + } + else + { + Events["ToggleBay"].guiName = StringUtils.Localize("#autoLOC_502051");//""Close" + } + if (deployState != null) + { + deployState.normalizedTime = HighLogic.LoadedSceneIsFlight ? 0 : toggleBay ? 1 : 0; + 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; + ml.deployState.normalizedTime = toggleBay ? 1 : 0; + } + } + } + } + public void Start() + { + MakeMissileArray(); + for (int i = 0; i < launchTransforms.Length; i++) + { + launchTransforms[i].localPosition = new Vector3(launchTransforms[i].localPosition.x, launchTransforms[i].localPosition.y, launchTransforms[i].localPosition.z + (attachOffset * Mathf.Max(Scale, Length))); + } + GameEvents.onEditorShipModified.Add(ShipModified); + if (HighLogic.LoadedSceneIsFlight) + { + GameEvents.onPartDie.Add(OnPartDie); + if (isClusterMissile && vessel.Parts.Count == 1) isLaunchedClusterMissile = true; + } + if (!string.IsNullOrEmpty(deployAnimationName)) + { + Events["ToggleBay"].guiActiveEditor = true; + deployState = GUIUtils.SetUpSingleAnimation(deployAnimationName, part); + if (deployState != null) + { + deployState.normalizedTime = HighLogic.LoadedSceneIsFlight ? 0 : toggleBay ? 1 : 0; + deployState.speed = 0; + deployState.enabled = true; + } + } + targetsAssigned = new List(); + launchTubes = launchTransforms.Length; + StartCoroutine(DelayedStart()); + } + + IEnumerator DelayedStart() + { + yield return new WaitForFixedUpdate(); + missileLauncher = part.FindModuleImplementing(); + missileSpawner = part.FindModuleImplementing(); + turret = part.FindModuleImplementing(); + if (turret != null) turret.missilepod = missileLauncher; + 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 ? 1 : salvoSize * 5; + missileSpawner.railAmmo = isClusterMissile ? 1 : launchTubes; + missileSpawner.maxAmmo = isClusterMissile ? 1 : launchTubes; + missileSpawner.Fields["railAmmo"].guiActiveEditor = false; + 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"); + } + if (BDArmorySettings.LIMITED_ORDINANCE) missileSpawner.railAmmo = isClusterMissile ? 1 : launchTubes; + 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; + missileLauncher.MissileReferenceTransform = part.FindModelTransform("missileTransform"); + if (!missileLauncher.MissileReferenceTransform) + { + missileLauncher.MissileReferenceTransform = launchTransforms[0]; + } + + if (isClusterMissile) + { + if (isLaunchedClusterMissile) + { + missileSpawner.MissileName = subMunitionName; + missileSpawner.railAmmo = launchTransforms.Length; + missileLauncher.DetonationDistance = clusterMissileTriggerDist; + missileLauncher.blastRadius = clusterMissileTriggerDist; + } + else + { + missileSpawner.MissileName = missileLauncher.missileName; //ClMsl set to base name in case of reloadable rails, reset to submuition name after launch + missileLauncher.DetonationDistance = 0; + missileLauncher.blastRadius = 0; + } + 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["railAmmo"].guiActive = false; + missileSpawner.Fields["railAmmo"].guiActiveEditor = false; + } + } + else + { + Fields["clusterMissileTriggerDist"].guiActive = false; + Fields["clusterMissileTriggerDist"].guiActiveEditor = false; + } + Fields["salvoSize"].guiActive = setSalvoSize; + Fields["salvoSize"].guiActiveEditor = setSalvoSize; + 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.Fields["decoupleForward"].guiActive = false; + missileLauncher.Fields["decoupleForward"].guiActiveEditor = false; + missileLauncher.decoupleForward = true; + } + float bRadius = 0; + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + 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; + var ML = parts.Current.partPrefab.FindModuleImplementing(); + if (!string.IsNullOrEmpty(subMunitionName)) + { + if (ML != null) + { + loadedMissileName = ML.GetShortName(); + var CLM = parts.Current.partPrefab.FindModuleImplementing(); + if (CLM != null) + if (CLM.isClusterMissile) + { + Fields["clusterMissileTriggerDist"].guiActive = true; + Fields["clusterMissileTriggerDist"].guiActiveEditor = true; + } + } + else Debug.LogError("[BDArmory.MultiMissileLauncher]: submunition MissileLauncher module null! Check subMunitionName is correct"); + } + break; + } + if (bRadius == 0) + { + Debug.Log("[multiMissileLauncher.GetBlastRadius] No BDExplosivePart found! Using default value"); + bRadius = BlastPhysicsUtils.CalculateBlastRange(tntMass); + } + missileLauncher.blastRadius = bRadius; + + if (missileLauncher.DetonationDistance == -1) + { + 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(); + + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) + continue; + if (parts.Current.partPrefab.partInfo.name != subMunitionName) continue; + if (LoadoutModified) UpdateFields(parts.Current.partPrefab.FindModuleImplementing(), false); + missileMass = parts.Current.partPrefab.mass; + break; + } + if (missileSpawner.maxAmmo > 1) + { + UI_FloatRange Ammo = (UI_FloatRange)missileSpawner.Fields["railAmmo"].uiControlEditor; + Ammo.onFieldChanged = updateOffset; + } + + if (string.IsNullOrEmpty(scaleTransformName)) + { + Fields["Scale"].guiActiveEditor = false; + } + else + { + ScaleTransform = part.FindModelTransform(scaleTransformName); + if (ScaleTransform != null) + { + ScaleTransformOrigScale = part.partInfo.partPrefab.FindModelTransform(scaleTransformName).localScale; //baseConfig ? ScaleTransform.localScale * (baseConfig.Scale / Scale) : ScaleTransform.localScale; + UI_FloatRange AWidth = (UI_FloatRange)Fields["Scale"].uiControlEditor; + AWidth.maxValue = scaleMax; + if (Scale > scaleMax) Scale = scaleMax; + AWidth.onFieldChanged = updateScale; + } + } + if (string.IsNullOrEmpty(lengthTransformName)) + { + Fields["Length"].guiActiveEditor = false; + } + else + { + LengthTransform = part.FindModelTransform(lengthTransformName); + if (LengthTransform != null) + { + LengthTransformOrigScale = part.partInfo.partPrefab.FindModelTransform(scaleTransformName).localScale; + UI_FloatRange ALength = (UI_FloatRange)Fields["Length"].uiControlEditor; + ALength.maxValue = scaleMax; + if (Length > scaleMax) Length = scaleMax; + ALength.onFieldChanged = updateLength; + } + } + if (adjustMissileVOffset || !string.IsNullOrEmpty(lengthTransformName)) + { + UI_FloatRange AOffset = (UI_FloatRange)Fields["attachOffset"].uiControlEditor; + AOffset.maxValue = offsetMax; + AOffset.minValue = -offsetMax; + AOffset.onFieldChanged = updateOffset; + } + else Fields["attachOffset"].guiActiveEditor = false; + + UpdateLengthAndScale(Scale, Length, attachOffset); + } + + public void updateMaxOffBoresight(float maxOffBoresight) + { + // Need to update the maxOffBoresight value with the value from ModuleMissileRearm + // except for in the case of this being a cluster missile or we're overriding the reference + // transform, as is the case with VLS cells + if (isClusterMissile || overrideReferenceTransform) + return; + + // Have to wait until missileLauncher is loaded before doing this + StartCoroutine(UpdateMaxOffBoresightRoutine(maxOffBoresight)); + } + + public IEnumerator UpdateMaxOffBoresightRoutine(float maxOffBoresight) + { + yield return new WaitUntil(() => missileLauncher != null); + + missileLauncher.maxOffBoresight = maxOffBoresight; + } + + public void updateScale(BaseField field, object obj) + { + ScaleTransform.localScale = ScaleTransformOrigScale * Scale; + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + var mml = sym.Current.FindModuleImplementing(); + if (mml == null) continue; + mml.Scale = Scale; + mml.UpdateLengthAndScale(Scale, Length, attachOffset); + } + if (LengthTransform) updateLength(null, null); + PopulateMissileDummies(); + } + public void updateLength(BaseField field, object obj) + { + LengthTransform.localScale = new Vector3(LengthTransformOrigScale.x, LengthTransformOrigScale.y, LengthTransformOrigScale.z * (ScaleTransform ? (Length / Scale) : Length)); + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + var mml = sym.Current.FindModuleImplementing(); + if (mml == null) continue; + mml.Length = Length; + mml.UpdateLengthAndScale(Scale, Length, attachOffset); + + } + PopulateMissileDummies(); + } + public void updateOffset(BaseField field, object obj) + { + for (int i = 0; i < launchTransforms.Length; i++) + { + launchTransforms[i].localPosition = new Vector3(launchTransforms[i].localPosition.x, launchTransforms[i].localPosition.y, attachOffset * Mathf.Max(Scale, Length)); + } + PopulateMissileDummies(true); + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + var mml = sym.Current.FindModuleImplementing(); + if (mml == null) continue; + mml.attachOffset = attachOffset; + mml.UpdateLengthAndScale(Scale, Length, attachOffset); + } + } + public void UpdateLengthAndScale(float scale, float length, float offset) + { + if (ScaleTransform != null) + ScaleTransform.localScale = ScaleTransformOrigScale * scale; + if (LengthTransform != null) + LengthTransform.localScale = new Vector3(LengthTransformOrigScale.x, LengthTransformOrigScale.y, (LengthTransformOrigScale.z / scale) * length); + if (!string.IsNullOrEmpty(lengthTransformName)) + { + for (int i = 0; i < launchTransforms.Length; i++) + { + launchTransforms[i].localPosition = new Vector3(launchTransforms[i].localPosition.x, launchTransforms[i].localPosition.y, attachOffset * Mathf.Max(Scale, Length)); + } + } + PopulateMissileDummies(true); + } + 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 == null) continue; + if (stackNode.Current?.nodeType != AttachNode.NodeType.Stack) continue; + if (stackNode.Current.id.Contains(RailNode)) + { + 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)); + if (adjustMissileVOffset) + { + var missileCOL = missile.GetComponentInChildren(); + if (missileCOL) attachedMissileDiameter = Mathf.Min(missileCOL.bounds.size.x, missileCOL.bounds.size.y, missileCOL.bounds.size.z); + } + PopulateMissileDummies(true); + MissileLauncher MLConfig = missile.FindModuleImplementing(); + LoadoutModified = true; + Fields["loadedMissileName"].guiActive = true; + Fields["loadedMissileName"].guiActiveEditor = true; + loadedMissileName = MLConfig.GetShortName(); + GUIUtils.RefreshAssociatedWindows(part); + if (missileSpawner) + { + missileSpawner.MissileName = subMunitionName; + missileSpawner.UpdateMissileValues(); + } + UpdateFields(MLConfig, true); + var explosivePart = missile.FindModuleImplementing(); + tntMass = explosivePart != null ? explosivePart.tntMass : 0; + missileLauncher.blastRadius = BlastPhysicsUtils.CalculateBlastRange(tntMass); + missileMass = missile.partInfo.partPrefab.mass; + EditorLogic.DeletePart(missile); + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + var mml = sym.Current.FindModuleImplementing(); + if (mml == null) continue; + mml.subMunitionName = subMunitionName; + mml.subMunitionPath = subMunitionPath; + mml.PopulateMissileDummies(true); + mml.LoadoutModified = true; + if (mml.missileSpawner) + { + mml.missileSpawner.MissileName = subMunitionName; + mml.missileSpawner.UpdateMissileValues(); + } + mml.UpdateFields(MLConfig, true); + mml.missileLauncher.blastRadius = BlastPhysicsUtils.CalculateBlastRange(tntMass); + } + } + } + } + } + } + } + + public string GetMeshurl(UrlDir.UrlConfig cfgdir) + { + //check if part uses a MODEL node to grab an (external?) .mu file + string url; + //float invRescaleFactor = 1f / part.rescaleFactor; + dummyScale = Vector3.one; //new Vector3(invRescaleFactor, invRescaleFactor, invRescaleFactor); + if (cfgdir.config.HasNode("MODEL")) + { + var MODEL = cfgdir.config.GetNode("MODEL"); + url = MODEL.GetValue("model") ?? ""; + if (MODEL.HasValue("scale")) + { + string[] strings = MODEL.GetValue("scale").Split(","[0]); + dummyScale.x *= float.Parse(strings[0]); + dummyScale.y *= float.Parse(strings[1]); + dummyScale.z *= float.Parse(strings[2]); + } + if (cfgdir.config.HasValue("rescaleFactor")) + { + float scale = float.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 = float.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.antiradTargetTypes = MLConfig.antiradTargetTypes; + 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.terminalGuidanceShouldActivate = MLConfig.terminalGuidanceShouldActivate; + missileLauncher.terminalGuidanceType = MLConfig.terminalGuidanceType; + missileLauncher.torpedo = MLConfig.torpedo; + missileLauncher.pronavGain = MLConfig.pronavGain; + missileLauncher.kappaAngle = MLConfig.kappaAngle; + missileLauncher.gLimit = MLConfig.gLimit; + missileLauncher.gMargin = MLConfig.gMargin; + missileLauncher.terminalHoming = MLConfig.terminalHoming; + missileLauncher.terminalHomingType = MLConfig.terminalHomingType; + missileLauncher.liftArea = MLConfig.liftArea; + missileLauncher.dragArea = MLConfig.dragArea; + missileLauncher.useSimpleDrag = MLConfig.useSimpleDrag; + missileLauncher.simpleCoD = MLConfig.simpleCoD; + missileLauncher.maxTorque = MLConfig.maxTorque; + missileLauncher.simpleStableTorque = MLConfig.simpleStableTorque; + missileLauncher.deployedDrag = MLConfig.deployedDrag; + missileLauncher.maneuvergLimit = MLConfig.maneuvergLimit; + 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.maxCruiseSpeed = MLConfig.CruiseSpeed; + missileLauncher.canCruisePopup = MLConfig.CruisePopup; + missileLauncher.canDetMinDist = MLConfig.DetonateAtMinimumDistance; + 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 + + if (configurableSettings) + { + missileLauncher.maxStaticLaunchRange = MLConfig.maxStaticLaunchRange; + missileLauncher.minStaticLaunchRange = MLConfig.minStaticLaunchRange; + missileLauncher.engageRangeMin = MLConfig.minStaticLaunchRange; + missileLauncher.engageRangeMax = MLConfig.maxStaticLaunchRange; + 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.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.GetBlastRadius(); + GUIUtils.RefreshAssociatedWindows(missileLauncher.part); + missileLauncher.ParseLiftDragSteerTorque(); + // Because we already set the values from the true base config, we do **not** check the base config in SetFields + missileLauncher.SetFields(false); + 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((int)salvoSize, launchTransforms.Length); + if (subMunitionPath != "") + { + PopulateMissileDummies(true); + } + UI_FloatRange salvo = (UI_FloatRange)Fields["salvoSize"].uiControlEditor; + salvo.maxValue = launchTransforms.Length; + } + public void PopulateMissileDummies(bool refresh = false) + { + bool populateDummies = true; + if (refresh && displayOrdinance) + { + populateDummies = SetupMissileDummyPool(subMunitionPath); + foreach (var existingDummy in part.GetComponentsInChildren()) + { + existingDummy.Deactivate(); //if changing out missiles loaded into a VLS or similar, reset missile dummies + } + } + int loadedOrdnance = (BDArmorySettings.INFINITE_ORDINANCE ? launchTransforms.Length : missileSpawner != null ? Math.Min((int)missileSpawner.railAmmo, launchTransforms.Length) : launchTransforms.Length); + for (int i = 0; i < loadedOrdnance; i++) + { + if (!refresh) + { + //if (missileSpawner.ammoCount > i || isClusterMissile) + //{ + // No point in checking since this already creates a new Vector3, may as well just set it... + //if (launchTransforms[i].localScale != new Vector3(1 / Scale, 1 / Scale, 1 / (Scale * Length))) + launchTransforms[i].localScale = new Vector3(1f / Scale, 1f / Scale, 1f / (LengthTransform != null ? Length : Scale)); + //} + tubesFired = 0; + } + else + { + if (!displayOrdinance) return; + if (!populateDummies) + { + Debug.LogError($"[BDArmory.MultiMissileLauncher]: Reminder! Model {subMunitionPath} not found. Cannot populate missile dummies!"); + return; + } + GameObject dummy = mslDummyPool[subMunitionPath].GetPooledObject(); + MissileDummy dummyThis = dummy.GetComponentInChildren(); + + launchTransforms[i].localScale = new Vector3(1f / Scale, 1f / Scale, 1f / (LengthTransform != null ? Length : Scale)); + + dummy.transform.localScale = dummyScale; + dummyThis.AttachAt(part, launchTransforms[i]); + if (adjustMissileVOffset && attachedMissileDiameter > 0) dummyThis.transform.localPosition = new Vector3(attachedMissileDiameter / 2, 0, 0); + var mslAnim = dummy.GetComponentInChildren(); + if (mslAnim != null) mslAnim.enabled = false; + if (!displayOrdinanceHasColliders) + { + var childColliders = dummy.GetComponentsInChildren(includeInactive: false); + foreach (var col in childColliders) + col.enabled = false; + } + } + } + } + public void fireMissile(bool killWhenDone = false) + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (isLaunchedClusterMissile) salvoSize = launchTransforms.Length; + if (!(missileSalvo != null)) + { + FiredByWM = missileLauncher.FiredByWM; + missileSalvo = StartCoroutine(salvoFire(killWhenDone)); + if (useSymCounterpart && !killWhenDone) + { + MissileFire.TargetData targetData = new MissileFire.TargetData(missileLauncher.targetGPSCoords, missileLauncher.TimeOfLastINS, missileLauncher.INStimetogo); + 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 (FiredByWM != null) FiredByWM.SendTargetDataToMissile(ml, missileLauncher.targetVessel != null ? missileLauncher.targetVessel.Vessel : null, false, targetData, true); + MissileLauncher launcher = ml as MissileLauncher; + if (launcher != null) + { + if (launcher.HasFired || launcher.launched) continue; + launcher.FireMissile(); + } + } + } + } + } + } + IEnumerator salvoFire(bool LaunchThenDestroy) + { + int launchesThisSalvo = 0; + float timeGap = (60 / rippleRPM) * TimeWarp.CurrentRate; + int TargetID = 0; + bool missileRegistry = true; + bool removeFromQueue = !isLaunchedClusterMissile; + List firedTargets = []; + //missileSpawner.MissileName = subMunitionName; + + if (FiredByWM != null) + { + if (FiredByWM.targetsAssigned.Count > 0) targetsAssigned.Clear(); + if (FiredByWM.multiMissileTgtNum >= 2 || (missileLauncher.engageMissile && FiredByWM.PDMslTgts.Count > 0)) + { + if (missileLauncher.engageMissile && FiredByWM.PDMslTgts.Count > 0) targetsAssigned.AddRange(FiredByWM.PDMslTgts); + else if (FiredByWM.targetsAssigned.Count > 0) targetsAssigned.AddRange(FiredByWM.targetsAssigned); + } + //Debug.Log($"[BDArmory.MultiMissileLauncherDebug]: Num of targets: {targetsAssigned.Count - 1}"); + if (targetsAssigned.Count < 1) + if (FiredByWM.currentTarget != null) targetsAssigned.Add(FiredByWM.currentTarget); + } + //else Debug.Log($"[BDArmory.MultiMissileLauncherDebug]: weaponmanager null!"); + if (deployState != null) + { + deployState.enabled = true; + deployState.speed = deploySpeed / deployState.length; + yield return new WaitWhileFixed(() => deployState != null && deployState.normalizedTime < 1); //wait for animation here + if (deployState != null) + { + deployState.normalizedTime = 1; + deployState.speed = 0; + deployState.enabled = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MultiMissileLauncher]: deploy anim complete"); + } + } + if (missileSpawner == null) yield break; // Died while waiting. + 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 (missileSpawner == null) yield break; // Died while waiting. + if (launchesThisSalvo >= (int)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 (!isLaunchedClusterMissile && (missileSpawner.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) + { + tubesFired = 0; + break; + } + tubesFired++; + launchesThisSalvo++; + launchTransforms[m].localScale = Vector3.zero; + //time to deduct ammo = !clustermissile or cluster missile still on plane + //time to not deduct ammo = in-flight clMsl + if (!missileSpawner.SpawnMissile(launchTransforms[m], offset * Length, !isLaunchedClusterMissile)) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning($"[BDArmory.MissileLauncher]: Failed to spawn a missile in {missileSpawner} on {vessel.vesselName}"); + continue; + } + if (!ignoreLauncherColliders) + { + var childColliders = missileSpawner.SpawnedMissile.GetComponentsInChildren(includeInactive: false); + foreach (var col in childColliders) + col.enabled = true; + } + MissileLauncher ml = missileSpawner.SpawnedMissile.FindModuleImplementing(); + MultiMissileLauncher mml = missileSpawner.SpawnedMissile.FindModuleImplementing(); + yield return new WaitUntilFixed(() => ml == null || ml.SetupComplete); // Wait until missile fully initialized. + if (ml == null || ml.gameObject == null || !ml.gameObject.activeInHierarchy) + { + if (ml is not null) Destroy(ml); // The gameObject is gone, make sure the module goes too. + continue; // The missile died for some reason, try the next tube. + } + if (mml != null && mml.isClusterMissile) + { + mml.clusterMissileTriggerDist = clusterMissileTriggerDist; + } + var tnt = VesselModuleRegistry.GetModule(vessel, true); + if (tnt != null) + { + tnt.sourcevessel = missileLauncher.SourceVessel; + tnt.isMissile = true; + } + if (ignoreLauncherColliders) + { + ml.useSimpleDragTemp = true; + ml.clearanceLength = Mathf.Max(missileSpawner.SpawnedMissile.collider.bounds.size.x, missileSpawner.SpawnedMissile.collider.bounds.size.y, missileSpawner.SpawnedMissile.collider.bounds.size.z); + } + ml.Team = Team; + ml.SourceVessel = missileLauncher.SourceVessel; + if (string.IsNullOrEmpty(ml.GetShortName())) + { + ml.shortName = missileLauncher.GetShortName() + " Missile"; + } + if (BDArmorySettings.DEBUG_MISSILES) ml.shortName = $"{ml.SourceVessel.GetName()}'s {missileLauncher.GetShortName()} Missile"; + ml.vessel.vesselName = ml.GetShortName(); + ml.TimeFired = Time.time; + if (turret) ml.missileTurret = turret; + if (!isClusterMissile) + { + ml.DetonationDistance = missileLauncher.DetonationDistance; + ml.decoupleForward = missileLauncher.decoupleForward; + ml.dropTime = missileLauncher.dropTime; + ml.decoupleSpeed = missileLauncher.decoupleSpeed; + } + ml.DetonateAtMinimumDistance = missileLauncher.DetonateAtMinimumDistance && missileLauncher.canDetMinDist; + 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 = Mathf.Min(missileLauncher.CruiseSpeed, missileLauncher.maxCruiseSpeed); + ml.CruisePredictionTime = missileLauncher.CruisePredictionTime; + ml.CruisePopup = missileLauncher.CruisePopup && missileLauncher.canCruisePopup; + } + + if (BDArmorySettings.DEBUG_MISSILES) + { + 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.loftState = LoftStates.Boost; + ml.TimeToImpact = float.PositiveInfinity; + } + /*if (missileLauncher.GuidanceMode == GuidanceModes.AAMHybrid) + { + ml.pronavGain = missileLauncher.pronavGain; + ml.terminalHomingRange = missileLauncher.terminalHomingRange; + ml.homingModeTerminal = missileLauncher.homingModeTerminal; + }*/ + + if (missileLauncher.GuidanceMode == GuidanceModes.Kappa) + { + ml.kappaAngle = missileLauncher.kappaAngle; + ml.LoftAngle = missileLauncher.LoftAngle; + ml.LoftMaxAltitude = missileLauncher.LoftMaxAltitude; + ml.LoftRangeOverride = missileLauncher.LoftRangeOverride; + ml.LoftTermAngle = missileLauncher.LoftTermAngle; + ml.loftState = LoftStates.Boost; + } + } + + 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 (BDArmorySettings.DEBUG_MISSILES) + { + 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.loftState = LoftStates.Boost; + ml.TimeToImpact = float.PositiveInfinity; + } + + if (missileLauncher.homingModeTerminal == GuidanceModes.Kappa) + { + ml.kappaAngle = missileLauncher.kappaAngle; + ml.LoftAngle = missileLauncher.LoftAngle; + ml.LoftMaxAltitude = missileLauncher.LoftMaxAltitude; + ml.LoftRangeOverride = missileLauncher.LoftRangeOverride; + ml.LoftTermAngle = missileLauncher.LoftTermAngle; + ml.loftState = LoftStates.Boost; + } + } + } + + //ml.decoupleSpeed = 5; + if (missileLauncher.GuidanceMode == GuidanceModes.AGM) + ml.maxAltitude = missileLauncher.maxAltitude; + ml.terminalGuidanceShouldActivate = missileLauncher.terminalGuidanceShouldActivate; + //if (isClusterMissile) ml.multiLauncher.overrideReferenceTransform = true; + if (FiredByWM != null) + { + if (ml.TargetingMode == TargetingModes.Heat || ml.TargetingMode == TargetingModes.Radar || ml.TargetingMode == TargetingModes.Gps || ml.TargetingMode == TargetingModes.Inertial) + { + //Debug.Log($"[BDArmory.MultiMissileLauncherDebug]: Beginning target distribution; Num of targets: {targetsAssigned.Count - 1}; wpm targets: {wpm.targetsAssigned.Count}"); + if (targetsAssigned.Count > 0 && salvoSize > 1) + { + if (TargetID <= Mathf.Min(targetsAssigned.Count - 1, FiredByWM.multiMissileTgtNum)) + { + for (int t = TargetID; t < Mathf.Min(targetsAssigned.Count - 1, FiredByWM.multiMissileTgtNum); t++) //MML targeting independant of MissileFire target assignment, + {// and each MMl will be independantly working off the same targets list, iterating over the same first couple targets + if (FiredByWM.missilesAway.ContainsKey(targetsAssigned[t])) + { + //Debug.Log($"[MML Targeting Debug] target {t} {targetsAssigned[t].Vessel.GetName()} already has {wpm.missilesAway[targetsAssigned[t]]}/{wpm.maxMissilesOnTarget} fired on it..."); + if (FiredByWM.missilesAway[targetsAssigned[t]][0] < FiredByWM.maxMissilesOnTarget) + { + TargetID = t; //so go through and advance the target list start point based on who's already been fully engaged + //Debug.Log($"[MML Targeting Debug] advancing targetID to {TargetID}: {targetsAssigned[TargetID].Vessel.GetName()}"); + break; + } + } + else + { + TargetID = t; + //Debug.Log($"[MML Targeting Debug] setting targetID to {TargetID}: {targetsAssigned[TargetID].Vessel.GetName()}"); + break; + } + } + } + //Debug.Log($"[MML Targeting Debug] TargetID is {TargetID} of {Mathf.Min((targetsAssigned.Count), wpm.multiMissileTgtNum)}"); + if (TargetID > Mathf.Min(targetsAssigned.Count - 1, FiredByWM.multiMissileTgtNum)) + { + TargetID = 0; //if more missiles than targets, loop target list + if (salvoSize > 1) + missileRegistry = false; //this isn't ignoring subsequent missiles in the salvo for some reason? + //Debug.Log($"[MML Targeting Debug] Reached end of target list, cycling"); + } + if (targetsAssigned.Count > 0 && targetsAssigned[TargetID] != null && targetsAssigned[TargetID].Vessel != null && (!ml.hasIFF || !Team.IsFriendly(targetsAssigned[TargetID].Team))) + { + if ((VectorUtils.Angle(targetsAssigned[TargetID].position - missileLauncher.MissileReferenceTransform.position, missileLauncher.GetForwardTransform()) < missileLauncher.maxOffBoresight) //is the target more-or-less in front of the missile(launcher)? + && ((ml.engageAir && targetsAssigned[TargetID].isFlying) || + (ml.engageGround && targetsAssigned[TargetID].isLandedOrSurfaceSplashed) || + (ml.engageSLW && targetsAssigned[TargetID].isUnderwater) || + (ml.engageMissile && targetsAssigned[TargetID].isMissile))) //check engagement envelope + { + if (ml.TargetingMode == TargetingModes.Heat) //need to input a heattarget, else this will just return MissileFire.CurrentTarget + { + Vector3 direction = (targetsAssigned[TargetID].position * targetsAssigned[TargetID].velocity.magnitude) - missileLauncher.MissileReferenceTransform.position; + ml.heatTarget = BDATargetManager.GetHeatTarget(ml.SourceVessel, ml.vessel, new Ray(missileLauncher.MissileReferenceTransform.position + (5 * missileLauncher.GetForwardTransform()), direction), TargetSignatureData.noTarget, ml.lockedSensorFOV * 0.5f, ml.heatThreshold, ml.frontAspectHeatModifier, true, ml.targetCoM, ml.lockedSensorFOVBias, ml.lockedSensorVelocityBias, ml.lockedSensorVelocityMagnitudeBias, ml.lockedSensorMinAngularVelocity, FiredByWM, targetsAssigned[TargetID], IFF: ml.hasIFF); + } + if (ml.TargetingMode == TargetingModes.Radar) + { + AssignRadarTarget(ml, targetsAssigned[TargetID].Vessel); + } + if (ml.TargetingMode == TargetingModes.Gps) + { + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(targetsAssigned[TargetID].Vessel.CoM, vessel.mainBody); + } + if (ml.TargetingMode == TargetingModes.Inertial) + { + AssignInertialTarget(ml, targetsAssigned[TargetID].Vessel); + } + ml.targetVessel = targetsAssigned[TargetID]; + ml.TargetAcquired = true; + firedTargets.Add(targetsAssigned[TargetID]); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher]: Assigning target {TargetID}: {targetsAssigned[TargetID].Vessel.GetName()}; total possible targets {targetsAssigned.Count - 1}"); + } + else //else try remaining targets on the list. + { + for (int t = TargetID; t < targetsAssigned.Count - 1; t++) + { + if (targetsAssigned[t] == null) continue; + if (ml.hasIFF && Team.IsFriendly(targetsAssigned[t].Team)) continue; + if ((ml.engageAir && !targetsAssigned[t].isFlying) || + (ml.engageGround && !targetsAssigned[t].isLandedOrSurfaceSplashed) || + (ml.engageSLW && !targetsAssigned[t].isUnderwater) || + (ml.engageMissile && !targetsAssigned[t].isMissile)) continue; //check engagement envelope + + if (VectorUtils.Angle(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 == TargetingModes.Heat) + { + Vector3 direction = (targetsAssigned[t].position * targetsAssigned[t].velocity.magnitude) - missileLauncher.MissileReferenceTransform.position; + ml.heatTarget = BDATargetManager.GetHeatTarget(ml.SourceVessel, ml.vessel, new Ray(missileLauncher.MissileReferenceTransform.position + (5 * missileLauncher.GetForwardTransform()), direction), TargetSignatureData.noTarget, ml.lockedSensorFOV * 0.5f, ml.heatThreshold, ml.frontAspectHeatModifier, true, ml.targetCoM, ml.lockedSensorFOVBias, ml.lockedSensorVelocityBias, ml.lockedSensorVelocityMagnitudeBias, ml.lockedSensorMinAngularVelocity, FiredByWM, targetsAssigned[t], IFF: ml.hasIFF); + } + if (ml.TargetingMode == TargetingModes.Radar) + { + AssignRadarTarget(ml, targetsAssigned[t].Vessel); + } + if (ml.TargetingMode == TargetingModes.Gps) + { + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(targetsAssigned[t].Vessel.CoM, vessel.mainBody); + } + if (ml.TargetingMode == TargetingModes.Inertial) + { + AssignInertialTarget(ml, targetsAssigned[t].Vessel); + } + ml.targetVessel = targetsAssigned[t]; + ml.TargetAcquired = true; + firedTargets.Add(targetsAssigned[t]); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher]: Assigning backup target (targetID {TargetID}) {targetsAssigned[t].Vessel.GetName()}"); + break; + } + } + 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 = targetsAssigned.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current == null) continue; + if (item.Current.Vessel == null) continue; + if (ml.hasIFF && Team.IsFriendly(item.Current.Team)) continue; + if ((ml.engageAir && !item.Current.isFlying) || + (ml.engageGround && !item.Current.isLandedOrSurfaceSplashed) || + (ml.engageSLW && !item.Current.isUnderwater) || + (ml.engageMissile && !item.Current.isMissile)) continue; //check engagement envelope + if (VectorUtils.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 == TargetingModes.Heat) + { + Vector3 direction = (item.Current.position * item.Current.velocity.magnitude) - missileLauncher.MissileReferenceTransform.position; + ml.heatTarget = BDATargetManager.GetHeatTarget(ml.SourceVessel, ml.vessel, new Ray(missileLauncher.MissileReferenceTransform.position + (5 * missileLauncher.GetForwardTransform()), direction), TargetSignatureData.noTarget, ml.lockedSensorFOV * 0.5f, ml.heatThreshold, ml.frontAspectHeatModifier, true, ml.targetCoM, ml.lockedSensorFOVBias, ml.lockedSensorVelocityBias, ml.lockedSensorVelocityMagnitudeBias, ml.lockedSensorMinAngularVelocity, FiredByWM, item.Current, IFF: ml.hasIFF); + } + if (ml.TargetingMode == TargetingModes.Radar) + { + AssignRadarTarget(ml, item.Current.Vessel); + } + if (ml.TargetingMode == TargetingModes.Gps) + { + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(item.Current.Vessel.CoM, vessel.mainBody); + } + if (ml.TargetingMode == TargetingModes.Inertial) + { + AssignInertialTarget(ml, item.Current.Vessel); + } + ml.targetVessel = item.Current; + ml.TargetAcquired = true; + firedTargets.Add(item.Current); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher]: original target out of sensor range; engaging {item.Current.Vessel.GetName()}"); + break; + } + } + } + } + TargetID++; + if (firedTargets.Count >= FiredByWM.multiMissileTgtNum) + { + targetsAssigned.Clear(); + targetsAssigned.AddRange(firedTargets); //we've found targets up to our target allowance; cull list down to just those for distributing remaining missiles of the salvo between, if any. + } + } + else FiredByWM.SendTargetDataToMissile(ml, missileLauncher.targetVessel != null ? missileLauncher.targetVessel.Vessel : null, false); + } + else + { + //if (tubesFired > 1) missileRegistry = false; + Vector3 targetGEOPos = Vector3.zero; + Vector3 targetINScoords = Vector3.zero; + float TimeOfLastINS = -1f; + float INStimetogo = -1f; + switch (missileLauncher.TargetingMode) + { + case TargetingModes.Heat: + targetGEOPos = missileLauncher.heatTarget.geoPos; + targetINScoords = VectorUtils.WorldPositionToGeoCoords(missileLauncher.heatTarget.predictedPosition, FlightGlobals.currentMainBody); + TimeOfLastINS = missileLauncher.heatTarget.timeAcquired; + INStimetogo = missileLauncher.heatTarget.age; + break; + case TargetingModes.Radar: + targetGEOPos = missileLauncher.radarTarget.geoPos; + targetINScoords = VectorUtils.WorldPositionToGeoCoords(missileLauncher.heatTarget.predictedPosition, FlightGlobals.currentMainBody); + TimeOfLastINS = missileLauncher.radarTarget.timeAcquired; + INStimetogo = missileLauncher.radarTarget.age; + break; + case TargetingModes.Laser: + targetGEOPos = VectorUtils.WorldPositionToGeoCoords(missileLauncher.TargetPosition, FlightGlobals.currentMainBody); + break; + case TargetingModes.Inertial: + targetGEOPos = VectorUtils.WorldPositionToGeoCoords(missileLauncher.TargetPosition, FlightGlobals.currentMainBody); + targetINScoords = missileLauncher.TargetINSCoords; + TimeOfLastINS = missileLauncher.TimeOfLastINS; + INStimetogo = missileLauncher.INStimetogo; + break; + case TargetingModes.None: + break; + default: + targetGEOPos = missileLauncher.targetGPSCoords; + break; + } + + ml.TargetPosition = missileLauncher.TargetPosition; + + switch (ml.TargetingMode) + { + case TargetingModes.Laser: + ml.targetGPSCoords = targetGEOPos; + ml.lockedCamera = missileLauncher.lockedCamera; + break; + case TargetingModes.Gps: + if (missileLauncher.lockedCamera != null) + targetGEOPos = VectorUtils.WorldPositionToGeoCoords(missileLauncher.lockedCamera.groundTargetPosition, FlightGlobals.currentMainBody); + else if (FiredByWM.vesselRadarData && missileLauncher.targetVessel != null) + { + if (FiredByWM.vesselRadarData.locked) + { + List possibleTargets = FiredByWM.vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == missileLauncher.targetVessel.Vessel) + { + targetGEOPos = possibleTargets[i].geoPos; + break; + } + } + } + } + ml.targetGPSCoords = targetGEOPos; + ml.lockedCamera = missileLauncher.lockedCamera; + break; + case TargetingModes.Heat: + ml.targetGPSCoords = targetGEOPos; + ml.heatTarget = missileLauncher.heatTarget; + break; + case TargetingModes.Radar: + ml.targetGPSCoords = targetGEOPos; + ml.radarTarget = missileLauncher.radarTarget; + ml.vrd = FiredByWM.vesselRadarData; + break; + case TargetingModes.AntiRad: + ml.targetGPSCoords = targetGEOPos; + break; + case TargetingModes.Inertial: + if (FiredByWM.vesselRadarData && missileLauncher.targetVessel != null) + { + TargetSignatureData INSTarget = TargetSignatureData.noTarget; + if (ml.GetWeaponClass() == WeaponClasses.SLW) + { + if (FiredByWM._sonarsEnabled) + INSTarget = FiredByWM.vesselRadarData.detectedRadarTarget(missileLauncher.targetVessel.Vessel, FiredByWM); //detected by radar scan? + } + else + { + if (FiredByWM._radarsEnabled) + INSTarget = FiredByWM.vesselRadarData.detectedRadarTarget(missileLauncher.targetVessel.Vessel, FiredByWM); //detected by radar scan? + if (!INSTarget.exists && FiredByWM._irstsEnabled) + INSTarget = FiredByWM.vesselRadarData.activeIRTarget(null, FiredByWM); //how about IRST? + } + if (INSTarget.exists) + { + VectorUtils.WorldPositionToGeoCoords(MissileGuidance.GetAirToAirFireSolution(ml, missileLauncher.targetVessel.Vessel, out INStimetogo), missileLauncher.targetVessel.Vessel.mainBody); + TimeOfLastINS = Time.time; + targetINScoords = INSTarget.geoPos; + } + } + ml.targetGPSCoords = targetGEOPos; + if (TimeOfLastINS > 0) + { + ml.TargetINSCoords = targetINScoords; + ml.TimeOfLastINS = TimeOfLastINS; + ml.INStimetogo = INStimetogo; + } + break; + } + ml.targetVessel = missileLauncher.targetVessel; + ml.TargetAcquired = true; + //Debug.Log("[BDArmory.MultiMissileLauncher]: Data transfer complete."); + } + } + else + { + FiredByWM.SendTargetDataToMissile(ml, missileLauncher.targetVessel != null ? missileLauncher.targetVessel.Vessel : null, false); + } + ml.GpsUpdateMax = FiredByWM.GpsUpdateMax; + } + if (missileRegistry || removeFromQueue) + { + BDATargetManager.FiredMissiles.Add(ml); //so multi-missile salvoes only count as a single missile fired by the WM for maxMissilesPerTarget + + if (removeFromQueue) + { + if (missileLauncher.radarTarget.exists && missileLauncher.radarTarget.lockedByRadar && missileLauncher.radarTarget.lockedByRadar.vessel != missileLauncher.SourceVessel) + { + MissileFire datalinkwpm = missileLauncher.radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateQueuedLaunches(missileLauncher.targetVessel, missileLauncher, false, false); + } + } + + if (FiredByWM) + { + if (removeFromQueue) + { + FiredByWM.UpdateQueuedLaunches(missileLauncher.targetVessel, missileLauncher, false); + removeFromQueue = false; + } + FiredByWM.UpdateMissilesAway(ml.targetVessel, ml); + } + + // Account for the datalink for the missileLauncher target + if (removeFromQueue) + { + if (missileLauncher.radarTarget.exists && missileLauncher.radarTarget.lockedByRadar && missileLauncher.radarTarget.lockedByRadar.vessel != missileLauncher.SourceVessel) + { + MissileFire datalinkwpm = ml.radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateQueuedLaunches(missileLauncher.targetVessel, missileLauncher, false, false); + } + } + + if (ml.radarTarget.exists && ml.radarTarget.lockedByRadar && ml.radarTarget.lockedByRadar.vessel != ml.SourceVessel) + { + MissileFire datalinkwpm = ml.radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateMissilesAway(ml.targetVessel, ml, false); + } + + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher]: Missile {ml.shortName} with target {(ml.targetVessel != null ? ml.targetVessel.Vessel.GetName() : "null vessel")} added to FiredMissiles."); + } + ml.FiredByWM = FiredByWM; + ml.launched = true; + if (ml.TargetPosition == Vector3.zero) ml.TargetPosition = missileLauncher.MissileReferenceTransform.position + (missileLauncher.MissileReferenceTransform.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(); + if (FiredByWM != null) FiredByWM.heatTarget = TargetSignatureData.noTarget; + } + + if (removeFromQueue) + { + if (missileLauncher.radarTarget.exists && missileLauncher.radarTarget.lockedByRadar && missileLauncher.radarTarget.lockedByRadar.vessel != missileLauncher.SourceVessel) + { + MissileFire datalinkwpm = missileLauncher.radarTarget.lockedByRadar.vessel.ActiveController().WM; + if (datalinkwpm) + datalinkwpm.UpdateQueuedLaunches(missileLauncher.targetVessel, missileLauncher, false, false); + } + } + + if (FiredByWM != null) + { + if (removeFromQueue) + { + Debug.LogWarning($"[BDArmory.MultiMissileLauncher]: {part.name} attempted to fire, all missiles failed to launch! Check your vessel design!"); + FiredByWM.UpdateQueuedLaunches(missileLauncher.targetVessel, missileLauncher, false); + removeFromQueue = false; + } + + using (List.Enumerator Tgt = targetsAssigned.GetEnumerator()) + while (Tgt.MoveNext()) + { + if (Tgt.Current == null) continue; + if (!firedTargets.Contains(Tgt.Current)) + Tgt.Current.Disengage(FiredByWM); + } + } + if (deployState != null) + { + yield return new WaitForSecondsFixed(deployTime); //wait for missile to clear bay + if (deployState != null) + { + deployState.enabled = true; + deployState.speed = -deploySpeed / deployState.length; + yield return new WaitWhileFixed(() => deployState != null && deployState.normalizedTime > 0); + if (deployState != null) + { + deployState.normalizedTime = 0; + deployState.speed = 0; + deployState.enabled = false; + } + } + } + if (missileLauncher == null) yield break; + if (tubesFired >= launchTransforms.Length) //add a timer for reloading a partially emptied MML if it hasn't been used for a while? + { + if (!isLaunchedClusterMissile && (BDArmorySettings.INFINITE_ORDINANCE || missileSpawner.ammoCount >= 0)) + 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 ((int)salvoSize < launchTransforms.Length && missileLauncher.reloadRoutine == null && (BDArmorySettings.INFINITE_ORDINANCE || missileSpawner.ammoCount > 0)) + { + if (launcherCooldown > 0) + { + missileLauncher.heatTimer = launcherCooldown; + yield return new WaitForSecondsFixed(launcherCooldown); + if (missileLauncher == null) yield break; + missileLauncher.launched = false; + missileLauncher.heatTimer = -1; + } + else + { + missileLauncher.heatTimer = -1; + missileLauncher.launched = false; + } + } + missileSalvo = null; + } + } + + void AssignInertialTarget(MissileLauncher ml, Vessel targetV) + { + TargetSignatureData tgtData = TargetSignatureData.noTarget; + if (FiredByWM.vesselRadarData) + { + if (FiredByWM._radarsEnabled) + { + tgtData = FiredByWM.vesselRadarData.detectedRadarTarget(targetV, FiredByWM); + ml.vrd = FiredByWM.vesselRadarData; + } + if (!tgtData.exists && FiredByWM._irstsEnabled) + { + tgtData = FiredByWM.vesselRadarData.activeIRTarget(targetV, FiredByWM); + } + } + if (tgtData.exists) + { + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(MissileGuidance.GetAirToAirFireSolution(ml, tgtData.position, tgtData.velocity), targetV.mainBody); + ml.TargetINSCoords = VectorUtils.WorldPositionToGeoCoords(tgtData.position, vessel.mainBody); + } + else + { + ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(ml.MissileReferenceTransform.position + ml.MissileReferenceTransform.forward * 10000, vessel.mainBody); + ml.TargetINSCoords = ml.targetGPSCoords; + } + } + + void AssignRadarTarget(MissileLauncher ml, Vessel targetV) + { + ml.vrd = FiredByWM.vesselRadarData; + if (FiredByWM.vesselRadarData) FiredByWM.vesselRadarData.TryLockTarget(targetV); + bool foundTarget = false; + if (FiredByWM.vesselRadarData && FiredByWM.vesselRadarData.locked) //if we have existing radar locks, use thsoe + { + List possibleTargets = FiredByWM.vesselRadarData.GetLockedTargets(); + TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == targetV) + { + lockedTarget = possibleTargets[i]; //send correct targetlock if firing multiple SARH missiles + foundTarget = true; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher]: Found locked Radar target {targetV.GetName()}"); + break; + } + } + ml.radarTarget = lockedTarget; + } + if (!foundTarget) + { + if (missileLauncher.MissileReferenceTransform.position.CloserToThan(targetV.CoM, ml.activeRadarRange)) + { + TargetSignatureData[] scannedTargets = new TargetSignatureData[(int)FiredByWM.multiMissileTgtNum]; + RadarUtils.RadarUpdateMissileLock(new Ray(ml.transform.position, ml.GetForwardTransform()), ml.maxOffBoresight, ref scannedTargets, 0.4f, ml); + TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + + for (int i = 0; i < scannedTargets.Length; i++) + { + if (scannedTargets[i].exists && scannedTargets[i].vessel == targetV) + { + lockedTarget = scannedTargets[i]; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher]: Found Radar target {targetV.GetName()}"); + break; + } + } + ml.radarTarget = lockedTarget; + if (BDArmorySettings.DEBUG_MISSILES) + { + if (!ml.radarTarget.exists) + Debug.Log($"[BDArmory.MultiMissileLauncher]: unable to lock Radar target {targetV.GetName()}, skipping"); + } + } + else + { + if (ml.radarLOAL) + { + ml.TargetPosition = targetV.CoM; //set initial target position to fly towards if LOAL instead of straight from launcher + ml.radarTarget = TargetSignatureData.noTarget; + } + } + } + //Debug.Log($"[BDArmory.MultiMissileLauncher]: {targetV.GetName()}; assigned radar target {(ml.radarTarget.exists ? ml.radarTarget.vessel.GetName() : "null")}"); + } + + public bool 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 false; + } + Template.SetActive(false); + Template.AddComponent(); + mslDummyPool[key] = ObjectPool.CreateObjectPool(Template, 10, true, true); + } + return 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($" - Warhead:"); + AvailablePart missilePart = null; + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + //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/Weapons/ModuleEMP.cs b/BDArmory/Weapons/ModuleEMP.cs new file mode 100644 index 000000000..eb77645e6 --- /dev/null +++ b/BDArmory/Weapons/ModuleEMP.cs @@ -0,0 +1,103 @@ +using BDArmory.Damage; +using BDArmory.Settings; +using BDArmory.Utils; +using System.Text; +using UnityEngine; + +namespace BDArmory.Weapons +{ + public class ModuleEMP : PartModule + { + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_EMPBlastRadius"),//EMP Blast Radius + UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] + public float proximity = 5000; + + [KSPField] + public bool AllowReboot = false; + + public bool Armed = false; + + static RaycastHit[] electroHits; + + public override void OnStart(StartState state) + { + if (HighLogic.LoadedSceneIsFlight) + { + part.force_activate(); + part.OnJustAboutToBeDestroyed += DetonateEMPRoutine; + if (electroHits == null) { electroHits = new RaycastHit[100]; } + } + base.OnStart(state); + } + + public void DetonateEMPRoutine() + { + if (!Armed) return; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleEMP]: Detonating EMP from {part.partInfo.name} with blast range {proximity}m."); + foreach (Vessel v in FlightGlobals.Vessels) + { + if (v == null || !v.loaded || v.packed) continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(v.vesselType)) continue; + if (!v.HoldPhysics) + { + double targetDistance = Vector3d.Distance(this.vessel.GetWorldPos3D(), v.GetWorldPos3D()); + + if (targetDistance <= proximity) + { + var EMPDamage = ((proximity - (float)targetDistance) * 10) * BDArmorySettings.DMG_MULTIPLIER; //this way craft at edge of blast might only get disabled instead of bricked + + Vector3 commandDir = Vector3.zero; + float shieldvalue = float.PositiveInfinity; + foreach (var moduleCommand in VesselModuleRegistry.GetModuleCommands(v)) + { + //see how many parts are between emitter and the nearest command part to see which one is least shielded + var distToCommand = commandDir.magnitude; + var ElecRay = new Ray(part.transform.position, commandDir); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Wheels); + var partCount = Physics.RaycastNonAlloc(ElecRay, electroHits, distToCommand, layerMask); + if (partCount == electroHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. + { + electroHits = Physics.RaycastAll(ElecRay, distToCommand, layerMask); + partCount = electroHits.Length; + } + for (int mwh = 0; mwh < partCount; ++mwh) + { + Part partHit = electroHits[mwh].collider.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; + float testShieldValue = 0; + //AoE EMP field EMP damage mitigation - -1 EMP damage per mm of conductive armor/5t of conductive hull mass per part occluding command part from emission source + var Armor = partHit.FindModuleImplementing(); + if (Armor != null && partHit.Rigidbody != null) + { + if (Armor.Diffusivity > 15) testShieldValue += Armor.Armour; + if (Armor.HullMassAdjust > 0) testShieldValue += (partHit.mass * 4); + } + if (testShieldValue < shieldvalue) shieldvalue = testShieldValue; + } + } + EMPDamage -= shieldvalue; + if (EMPDamage > 0) + { + var emp = v.rootPart.FindModuleImplementing(); + if (emp == null) + { + emp = (ModuleDrainEC)v.rootPart.AddModule("ModuleDrainEC"); + } + emp.softEMP = AllowReboot; //can bypass DMP damage cap + emp.incomingDamage = EMPDamage; + } + } + } + } + } + + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + output.Append(System.Environment.NewLine); + output.AppendLine($"- EMP Blast Radius: {proximity} m"); + return output.ToString(); + } + } +} diff --git a/BDArmory/Weapons/ModuleWeapon.cs b/BDArmory/Weapons/ModuleWeapon.cs new file mode 100644 index 000000000..d733d224a --- /dev/null +++ b/BDArmory/Weapons/ModuleWeapon.cs @@ -0,0 +1,7427 @@ +using BDArmory.Bullets; +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.GameModes; +using BDArmory.ModIntegration; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using BDArmory.Weapons.Missiles; +using KSP.UI.Screens; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using UniLinq; +using UnityEngine; +using static BDArmory.Bullets.PooledBullet; + +namespace BDArmory.Weapons +{ + public class ModuleWeapon : EngageableWeapon, IBDWeapon + { + #region Declarations + + public static ObjectPool bulletPool; + + public static Dictionary rocketPool = new Dictionary(); //for ammo switching + public static ObjectPool shellPool; + public static ObjectPool beamConeFX; + + Coroutine startupRoutine; + Coroutine shutdownRoutine; + Coroutine standbyRoutine; + Coroutine reloadRoutine; + Coroutine chargeRoutine; + + bool finalFire; + + public int rippleIndex = 0; + public string OriginalShortName { get; private set; } + + // WeaponTypes.Cannon is deprecated. identical behavior is achieved with WeaponType.Ballistic and bulletInfo.explosive = true. + public enum WeaponTypes + { + Ballistic, + Rocket, //Cannon's deprecated, lets use this for rocketlaunchers + Laser + } + + public enum WeaponStates + { + Enabled, + Disabled, + PoweringUp, + PoweringDown, + Locked, + Standby, // Not currently firing, but can still track the current target. + EnabledForSecondaryFiring // Enabled, but only for secondary firing. + } + + //public enum BulletDragTypes + //{ + // None, + // AnalyticEstimate, + // NumericalIntegration + //} + + //public enum FuzeTypes + //{ + // None, //So very tempted to have none be 'no fuze', and HE rounds with fuzetype = None act just like standard slug rounds + // Timed, //detonates after set flighttime. Main use case probably AA, assume secondary contact fuze + // Proximity, //detonates when in proximity to target. No need for secondary contact fuze + // Flak, //detonates when in proximity or after set flighttime. Again, shouldn't need secondary contact fuze + // Delay, //detonates 0.02s after any impact. easily defeated by whipple shields + // Penetrating,//detonates 0.02s after penetrating a minimum thickness of armor. will ignore lightly armored/soft hits + // 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, + Missile, + Omni, + None + } + public WeaponStates weaponState = WeaponStates.Disabled; + + //animations + private float fireAnimSpeed = 1; + //is set when setting up animation so it plays a full animation for each shot (animation speed depends on rate of fire) + + public float bulletBallisticCoefficient; + + public WeaponTypes eWeaponType; + + public BulletFuzeTypes eFuzeType; + + //public PooledBulletTypes 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 wasFiring; + //used for knowing when to stop looped audio clip (when you're not shooting, but you were) + + AudioClip reloadCompleteAudioClip; + AudioClip fireSound; + AudioClip overheatSound; + AudioClip chargeSound; + AudioSource audioSource; + AudioSource audioSource2; + AudioLowPassFilter lowpassFilter; + + private BDStagingAreaGauge gauge; + private int AmmoID; + private int ECID; + public int ResID_Ammo => AmmoID; + public int ResID_SecAmmo => ECID; + //AI + public bool aiControlled = false; + public bool autoFire; + public string autoFireFailReason = ""; + public float autoFireLength = 0; + public float autoFireTimer = 0; + public float autofireShotCount = 0; + bool aimAndFireIfPossible = false; + bool aimOnly = false; + + //used by AI to lead moving targets + private float targetDistance = 8000f; + private float origTargetDistance = 8000f; + public float targetRadius = 35f; // Radius of target 2° @ 1km. + public float targetAdjustedMaxCosAngle + { + get + { + 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 : Mathf.Max(1f - 0.5f * theta * theta, 0); // Approximation to cos(theta). (cos(x) = 1-x^2/2!+O(x^4)) + } + } + public Vector3 atprTargetPosition; + public Vector3 targetPosition; + public Vector3 targetVelocity; // local frame velocity + readonly SmoothingV3 targetVelocitySmoothing = new(); // Smoothing for the target's velocity, required for long-range aiming. + public Vector3 targetAcceleration; // local frame + readonly SmoothingV3 targetAccelerationSmoothing = new(); // Smoothing for the target's acceleration, required for long-range aiming. + private Vector3 smoothedPartVelocity; // Also apply smoothing to the part's velocity, required for long-range aiming. + readonly SmoothingV3 partVelocitySmoothing = new(); // Smoothing for the part's velocity, required for long-range aiming. + private Vector3 smoothedPartAcceleration; // Also apply smoothing to the part's acceleration, required for long-range aiming. + readonly SmoothingV3 partAccelerationSmoothing = new(); // Smoothing for the part's acceleration, required for long-range aiming. + readonly SmoothingV3 smoothedRelativeFinalTarget = new(0.5f); // Smoothing for the finalTarget aim-point: half-life of 1 frame. This seems good. More than 5 frames (0.1s) seems too slow. + public int shotsFiredSinceAcquiringTarget = 0; + public float targetAcquisitionTime = 0; + public bool targetIsLandedOrSplashed = false; // Used in the targeting simulations to know whether to separate gravity from other acceleration. + private float lastTimeToCPA = -1, deltaTimeToCPA = 0; + float bulletTimeToCPA; // Time until the bullet is expected to reach the closest point to the target. Used for timing-based bullet detonation. + public Vector3 finalAimTarget; + Vector3 staleFinalAimTarget, staleTargetVelocity, staleTargetAcceleration, stalePartVelocity; + public Vessel visualTargetVessel + { + get + { + if (_visualTargetVessel != null && !_visualTargetVessel.gameObject.activeInHierarchy) _visualTargetVessel = null; + return _visualTargetVessel; + } + set + { + lastVisualTargetVessel = _visualTargetVessel; + _visualTargetVessel = value; + } + } + Vessel _visualTargetVessel; + public Vessel lastVisualTargetVessel + { + get + { + if (_lastVisualTargetVessel != null && !_lastVisualTargetVessel.gameObject.activeInHierarchy) _lastVisualTargetVessel = null; + return _lastVisualTargetVessel; + } + set { _lastVisualTargetVessel = value; } + } + Vessel _lastVisualTargetVessel; + public Part visualTargetPart + { + get + { + if (_visualTargetPart != null && !_visualTargetPart.gameObject.activeInHierarchy) _visualTargetPart = null; + return _visualTargetPart; + } + set { _visualTargetPart = value; } + } + Part _visualTargetPart; + public PooledBullet tgtShell = null; + public PooledRocket tgtRocket = null; + Vector3 closestTarget = Vector3.zero; + Vector3 tgtVelocity = Vector3.zero; + + private int targetID = 0; + bool targetAcquired; + public bool targetInVisualRange = false; + + public bool targetCOM = true; + public bool targetCockpits = false; + public bool targetEngines = false; + public bool targetWeapons = false; + public bool targetMass = false; + public bool targetRandom = false; + + 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; + public float staleGoodTargetTime = 0; + + public Vector3? FiringSolutionVector => finalAimTarget.IsZero() ? (Vector3?)null : (finalAimTarget - fireTransforms[0].position).normalized; + + // For aiming corrections for offset/non-centerline fixed weapon positions + public Vector3 offsetWeaponPosition = Vector3.zero; + public Vector3 offsetWeaponDirection = Vector3.zero; + + public bool recentlyFiring //used by guard to know if it should evade this + { + get { return timeSinceFired < 1; } + } + + //used to reduce volume of audio if multiple guns are being fired (needs to be improved/changed) + //private int numberOfGuns = 0; + + //AI will fire gun if target is within this Cos(angle) of barrel + public float maxAutoFireCosAngle = 0.9993908f; //corresponds to ~2 degrees + + //aimer textures + Vector3 pointingAtPosition; + float pointingDistance = 500f; + Vector3 bulletPrediction; + Vector3 fixedLeadOffset = Vector3.zero; + + float predictedFlightTime = 1; //for rockets + Vector3 trajectoryOffset = Vector3.zero; + + //gapless particles + List gaplessEmitters = new List(); + + //muzzleflash emitters + List> muzzleFlashList; + + //module references + [KSPField] public int turretID = 0; + public ModuleTurret turret; + public List customTurret = new List(); + public MissileFire WeaponManager + { + get + { + if (_weaponManager == null || !_weaponManager.IsPrimaryWM || _weaponManager.vessel != vessel) + _weaponManager = vessel && vessel.loaded ? vessel.ActiveController().WM : null; + return _weaponManager; + } + } + MissileFire _weaponManager; + + public bool pointingAtSelf; //true if weapon is pointing at own vessel + bool userFiring; + Vector3 laserPoint; + public bool slaved; + public bool GPSTarget; + public bool radarTarget; + + public Transform turretBaseTransform + { + get + { + if (turret) + { + return turret.yawTransform.parent; + } + else + { + return fireTransforms[0]; + } + } + } + + public float maxPitch + { + get { return turret ? turret.maxPitch : customMaxPitch; } + } + public float minPitch + { + get { return turret ? turret.minPitch : customMinPitch; } + } + public float yawRange + { + get { return turret ? turret.yawRange : customYaw; } + } + float customYaw = 0; + float customMinPitch = 0; + float customMaxPitch = 0; + + + //weapon interface + public WeaponClasses GetWeaponClass() + { + if (eWeaponType == WeaponTypes.Ballistic) + { + return WeaponClasses.Gun; + } + else if (eWeaponType == WeaponTypes.Rocket) + { + return WeaponClasses.Rocket; + } + else + { + return WeaponClasses.DefenseLaser; + } + } + public ModuleWeapon GetWeaponModule() + { + return this; + } + public Part GetPart() + { + return part; + } + + public double ammoCount; + public double ammoMaxCount; + public string ammoLeft; //#191 + + public string GetSubLabel() //think BDArmorySetup only calls this for the first instance of a particular ShortName, so this probably won't result in a group of n guns having n GetSublabelCalls per frame + { + //using (List.Enumerator craftPart = vessel.parts.GetEnumerator()) + //{ + ammoLeft = $"Ammo Left: {ammoCount:0}"; + int lastAmmoID = AmmoID; + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.GetShortName() != GetShortName()) continue; + if (weapon.Current.weaponChannel > weaponChannel) continue; + if (weapon.Current.AmmoID != AmmoID && weapon.Current.AmmoID != lastAmmoID) + { + GetAmmoCount(weapon.Current.AmmoID, out double ammoCurrent, out double ammoMax); + ammoLeft += $"; {ammoCurrent:0}"; + lastAmmoID = weapon.Current.AmmoID; + } + } + //} + return ammoLeft; + } + public string GetMissileType() + { + return string.Empty; + } + public float GetEngageFOV() + { + return -1; + } + public string GetPartName() + { + return WeaponName; + } + + public float GetEngageRange() + { + return engageRangeMax; + } + + public bool resourceSteal = false; + public float strengthMutator = 1; + public bool instagib = false; + + Vector3 debugTargetPosition; + Vector3 debugLastTargetPosition; + Vector3 debugRelVelAdj; + Vector3 debugAccAdj; + Vector3 debugGravAdj; + + SourceInfo sourceInfo; + GraphicsInfo graphicsInfo; + NukeInfo nukeInfo; + + #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] + public string fireTransformName = "fireTransform"; + public Transform[] fireTransforms; + + [KSPField] + public string muzzleTransformName = "muzzleTransform"; + + [KSPField] + public string shellEjectTransformName = "shellEject"; + public Transform[] shellEjectTransforms; + + [KSPField] + public float shellEjectDelay = 0; + + [KSPField] + public float shellEjectLifeTime = 2; + + [KSPField] + public Vector3 shellEjectVelocity = new(0, 0, 7); + + [KSPField] + public float shellEjectDeviation = 0.5f; + + [KSPField] + public bool hasDeployAnim = false; + + [KSPField] + public string deployAnimName = "deployAnim"; + AnimationState deployState; + + [KSPField] + public bool hasReloadAnim = false; + + [KSPField] + public string reloadAnimName = "reloadAnim"; + AnimationState reloadState; + + [KSPField] + public bool hasChargeAnimation = false; + + [KSPField] + public string chargeAnimName = "chargeAnim"; + AnimationState chargeState; + + [KSPField] + public bool hasChargeHoldAnimation = false; + + [KSPField] + public string chargeHoldAnimName = "chargeHoldAnim"; + AnimationState chargeHoldState; + + [KSPField] + public bool hasFireAnimation = false; + + [KSPField] + public string fireAnimName = "fireAnim"; + + [KSPField] + public float fireAnimOverrideSpeed = -1; + + AnimationState[] fireState = new AnimationState[0]; + //private List fireState; + + [KSPField] + public bool spinDownAnimation = false; + private bool spinningDown; + + //weapon specifications + [KSPField(advancedTweakable = false, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_TurretID"),//Custom Turret ID + UI_FloatRange(minValue = 0f, maxValue = 20f, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float customTurretID = 0; + + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_FiringPriority"), + UI_FloatRange(minValue = 0, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float priority = 0; //per-weapon priority selection override + + [KSPField(isPersistant = true)] + 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, affectSymCounterparts = UI_Scene.All)] + public float fireBurstLength = 1; + + [KSPField(isPersistant = true)] + public bool FireAngleOverride = false; + + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_FiringAngle"), + UI_FloatRange(minValue = 0f, maxValue = 4, stepIncrement = 0.05f, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + public float FiringTolerance = 1.0f; //per-weapon override of maxcosfireangle + + [KSPField] + public float maxTargetingRange = 2000; //max range for raycasting and sighting + + [KSPField] + public float SpoolUpTime = -1; //barrel spin-up period for gas-driven rotary cannon and similar + float spooltime = 0; + + [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; //RoF slider + + public float baseRPM = 650; + + [KSPField] + public bool isChaingun = false; //does the gun have adjustable RoF + + [KSPField] + public float maxDeviation = 1; //inaccuracy two standard deviations in degrees (two because backwards compatibility :) + public float baseDeviation = 1; + + [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 + + [KSPField] + public float caliber = 30; //caliber in mm, used for penetration calcs + + [KSPField] + public float bulletDmgMult = 1; //Used for heat damage modifier for non-explosive bullets + + [KSPField] + public float bulletVelocity = 1030; //velocity in meters/second + + [KSPField] + public float baseBulletVelocity = -1; //vel of primary ammo type for mixed belts + + public int ProjectileCount = 1; + + [KSPField] + public bool BeltFed = true; //draws from an ammo bin; default behavior + + [KSPField] + public int RoundsPerMag = 1; //For weapons fed from clips/mags. left at one as sanity check, incase this not set if !BeltFed + public int RoundsRemaining = 0; + public bool isReloading; + + [KSPField] + public bool crewserved = false; //does the weapon need a gunner? + public bool hasGunner = true; //if so, are they present? + private KerbalSeat gunnerSeat; + private bool gunnerSeatLookedFor = false; + + [KSPField] + public float ReloadTime = 10; + public float ReloadTimer = 0; + public float ReloadAnimTime = 10; + public float AnimTimer = 0; + + [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; + [KSPField] + public bool postFireChargeAnim = false; + bool hasCharged = false; + [KSPField] + public float chargeHoldLength = 1; + [KSPField] + public string bulletDragTypeName = "AnalyticEstimate"; // deprecated, we now entirely rely on bulletInfo + public BulletDragTypes bulletDragType; + + //drag area of the bullet in m^2; equal to Cd * A with A being the frontal area of the bullet; as a first approximation, take Cd to be 0.3 + //bullet mass / bullet drag area. Used in analytic estimate to speed up code + [KSPField] + public float bulletDragArea = 1.209675e-5f; + + private BulletInfo bulletInfo; + private BulletInfo[] bulletInfoList; + + [KSPField] + public string bulletType = "def"; + + public string currentType = "def"; + public int currentTypeIndex = 0; + + [KSPField] + public string ammoName = "50CalAmmo"; //resource usage + + [KSPField] + public float requestResourceAmount = 1; //amount of resource/ammo to deplete per shot + + private bool electricResource = false; + + [KSPField] + public string secondaryAmmoName = "ElectricCharge"; //resource usage + + [KSPField] + public float ECPerShot = 0; //EC to use per shot for weapons like railguns - DEPRECATED + + [KSPField] + public float secondaryAmmoPerShot = 0; //EC to use per shot for weapons like railguns + + private bool secECResource = false; + + [KSPField] + public float shellScale = 0.66f; //scale of shell to eject + + [KSPField] + public bool hasRecoil = true; + + [KSPField] + public float recoilReduction = 1; //for reducing recoil on large guns with built in compensation + + //[KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FireLimits"),//Fire Limits + // UI_Toggle(disabledText = "#LOC_BDArmory_FireLimits_disabledText", enabledText = "#LOC_BDArmory_FireLimits_enabledText")]//None--In range + [KSPField] + public bool onlyFireInRange = true; + // UNUSED, supposedly once prevented firing when gun's turret is trying to exceed gimbal limits + + [KSPField] + public bool bulletDrop = true; //projectiles are affected by gravity + + [KSPField] + public string weaponType = "ballistic"; + //ballistic, cannon or laser + + //laser info + [KSPField] + public float laserDamage = 10000; //base damage/second of lasers + [KSPField] + public float laserMaxDamage = -1; //maximum damage/second of lasers if laser growth enabled + public float baseLaserdamage; + [KSPField] + public float LaserGrowTime = -1; //time laser to be fired to go from base to max damage + [KSPField] public bool DynamicBeamColor = false; //beam color changes longer laser fired, for growlasers + bool dynamicFX = false; + [KSPField] public float beamScrollRate = 0.5f; //Beam texture scroll rate, for plasma beams, etc + private float Offset = 0; + [KSPField] public float beamScalar = 0.01f; //x scaling for beam texture. lower is more stretched + [KSPField] public bool pulseLaser = false; //pulse vs beam + public bool pulseInConfig = false; //record if pulse laser in config for resetting lasers post mutator + [KSPField] public bool HEpulses = false; //do the pulses have blast damage + [KSPField] public bool HeatRay = false; //conic AoE + [KSPField] public bool electroLaser = false; //Drains EC from target/induces EMP effects TODO - since this is pressed into service for rockets and bullets as well, rename this to EMPWeapon or similar and add deprecation support for the old field name + [KSPField] public bool conicAoE = false; //is a Microwave Emitter or similar that does a conical AoE isntead of a linear beam + [KSPField] public float beamFOV = 20; //FoV angle of the beam. Is total width of field, not angle of divergence from muzzle + [KSPField] public bool friendlyFire = true; //will this also affect friendly vessels in AoE + float beamDuration = 0.1f; // duration of pulselaser beamFX + float beamScoreTime = 0.2f; //frequency of score accumulation for beam lasers, currently 5x/sec + float BeamTracker = 0; // timer for scoring shots fired for beams + float ScoreAccumulator = 0; //timer for scoring shots hit for beams + bool grow = true; + + LineRenderer[] laserRenderers; + GameObject[] beamCone; + Renderer[] r_cone; + + static RaycastHit[] electrolaserHits; + static RaycastHit[] reverseELHits; + static RaycastHit[] orderedELHits; + + LineRenderer trajectoryRenderer; + List trajectoryPoints; + + public string rocketModelPath; + public float rocketMass = 1; + public float thrust = 1; + public float thrustTime = 1; + public float blastRadius = 1; + public bool choker = false; + public bool descendingOrder = true; + public float thrustDeviation = 0.10f; + [KSPField] public bool rocketPod = true; //is the RL a rocketpod, or a gyrojet gun? + [KSPField] public bool externalAmmo = true; // weapon is supplied by external ammo boxes isntead of internal supply (e.g. guns vs rocket pods) + Transform[] rockets; + double rocketsMax; + private RocketInfo rocketInfo; + + 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 List attributeList; + + public bool explosive = false; + public bool beehive = false; + public bool incendiary = false; + public bool impulseWeapon = false; + public bool graviticWeapon = false; + + [KSPField] + public float Impulse = 0; + + [KSPField] + public float massAdjustment = 0; //tons + + + //deprectated + //[KSPField] public float cannonShellRadius = 30; //max radius of explosion forces/damage + //[KSPField] public float cannonShellPower = 8; //explosion's impulse force + //[KSPField] public float cannonShellHeat = -1; //if non-negative, heat damage + + //projectile graphics + [KSPField] + public string projectileColor = "255, 130, 0, 255"; //final color of projectile; left public for lasers + Color projectileColorC; + string[] endColorS; + [KSPField] + public bool fadeColor = false; + + [KSPField] + public string startColor = "255, 160, 0, 200"; + //if fade color is true, projectile starts at this color + string[] startColorS; + Color startColorC; + + [KSPField] + public float tracerStartWidth = 0.25f; //set from bulletdefs, left for lasers + + [KSPField] + public float tracerEndWidth = 0.2f; + + [KSPField] + public float tracerMaxStartWidth = 0.5f; //set from bulletdefs, left for lasers + + [KSPField] + public float tracerMaxEndWidth = 0.5f; + + float tracerBaseSWidth = 0.25f; // for laser FX + float tracerBaseEWidth = 0.2f; // for laser FX + [KSPField] + public float tracerLength = 0; + //if set to zero, tracer will be the length of the distance covered by the projectile in one physics timestep + + [KSPField] + public float tracerDeltaFactor = 2.65f; + + [KSPField] + public float nonTracerWidth = 0.01f; + + [KSPField] + public int tracerInterval = 0; + + [KSPField] + public float tracerLuminance = 1.75f; + + [KSPField] + public float bulletLuminance = 0.75f; + + [KSPField] + public bool tracerOverrideWidth = false; + + int tracerIntervalCounter; + + [KSPField] + public string bulletTexturePath = "BDArmory/Textures/bullet"; + + [KSPField] + public string smokeTexturePath = ""; //"BDArmory/Textures/tracerSmoke"; + + [KSPField] + public string laserTexturePath = "BDArmory/Textures/laser"; + + [KSPField] + public string laserModelPath = "BDArmory/Models/laser/laserCone"; + + public List laserTexList; + + [KSPField] + public bool oneShotWorldParticles = false; + + //heat + [KSPField] + public float maxHeat = 3600; + + [KSPField] + public float heatPerShot = 75; + + [KSPField] + public float heatLoss = 250; + + //canon explosion effects + public static string defaultExplModelPath = "BDArmory/Models/explosion/explosion"; + [KSPField] + public string explModelPath = defaultExplModelPath; + + public static string defaultExplSoundPath = "BDArmory/Sounds/explode1"; + [KSPField] + public string explSoundPath = defaultExplSoundPath; + + //Used for scaling laser damage down based on distance. + [KSPField] + public float tanAngle = 0.0001f; + //Angle of divergeance/2. Theoretical minimum value calculated using θ = (1.22 L/RL)/2, + //where L is laser's wavelength and RL is the radius of the mirror (=gun). + + //audioclip paths + [KSPField] + public string fireSoundPath = "BDArmory/Parts/50CalTurret/sounds/shot"; + + [KSPField] + public string overheatSoundPath = "BDArmory/Parts/50CalTurret/sounds/turretOverheat"; + + [KSPField] + public string chargeSoundPath = "BDArmory/Parts/ABL/sounds/charge"; + + [KSPField] + public string rocketSoundPath = "BDArmory/Sounds/rocketLoop"; + + //audio + [KSPField] + public bool oneShotSound = true; + //play audioclip on every shot, instead of playing looping audio while firing + + [KSPField] + public float soundRepeatTime = 1; + //looped audio will loop back to this time (used for not playing the opening bit, eg the ramp up in pitch of gatling guns) + + [KSPField] + public string reloadAudioPath = string.Empty; + AudioClip reloadAudioClip; + + [KSPField] + public string reloadCompletePath = string.Empty; + + [KSPField] + public bool showReloadMeter = false; //used for cannons or guns with extremely low rate of fire + + //Air Detonating Rounds + //public bool airDetonation = false; + public bool proximityDetonation = false; + //public bool airDetonationTiming = true; + + [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_DefaultDetonationRange", guiActiveEditor = false)]//Fuzed Detonation Range + public float defaultDetonationRange = 3500; // maxairDetrange works for altitude fuzing, use this for VT fuzing + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ProximityFuzeRadius"), UI_FloatRange(minValue = 0f, maxValue = 300f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Proximity Fuze Radius + public float detonationRange = -1f; // give ability to set proximity range + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DetonateAtMinimumDistance"), UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled")] + public bool detonateAtMinimumDistance = true; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Ammo_Type"),//Ammunition Types + UI_FloatRange(minValue = 1, maxValue = 999, stepIncrement = 1, scene = UI_Scene.All)] + public float AmmoTypeNum = 1; + private int ammoTypeIndex { get { return (int)AmmoTypeNum - 1; } } + + [KSPField(isPersistant = true)] + public bool advancedAmmoOption = false; + + [KSPEvent(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_simple", active = true)]//Disable Engage Options + public void ToggleAmmoConfig() + { + advancedAmmoOption = !advancedAmmoOption; + + if (advancedAmmoOption == true) + { + Events["ToggleAmmoConfig"].guiName = StringUtils.Localize("#LOC_BDArmory_advanced");//"Advanced Ammo Config" + Events["ConfigAmmo"].guiActive = true; + Events["ConfigAmmo"].guiActiveEditor = true; + Fields["AmmoTypeNum"].guiActive = false; + Fields["AmmoTypeNum"].guiActiveEditor = false; + } + else + { + 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; + } + GUIUtils.RefreshAssociatedWindows(part); + } + [KSPField(advancedTweakable = true, isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_useBelt")]//Using Custom Loadout + public bool useCustomBelt = false; + + [KSPEvent(advancedTweakable = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_Ammo_Setup")]//Configure Ammo Loadout + public void ConfigAmmo() + { + BDAmmoSelector.Instance.Open(this, new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); + } + + [KSPField(isPersistant = true)] + public string SelectedAmmoType; + + public List ammoList; + + [KSPField(isPersistant = true)] + public string ammoBelt = "def"; + + public List customAmmoBelt; + private int[] customAmmoBeltIndexes; + + int AmmoIntervalCounter = 0; + + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Ammo_LoadedAmmo")]//Status + 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( + options = new string[] { + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16" + }, + display = new string[] { + "Disabled", + "AG1", + "AG2", + "AG3", + "AG4", + "AG5", + "AG6", + "AG7", + "AG8", + "AG9", + "AG10", + "Lights", + "RCS", + "SAS", + "Brakes", + "Abort", + "Gear" + } + )] + public string deployWepGroup = "0"; + + [KSPField(isPersistant = true)] + public bool canHotSwap = false; //for select weapons that it makes sense to be able to swap ammo types while in-flight, like the Abrams turret + + //auto proximity tracking + [KSPField] + public float autoProxyTrackRange = 0; + public bool atprAcquired; + int aptrTicker; + + public float timeFired; // Note: this is technically off by Time.fixedDeltaTime (since it's meant to be within the range [Time.time <—> Time.time + Time.fixedDeltaTime]), but so is Time.time in timeSinceFired, so we can skip adding the constant. + public float timeSinceFired => Time.time - timeFired; + public float initialFireDelay = 0; //used to ripple fire multiple weapons of this type + float InitialFireDelay + { + get + { + var wm = WeaponManager; + return wm && wm.barrageStagger > 0 ? initialFireDelay * wm.barrageStagger : initialFireDelay; + } + } + + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Barrage")]//Barrage + public bool useRippleFire = true; + + public bool canRippleFire = true; + + [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ToggleBarrage")]//Toggle Barrage + public void ToggleRipple() + { + 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.useRippleFire = !weapon.Current.useRippleFire; + } + } + } + + [KSPField(isPersistant = true)] + public bool useThisWeaponForAim = false; + + [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_AimOverrideFalse")]//"Aim With This Weapon" + public void setAimOverride() + { + useThisWeaponForAim = !useThisWeaponForAim; + if (useThisWeaponForAim == false) + { + Events["setAimOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_AimOverrideFalse");//"Aim With This Weapon" + } + else + { + Events["setAimOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_AimOverrideTrue");//"Revert Aim Override" + using (List.Enumerator craftPart = EditorLogic.fetch.ship.parts.GetEnumerator()) + while (craftPart.MoveNext()) + { + if (craftPart.Current == null) continue; + using (List.Enumerator weapon = craftPart.Current.FindModulesImplementing().GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current == this) continue; //setting this here instead of craftPart.Current in case part has multiple weapon modules + if (weapon.Current.GetShortName() != shortName) continue; + if (weapon.Current.useThisWeaponForAim) + { + weapon.Current.useThisWeaponForAim = false; + weapon.Current.Events["setAimOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_AimOverrideFalse");//"Aim With This Weapon" + GUIUtils.RefreshAssociatedWindows(weapon.Current.part); + } + } + } + } + } + + [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; + + [KSPField] + public float sightingAccuracy = 1f; // In milliradians (for the visual aiming malus). + float malusSightingAccuracy = 0.01f; // 10 * tan(sightingAccuracy / 1000). The bounded slow random walk that the malus performs reaches approx 0.1 this size. + [KSPField] + public float malusReductionPerShot = 0.1f; // Scale the per shot reduction in the visual aiming malus. + public float malusReduction = 1f; + + IEnumerator IncrementRippleIndex(float delay) + { + if (isRippleFiring) delay = 0; + if (delay > 0) + { + yield return new WaitForSecondsFixed(delay); + } + var wm = WeaponManager; + if (wm == null || wm.vessel != vessel) yield break; + wm.incrementRippleIndex(WeaponName); + + //Debug.Log("[BDArmory.ModuleWeapon]: incrementing ripple index to: " + wm.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 + + [KSPAction("Toggle Weapon")] + public void AGToggle(KSPActionParam param) + { + Toggle(); + } + + [KSPField(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_Status")]//Status + public string guiStatusString = + "Disabled"; + + //PartWindow buttons + [KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_Toggle")]//Toggle + public void Toggle() + { + if (weaponState == WeaponStates.Disabled || weaponState == WeaponStates.PoweringDown) + { + EnableWeapon(); + } + else + { + DisableWeapon(); + } + } + + bool agHoldFiring; + + [KSPAction("Fire (Toggle)")] + public void AGFireToggle(KSPActionParam param) + { + agHoldFiring = (param.type == KSPActionType.Activate); + } + + [KSPAction("Fire (Hold)")] + public void AGFireHold(KSPActionParam param) + { + StartCoroutine(FireHoldRoutine(param.group)); + } + + IEnumerator FireHoldRoutine(KSPActionGroup group) + { + KeyBinding key = OtherUtils.AGEnumToKeybinding(group); + if (key == null) + { + yield break; + } + + while (key.GetKey()) + { + agHoldFiring = true; + yield return null; + } + + agHoldFiring = false; + yield break; + } + + [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_Jettison", active = true, guiActiveEditor = false)]//Jettison + public void Jettison() // make rocketpods jettisonable + { + if ((turret || eWeaponType != WeaponTypes.Rocket) || (eWeaponType == WeaponTypes.Rocket && (!rocketPod || (rocketPod && externalAmmo)))) + { + return; + } + part.decouple(0); + if (WeaponManager != null) + WeaponManager.UpdateList(); + } + [KSPAction("Jettison")] // Give them an action group too. + public void AGJettison(KSPActionParam param) + { + Jettison(); + } + #endregion KSPActions + + #region KSP Events + + public override void OnAwake() + { + base.OnAwake(); + + part.stagingIconAlwaysShown = true; + part.stackIconGrouping = StackIconGrouping.SAME_TYPE; + } + + public void Start() + { + part.stagingIconAlwaysShown = true; + part.stackIconGrouping = StackIconGrouping.SAME_TYPE; + + Events["HideUI"].active = false; + Events["ShowUI"].active = true; + ParseWeaponType(weaponType); + + // extension for feature_engagementenvelope + if (dualModeAPS) isAPS = true; + if (isAPS) + { + engageMissile = false; //missiles targeted separately from base WM targeting logic, having this is unnecessary and can cause problems with radar slaving + Fields["engageMissile"].guiActive = false; + Fields["engageMissile"].guiActiveEditor = 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(minSafeDistance, maxEffectiveDistance); + if (string.IsNullOrEmpty(GetShortName())) + { + shortName = part.partInfo.title; + } + OriginalShortName = 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()) + { + if (emitter.Current == null) continue; + emitter.Current.emit = false; + EffectBehaviour.AddParticleEmitter(emitter.Current); + } + + if (eWeaponType != WeaponTypes.Laser || (eWeaponType == WeaponTypes.Laser && pulseLaser)) + { + try + { + baseRPM = float.Parse(ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleWeapon", "roundsPerMinute", "fireTransformName", fireTransformName)); //if multiple moduleWeapons, make sure this grabs the right one unsing fireTransformname as an ID + } + catch + { + baseRPM = 3000; + Debug.LogError($"[BDArmory.ModuleWeapon] {shortName} missing roundsPerMinute field in .cfg! Fix your .cfg!"); + } + + if (!isChaingun) + roundsPerMinute = baseRPM; + else if (roundsPerMinute > baseRPM) + roundsPerMinute = baseRPM; + } + else baseRPM = 3000; + + if (roundsPerMinute >= 1500 || (eWeaponType == WeaponTypes.Laser && !pulseLaser)) + { + Events["ToggleRipple"].guiActiveEditor = false; + Fields["useRippleFire"].guiActiveEditor = false; + useRippleFire = false; + canRippleFire = false; + if (HighLogic.LoadedSceneIsFlight) + { + using (List.Enumerator craftPart = vessel.parts.GetEnumerator()) //set other weapons in the group to ripple = false if the group contains a weapon with RPM > 1500, should fix the brownings+GAU WG, GAU no longer overheats exploit + { + using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + if (weapon.Current.isAPS) continue; + if (weapon.Current.GetShortName() != GetShortName()) continue; + if (weapon.Current.roundsPerMinute >= 1500 || (weapon.Current.eWeaponType == WeaponTypes.Laser && !weapon.Current.pulseLaser)) continue; + weapon.Current.canRippleFire = false; + weapon.Current.useRippleFire = false; + } + } + } + } + + if (!(isChaingun || eWeaponType == WeaponTypes.Rocket))//disable rocket RoF slider for non rockets + { + Fields["roundsPerMinute"].guiActiveEditor = false; + } + else + { + try + { + UI_FloatRange RPMEditor = (UI_FloatRange)Fields["roundsPerMinute"].uiControlEditor; // FIXME this is throwing an invalid cast for rocket pods sometimes + if (isChaingun) + { + RPMEditor.maxValue = baseRPM; + RPMEditor.minValue = baseRPM / 2; + RPMEditor.onFieldChanged = AccAdjust; + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.DEBUG]: {e.Message}\n{e.StackTrace}"); + } + } + + ammoList = BDAcTools.ParseNames(bulletType); + if (ammoList.Count > 1) + { + if (advancedAmmoOption == true) + { + Events["ToggleAmmoConfig"].guiName = StringUtils.Localize("#LOC_BDArmory_advanced");//"Advanced Ammo Config" + Events["ConfigAmmo"].guiActive = true; + Events["ConfigAmmo"].guiActiveEditor = true; + Fields["AmmoTypeNum"].guiActive = false; + Fields["AmmoTypeNum"].guiActiveEditor = false; + } + else + { + Events["ToggleAmmoConfig"].guiName = StringUtils.Localize("#LOC_BDArmory_simple");//"Simple Ammo Config + Events["ConfigAmmo"].guiActive = false; + Events["ConfigAmmo"].guiActiveEditor = false; + Fields["AmmoTypeNum"].guiActiveEditor = true; + if (!canHotSwap) + Fields["AmmoTypeNum"].guiActive = false; + else + Fields["AmmoTypeNum"].guiActive = true; + } + UI_FloatRange ATrangeEditor = (UI_FloatRange)Fields["AmmoTypeNum"].uiControlEditor; + ATrangeEditor.maxValue = (float)ammoList.Count; + ATrangeEditor.onFieldChanged = SetupAmmo; + UI_FloatRange ATrangeFlight = (UI_FloatRange)Fields["AmmoTypeNum"].uiControlFlight; + ATrangeFlight.maxValue = (float)ammoList.Count; + ATrangeFlight.onFieldChanged = SetupAmmo; + } + else //disable ammo selector + { + Fields["AmmoTypeNum"].guiActive = false; + Fields["AmmoTypeNum"].guiActiveEditor = false; + Events["ToggleAmmoConfig"].guiActiveEditor = false; + } + UI_FloatRange FAOEditor = (UI_FloatRange)Fields["FiringTolerance"].uiControlEditor; + FAOEditor.onFieldChanged = FAOCos; + UI_FloatRange FAOFlight = (UI_FloatRange)Fields["FiringTolerance"].uiControlFlight; + FAOFlight.onFieldChanged = FAOCos; + Fields["FiringTolerance"].guiActive = FireAngleOverride; + Fields["FiringTolerance"].guiActiveEditor = FireAngleOverride; + Fields["fireBurstLength"].guiActive = BurstOverride; + Fields["fireBurstLength"].guiActiveEditor = BurstOverride; + if (BurstFire) + { + BeltFed = false; + } + if (eWeaponType == WeaponTypes.Ballistic) + { + rocketPod = false; + bulletInfoList = new BulletInfo[ammoList.Count]; + for (int i = 0; i < ammoList.Count; ++i) + { + bulletInfoList[i] = BulletInfo.bullets[ammoList[i]]; + } + } + if (eWeaponType == WeaponTypes.Rocket) + { + try + { + externalAmmo = bool.Parse(ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleWeapon", "externalAmmo")); + } + catch + { + externalAmmo = false; + Debug.LogError($"[BDArmory.ModuleWeapon] {shortName} missing externalAmmo field in .cfg! Fix your .cfg!"); + } + if (rocketPod && externalAmmo) + { + BeltFed = false; + PartResource rocketResource = GetRocketResource(); + if (rocketResource != null) + { + part.resourcePriorityOffset = +2; //make rocketpods draw from internal ammo first, if any, before using external supply + } + } + if (!rocketPod) + { + externalAmmo = true; + } + Events["ToggleAmmoConfig"].guiActiveEditor = false; + } + if (eWeaponType == WeaponTypes.Laser) + { + if (!pulseLaser) + { + roundsPerMinute = 3000; //50 rounds/sec or 1 'round'/FixedUpdate + } + else + { + pulseInConfig = true; + } + if (HEpulses) + { + pulseLaser = true; + HeatRay = false; + } + if (HeatRay) + { + HEpulses = false; + electroLaser = false; + } + rocketPod = false; + //disable fuze GUI elements + Fields["defaultDetonationRange"].guiActive = false; + Fields["defaultDetonationRange"].guiActiveEditor = false; + Fields["detonationRange"].guiActive = false; + Fields["detonationRange"].guiActiveEditor = false; + Fields["guiAmmoTypeString"].guiActiveEditor = false; //ammoswap + Fields["guiAmmoTypeString"].guiActive = false; + Events["ToggleAmmoConfig"].guiActiveEditor = false; + tracerBaseSWidth = tracerStartWidth; + tracerBaseEWidth = tracerEndWidth; + laserTexList = BDAcTools.ParseNames(laserTexturePath); + if (laserMaxDamage < 0) laserMaxDamage = laserDamage; + if (laserTexList.Count > 1) dynamicFX = true; + } + muzzleFlashList = new List>(); + List emitterList = BDAcTools.ParseNames(muzzleTransformName); + for (int i = 0; i < emitterList.Count; i++) + { + List muzzleFlashEmitters = new List(); + using (var mtf = part.FindModelTransforms(emitterList[i]).AsEnumerable().GetEnumerator()) + while (mtf.MoveNext()) + { + 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; + } + muzzleFlashList.Add(muzzleFlashEmitters); + } + if (HighLogic.LoadedSceneIsFlight) + { + if (bulletPool == null) + { + SetupBulletPool(); // Always set up the bullet pool in case the ammo type has bullet submunitions (it's not that big anyway). + } + if (eWeaponType == WeaponTypes.Ballistic) + { + if (shellPool == null) + { + SetupShellPool(); + } + if (useCustomBelt) + { + if (!string.IsNullOrEmpty(ammoBelt) && ammoBelt != "def") + { + if (ammoList.Count == 0) + { + Debug.LogError($"[BDArmory.ModuleWeapon]: Weapon {WeaponName} has no valid ammo types! Reverting to 'def'."); + ammoList = new List { "def" }; + } + customAmmoBelt = BDAcTools.ParseNames(ammoBelt); + customAmmoBeltIndexes = new int[customAmmoBelt.Count]; + int currIndex = -1; + for (int i = 0; i < customAmmoBelt.Count; ++i) + { + currIndex = ammoList.IndexOf(customAmmoBelt[i]); + if (currIndex < 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 {ammoList[0]}"); + customAmmoBelt[i] = ammoList[0]; + customAmmoBeltIndexes[i] = 0; + } + else + { + customAmmoBeltIndexes[i] = currIndex; + } + } + baseBulletVelocity = bulletInfoList[customAmmoBeltIndexes[0]].bulletVelocity; + } + else //belt is empty/"def" reset useAmmoBelt + { + useCustomBelt = false; + } + } + } + if (eWeaponType == WeaponTypes.Rocket) + { + if (rocketPod)// only call these for rocket pods + { + MakeRocketArray(); + UpdateRocketScales(); + } + else + { + if (shellPool == null) + { + SetupShellPool(); + } + } + } + + //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); + if (shellEjectTransforms.Length > 0 && shellPool == null) SetupShellPool(); + + //setup emitters + using (var pe = part.FindModelComponents().AsEnumerable().GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + pe.Current.maxSize *= part.rescaleFactor; + pe.Current.minSize *= part.rescaleFactor; + pe.Current.shape3D *= part.rescaleFactor; + pe.Current.shape2D *= part.rescaleFactor; + pe.Current.shape1D *= part.rescaleFactor; + + if (pe.Current.useWorldSpace && !oneShotWorldParticles) + { + BDAGaplessParticleEmitter gpe = pe.Current.gameObject.AddComponent(); + gpe.part = part; + gaplessEmitters.Add(gpe); + } + else + { + EffectBehaviour.AddParticleEmitter(pe.Current); + } + } + + //setup projectile colors + projectileColorC = GUIUtils.ParseColor255(projectileColor); + endColorS = projectileColor.Split(","[0]); + + startColorC = GUIUtils.ParseColor255(startColor); + startColorS = startColor.Split(","[0]); + + //init and zero points + targetPosition = Vector3.zero; + pointingAtPosition = Vector3.zero; + bulletPrediction = Vector3.zero; + + //setup audio + SetupAudio(); + if (eWeaponType == WeaponTypes.Laser || ChargeTime > 0) + { + chargeSound = SoundUtils.GetAudioClip(chargeSoundPath); + } + // Setup gauges + gauge = (BDStagingAreaGauge)part.AddModule("BDStagingAreaGauge"); + gauge.AmmoName = ammoName; + + var AmmoDef = PartResourceLibrary.Instance.GetDefinition(ammoName); + if (AmmoDef != null) + { + AmmoID = AmmoDef.id; + electricResource = PartResourceLibrary.Instance.GetDefinition(AmmoID).density == 0; + } + else + Debug.LogError($"[BDArmory.ModuleWeapon]: Resource definition for {ammoName} not found!"); + var SecAmmoDef = PartResourceLibrary.Instance.GetDefinition(secondaryAmmoName); + if (SecAmmoDef != null) + { + ECID = SecAmmoDef.id; + secECResource = PartResourceLibrary.Instance.GetDefinition(ECID).density == 0; + } + else + Debug.LogError($"[BDArmory.ModuleWeapon]: Resource definition for {secondaryAmmoName} not found!"); + + if (ECPerShot != 0) + { + Debug.LogWarning($"[BDArmory.ModuleWeapon]: Weapon part {part.name} is using deprecated 'ecPerShot' attribute. Please update the config to use 'secondaryAmmoPerShot' instead."); + secondaryAmmoPerShot = ECPerShot; + } //laser setup + if (eWeaponType == WeaponTypes.Laser) + { + if (electrolaserHits == null) { electrolaserHits = new RaycastHit[100]; } + if (reverseELHits == null) { reverseELHits = new RaycastHit[100]; } + if (orderedELHits == null) { orderedELHits = new RaycastHit[100]; } + if (beamCone == null) { beamCone = new GameObject[fireTransforms.Length]; } + if (r_cone == null) { r_cone = new Renderer[fireTransforms.Length]; } + SetupLaserSpecifics(); + if (maxTargetingRange < maxEffectiveDistance) + { + maxEffectiveDistance = maxTargetingRange; + } + baseLaserdamage = laserDamage; + } + if (crewserved) + { + CheckCrewed(); + } + + if (ammoList.Count > 1) + { + UI_FloatRange ATrangeFlight = (UI_FloatRange)Fields["AmmoTypeNum"].uiControlFlight; + ATrangeFlight.maxValue = (float)ammoList.Count; + if (!canHotSwap) + { + Fields["AmmoTypeNum"].guiActive = false; + } + } + baseDeviation = maxDeviation; //store original MD value + + var weaponManager = WeaponManager; + sourceInfo = new SourceInfo(vessel, weaponManager ? weaponManager.teamString : null, part, Vector3.zero); + graphicsInfo = new GraphicsInfo(bulletTexturePath, projectileColorC, startColorC, + tracerStartWidth, tracerEndWidth, tracerLength, tracerLuminance, tracerDeltaFactor, + smokeTexturePath, explModelPath, explSoundPath); + nukeInfo = new NukeInfo(); + } + 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); + customAmmoBeltIndexes = new int[customAmmoBelt.Count]; + int currIndex = -1; + for (int i = 0; i < customAmmoBelt.Count; ++i) + { + currIndex = ammoList.IndexOf(customAmmoBelt[i]); + if (currIndex < 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 {ammoList[0]}"); + customAmmoBelt[i] = ammoList[0]; + customAmmoBeltIndexes[i] = 0; + } + else + { + customAmmoBeltIndexes[i] = currIndex; + } + } + baseBulletVelocity = BulletInfo.bullets[customAmmoBelt[0].ToString()].bulletVelocity; + } + else + { + useCustomBelt = false; + } + } + GameEvents.onEditorPartPlaced.Add(OnEditorPartPlaced); + FindTurretInParents(part); + } + malusSightingAccuracy = 10f * Mathf.Tan(sightingAccuracy / 1000f); + //turret setup + using (List.Enumerator turr = part.FindModulesImplementing().GetEnumerator()) + while (turr.MoveNext()) + { + if (turr.Current == null) continue; + if (turr.Current.turretID != turretID) continue; + turret = turr.Current; + turret.SetReferenceTransform(fireTransforms[0]); + break; + } + if (yawRange == 0 && maxPitch == minPitch) + { + turret = null; + } + if (!turret) + { + Fields["onlyFireInRange"].guiActive = false; + Fields["onlyFireInRange"].guiActiveEditor = false; + } + if (HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight) + { + if ((turret || eWeaponType != WeaponTypes.Rocket) || (eWeaponType == WeaponTypes.Rocket && (!rocketPod || (rocketPod && externalAmmo)))) + { + Events["Jettison"].guiActive = false; + Actions["AGJettison"].active = false; + } + } + //custom turret setup + if (HighLogic.LoadedSceneIsFlight && customTurretID > 0) + { + float yaw = 0; + float minP = 0; + float maxP = 0; + using (var servo = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (servo.MoveNext()) + { + if (servo.Current == null) continue; + if ((int)servo.Current.turretID != (int)customTurretID) continue; + customTurret.Add(servo.Current); + servo.Current.SetReferenceTransform(fireTransforms[0]); + if (servo.Current.fullRotation) yaw = 360; + else + { + float tempyaw = servo.Current.maxYaw - servo.Current.minYaw; + if (tempyaw > yaw) + yaw = tempyaw; + } + minP += servo.Current.minPitch; + maxP += servo.Current.maxPitch; + } + customYaw = yaw; + customMinPitch = minP; + customMaxPitch = maxP; + if (customTurret.Count == 0) customTurretID = 0; + } + //setup animations + if (hasDeployAnim) + { + deployState = GUIUtils.SetUpSingleAnimation(deployAnimName, part); + Events["ToggleDeploy"].guiActiveEditor = true; + 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 = 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 (hasChargeHoldAnimation) + { + chargeHoldState = GUIUtils.SetUpSingleAnimation(chargeHoldAnimName, part); + if (chargeHoldState != null) + { + chargeHoldState.normalizedTime = 0; + chargeHoldState.speed = 0; + chargeHoldState.enabled = true; + } + } + } + if (hasFireAnimation) + { + List animList = BDAcTools.ParseNames(fireAnimName); + //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] = GUIUtils.SetUpSingleAnimation(animList[i].ToString(), part); + //Debug.Log("[BDArmory.ModuleWeapon] Added fire anim " + i); + fireState[i].normalizedTime = 0; + } + catch + { + Debug.LogWarning($"[BDArmory.ModuleWeapon]: {OriginalShortName} is missing fire anim " + i); + } + } + } + /* + if (graviticWeapon) + { + GraviticInConfig = true; + } + if (impulseWeapon) + { + ImpulseInConfig = true; + }*/ + if (eWeaponType != WeaponTypes.Laser) + { + SetupAmmo(null, null); + + if (eWeaponType == WeaponTypes.Rocket) + { + if (rocketInfo == null) + { + //if (BDArmorySettings.DEBUG_WEAPONS) + Debug.LogWarning("[BDArmory.ModuleWeapon]: Failed To load rocket : " + currentType); + } + else + { + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.ModuleWeapon]: AmmoType Loaded : " + currentType); + if (beehive) + { + string[] subMunitionData = bulletInfo.subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + string[] subrocketData = rocketInfo.subMunitionType.Split(new char[] { ';' }); + string rocketType = subMunitionData[0]; + if (!BulletInfo.bulletNames.Contains(projType) || !RocketInfo.rocketNames.Contains(rocketType)) + { + beehive = false; + Debug.LogWarning("[BDArmory.ModuleWeapon]: Invalid submunition on : " + currentType); + } + else + { + if (RocketInfo.rocketNames.Contains(rocketType)) + { + RocketInfo sRocket = RocketInfo.rockets[rocketType]; + SetupRocketPool(sRocket.name, sRocket.rocketModelPath); //Will need to move this if rockets ever get ammobelt functionality + } + } + } + } + } + else + { + if (bulletInfo == null) + { + //if (BDArmorySettings.DEBUG_WEAPONS) + Debug.LogWarning("[BDArmory.ModuleWeapon]: Failed To load bullet : " + currentType); + ParseBulletDragType(); // Have to parse the bullet drag type if there's no bulletInfo for + // compatibility with older mods where bulletDragTypeName was part of ModuleWeapon's specs. + } + else + { + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.ModuleWeapon]: BulletType Loaded : " + currentType); + if (beehive) + { + string[] subMunitionData = bulletInfo.subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + if (!BulletInfo.bulletNames.Contains(projType)) + { + beehive = false; + Debug.LogWarning("[BDArmory.ModuleWeapon]: Invalid submunition on : " + currentType); + } + } + } + } + } + + BDArmorySetup.OnVolumeChange += UpdateVolume; + if (HighLogic.LoadedSceneIsFlight) + { TimingManager.FixedUpdateAdd(TimingManager.TimingStage.FashionablyLate, AimAndFire); } + CustomFireKey = new BDInputInfo(customFireKey, "Custom Fire"); + + if (HighLogic.LoadedSceneIsFlight) + { + if (isAPS) + { + // Wait a frame before enabling the weapon so that all symmetric weapons have started. + StartCoroutine(EnableWeaponNextFrame()); + } + } + + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 59) + { + if (WeaponName == "bahaTurret") + { + maxEffectiveDistance = 1000; + InitializeEngagementRange(minSafeDistance, 1000); + engageRangeMax = 1000; + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60) + { + if (WeaponName == "bahaChemLaser") + { + if (turret != null) + { + turret.minPitch = -0.1f; + turret.maxPitch = 0.1f; + turret.yawRange = 0.2f; + } + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 65) + { + if (HighLogic.LoadedSceneIsFlight) + { + using (var engines = VesselModuleRegistry.GetModuleEngines(vessel).GetEnumerator()) + while (engines.MoveNext()) + { + if (engines.Current == null) continue; + MultiModeEngine mme = engines.Current.part.FindModuleImplementing(); + + if (mme && engines.Current.engineID == "Dry") continue; + float engineThrust = engines.Current.maxThrust * (mme != null ? 2 : 1); //AB velCurves tend to be around 2x at ~300m/s, will add extra thrust after initial jousts, but AB engines also capable of faster accel/energy recovery + S6R5dynamicRecoil += Mathf.Max(0f, engineThrust * (engines.Current.thrustPercentage / 100f)); + Debug.Log("[BDArmory.ModuleWeapon]: S6R5 DynamicRecoil set to : " + Mathf.CeilToInt(S6R5dynamicRecoil * 2)); + } + } + } + PAWRefresh(); // Refresh weapon PAW to enable/disable any extra options + } + + IEnumerator EnableWeaponNextFrame() + { + yield return new WaitForFixedUpdate(); + EnableWeapon(); + } + + private float S6R5dynamicRecoil; + + void OnDestroy() + { + if (muzzleFlashList != null) + foreach (var pelist in muzzleFlashList) + foreach (var pe in pelist) + if (pe) EffectBehaviour.RemoveParticleEmitter(pe); + foreach (var pe in part.FindModelComponents()) + if (pe) EffectBehaviour.RemoveParticleEmitter(pe); + if (laserRenderers != null) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + if (beamCone[c] != null) + beamCone[c].SetActive(false); + } + } + BDArmorySetup.OnVolumeChange -= UpdateVolume; + WeaponNameWindow.OnActionGroupEditorOpened.Remove(OnActionGroupEditorOpened); + WeaponNameWindow.OnActionGroupEditorClosed.Remove(OnActionGroupEditorClosed); + GameEvents.onEditorPartPlaced.Remove(OnEditorPartPlaced); + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.FashionablyLate, AimAndFire); + } + public void PAWRefresh() + { + if (eFuzeType == BulletFuzeTypes.Proximity || eFuzeType == BulletFuzeTypes.Flak || eFuzeType == BulletFuzeTypes.Timed || beehive) + { + Fields["defaultDetonationRange"].guiActive = true; + Fields["defaultDetonationRange"].guiActiveEditor = true; + Fields["detonationRange"].guiActive = true; + Fields["detonationRange"].guiActiveEditor = true; + // detonationRange = -1; + } + else + { + Fields["defaultDetonationRange"].guiActive = false; + Fields["defaultDetonationRange"].guiActiveEditor = false; + Fields["detonationRange"].guiActive = false; + Fields["detonationRange"].guiActiveEditor = false; + } + if (eWeaponType == WeaponTypes.Rocket && proximityDetonation) + { + Fields["detonateAtMinimumDistance"].guiActive = true; + Fields["detonateAtMinimumDistance"].guiActiveEditor = true; + } + else + { + Fields["detonateAtMinimumDistance"].guiActive = false; + Fields["detonateAtMinimumDistance"].guiActiveEditor = false; + } + if (useThisWeaponForAim) + Events["setAimOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_AimOverrideTrue");//"Revert Aim Override" + else + Events["setAimOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_AimOverrideFalse");//"Aim With This Weapon" + + 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) + { + Events["ToggleOverrideAngle"].guiName = StringUtils.Localize("#LOC_BDArmory_FireAngleOverride_Enable");// Enable Firing Angle Override + } + else + { + Events["ToggleOverrideAngle"].guiName = StringUtils.Localize("#LOC_BDArmory_FireAngleOverride_Disable");// Disable Firing Angle Override + } + + Fields["FiringTolerance"].guiActive = FireAngleOverride; + Fields["FiringTolerance"].guiActiveEditor = FireAngleOverride; + + GUIUtils.RefreshAssociatedWindows(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) + { + Events["ToggleBurstLengthOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_BurstLengthOverride_Enable");// Enable Firing Angle Override + } + else + { + Events["ToggleBurstLengthOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_BurstLengthOverride_Disable");// Disable Firing Angle Override + } + + Fields["fireBurstLength"].guiActive = BurstOverride; + Fields["fireBurstLength"].guiActiveEditor = BurstOverride; + + GUIUtils.RefreshAssociatedWindows(part); + } + + public bool toggleDeployState = true; + [KSPEvent(guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ToggleAnimation", active = true)]//Disable Engage Options + public void ToggleDeploy() + { + toggleDeployState = !toggleDeployState; + + if (toggleDeployState == false) + { + Events["ToggleDeploy"].guiName = StringUtils.Localize("#autoLOC_6001080");//"Deploy" + } + else + { + Events["ToggleDeploy"].guiName = StringUtils.Localize("#autoLOC_6001339");//""Retract" + } + if (deployState != null) + { + deployState.normalizedTime = HighLogic.LoadedSceneIsFlight ? 0 : toggleDeployState ? 1 : 0; + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + if (pSym.Current != part && pSym.Current.vessel == vessel) + { + var wep = pSym.Current.FindModuleImplementing(); + if (wep == null) continue; + wep.deployState.normalizedTime = toggleDeployState ? 1 : 0; + } + } + } + } + + void FAOCos(BaseField field, object obj) + { + maxAutoFireCosAngle = Mathf.Cos((FiringTolerance * Mathf.Deg2Rad)); + } + void AccAdjust(BaseField field, object obj) + { + maxDeviation = baseDeviation + ((baseDeviation / (baseRPM / roundsPerMinute)) - baseDeviation); + maxDeviation *= Mathf.Clamp(bulletInfo.projectileCount / 5, 1, 5); //modify deviation if shot vs slug + } + public string WeaponStatusdebug() + { + string status = "Weapon Type: "; + /* + if (eWeaponType == WeaponTypes.Ballistic) + status += "Ballistic; BulletType: " + currentType; + if (eWeaponType == WeaponTypes.Rocket) + status += "Rocket; RocketType: " + currentType + "; " + rocketModelPath; + if (eWeaponType == WeaponTypes.Laser) + status += "Laser"; + status += "; RoF: " + roundsPerMinute + "; deviation: " + maxDeviation + "; instagib = " + instagib; + */ + status += "-Lead Offset: " + GetLeadOffset() + "; FinalAimTgt: " + finalAimTarget + "; tgt: " + visualTargetVessel.GetName() + "; tgt Pos: " + targetPosition + "; pointingAtSelf: " + pointingAtSelf + "; tgt CosAngle " + targetCosAngle + "; wpn CosAngle " + targetAdjustedMaxCosAngle + "; Wpn Autofire " + autoFire; + + return status; + } + + void OnEditorPartPlaced(Part p) + { + if (p = part) FindTurretInParents(part); + } + private void FindTurretInParents(Part p) + { + if (p == null) + { + Fields["customTurretID"].guiActiveEditor = false; + return; + } + var turret = p.FindModuleImplementing(); + if (turret != null) + { + Fields["customTurretID"].guiActiveEditor = true; + return; + } + FindTurretInParents(p.parent); + } + + bool fireConditionCheck => ((((userFiring || agHoldFiring) && !isAPS) || autoFire) && (!turret || turret.TargetInRange(finalAimTarget, float.MaxValue, 10))) || (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) + { + if (lowpassFilter) + { + if (InternalCamera.Instance && InternalCamera.Instance.isActive) + { + lowpassFilter.enabled = true; + } + else + { + lowpassFilter.enabled = false; + } + } + + 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 = (((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 + { + audioSource.Stop(); + wasFiring = false; + audioSource2.PlayOneShot(overheatSound); + } + } + } + else + { + if (!oneShotSound) + { + audioSource.Stop(); + } + autoFire = false; + autoFireFailReason = "Disabled"; + } + + if (spinningDown && spinDownAnimation) + { + if (hasFireAnimation) + { + for (int i = 0; i < fireState.Length; i++) + { + if (fireState[i].normalizedTime > 1) fireState[i].normalizedTime = 0; + fireState[i].speed = fireAnimSpeed; + fireAnimSpeed = Mathf.Lerp(fireAnimSpeed, 0, 0.04f); + } + } + } + // Draw gauges + if (vessel.isActiveVessel) + { + gauge.UpdateAmmoMeter((float)((ammoCount >= requestResourceAmount ? ammoCount : 0) / ammoMaxCount)); + + if (showReloadMeter) + { + { + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + gauge.UpdateReloadMeter(timeSinceFired * BDArmorySettings.FIRE_RATE_OVERRIDE / 60); + else + gauge.UpdateReloadMeter(timeSinceFired * roundsPerMinute / fireTransforms.Length / 60); + } + } + if (isReloading) + { + gauge.UpdateReloadMeter(ReloadTimer); + } + gauge.UpdateHeatMeter(heat / maxHeat); + } + } + } + + void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight && !vessel.packed) + { + if (!vessel.IsControllable) + { + if (!(weaponState == WeaponStates.PoweringDown || weaponState == WeaponStates.Disabled)) + { + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Vessel {vessel.vesselName} is uncontrollable, disabling weapon " + part.name); + DisableWeapon(); + } + return; + } + + UpdateHeat(); + if (weaponState == WeaponStates.Standby && (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) { aimOnly = true; } + 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). + } + else if (eWeaponType == WeaponTypes.Laser) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(false); + } + } + } + //audioSource.Stop(); + } + GetAmmoCount(AmmoID, out ammoCount, out ammoMaxCount); + if (!BeltFed) + { + ReloadWeapon(); + } + if (crewserved) + { + CheckCrewed(); + } + } + } + + private void UpdateMenus(bool visible) + { + Events["HideUI"].active = visible; + Events["ShowUI"].active = !visible; + } + + private void OnActionGroupEditorOpened() + { + Events["HideUI"].active = false; + Events["ShowUI"].active = false; + } + + private void OnActionGroupEditorClosed() + { + Events["HideUI"].active = false; + Events["ShowUI"].active = true; + } + + [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_HideWeaponGroupUI", active = false)]//Hide Weapon Group UI + public void HideUI() + { + WeaponGroupWindow.HideGUI(); + UpdateMenus(false); + } + + [KSPEvent(guiActiveEditor = true, guiName = "#LOC_BDArmory_SetWeaponGroupUI", active = false)]//Set Weapon Group UI + public void ShowUI() + { + WeaponGroupWindow.ShowGUI(this); + UpdateMenus(true); + } + + void OnGUI() + { + 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 && (GPSTarget || slaved || MouseAimFlight.IsMouseAimActive || yawRange < 1 && maxPitch - minPitch < 1) + && (BDArmorySettings.AIM_ASSIST_MODE || !turret)) + { + if (BDArmorySettings.AIM_ASSIST_MODE) // Target + reticlePosition = pointingAtPosition + fixedLeadOffset / targetDistance * pointingDistance; + else // Aimer + reticlePosition = transform.position + (finalAimTarget - transform.position).normalized * pointingDistance; + + if (!slaved && !GPSTarget) + { + GUIUtils.DrawLineBetweenWorldPositions(pointingAtPosition, reticlePosition, 2, new Color(0, 1, 0, 0.6f)); + } + + GUIUtils.DrawTextureOnWorldPos(pointingAtPosition, BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); + + if (atprAcquired) + { + GUIUtils.DrawTextureOnWorldPos(atprTargetPosition, BDArmorySetup.Instance.openGreenSquare, new Vector2(20, 20), 0); + } + } + else + { + reticlePosition = bulletPrediction; + } + } + else + { + reticlePosition = pointingAtPosition; + } + + Texture2D texture; + if (VectorUtils.Angle(pointingAtPosition - transform.position, finalAimTarget - transform.position) < 1f) + { + texture = BDArmorySetup.Instance.greenSpikedPointCircleTexture; + } + else + { + texture = BDArmorySetup.Instance.greenPointCircleTexture; + } + GUIUtils.DrawTextureOnWorldPos(reticlePosition, texture, new Vector2(size, size), 0); + + if (BDArmorySettings.DEBUG_LINES) + { + if (targetAcquired) + { + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, targetPosition, 2, Color.blue); + } + } + } + + if (HighLogic.LoadedSceneIsEditor && BDArmorySetup.showWeaponAlignment && !(isAPS && !dualModeAPS)) + { + DrawAlignmentIndicator(); + } + + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS && (weaponState == WeaponStates.Enabled || weaponState == WeaponStates.EnabledForSecondaryFiring) && vessel && !vessel.packed && !MapView.MapIsEnabled) + { + GUIUtils.MarkPosition(debugTargetPosition, transform, Color.grey); //lets not have two MarkPositions use the same color... + Vector3 pos = debugTargetPosition; + GUIUtils.DrawLineBetweenWorldPositions(pos, pos + debugRelVelAdj, 2, Color.green); + pos += debugRelVelAdj; + GUIUtils.DrawLineBetweenWorldPositions(pos, pos + debugAccAdj, 2, Color.magenta); + pos += debugAccAdj; + GUIUtils.DrawLineBetweenWorldPositions(pos, pos + debugGravAdj, 2, Color.yellow); + GUIUtils.MarkPosition(finalAimTarget, transform, Color.cyan, size: 4); + if (targetInVisualRange && BDArmorySettings.AIMING_VISUAL_MALUS > 0) + { + pos += debugGravAdj; + GUIUtils.DrawLineBetweenWorldPositions(pos, pos + BDArmorySettings.AIMING_VISUAL_MALUS * kinematicAimMalus, 2, Color.black); + } + } + } + + #endregion KSP Events + //some code organization + //Ballistics + #region Guns + private void Fire() + { + if (BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + + float timeGap = GetTimeGap(); + if (timeSinceFired > timeGap + && !isOverheated + && !isReloading + && !pointingAtSelf + && (aiControlled || !GUIUtils.CheckMouseIsOnGui()) + && WMgrAuthorized()) + { + bool effectsShot = false; + if (!useRippleFire || barrelIndex == 0) + CheckLoadedAmmo(); + //Transform[] fireTransforms = part.FindModelTransforms("fireTransform"); + for (float iTime = Mathf.Min(timeSinceFired - timeGap, TimeWarp.fixedDeltaTime); iTime > 1e-4f; iTime -= timeGap) // Use 1e-4f instead of 0 to avoid jitter. + { + for (int i = 0; i < fireTransforms.Length; i++) + { + if ((!useRippleFire || fireState.Length == 1) || (useRippleFire && i == barrelIndex)) + { + if (CanFire(requestResourceAmount)) + { + Transform fireTransform = fireTransforms[i]; + spinningDown = false; + + //recoil + if (hasRecoil) + { + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 65) + part.rb.AddForceAtPosition(-fireTransform.forward * ((S6R5dynamicRecoil * 2) / (roundsPerMinute / 60)), + fireTransform.position, ForceMode.Impulse); + else + //doesn't take propellant gas 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); + } + + if (!effectsShot) + { + WeaponFX(); + effectsShot = true; + } + + sourceInfo.vessel = vessel; // The vessel might have changed if it's on a detachable fighter, for example. + sourceInfo.team = WeaponManager.teamString; // Similarly, teams may change if reassigned after spawning. + sourceInfo.position = fireTransform.position; + graphicsInfo.projectileColor = projectileColorC; + graphicsInfo.startColor = startColorC; + if (i == 0) + tracerIntervalCounter++; + if (tracerIntervalCounter > tracerInterval) + { + if (i == fireTransforms.Length - 1) + tracerIntervalCounter = 0; + graphicsInfo.tracerStartWidth = tracerStartWidth; + graphicsInfo.tracerEndWidth = tracerEndWidth; + graphicsInfo.tracerLength = tracerLength; + graphicsInfo.tracerLuminance = tracerLuminance; + } + else + { + graphicsInfo.tracerStartWidth = nonTracerWidth; + graphicsInfo.tracerEndWidth = nonTracerWidth; + graphicsInfo.tracerLuminance = bulletLuminance; + + if (!string.IsNullOrEmpty(smokeTexturePath)) + { + graphicsInfo.projectileColor = Color.grey; + graphicsInfo.startColor = Color.grey; + graphicsInfo.tracerLength = 0; + graphicsInfo.tracerLuminance = -1; + graphicsInfo.projectileColor.a *= 0.5f; + } + graphicsInfo.startColor.a *= 0.5f; + graphicsInfo.projectileColor.a *= 0.5f; + } + + Vessel targetV = null; + float guidance = 0f; + + if (visualTargetVessel != null && (targetAcquisitionType == TargetAcquisitionType.Radar || targetAcquisitionType == TargetAcquisitionType.Slaved)) + { + targetV = visualTargetVessel; + guidance = bulletInfo.guidanceDPS; + } + + float dmgMultiplier = strengthMutator; + if (instagib) + { + dmgMultiplier = -1; + } + + timeFired = Time.time - iTime; + var wm = WeaponManager; + if (isRippleFiring && wm && wm.barrageStagger > 0) // Add variability to fired time to cause variability in reload time. + { + var reloadVariability = UnityEngine.Random.Range(-wm.barrageStagger, wm.barrageStagger); + timeFired += reloadVariability; + } + + FireBullet(bulletInfo, ProjectileCount, sourceInfo, graphicsInfo, nukeInfo, + bulletDrop, (isAPS && delayTime > -1) ? delayTime - Time.time : Mathf.Max(maxTargetingRange, defaultDetonationRange) / bulletVelocity * 1.1f, + iTime, detonationRange, bulletTimeToCPA, + isAPS, isAPS ? tgtRocket : null, isAPS ? tgtShell : null, resourceSteal, instagib ? -1 : dmgMultiplier, bulletDmgMult, + true, 0f, 0f, fireTransform.forward, false, maxDeviation, targetV, guidance, true); + + //heat + heat += heatPerShot; + + RoundsRemaining++; + if (BurstOverride) + { + autofireShotCount++; + } + + // APS + if (isAPS && (tgtShell != null || tgtRocket != null)) + { + StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); + } + } + else + { + spinningDown = true; + if (!oneShotSound && wasFiring) + { + audioSource.Stop(); + wasFiring = false; + audioSource2.PlayOneShot(overheatSound); + } + } + } + } + } + + if (fireState.Length > 1) + { + barrelIndex++; + animIndex++; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + 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 + { + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; + if (barrelIndex >= fireTransforms.Length) + { + barrelIndex = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + GetShortName() + " reset"); + } + } + if (animIndex >= fireState.Length) animIndex = 0; + } + else + { + 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; + //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 + } + } + } + else + { + spinningDown = true; + } + } + + public bool CanFireSoon() + { + float timeGap = GetTimeGap(); + + var wm = WeaponManager; + if (wm == null) return false; + if (timeGap <= wm.targetScanInterval) + return true; + else + return timeSinceFired >= timeGap - wm.targetScanInterval; + } + #endregion Guns + //lasers + #region LaserFire + private bool FireLaser() + { + float chargeAmount; + if (pulseLaser) + { + chargeAmount = requestResourceAmount; + } + else + { + chargeAmount = requestResourceAmount * TimeWarp.fixedDeltaTime; + } + + float timeGap = GetTimeGap(); + beamDuration = Math.Min(timeGap * 0.8f, 0.1f); + + if (timeSinceFired > timeGap + && !isOverheated + // && !isReloading + && !pointingAtSelf + && (aiControlled || !GUIUtils.CheckMouseIsOnGui()) + && WMgrAuthorized()) + { + if (CanFire(chargeAmount)) + { + var aName = vessel.GetName(); + if (pulseLaser) + { + for (float iTime = Mathf.Min(timeSinceFired - timeGap, TimeWarp.fixedDeltaTime); iTime > 1e-4f; iTime -= timeGap) + { + timeFired = Time.time - iTime; + BDACompetitionMode.Instance.Scores.RegisterShot(aName); + if (!conicAoE) + LaserBeam(aName); + else + MicrowaveBeam(aName); + } + heat += heatPerShot; + + if (fireState.Length > 1) + { + barrelIndex++; + animIndex++; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + 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) + { + barrelIndex = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + GetShortName() + " reset"); + } + } + if (animIndex >= fireState.Length) animIndex = 0; + } + else + { + if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) + { + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; + } + } + } + else + { + if (!conicAoE) + LaserBeam(aName); + else + MicrowaveBeam(aName); + heat += heatPerShot * TimeWarp.CurrentRate; + BeamTracker += 0.02f; + if (BeamTracker > beamScoreTime) + { + BDACompetitionMode.Instance.Scores.RegisterShot(aName); + } + timeFired = Time.time; + } + if (!BeltFed) + { + RoundsRemaining++; + } + if (BurstOverride) + { + autofireShotCount++; + } + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + private void LaserBeam(string vesselname) + { + if (BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + WeaponFX(); + for (int i = 0; i < fireTransforms.Length; i++) + { + if (!useRippleFire || !pulseLaser || fireState.Length == 1 || (useRippleFire && i == barrelIndex)) + { + float damage = 0; + float initialDamage = laserDamage * 0.425f; + Transform tf = fireTransforms[i]; + LineRenderer lr = laserRenderers[i]; + Vector3 rayDirection = tf.forward; + + Vector3 targetDirection = Vector3.zero; //autoTrack enhancer + Vector3 targetDirectionLR = tf.forward; + if (pulseLaser) + { + 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 > turret.minPitch))) // causes laser to snap to target CoM if close enough. changed to only apply to turrets + && VectorUtils.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; //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 raycastDistance = isAPS ? (tgtShell != null ? (tgtShell.currentPosition - tf.position).magnitude : tgtRocket != null ? (tgtRocket.currentPosition - tf.position).magnitude : maxTargetingRange) : maxTargetingRange; // Only raycast to the incoming projectile if APS. + var hitCount = Physics.RaycastNonAlloc(ray, laserHits, raycastDistance, 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, raycastDistance, layerMask1); + hitCount = laserHits.Length; + } + //Debug.Log($"[LASER DEBUG] hitCount: {hitCount}"); + if (hitCount > 0) + { + var orderedHits = laserHits.Take(hitCount).OrderBy(x => x.distance); + using (var hitsEnu = orderedHits.GetEnumerator()) + { + Vector3 hitPartVelocity = Vector3.zero; + while (hitsEnu.MoveNext()) + { + var hitPart = hitsEnu.Current.collider.gameObject.GetComponentInParent(); + if (hitPart != null) // Don't ignore terrain hits. + { + hitPartVelocity = hitPart.vessel.Velocity() - BDKrakensbane.FrameVelocityV3f; + } + break; + } + var hit = hitsEnu.Current; + lr.useWorldSpace = true; + laserPoint = hit.point + TimeWarp.fixedDeltaTime * hitPartVelocity; + + lr.SetPosition(0, tf.position + (part.rb.velocity * Time.fixedDeltaTime)); + lr.SetPosition(1, laserPoint); + + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (p && p.vessel && p.vessel != vessel) + { + float distance = hit.distance; + if (instagib) + { + p.AddInstagibDamage(); + ExplosionFx.CreateExplosion(hit.point, 1, "BDArmory/Models/explosion/explosion", explSoundPath, ExplosionSourceType.Bullet, 0, null, vessel.vesselName, null, Hitpart: p); + } + else + { + if (electroLaser || HeatRay) + { + if (electroLaser) + { + //Due to Electrolasers/lightning bolts being a point source, and no guarantee that a craft is homogeneous material for armor/hull, calculating + //the path electricity would take to get to craft electrics to affect them is going to be a mess one way or another + ///////////////////////////////////////////////// + if (!VesselModuleRegistry.IgnoredVesselTypes.Contains(p.vessel.vesselType)) + { + float EMPDamage = laserDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime) * (BDArmorySettings.DMG_MULTIPLIER / 100); + Part closestCommand = null; + float distToCommandSqr = float.PositiveInfinity; //lets find out which command part is closest to the hit + Vector3 commandDir = Vector3.zero; + float distToCommand = float.PositiveInfinity; + foreach (var moduleCommand in VesselModuleRegistry.GetModuleCommands(p.vessel)) + { + float evalDist = (moduleCommand.part.transform.position - p.transform.position).sqrMagnitude; + if (evalDist < distToCommandSqr) + { + closestCommand = moduleCommand.part; + commandDir = closestCommand.transform.position - p.transform.position; + distToCommandSqr = evalDist; + } + } + distToCommand = commandDir.magnitude; + //then see how many parts are between the root of the hit part and the nearest command part + var ElecRay = new Ray(p.transform.position, commandDir); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Wheels); + var partCount = Physics.RaycastNonAlloc(ElecRay, electrolaserHits, distToCommand, layerMask); + if (partCount == electrolaserHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. + { + electrolaserHits = Physics.RaycastAll(ElecRay, distToCommand, layerMask); + partCount = electrolaserHits.Length; + } + //not ideal, since it's abstracted and can return erroneous electricity paths if, say, hit on a tip of a stock part wing, since that would likely have an unobstructed LoS to a cockpit, but that's a for later problem + var reverseRay = new Ray(ElecRay.origin + distToCommand * ElecRay.direction, -ElecRay.direction); + var reversePartCount = Physics.RaycastNonAlloc(reverseRay, reverseELHits, distToCommand, layerMask); + if (reversePartCount == reverseELHits.Length) + { + reverseELHits = Physics.RaycastAll(reverseRay, distToCommand, layerMask); + reversePartCount = reverseELHits.Length; + } + for (int h = 0; h < reversePartCount; ++h) + { + reverseELHits[h].distance = distToCommand - reverseELHits[h].distance; + reverseELHits[h].normal = -reverseELHits[h].normal; + } + // This is the most expensive part of this method + var totalPartCount = partCount + reversePartCount; + if (orderedELHits.Length < totalPartCount) Array.Resize(ref orderedELHits, totalPartCount); + Array.Copy(electrolaserHits, orderedELHits, partCount); + Array.Copy(reverseELHits, 0, orderedELHits, partCount, reversePartCount); + Array.Sort(reverseELHits, 0, totalPartCount, RaycastHitComparer.raycastHitComparer); // This generates garbage, but less than other methods using Linq or Lists. + + bool hullConduction = false; + for (int tpc = 0; tpc < totalPartCount; ++tpc) + { + hit = orderedELHits[tpc]; + Part partHit = hit.collider.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.vessel != p.vessel) continue; + var Armor = partHit.FindModuleImplementing(); + //now that we have a path for the electricity to take to a cockpit, what sort of armor/hull modifiers are present? + //Treating 'None' armor type as something akin to pre-stressed commerical-grade aluminium sheeting over an internal frame, and is regarded as conductive + //using Diffusivity as a hack conductivity value; diff >= 15 assumed to be a metal of some sort, and + //conductive (not worrying about Ohms and resistance), and below that non-conductive/insulated. + //Similarly, assuming that Hull massMod of >= 1 to be metal of some sort, and < to be some sort of wood/composite/etc + if (Armor != null && partHit.Rigidbody != null) + { + if (Armor.Diffusivity > 15) + { + if (Armor.HullMassAdjust < 0) + { + hullConduction = false; + continue; //conductive armor and non-Con hull, charge moves through armor to next part + } + else + { + hullConduction = true; + continue; //conductive armor and Con hull, charge moves through armor and hull to next part + } + } + else //non-conductive armor + { + if (Armor.HullMassAdjust >= 0) //and conductive hull + { + if (hullConduction) continue;//charge already flowing through hull to next part + else + { + EMPDamage -= 2 * Armor.Armour; //-2 EMP damage per mm of insulating armor charge has to pass through to get to conductive amterials on other side + hullConduction = true; + continue; + } + } + else //and non-con hull + { + if (!hullConduction) + EMPDamage -= 2 * Armor.Armour; + if (EMPDamage > 100) + { + EMPDamage /= 4; + hullConduction = true; //assumes even a full wood hull,e.g. would still have fuel lines/wiring busses/cable runs/ammo feeds/etc that could be used as an electricity path + continue; + } + else break; //insufficient charge to pass through non-conductive material + } + + } + } + } + if (EMPDamage > 0) + { + var emp = p.vessel.rootPart.FindModuleImplementing(); + if (emp == null) + { + emp = (ModuleDrainEC)p.vessel.rootPart.AddModule("ModuleDrainEC"); + //Debug.Log($"[BDArmory.ModuleWeapon]: EMP Module added to {p.vessel.GetName()}: {p.vessel.rootPart.partInfo.title}"); + } + emp.softEMP = true; + emp.incomingDamage = EMPDamage; + damage = EMPDamage; + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.ModuleWeapon]: {p.vessel.GetName()} receiving {laserDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime) * (BDArmorySettings.DMG_MULTIPLIER / 100)} EMP damage, passing through {totalPartCount} parts; EMP buildup applied: {EMPDamage}"); + } + } + else + { + p.skinTemperature += (laserDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime * (BDArmorySettings.DMG_MULTIPLIER / 100))); //add modifier to adjust damage by armor diffusivity value? + + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Heatray Applying {damage} heat to {p.name}"); + } + } + } + else + { + HitpointTracker armor = p.GetComponent(); + if (laserDamage > 0) + { + 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 (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.Armor) / 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) + { + damage = initialDamage * TimeWarp.fixedDeltaTime; + } + } + p.ReduceArmor(damage); //really should be tied into diffusivity, 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 (HEpulses) + { + ExplosionFx.CreateExplosion(hit.point, + (laserDamage / 10000), + explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName, null, Hitpart: p); + } + if (Impulse != 0) + { + 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, laserPoint, 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; + } + } + } + } + var aName = vesselname; + var tName = p.vessel.GetName(); + + if (BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, Mathf.Abs(damage))) + { + if (pulseLaser || (!pulseLaser && ScoreAccumulator > beamScoreTime)) // Score hits with pulse lasers or when the score accumulator is sufficient. + { + ScoreAccumulator = 0; + BDACompetitionMode.Instance.Scores.RegisterBulletHit(aName, tName, WeaponName, distance); + 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 + { + ScoreAccumulator += TimeWarp.fixedDeltaTime; + } + } + + if (timeSinceFired > 6 / 120 && BDArmorySettings.BULLET_HITS) + { + BulletHitFX.CreateBulletHit(p, hit.point, hit, hit.normal, false, 10, 0, WeaponManager.Team.Name); + } + } + else + { + if (electroLaser || HeatRay) continue; + var angularSpread = tanAngle * 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. + initialDamage = laserDamage / (1 + Mathf.PI * angularSpread * angularSpread) * 0.425f; + if (!BDArmorySettings.PAINTBALL_MODE) ProjectileUtils.CheckBuildingHit(hit, initialDamage, pulseLaser); + if (HEpulses) + { + ExplosionFx.CreateExplosion(hit.point, + (laserDamage / 10000), + explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName, null); + } + } + } + } + else + { + if (isAPS && !pulseLaser) + laserPoint = lr.transform.InverseTransformPoint( + tgtShell != null ? (tgtShell.currentPosition - TimeWarp.fixedDeltaTime * BDKrakensbane.FrameVelocityV3f) : // Bullets have already moved, so we need to correct for the frame velocity. + tgtRocket != null ? (tgtRocket.currentPosition + TimeWarp.fixedDeltaTime * (tgtRocket.currentVelocity - BDKrakensbane.FrameVelocityV3f)) : // Rockets have had frame velocity corrections applied, but not physics. + (targetDirectionLR * maxTargetingRange) + tf.position + ); + else + laserPoint = lr.transform.InverseTransformPoint((targetDirectionLR * maxTargetingRange) + tf.position); + lr.SetPosition(1, laserPoint); + if (HEpulses) + { + ExplosionFx.CreateExplosion(tf.position + rayDirection * raycastDistance, + (laserDamage / 10000), + explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName, null); + } + } + } + if (BDArmorySettings.DISCO_MODE) + { + projectileColorC = Color.HSVToRGB(Mathf.Lerp(tracerEndWidth, grow ? 1 : 0, 0.35f), 1, 1); + tracerStartWidth = Mathf.Lerp(tracerStartWidth, grow ? 1 : 0.05f, 0.35f); //add new tracerGrowWidth field? + tracerEndWidth = Mathf.Lerp(tracerEndWidth, grow ? 1 : 0.05f, 0.35f); //add new tracerGrowWidth field? + if (grow && tracerStartWidth > 0.95) grow = false; + if (!grow && tracerStartWidth < 0.06f) grow = true; + UpdateLaserSpecifics(true, dynamicFX, true, false); + } + } + } + + //Conic AoE 'beam' for AoE beam weapons - tractor beams, microwave EMP, heatrays, etc. + private void MicrowaveBeam(string vesselname) + { + if (BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + WeaponFX(); + float damage = 0; + float initialDamage = laserDamage * 0.425f; + var beamLength = engageRangeMax / 1000; + var beamAngle = Mathf.Tan(beamFOV * Mathf.Deg2Rad) * beamLength; //assumes a default 1km long, 45deg angle cone model + //Also, TOD - add conic AoE ingo to the GetInfo weapon infocard + Vector3 beamScale = new Vector3(beamAngle, beamAngle, beamLength); //this need a 1/localscale for oddly scaled parts? + for (int i = 0; i < fireTransforms.Length; i++) + { + if (!useRippleFire || !pulseLaser || fireState.Length == 1 || (useRippleFire && i == barrelIndex)) + { + beamCone[i].SetActive(true); + beamCone[i].transform.position = fireTransforms[i].position; + beamCone[i].transform.localScale = beamScale; + beamCone[i].transform.rotation = fireTransforms[i].rotation; + } + } + // technically, since we are loading in a Model for the beam cone FX, we could attach a IsTrigger collider to it and then just do a onTriggerEnter call instead + //but since we'd still need vessel filtering for stuff behind terrain/?underwater? + LoS/occulsion raycasts for non-EMP damage types (and EMP damage only needs a single Vessel hit, not many per part hits) + //this ends up for now being the simpler implementation + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + if (loadedvessels.Current == null || !loadedvessels.Current.loaded) continue; + if (VesselModuleRegistry.IgnoredVesselTypes.Contains(loadedvessels.Current.vesselType)) continue; + + if (Vector3.Angle(loadedvessels.Current.CoM - fireTransforms[0].transform.position, fireTransforms[0].forward) > beamFOV / 2f) continue; + if (loadedvessels.Current.IsUnderwater()) continue; //would microwaves work underwater...? + if (!friendlyFire) //don't affect friendly targets. Something something phased array dynamic beam shaping + { + var wms = VesselModuleRegistry.GetModule(loadedvessels.Current); + if (wms == null || wms.Team == WeaponManager.Team) continue; + } + if (!loadedvessels.Current.Splashed && vessel.IsUnderwater()) //not sure if a water transition should serve as a barrier, but easily commented out + continue; + if (loadedvessels.Current == vessel) continue; + float distance = (loadedvessels.Current.CoM - fireTransforms[0].transform.position).magnitude; + if (distance > maxTargetingRange) continue; + 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 * 0.425f) / (1 + Mathf.PI * angularSpread * angularSpread); + + if (electroLaser) + { + float EMPDamage = initialDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime) * (BDArmorySettings.DMG_MULTIPLIER / 100); + Vector3 commandDir = Vector3.zero; + float shieldvalue = 0; + foreach (var moduleCommand in VesselModuleRegistry.GetModuleCommands(loadedvessels.Current)) + { + //Debug.Log($"checking cockpit {moduleCommand.part.partInfo.title}"); + //see how many parts are between emitter and the nearest command part to see which one is least shielded + commandDir = moduleCommand.part.transform.position - fireTransforms[barrelIndex].transform.position; + var distToCommand = commandDir.magnitude; + var ElecRay = new Ray(fireTransforms[barrelIndex].position, commandDir); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Wheels); + var partCount = Physics.RaycastNonAlloc(ElecRay, electrolaserHits, distToCommand, layerMask); + if (partCount == electrolaserHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. + { + electrolaserHits = Physics.RaycastAll(ElecRay, distToCommand, layerMask); + partCount = electrolaserHits.Length; + } + //Debug.Log($"parts between emission and cockpit: {partCount}"); + float testShieldValue = 0; + for (int mwh = 0; mwh < partCount; ++mwh) + { + Part partHit = electrolaserHits[mwh].collider.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; + + //AoE EMP field EMP damage mitigation - -1 EMP damage per mm of conductive armor/5t of conductive hull mass per part occluding command part from emission source + var Armor = partHit.FindModuleImplementing(); + if (Armor != null && partHit.Rigidbody != null) + { + if (Armor.Diffusivity > 15) testShieldValue += Armor.Armour; + if (Armor.HullMassAdjust >= 0) testShieldValue += (partHit.mass * 4); + } + } + if (shieldvalue < testShieldValue) shieldvalue = testShieldValue; + } + EMPDamage -= shieldvalue; + if (EMPDamage > 0) + { + var emp = loadedvessels.Current.rootPart.FindModuleImplementing(); + if (emp == null) + { + emp = (ModuleDrainEC)loadedvessels.Current.rootPart.AddModule("ModuleDrainEC"); + //Debug.Log($"[BDArmory.ModuleWeapon]: EMP Module added to {p.vessel.GetName()}: {p.vessel.rootPart.partInfo.title}"); + } + emp.softEMP = true; + emp.incomingDamage = EMPDamage; + damage = EMPDamage; + //if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.ModuleWeapon]: {loadedvessels.Current.GetName()} receiving {EMPDamage} EMP damage; EMP buildup applied: {initialDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime) * (BDArmorySettings.DMG_MULTIPLIER / 100)}; reduction from armor: {shieldvalue}"); + } + } + else + { + using (var parts = loadedvessels.Current.Parts.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current == null) continue; + if (ProjectileUtils.IsIgnoredPart(parts.Current)) continue; + Ray ray = new Ray(fireTransforms[barrelIndex].position, parts.Current.CenterOfDisplacement - fireTransforms[barrelIndex].position); + if (Physics.Raycast(ray, out RaycastHit h, maxTargetingRange, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels))) + { + var hitPart = h.collider.gameObject.GetComponentInParent(); + var hitEVA = h.collider.gameObject.GetComponentUpwards(); + if (hitEVA != null) hitPart = hitEVA.part; + if (hitPart != null) + { + if (instagib) + { + hitPart.AddInstagibDamage(); + ExplosionFx.CreateExplosion(hitPart.transform.position, 1, "BDArmory/Models/explosion/explosion", explSoundPath, ExplosionSourceType.Bullet, 0, null, vessel.vesselName, null, Hitpart: hitPart); + continue; + } + if (hitPart == parts.Current) + { + HitpointTracker armor = hitPart.GetComponent(); + if (laserDamage > 0) + { + 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.Armor) / 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) + { + damage = initialDamage * TimeWarp.fixedDeltaTime; + } + } + hitPart.ReduceArmor(damage); //really should be tied into diffusivity, density, and SafeUseTemp - lasers would need to melt/ablate material away; needs to be in cm^3. Review later + hitPart.AddDamage(damage); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Damage Applied to {hitPart.name} on {hitPart.vessel.GetName()}: {damage}"); + } + if (HEpulses) + { + ExplosionFx.CreateExplosion(h.point, + (laserDamage / 10000), + explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName, null, Hitpart: hitPart); + } + if (HeatRay) + { + float heatDmg = initialDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime) * (BDArmorySettings.DMG_MULTIPLIER / 100); + hitPart.skinTemperature += heatDmg; //add modifier to adjust damage by armor diffusivity value + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Heatray Applying {heatDmg} heat to {hitPart.name}"); + damage += heatDmg; + } + if (Impulse != 0) + { + if (!pulseLaser) + { + Impulse *= TimeWarp.fixedDeltaTime; + } + if (hitPart.rb != null && hitPart.rb.mass > 0) + { + hitPart.rb.AddForceAtPosition((hitPart.transform.position - fireTransforms[barrelIndex].position).normalized * (float)Impulse, hitPart.CenterOfDisplacement, ForceMode.Impulse); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Impulse of {Impulse} Applied to {hitPart.vessel.GetName()}"); + damage += Impulse / 100; + } + } + if (graviticWeapon) + { + if (hitPart.rb != null && hitPart.rb.mass > 0) + { + float duration = BDArmorySettings.WEAPON_FX_DURATION; + if (!pulseLaser) + { + duration = BDArmorySettings.WEAPON_FX_DURATION * TimeWarp.fixedDeltaTime; + } + var ME = hitPart.FindModuleImplementing(); + if (ME == null) + { + ME = (ModuleMassAdjust)hitPart.AddModule("ModuleMassAdjust"); + } + ME.massMod += (massAdjustment * TimeWarp.fixedDeltaTime); + ME.duration += duration; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Gravitic Buildup Applied to {hitPart.vessel.GetName()}: {massAdjustment}t added"); + //if (laserDamage == 0) + damage += massAdjustment * 100; + } + } + } + } + } + } + } + + var aName = vesselname; + var tName = loadedvessels.Current.GetName(); + if (BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, Mathf.Abs(damage))) + { + if (pulseLaser || (!pulseLaser && ScoreAccumulator > beamScoreTime)) // Score hits with pulse lasers or when the score accumulator is sufficient. + { + ScoreAccumulator = 0; + BDACompetitionMode.Instance.Scores.RegisterBulletHit(aName, tName, WeaponName, distance); + 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 + { + ScoreAccumulator += TimeWarp.fixedDeltaTime; + } + } + } + } + + public void SetupLaserSpecifics() + { + //chargeSound = SoundUtils.GetAudioClip(chargeSoundPath); + if (HighLogic.LoadedSceneIsFlight) + { + audioSource.clip = fireSound; + } + Color laserColor = GUIUtils.ParseColor255(projectileColor); + laserColor.a = laserColor.a / 2; + if (conicAoE) + { + var cone = GameDatabase.Instance.GetModel(laserModelPath); + cone.SetActive(false); + beamConeFX = ObjectPool.CreateObjectPool(cone, 10, true, true); + if (beamConeFX != null) + { + for (int i = 0; i < fireTransforms.Length; i++) + { + Transform tf = fireTransforms[i]; + beamCone[i] = beamConeFX.GetPooledObject(); + beamCone[i].transform.SetPositionAndRotation(tf.position, tf.rotation); + beamCone[i].transform.localScale = Vector3.zero; + r_cone[i] = beamCone[i].GetComponentInChildren(); + r_cone[i].material = new Material(Shader.Find("KSP/Particles/Additive")); + r_cone[i].material.SetColor("_TintColor", laserColor); + r_cone[i].material.mainTexture = GameDatabase.Instance.GetTexture("BDArmory/Models/laser/laserTex", false); + } + } + } + else + { + if (laserRenderers == null) + { + laserRenderers = new LineRenderer[fireTransforms.Length]; + } + for (int i = 0; i < fireTransforms.Length; i++) + { + Transform tf = fireTransforms[i]; + laserRenderers[i] = tf.gameObject.AddOrGetComponent(); + laserRenderers[i].material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + laserRenderers[i].material.SetColor("_TintColor", laserColor); + laserRenderers[i].material.mainTexture = GameDatabase.Instance.GetTexture(laserTexList[0], false); + laserRenderers[i].material.SetTextureScale("_MainTex", new Vector2(0.01f, 1)); + laserRenderers[i].textureMode = LineTextureMode.Tile; + laserRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; //= false; + laserRenderers[i].receiveShadows = false; + laserRenderers[i].startWidth = tracerStartWidth; + laserRenderers[i].endWidth = tracerEndWidth; + laserRenderers[i].positionCount = 2; + laserRenderers[i].SetPosition(0, Vector3.zero); + laserRenderers[i].SetPosition(1, Vector3.zero); + laserRenderers[i].useWorldSpace = false; + laserRenderers[i].enabled = false; + } + } + } + public void UpdateLaserSpecifics(bool newColor, bool newTex, bool newWidth, bool newOffset) + { + if (conicAoE) + { + if (r_cone == null) return; + + for (int i = 0; i < fireTransforms.Length; i++) + { + if (newColor) + { + r_cone[i].material.SetColor("_TintColor", projectileColorC); //change beam to new color + } + } + } + else + { + if (laserRenderers == null) + { + return; + } + for (int i = 0; i < fireTransforms.Length; i++) + { + if (newColor) + { + laserRenderers[i].material.SetColor("_TintColor", projectileColorC); //change beam to new color + } + if (newTex) + { + laserRenderers[i].material.mainTexture = GameDatabase.Instance.GetTexture(laserTexList[UnityEngine.Random.Range(0, laserTexList.Count - 1)], false); //add support for multiple tex patchs, randomly cycle through + laserRenderers[i].material.SetTextureScale("_MainTex", new Vector2(beamScalar, 1)); + } + if (newWidth) + { + laserRenderers[i].startWidth = tracerStartWidth; + laserRenderers[i].endWidth = tracerEndWidth; + } + if (newOffset) + { + Offset += beamScrollRate; + laserRenderers[i].material.SetTextureOffset("_MainTex", new Vector2(Offset, 0)); + } + } + } + } + #endregion + //Rockets + #region RocketFire + // this is the extent of RocketLauncher code that differs from ModuleWeapon + public void FireRocket() //#11, #673 + { + int rocketsLeft; + + float timeGap = GetTimeGap(); + if (timeSinceFired > timeGap + && !isReloading + && !pointingAtSelf + && (aiControlled || !GUIUtils.CheckMouseIsOnGui()) + && WMgrAuthorized()) + {// fixes rocket ripple code for proper rippling + bool effectsShot = false; + var wm = WeaponManager; + for (float iTime = Mathf.Min(timeSinceFired - timeGap, TimeWarp.fixedDeltaTime); iTime > 1e-4f; iTime -= timeGap) + { + if (BDArmorySettings.INFINITE_AMMO) + { + rocketsLeft = 1; + } + else + { + if (!externalAmmo) + { + PartResource rocketResource = GetRocketResource(); + rocketsLeft = (int)rocketResource.amount; + } + else + { + vessel.GetConnectedResourceTotals(AmmoID, out double ammoCurrent, out double ammoMax); + rocketsLeft = rocketPod ? Mathf.Clamp((int)(RoundsPerMag - RoundsRemaining), 0, Mathf.Clamp((int)ammoCurrent, 0, RoundsPerMag)) : (int)ammoCurrent; + } + } + if (rocketsLeft >= 1) + { + if (rocketPod) + { + for (int s = 0; s < ProjectileCount; s++) + { + Transform currentRocketTfm = rockets[rocketsLeft - 1]; + GameObject rocketObj = rocketPool[SelectedAmmoType].GetPooledObject(); + rocketObj.transform.position = currentRocketTfm.position; + //rocketObj.transform.rotation = currentRocketTfm.rotation; + rocketObj.transform.rotation = currentRocketTfm.parent.rotation; + rocketObj.transform.localScale = part.rescaleFactor * Vector3.one; + PooledRocket rocket = rocketObj.GetComponent(); + rocket.explModelPath = explModelPath; + 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.detonateAtMinimumDistance = detonateAtMinimumDistance; + rocket.detonationRange = detonationRange; + // rocket.maxAirDetonationRange = maxAirDetonationRange; + rocket.timeToDetonation = predictedFlightTime; + rocket.tntMass = rocketInfo.tntMass; + rocket.shaped = rocketInfo.shaped; + 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; + rocket.incendiary = incendiary; + rocket.randomThrustDeviation = thrustDeviation; + rocket.bulletDmgMult = bulletDmgMult; + rocket.sourceVessel = vessel; + rocket.sourceWeapon = part; + rocketObj.transform.SetParent(currentRocketTfm.parent); + rocket.rocketName = GetShortName() + " rocket"; + rocket.team = wm.Team.Name; + rocket.parentRB = part.rb; + rocket.rocket = RocketInfo.rockets[currentType]; + rocket.rocketSoundPath = rocketSoundPath; + rocket.thief = resourceSteal; //currently will only steal on direct hit + rocket.dmgMult = strengthMutator; + if (instagib) rocket.dmgMult = -1; + if (isAPS) + { + rocket.isAPSprojectile = true; + rocket.tgtShell = tgtShell; + rocket.tgtRocket = tgtRocket; + if (delayTime > 0) rocket.lifeTime = delayTime; + } + rocket.isSubProjectile = false; + rocketObj.SetActive(true); + } + if (!BDArmorySettings.INFINITE_AMMO) + { + if (externalAmmo) + { + part.RequestResource(ammoName.GetHashCode(), (double)requestResourceAmount, BDArmorySettings.WEAPONS_RESPECT_CROSSFEED ? ResourceFlowMode.STACK_PRIORITY_SEARCH : ResourceFlowMode.ALL_VESSEL); + } + else + { + GetRocketResource().amount--; + } + } + if (!BeltFed) + { + RoundsRemaining++; + } + if (BurstOverride) + { + autofireShotCount++; + } + UpdateRocketScales(); + } + else + { + if (!isOverheated) + { + for (int i = 0; i < fireTransforms.Length; i++) + { + if ((!useRippleFire || fireState.Length == 1) || (useRippleFire && i == barrelIndex)) + { + for (int s = 0; s < ProjectileCount; s++) + { + Transform currentRocketTfm = fireTransforms[i]; + GameObject rocketObj = rocketPool[SelectedAmmoType].GetPooledObject(); + rocketObj.transform.position = currentRocketTfm.position; + //rocketObj.transform.rotation = currentRocketTfm.rotation; + rocketObj.transform.rotation = currentRocketTfm.parent.rotation; + rocketObj.transform.localScale = part.rescaleFactor * Vector3.one; + PooledRocket rocket = rocketObj.GetComponent(); + rocket.explModelPath = explModelPath; + 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.detonateAtMinimumDistance = detonateAtMinimumDistance; + rocket.detonationRange = detonationRange; + // rocket.maxAirDetonationRange = maxAirDetonationRange; + rocket.timeToDetonation = predictedFlightTime; + rocket.tntMass = rocketInfo.tntMass; + rocket.shaped = rocketInfo.shaped; + 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; + rocket.incendiary = incendiary; + rocket.randomThrustDeviation = thrustDeviation; + rocket.bulletDmgMult = bulletDmgMult; + rocket.sourceVessel = vessel; + rocket.sourceWeapon = part; + rocketObj.transform.SetParent(currentRocketTfm); + rocket.parentRB = part.rb; + rocket.rocket = RocketInfo.rockets[currentType]; + rocket.rocketName = GetShortName() + " rocket"; + rocket.team = wm.Team.Name; + rocket.rocketSoundPath = rocketSoundPath; + rocket.thief = resourceSteal; + rocket.dmgMult = strengthMutator; + if (instagib) rocket.dmgMult = -1; + if (isAPS) + { + rocket.isAPSprojectile = true; + rocket.tgtShell = tgtShell; + rocket.tgtRocket = tgtRocket; + if (delayTime > 0) rocket.lifeTime = delayTime; + } + rocket.isSubProjectile = false; + rocketObj.SetActive(true); + } + if (!BDArmorySettings.INFINITE_AMMO) + { + part.RequestResource(ammoName.GetHashCode(), (double)requestResourceAmount, BDArmorySettings.WEAPONS_RESPECT_CROSSFEED ? ResourceFlowMode.STACK_PRIORITY_SEARCH : ResourceFlowMode.ALL_VESSEL); + } + heat += heatPerShot; + if (!BeltFed) + { + RoundsRemaining++; + } + if (BurstOverride) + { + autofireShotCount++; + } + } + } + } + } + if (!effectsShot) + { + WeaponFX(); + effectsShot = true; + } + timeFired = Time.time - iTime; + } + } + if (fireState.Length > 1) + { + barrelIndex++; + animIndex++; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + 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) + { + barrelIndex = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + GetShortName() + " reset"); + } + } + if (animIndex >= fireState.Length) animIndex = 0; + } + else + { + if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) + { + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; + } + } + if (isAPS && (tgtShell != null || tgtRocket != null)) + { + StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); + } + } + } + + void MakeRocketArray() + { + Transform rocketsTransform = part.FindModelTransform("rockets");// important to keep this seperate from the fireTransformName transform + int numOfRockets = rocketsTransform.childCount; // due to rockets.Rocket_n being inconsistantly aligned + rockets = new Transform[numOfRockets]; // (and subsequently messing up the aim() vestors) + if (rocketPod) // and this overwriting the previous fireTransFormName -> fireTransForms + { + RoundsPerMag = numOfRockets; + } + for (int i = 0; i < numOfRockets; i++) + { + string rocketName = rocketsTransform.GetChild(i).name; + int rocketIndex = int.Parse(rocketName.Substring(7)) - 1; + rockets[rocketIndex] = rocketsTransform.GetChild(i); + } + if (!descendingOrder) Array.Reverse(rockets); + } + + void UpdateRocketScales() + { + double rocketQty = 0; + + if (!externalAmmo) + { + PartResource rocketResource = GetRocketResource(); + if (rocketResource != null) + { + rocketQty = rocketResource.amount; + rocketsMax = rocketResource.maxAmount; + } + else + { + rocketQty = 0; + rocketsMax = 0; + } + } + else + { + rocketQty = (RoundsPerMag - RoundsRemaining); + rocketsMax = Mathf.Min(RoundsPerMag, (float)ammoCount); + } + var rocketsLeft = Math.Floor(rocketQty); + + for (int i = 0; i < rocketsMax; i++) + { + if (i < rocketsLeft) rockets[i].localScale = Vector3.one; + else rockets[i].localScale = Vector3.zero; + } + } + + public PartResource GetRocketResource() + { + using (IEnumerator res = part.Resources.GetEnumerator()) + while (res.MoveNext()) + { + if (res.Current == null) continue; + if (res.Current.resourceName == ammoName) return res.Current; + } + return null; + } + #endregion RocketFire + //Shared FX and resource consumption code + #region WeaponUtilities + + /// + /// Get the time gap between shots for the weapon. + /// + /// + float GetTimeGap() + { + if (eWeaponType == WeaponTypes.Laser && !pulseLaser) return 0; + float timeGap = 60 / roundsPerMinute * TimeWarp.CurrentRate; // RPM * barrels + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + timeGap = 60 / BDArmorySettings.FIRE_RATE_OVERRIDE * TimeWarp.CurrentRate; + switch (eWeaponType) + { + // FIXME These are functionally the same as before. Are these actually correct, particularly the case for rockets? + case WeaponTypes.Ballistic: + // FIXME This should also be being called on guns with multiple fireanims(and thus multiple independant barrels); is causing twinlinked weapons to gain 2x firespeed in barrageMode + if (!(useRippleFire && fireState.Length > 1)) + timeGap *= fireTransforms.Length; // RPM compensating for barrel count + break; + case WeaponTypes.Rocket: + if (!rocketPod) + timeGap *= fireTransforms.Length; + if (useRippleFire && fireState.Length > 1) + timeGap /= fireTransforms.Length; + break; + case WeaponTypes.Laser: + if (!(useRippleFire && fireState.Length > 1)) + timeGap *= fireTransforms.Length; + break; + } + return timeGap; + } + + void GetAmmoCount(int resourceID, out double ammoC, out double ammoM) + { + if (externalAmmo) + part.GetConnectedResourceTotals(resourceID, + BDArmorySettings.WEAPONS_RESPECT_CROSSFEED ? ResourceFlowMode.STACK_PRIORITY_SEARCH : ResourceFlowMode.ALL_VESSEL, + out ammoC, out ammoM); + else part.GetConnectedResourceTotals(resourceID, ResourceFlowMode.NO_FLOW, out ammoC, out ammoM); + } + + bool CanFire(float AmmoPerShot) + { + if (!hasGunner) + { + ScreenMessages.PostScreenMessage(StringUtils.Localize("#autoLOC_211097"), 5.0f, ScreenMessageStyle.UPPER_CENTER); // #autoLOC_211097 = No crew on part! + return false; + } + if (BDArmorySettings.INFINITE_AMMO) return true; + bool secondaryAmmo = false; + if (secondaryAmmoPerShot != 0) + { + if (!(secECResource && CheatOptions.InfiniteElectricity)) //else skip to main ammo as EC is accounted for + { + vessel.GetConnectedResourceTotals(ECID, out double EcCurrent, out double ecMax); + if (EcCurrent > secondaryAmmoPerShot * (secECResource ? 0.95f : 1)) + { + secondaryAmmo = true; + if (requestResourceAmount == 0) //weapon only uses secondaryAmmoName for some reason? + { + part.RequestResource(ECID, secondaryAmmoPerShot, (secECResource || !BDArmorySettings.WEAPONS_RESPECT_CROSSFEED) ? ResourceFlowMode.ALL_VESSEL : ResourceFlowMode.STACK_PRIORITY_SEARCH); + return true; + } + } + else + { + if (part.vessel.isActiveVessel) ScreenMessages.PostScreenMessage($"{part.partInfo.title} {StringUtils.Localize("#autoLOC_244332")} {PartResourceLibrary.Instance.GetDefinition(secondaryAmmoName).displayName}", 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 + } + } + if ((electricResource && CheatOptions.InfiniteElectricity)) + { + if (secondaryAmmo) //secondary ammo isn't electricity, so consume requisite ammount + { + part.RequestResource(ECID, secondaryAmmoPerShot, (secECResource || !BDArmorySettings.WEAPONS_RESPECT_CROSSFEED) ? ResourceFlowMode.ALL_VESSEL : ResourceFlowMode.STACK_PRIORITY_SEARCH); + } + return true; + } + else + { + GetAmmoCount(AmmoID, out double ammoCurrent, out double ammoMax); + ammoCount = ammoCurrent; + if (ammoCount >= AmmoPerShot * 0.995f) //catch floating point errors from fractional ammo spread across multiple boxes + // TODO?? Change code to some sort of list of ammoboxes to only draw from box with current highest resouce amount to do proper integer reductions? + { + if (secondaryAmmo) //moving this here so weapon only drains secondary Ammo if primary ammo also exists + { + part.RequestResource(ECID, secondaryAmmoPerShot, (secECResource || !BDArmorySettings.WEAPONS_RESPECT_CROSSFEED) ? ResourceFlowMode.ALL_VESSEL : ResourceFlowMode.STACK_PRIORITY_SEARCH); + } + if (part.RequestResource(ammoName.GetHashCode(), (double)AmmoPerShot, (electricResource || !BDArmorySettings.WEAPONS_RESPECT_CROSSFEED) ? ResourceFlowMode.ALL_VESSEL : externalAmmo ? (BDArmorySettings.WEAPONS_RESPECT_CROSSFEED ? ResourceFlowMode.STACK_PRIORITY_SEARCH : ResourceFlowMode.ALL_VESSEL) : ResourceFlowMode.NO_FLOW) > 0) //for guns with internal ammo supplies and no external, only draw from the weapon part + { + 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; + return false; + } + + void PlayFireAnim() + { + if (hasCharged) + { + if (hasChargeHoldAnimation) + chargeHoldState.enabled = false; + else if (hasChargeAnimation) chargeState.enabled = false; + } + //Debug.Log("[BDArmory.ModuleWeapon]: fireState length = " + fireState.Length); + for (int i = 0; i < fireState.Length; i++) + { + // 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) + unclampedSpeed = (BDArmorySettings.FIRE_RATE_OVERRIDE * fireState[i].length) / 60f; + + float lowFramerateFix = 1; + if (roundsPerMinute > 500f) + { + lowFramerateFix = (0.02f / Time.deltaTime); + } + fireAnimSpeed = fireAnimOverrideSpeed > 0 ? fireAnimOverrideSpeed : Mathf.Clamp(unclampedSpeed, 1f * lowFramerateFix, 20f * lowFramerateFix); + fireState[i].enabled = true; + if (unclampedSpeed == fireAnimSpeed || fireState[i].normalizedTime > 1) + { + fireState[i].normalizedTime = 0; + } + 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 + "normalizedTime: " + fireState[i].normalizedTime); + } + } + } + + void WeaponFX() + { + //sound + if (ChargeTime > 0 && !hasCharged) + { + audioSource.Stop(); + } + if (oneShotSound) + { + audioSource.Stop(); + audioSource.PlayOneShot(fireSound); + } + else + { + wasFiring = true; + if (!audioSource.isPlaying) + { + if (audioSource2.isPlaying) audioSource2.Stop(); // Stop any continuing cool-down sounds. + audioSource.clip = fireSound; + audioSource.loop = (soundRepeatTime == 0); + audioSource.time = 0; + audioSource.Play(); + } + else + { + if (audioSource.time >= fireSound.length) + { + audioSource.time = soundRepeatTime; + } + } + } + //animation + if (hasFireAnimation) + { + PlayFireAnim(); + } + + for (int i = 0; i < muzzleFlashList.Count; i++) + { + if ((!useRippleFire || fireState.Length == 1) || useRippleFire && i == barrelIndex) + //muzzle flash + using (List.Enumerator pEmitter = muzzleFlashList[i].GetEnumerator()) + while (pEmitter.MoveNext()) + { + if (pEmitter.Current == null) continue; + if (pEmitter.Current.useWorldSpace && !oneShotWorldParticles) continue; + if (pEmitter.Current.maxEnergy < 0.5f) + { + float twoFrameTime = Mathf.Clamp(Time.deltaTime * 2f, 0.02f, 0.499f); + pEmitter.Current.maxEnergy = twoFrameTime; + pEmitter.Current.minEnergy = twoFrameTime / 3f; + } + pEmitter.Current.Emit(); + } + using (List.Enumerator gpe = gaplessEmitters.GetEnumerator()) + while (gpe.MoveNext()) + { + if (gpe.Current == null) continue; + gpe.Current.EmitParticles(); + } + } + //shell ejection + if (BDArmorySettings.EJECT_SHELLS) + { + for (int i = 0; i < shellEjectTransforms.Length; ++i) + { + if ((!useRippleFire || fireState.Length == 1) || (useRippleFire && i == barrelIndex)) + StartCoroutine(EjectShell(shellEjectDelay, i)); + } + } + } + + IEnumerator EjectShell(float delay, int ejectTransformIndex) + { + if (delay > 0) yield return new WaitForSecondsFixed(delay); + if (part == null || part.rb == null) yield break; + + GameObject ejectedShell = shellPool.GetPooledObject(); + ejectedShell.transform.position = shellEjectTransforms[ejectTransformIndex].position; + ejectedShell.transform.rotation = shellEjectTransforms[ejectTransformIndex].rotation; + ejectedShell.transform.localScale = Vector3.one * shellScale; + ShellCasing shellComponent = ejectedShell.GetComponent(); + shellComponent.initialV = part.rb.velocity; + shellComponent.configV = shellEjectVelocity; + shellComponent.configD = shellEjectDeviation; + shellComponent.lifeTime = shellEjectLifeTime; + ejectedShell.SetActive(true); + + } + + private void CheckLoadedAmmo() + { + if (!useCustomBelt) return; + if (customAmmoBelt.Count < 1) return; + if (AmmoIntervalCounter == 0 || (AmmoIntervalCounter > 0 && customAmmoBeltIndexes[AmmoIntervalCounter] != customAmmoBeltIndexes[AmmoIntervalCounter - 1])) + { + SetupAmmo(null, null); + } + AmmoIntervalCounter++; + if (AmmoIntervalCounter == customAmmoBelt.Count) + { + AmmoIntervalCounter = 0; + } + } + #endregion WeaponUtilities + //misc. like check weaponmgr + #region WeaponSetup + bool WMgrAuthorized() + { + if (vessel.isActiveVessel) + { + var wm = WeaponManager; + if (wm != null && wm.hasSingleFired) return false; // Manual firing + else return true; + } + else // AI firing + { + return true; + } + } + + void CheckWeaponSafety() + { + pointingAtSelf = false; + + // While I'm not saying vessels larger than 500m are impossible, let's be practical here + const float maxCheckRange = 500f; + 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, pointingDistance, layerMask1)) + { + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + if (p && p.vessel && p.vessel == vessel) + { + pointingAtSelf = true; + break; + } + } + + pointingAtPosition = fireTransforms[i].position + (ray.direction * pointingDistance); + } + } + + HashSet enabledStates = new HashSet { WeaponStates.Enabled, WeaponStates.PoweringUp, WeaponStates.Locked }; + public void EnableWeapon(bool secondaryFiring = false) + { + if (enabledStates.Contains(weaponState) || (secondaryFiring && weaponState == WeaponStates.EnabledForSecondaryFiring)) + return; + + StopShutdownStartupRoutines(); + startupRoutine = StartCoroutine(StartupRoutine(secondaryFiring: secondaryFiring)); + } + + HashSet disabledStates = new HashSet { WeaponStates.Disabled, WeaponStates.PoweringDown }; + public void DisableWeapon() + { + if (dualModeAPS) isAPS = true; + if (isAPS && WeaponManager != null) + { + if (ammoCount > 0 || BDArmorySettings.INFINITE_AMMO) + { + //EnableWeapon(); + aiControlled = true; + return; + } + } + if (shutdownRoutine != null) + return; + if (disabledStates.Contains(weaponState)) + return; + + StopShutdownStartupRoutines(); + + if (part.isActiveAndEnabled) shutdownRoutine = StartCoroutine(ShutdownRoutine()); + } + + 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)) + { + StopShutdownStartupRoutines(); + standbyRoutine = StartCoroutine(StandbyRoutine()); + } + else + { + weaponState = WeaponStates.Standby; + UpdateGUIWeaponState(); + BDArmorySetup.Instance.UpdateCursorState(); + } + } + + public void ParseWeaponType(string type) + { + type = type.ToLower(); + + switch (type) + { + case "ballistic": + eWeaponType = WeaponTypes.Ballistic; + break; + case "rocket": + eWeaponType = WeaponTypes.Rocket; + break; + case "laser": + eWeaponType = WeaponTypes.Laser; + break; + case "cannon": + // Note: this type is deprecated. behavior is duplicated with Ballistic and bulletInfo.explosive = true + // Type remains for backward compatability for now. + eWeaponType = WeaponTypes.Ballistic; + break; + } + } + #endregion WeaponSetup + + #region Audio + + void UpdateVolume() + { + if (audioSource) + { + audioSource.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; + } + if (audioSource2) + { + audioSource2.volume = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; + } + if (lowpassFilter) + { + lowpassFilter.cutoffFrequency = BDArmorySettings.IVA_LOWPASS_FREQ; + } + } + + void SetupAudio() + { + fireSound = SoundUtils.GetAudioClip(fireSoundPath); + overheatSound = SoundUtils.GetAudioClip(overheatSoundPath); + if (!audioSource) //Fire sound + { + audioSource = gameObject.AddComponent(); + audioSource.bypassListenerEffects = true; + audioSource.minDistance = .3f; + audioSource.maxDistance = Mathf.Clamp(100 * caliber / 2, 1000, 10000); //gunshots of bigger guns carry further + audioSource.priority = 10; + audioSource.dopplerLevel = 0; + audioSource.spatialBlend = 1; + } + + if (!audioSource2) //overheat/reload/reload complete + { + audioSource2 = gameObject.AddComponent(); + audioSource2.bypassListenerEffects = true; + audioSource2.minDistance = .3f; + audioSource2.maxDistance = 1000; + audioSource2.dopplerLevel = 0; + audioSource2.priority = 10; + audioSource2.spatialBlend = 1; + } + + if (reloadAudioPath != string.Empty) + { + reloadAudioClip = SoundUtils.GetAudioClip(reloadAudioPath); + } + if (reloadCompletePath != string.Empty) + { + reloadCompleteAudioClip = SoundUtils.GetAudioClip(reloadCompletePath); + } + + if (!lowpassFilter && gameObject.GetComponents().Length == 0) + { + lowpassFilter = gameObject.AddComponent(); + lowpassFilter.cutoffFrequency = BDArmorySettings.IVA_LOWPASS_FREQ; + lowpassFilter.lowpassResonanceQ = 1f; + } + + UpdateVolume(); + } + + #endregion Audio + + #region Targeting + public Vector3 kinematicAimMalus = default, kinematicAimMalusDelta = default; + // float rangeAimMalus = 0; + void Aim() + { + var wm = WeaponManager; + //AI control + if (aiControlled && !slaved && !GPSTarget) + { + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + if (!targetAcquired && (!wm || Time.time - staleGoodTargetTime > Mathf.Max(60f / BDArmorySettings.FIRE_RATE_OVERRIDE, wm.targetScanInterval))) + { + autoFire = false; + return; + } + } + else + { + if (!targetAcquired && (!wm || Time.time - staleGoodTargetTime > Mathf.Max(60f / roundsPerMinute, wm.targetScanInterval))) + { + autoFire = false; + autoFireFailReason = "Stale target expired"; + return; + } + } + } + + Vector3 finalTarget = targetPosition; + bool manualAiming = false; + if (aiControlled && !slaved && wm != null && (!targetAcquired || (wm.staleTarget && wm.detectedTargetTimeout > 0))) + { + if (wm.staleTarget && staleGoodTargetTime > 0 && staleGoodTargetTime <= wm.detectedTargetTimeout) //cap staletarget prediction to point when target forgotten + { + if (BDKrakensbane.IsActive) + { + staleFinalAimTarget -= 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. + var timeSinceGood = Time.time - staleGoodTargetTime; + finalAimTarget = AIUtils.PredictPosition(staleFinalAimTarget, staleTargetVelocity - BDKrakensbane.FrameVelocityV3f, staleTargetAcceleration, timeSinceGood / (1f + timeSinceGood / 30f)); // Smoothly limit prediction to 30s to prevent wild aiming. + switch (eWeaponType) + { + case WeaponTypes.Ballistic: + case WeaponTypes.Rocket: // Perform same correction for rockets as for bullets (it's probably fine). + finalAimTarget += lastTimeToCPA * (stalePartVelocity - BDKrakensbane.FrameVelocityV3f - smoothedPartVelocity); // Account for our own velocity changes. + break; + // Lasers have no timeToCPA correction. + } + + // if (FlightGlobals.ActiveVessel == vessel) Debug.Log($"DEBUG t: {Time.time}, tgt acq: {targetAcquired}, stale: {wm.staleTarget}, Stale aimer: aim at {finalAimTarget:G3} ({finalAimTarget.magnitude:G3}m), last: {staleFinalAimTarget:G3}, Δt: {timeSinceGood:F2}s ({timeSinceGood / (1f + timeSinceGood / 30f):F2}s)"); + 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 - staleGoodTargetTime); + } + } + if (!targetAcquired) + { + if (turret) turret.ReturnTurret(); + for (int i = 0; i < customTurret.Count; i++) + { + if (customTurret[i] == null) continue; + if (customTurret[i].vessel != vessel) continue; + customTurret[i].ReturnTurret(); + } + } + } + else + { + Transform fireTransform = fireTransforms[0]; + if (eWeaponType == WeaponTypes.Rocket && rocketPod) + { + fireTransform = rockets[0].parent; // support for legacy RLs + } + if (!slaved && !GPSTarget && !aiControlled && !isAPS && (vessel.isActiveVessel || BDArmorySettings.REMOTE_SHOOTING)) + { + manualAiming = true; + bool foundTarget = targetAcquired; + if (!targetAcquired) + { // Override the smoothing (which isn't run without a target otherwise). + smoothedPartVelocity = part.rb.velocity; + smoothedPartAcceleration = vessel.acceleration_immediate; + } + if (yawRange > 0 || maxPitch > minPitch) + { + //MouseControl + var camera = FlightCamera.fetch; + Ray ray; + if (!MouseAimFlight.IsMouseAimActive) + { + Vector3 mouseAim = new(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0); + ray = camera.mainCamera.ViewportPointToRay(mouseAim); + } + else + { + Vector3 mouseAimFlightTarget = MouseAimFlight.GetMouseAimTarget; + ray = new Ray(camera.transform.position, mouseAimFlightTarget); + } + + float maxAimRange = targetAcquired ? (targetPosition - ray.origin).magnitude : maxTargetingRange; + if (Physics.Raycast(ray, out RaycastHit hit, maxTargetingRange, layerMask1)) + { + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + + if (p != null && p.vessel != null && p.vessel == vessel) //aim through self vessel if occluding mouseray + { + targetPosition = ray.origin + ray.direction * maxAimRange; + } + else + { + targetPosition = hit.point; + } + if (p != null && p.rb != null && p.vessel != null) + { + foundTarget = true; + targetVelocity = p.rb.velocity; + targetAcceleration = p.vessel.acceleration_immediate; + targetIsLandedOrSplashed = p.vessel.LandedOrSplashed; + } + } + else + { + if (visualTargetVessel != null && visualTargetVessel.loaded) + { + foundTarget = true; + if (!targetCOM && visualTargetPart != null) + { + targetPosition = ray.origin + ray.direction * Vector3.Distance(visualTargetPart.transform.position, ray.origin); + targetVelocity = visualTargetPart.rb.velocity; + targetAcceleration = visualTargetPart.vessel.acceleration; + targetIsLandedOrSplashed = visualTargetPart.vessel.LandedOrSplashed; + } + else + { + targetPosition = ray.origin + ray.direction * Vector3.Distance(visualTargetVessel.transform.position, ray.origin); + targetVelocity = visualTargetVessel.rb_velocity; + targetAcceleration = visualTargetVessel.acceleration; + targetIsLandedOrSplashed = visualTargetVessel.LandedOrSplashed; + } + } + else + { + targetPosition = ray.origin + ray.direction * maxAimRange; + } + } + } + else if (!targetAcquired && (wm == null || !wm.staleTarget)) + { + float maxAimRange = targetAcquired ? (targetPosition - fireTransform.position).magnitude : maxTargetingRange; + targetPosition = fireTransform.position + fireTransform.forward * maxAimRange; // For fixed weapons, aim straight ahead (needed for targetDistance below for the trajectory sim) if no current target. + } + if (!foundTarget) + { + targetVelocity = -BDKrakensbane.FrameVelocityV3f; // Stationary targets' rigid bodies are being moved opposite to the Krakensbane frame velocity. + targetAcceleration = Vector3.zero; + targetIsLandedOrSplashed = true; + } + finalTarget = targetPosition; // In case aim assist and AI control is off. + } + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if ((FlightGlobals.getAltitudeAtPos(targetPosition) < 0) && (FlightGlobals.getAltitudeAtPos(targetPosition) + targetRadius > 0)) //vessel not completely submerged + { + if (caliber < 75 || eWeaponType == WeaponTypes.Laser) + { + targetPosition += VectorUtils.GetUpDirection(targetPosition) * Mathf.Abs(FlightGlobals.getAltitudeAtPos(targetPosition)); //set targetposition to surface directly above target + } + } + } + //aim assist + Vector3 originalTarget = targetPosition; + var supported = targetIsLandedOrSplashed || targetAcceleration.sqrMagnitude == 0; // Assume non-accelerating targets are "supported". + if (!manualAiming) + { + // Correct for the FI, which hasn't run yet, but does before visuals are next shown. This should synchronise the target's position and velocity with the bullet at the start of the next frame. + targetPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, Time.fixedDeltaTime); + targetVelocity += Time.fixedDeltaTime * targetAcceleration; + + // Correct for unity integration system + // Unity uses semi-implicit euler method during fixed updates. This means the velocity is updated first, and then position. + // This creates consistent errors that the following velocity offset compensates for. + targetVelocity += 0.5f * Time.fixedDeltaTime * (supported ? targetAcceleration : targetAcceleration - (Vector3)FlightGlobals.getGeeForceAtPosition(targetPosition)); + // There is no equivalent correction for the weapon part due to our specific placement of the bullet with the given velocity. + } + Vector3 firePosition = fireTransform.position; + targetDistance = Vector3.Distance(targetPosition, firePosition); + origTargetDistance = targetDistance; + + RunTrajectorySimulation(); // Run the trajectory sim after picking a target for this frame, otherwise a bunch of stuff is reset between frames. This is required for the rocket aiming. + + if (BDArmorySettings.AIM_ASSIST || aiControlled) + { + switch (eWeaponType) + { + case WeaponTypes.Ballistic: //Gun targeting + { + /* There are 3 main situations that the aiming code needs to satisfy: + - Static: where the two vessels are supported on or near the surface of Kerbin. + In this situation, there is no effect from velocity, acceleration or Krakensbane, just the variation in gravity over the path of the bullet. + VM gives perfectly stationary vessels, and the kinematic smoothing should give sufficiently static landed/splashed vessels. + The numerical integrator for the target is irrelevant here due to the target being static. + This situation is useful for getting the initial setup of the bullets and their trajectories (e.g., bullet drop, iTime) correct. Bullets should closely follow the debug lines. + - Translational: where variation in gravity is negligible, e.g., at the limit of Kerbin's SoI. + In this situation, the solution should be analytically solvable for constantly accelerating vessels once changes in the Krakensbane velocity offloading are accounted for. + This situation is useful for getting the velocity corrections to finalTarget correct (e.g., part.rb.velocity)'b'. + The solution from numerical simulation should agree closely with the analytic solution here — can be used to partially validate the numerical accuracy of KSP/Unity (only partially since the simplicity of the Hamiltonian may mean that the integrators appear more accurate here than they would normally be). + This situation also covers most short-range conditions during in atmosphere dogfights, which generally are sufficiently covered by the analytic solution. + FIXME There still seems to be a small offset for the non-active vessels (e.g., firing from the active vessel, then switching to the targetted vessel shortly before the bullets arrive shows an offset of ~1m.) + - Orbital (>100km): where varying gravity, vessel acceleration and Krakensbane must all be accounted for. + In this situation, the numerical integrators for bullets and the target need to closely approximate their actual trajectories. + This situation is useful for making sure that the way finalTarget is calculated works in this geometry (i.e., is combining bulletDrop and part.rb.velocity to get the firing direction sufficient or are they just the lowest order terms when in orbit?) and dealing with KSP's orbital drift compensation (separate gravitational vs local acceleration). + For <100km orbits, corrections due to Krakensbane may need adjusting. + */ + Vector3 bulletInitialPosition, relativePosition, bulletEffectiveVelocity, relativeVelocity, bulletAcceleration, relativeAcceleration, targetPredictedPosition, bulletDropOffset, bulletInitialVelocityDelta; + float timeToCPA; + Vector3 firingDirection, lastFiringDirection; + + var timeGap = GetTimeGap(); + var iTime = timeSinceFired - timeGap >= TimeWarp.fixedDeltaTime ? + TimeWarp.fixedDeltaTime : + TimeWarp.fixedDeltaTime - (TimeWarp.fixedDeltaTime + timeGap - timeSinceFired) % TimeWarp.fixedDeltaTime; // This is the iTime correction for the frame that the gun will actually fire on. + if (iTime < 1e-4f) iTime = TimeWarp.fixedDeltaTime; // Avoid jitter by aliasing iTime < 1e-4 to TimeWarp.fixedDeltaTime for the frame after. + firePosition = AIUtils.PredictPosition(fireTransforms[0].position, smoothedPartVelocity, smoothedPartAcceleration, Time.fixedDeltaTime); // Position of the end of the barrel at the start of the next frame. + + firingDirection = smoothedRelativeFinalTarget.At(Time.fixedDeltaTime).normalized; // Estimate of the current firing direction for this frame based on the previous frames. + bulletAcceleration = bulletDrop ? (Vector3)FlightGlobals.getGeeForceAtPosition(firePosition) : Vector3.zero; // Acceleration at the start point. + bulletInitialPosition = AIUtils.PredictPosition(firePosition, baseBulletVelocity * firingDirection, bulletAcceleration, iTime); // Bullets are initially placed up to 1 frame ahead (iTime). + bulletInitialVelocityDelta = iTime * bulletAcceleration; + + // Check whether we should use the analytic solution or the numeric one. These initial values don't affect the numeric solution. + if (lastTimeToCPA >= 0) + { + timeToCPA = lastTimeToCPA + deltaTimeToCPA; // Use the previous timeToCPA adjusted for the previous delta as a decent initial estimate. + } + else + { + relativePosition = targetPosition - bulletInitialPosition; + relativeVelocity = targetVelocity - (smoothedPartVelocity + baseBulletVelocity * firingDirection); + timeToCPA = BDAMath.Sqrt(relativePosition.sqrMagnitude / relativeVelocity.sqrMagnitude); // Rough initial estimate. + } + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, timeToCPA); + bulletAcceleration = bulletDrop ? (Vector3)FlightGlobals.getGeeForceAtPosition((bulletInitialPosition + targetPredictedPosition) / 2f) : Vector3.zero; // Average acceleration over the bullet's path. Drag is ignored. + var offTarget = Vector3.Dot(firingDirection, fireTransforms[0].forward) < 0.985f; // More than 10° off-target. This should cover most cases, even when not using the analytic solution. + bool useAnalyticAiming = timeToCPA * bulletAcceleration.magnitude < 100f; // TODO This condition could be improved to better cover all situations where the analytic solution isn't sufficiently accurate. + if (offTarget || useAnalyticAiming) // The gun is significantly off-target or we want the optimum analytic solution => perform a loop based on an "optimal" firing direction. + { + // For artillery (if it ever gets implemented), TimeToCPA needs to use the furthest time, not the closest (AIUtils.CPAType). + int count = 0; + // This loop is correct for situation 1. + // It also appears to be correct for situation 2, but accuracy is different depending on which vessel has focus. + // - From the target's perspective, the shots are quite accurate. + // - From the shooter's perspective, the shots are often wide, but not consistently. + do + { + // Note: Bullets are initially placed up to 1 frame ahead (iTime) to compensate for where they would move to during this physics frame. + // Also, we have already adjusted the target's position and velocity for where it ought to be next frame. + // Thus, the following calculations are based on the state at the start of the next frame. + // It is also using the firing direction from the initial estimate, so it is effectively always performing 2 iterations (1 initial and 1 here). + lastFiringDirection = firingDirection; + bulletEffectiveVelocity = smoothedPartVelocity + baseBulletVelocity * firingDirection + bulletInitialVelocityDelta; + bulletInitialPosition = firePosition + iTime * baseBulletVelocity * firingDirection; + bulletAcceleration = bulletDrop ? (Vector3)FlightGlobals.getGeeForceAtPosition((bulletInitialPosition + targetPredictedPosition) / 2f) : Vector3.zero; // Drag is ignored. + relativePosition = targetPosition - bulletInitialPosition; + relativeVelocity = targetVelocity - bulletEffectiveVelocity; + relativeAcceleration = targetAcceleration - bulletAcceleration; + timeToCPA = AIUtils.TimeToCPA(relativePosition, relativeVelocity, relativeAcceleration, maxTargetingRange / bulletEffectiveVelocity.magnitude); // time to CPA from the next frame (where the bullet starts). + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, timeToCPA); + bulletDropOffset = -0.5f * (timeToCPA + iTime) * (timeToCPA + iTime) * bulletAcceleration; // The bullet starts on the next frame so it uses timeToCPA+iTime, other uses use timeToCPA+Time.fixedDeltaTime. + finalTarget = targetPredictedPosition + bulletDropOffset - (timeToCPA + Time.fixedDeltaTime) * smoothedPartVelocity; + firingDirection = (finalTarget - fireTransforms[0].position).normalized; + } while (++count < 10 && Vector3.Dot(lastFiringDirection, firingDirection) < 0.9998f); // ~1° margin of error is sufficient to prevent premature firing (usually) + } + else // Reasonably on-target and the analytic solution isn't accurate enough. + { + // Note: we can't base this on the firing direction from the analytic solution as the single step is not enough to converge sufficiently accurately from the analytic solution to the correct solution. + // Instead, we must rely on the convergence over time based on our estimate from the previous frame (which is very quick unless near the limits of the weapon). + // This is correct for situations 1 and 2. + // It suffers the same accuracy noise as the analytic solution. + // However, there seems to be some inconsistencies between the analytic and numeric solutions when the CPA distance is non-zero (t<0 or t>max) or when the solver switches between roots of the cubic. Fortunately, these situations are generally only for extreme situations. + // Also, the numeric solution is giving strangely discrete values initially. + // For situation 3, the solution is not quite right, but is fairly good for high accelerations when within 5-15km. + + bulletEffectiveVelocity = smoothedPartVelocity + baseBulletVelocity * firingDirection; + + var (simBulletCPA, simTargetCPA, simTimeToCPA) = BallisticTrajectoryClosestApproachSimulation( + bulletInitialPosition, + bulletEffectiveVelocity + bulletInitialVelocityDelta, + bulletDrop, + targetPosition, + targetVelocity, + targetAcceleration, + supported, + BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER * Time.fixedDeltaTime, + maxTargetingRange / bulletEffectiveVelocity.magnitude, + AIUtils.CPAType.Earliest + ); + timeToCPA = simTimeToCPA; + bulletDropOffset = AIUtils.PredictPosition(bulletInitialPosition, bulletEffectiveVelocity, Vector3.zero, timeToCPA) - simBulletCPA; // Bullet drop is the acceleration component. + finalTarget = simTargetCPA + bulletDropOffset - (timeToCPA + Time.fixedDeltaTime) * smoothedPartVelocity; + } + if (lastTimeToCPA >= 0) + { + deltaTimeToCPA = timeToCPA - lastTimeToCPA; + smoothedRelativeFinalTarget.Update(finalTarget - fireTransforms[0].position); + } + else + { + deltaTimeToCPA = 0; + smoothedRelativeFinalTarget.Reset(finalTarget - fireTransforms[0].position); + } + lastTimeToCPA = timeToCPA; + bulletTimeToCPA = timeToCPA; + 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 = timeToCPA * (targetVelocity - smoothedPartVelocity); + debugAccAdj = 0.5f * timeToCPA * timeToCPA * targetAcceleration; + debugGravAdj = bulletDropOffset; + // var missDistance = AIUtils.PredictPosition(relativePosition, 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); + } + } + break; + case WeaponTypes.Rocket: //Rocket targeting + { + finalTarget = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, predictedFlightTime) + trajectoryOffset; + targetDistance = Mathf.Clamp(Vector3.Distance(targetPosition, firePosition), 0, maxTargetingRange); + } + break; + } + } + if (targetInVisualRange && BDArmorySettings.AIMING_VISUAL_MALUS > 0) // Apply a malus to visual aiming from mk1 eyeballs. + { + // We want a slow random walk that improves rapidly with shots fired. + // float size = BDArmorySettings.AIMING_VISUAL_MALUS * ((smoothedPartVelocity - targetVelocity).OneNorm() / (1 + shotsFiredSinceAcquiringTarget) + BDArmorySettings.AIMING_VISUAL_MALUS * (smoothedPartAcceleration - targetAcceleration).OneNorm()); + // kinematicAimMalus = factor * kinematicAimMalus + (1f - factor) * size * UnityEngine.Random.insideUnitSphere; + malusReduction = (1f + Mathf.Min(malusReductionPerShot * shotsFiredSinceAcquiringTarget, 99f)) * (1f + Mathf.Min(Time.time - targetAcquisitionTime, 9f)); + float size = malusSightingAccuracy * targetDistance * ((smoothedPartVelocity - targetVelocity).OneNorm() + 1f) / malusReduction + (smoothedPartAcceleration - targetAcceleration).OneNorm(); + kinematicAimMalusDelta = 0.99f * kinematicAimMalusDelta + 0.01f * size * UnityEngine.Random.insideUnitSphere; + kinematicAimMalus = 0.9f * kinematicAimMalus + 0.1f / malusReduction * kinematicAimMalusDelta; + // rangeAimMalus = factor * rangeAimMalus + (1f - factor) / (1 + shotsFiredSinceAcquiringTarget) * UnityEngine.Random.Range(-0.01f, 0.01f); + finalTarget += BDArmorySettings.AIMING_VISUAL_MALUS * kinematicAimMalus; + fixedLeadOffset = originalTarget - finalTarget; + // fixedLeadOffset *= 1f + rangeAimMalus; // Implicitly affected by targetDistance. + finalAimTarget = originalTarget - fixedLeadOffset; + targetDistance = Mathf.Clamp(Vector3.Distance(targetPosition, firePosition), 0, maxTargetingRange); // Move firePosition declaration outside the aim assist logic and set it for both rockets and guns. + } + else + { + fixedLeadOffset = originalTarget - finalTarget; //for aiming fixed guns to moving target + finalAimTarget = finalTarget; + } + staleFinalAimTarget = finalAimTarget; + staleTargetVelocity = targetVelocity + BDKrakensbane.FrameVelocityV3f; + staleTargetAcceleration = targetAcceleration; + stalePartVelocity = smoothedPartVelocity + BDKrakensbane.FrameVelocityV3f; + staleGoodTargetTime = Time.time; + //airdetonation + if (eFuzeType == BulletFuzeTypes.Timed || eFuzeType == BulletFuzeTypes.Flak) + { + if (targetAcquired) + { + defaultDetonationRange = targetDistance;// adds variable time fuze if/when proximity fuzes fail + } + else + { + defaultDetonationRange = maxEffectiveDistance; //airburst at max range + } + } + } + + //final turret aiming + if (slaved && !targetAcquired) return; + if (turret) + { + bool origSmooth = turret.smoothRotation; + if (aiControlled || slaved) + { + turret.smoothRotation = false; + } + turret.AimToTarget(finalAimTarget); //no aimbot turrets when target out of sight + turret.smoothRotation = origSmooth; + } + for (int i = 0; i < customTurret.Count; i++) + { + if (customTurret[i] == null) continue; + if (customTurret[i].vessel != vessel) continue; + customTurret[i].AimToTarget(finalAimTarget); //no aimbot turrets when target out of sight + } + } + + /// + /// 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. + /// + /// For bullets, this isn't used for aiming, only for visuals. But for rockets it is. + public void RunTrajectorySimulation() + { + if ((eWeaponType == WeaponTypes.Rocket && ((BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS && vessel.isActiveVessel) || aiControlled)) || + (BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS && + (BDArmorySettings.DEBUG_LINES || (vessel && vessel.isActiveVessel && !aiControlled && !MapView.MapIsEnabled && !pointingAtSelf && eWeaponType != WeaponTypes.Rocket)))) + { + Transform fireTransform = fireTransforms[0]; + + if (eWeaponType == WeaponTypes.Rocket && rocketPod) + { + fireTransform = rockets[0].parent; // support for legacy RLs + } + + if ((eWeaponType == WeaponTypes.Laser || (eWeaponType == WeaponTypes.Ballistic && !bulletDrop)) && BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS) + { + Ray ray = new Ray(fireTransform.position, fireTransform.forward); + RaycastHit rayHit; + if (Physics.Raycast(ray, out rayHit, maxTargetingRange, layerMask1)) + { + bulletPrediction = rayHit.point; + } + else + { + bulletPrediction = ray.GetPoint(maxTargetingRange); + } + pointingAtPosition = ray.GetPoint(maxTargetingRange); + } + else if (eWeaponType == WeaponTypes.Ballistic && BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS) + { + var timeGap = GetTimeGap(); + var iTime = timeSinceFired - timeGap >= TimeWarp.fixedDeltaTime ? + TimeWarp.fixedDeltaTime : + TimeWarp.fixedDeltaTime - (TimeWarp.fixedDeltaTime + timeGap - timeSinceFired) % TimeWarp.fixedDeltaTime; // This is the iTime correction for the frame that the gun will actually fire on. + if (iTime < 1e-4f) iTime = TimeWarp.fixedDeltaTime; // Avoid jitter by aliasing iTime < 1e-4 to TimeWarp.fixedDeltaTime for the frame after. + var firePosition = AIUtils.PredictPosition(fireTransform.position, part.rb.velocity, vessel.acceleration_immediate, Time.fixedDeltaTime); // Position of the end of the barrel at the start of the next frame. + var bulletAcceleration = bulletDrop ? (Vector3)FlightGlobals.getGeeForceAtPosition(firePosition) : Vector3.zero; // Acceleration at the start point. + var simCurrPos = AIUtils.PredictPosition(firePosition, baseBulletVelocity * fireTransform.forward, bulletAcceleration, iTime); // Bullets are initially placed up to 1 frame ahead (iTime). + + if (Physics.Raycast(new Ray(firePosition, simCurrPos - firePosition), out RaycastHit hit, (simCurrPos - firePosition).magnitude, layerMask1)) // Check between the barrel and the point the bullet appears. + { + bulletPrediction = hit.point; + } + else + { + Vector3 simVelocity = part.rb.velocity + BDKrakensbane.FrameVelocityV3f + baseBulletVelocity * fireTransform.forward + iTime * bulletAcceleration; + 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 with Euler variants (what was used before). Always take at least 2 steps though. + BallisticTrajectorySimulation(ref simCurrPos, simVelocity, maxTargetingRange / baseBulletVelocity / Vector3.Dot((targetPosition - simCurrPos).normalized, fireTransform.forward), simDeltaTime, 0, FlightGlobals.getAltitudeAtPos(targetPosition) < 0); + bulletPrediction = simCurrPos; + } + } + else if (eWeaponType == WeaponTypes.Rocket) + { + float simTime = 0; + float maxTime = rocketInfo.lifeTime; + Vector3 startPos = fireTransform.position; + Vector3 simVelocity = smoothedPartVelocity; // Use the velocity in the local velocity frame. + Vector3 simCurrPos = startPos; + Vector3 rocketDirection = fireTransform.forward; + Quaternion simRotation = fireTransform.rotation; + Vector3 simInvInitialDirection = Quaternion.Inverse(simRotation) * rocketDirection; + Vector3 closestPointOfApproach = simCurrPos; + float timeToCPA; + Vector3 targetPredictedPosition; + bool hitDetected = false; + float atmosMultiplier = Mathf.Clamp01(2.5f * (float)FlightGlobals.getAtmDensity(vessel.staticPressurekPa, vessel.externalTemperature, vessel.mainBody)); + var wm = WeaponManager; + bool slaved = turret && wm && (wm.slavingTurrets || wm.guardMode); + bool inOrbit = vessel.InOrbit(); // When in orbit, ignore raycasts + + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + { + trajectoryPoints ??= []; + trajectoryPoints.Clear(); + trajectoryPoints.Add(simCurrPos); + } + + // Initial frame (no forces or collision/proximity detection) + float simDeltaTime = Time.fixedDeltaTime; + simCurrPos += simDeltaTime * simVelocity; + simTime += simDeltaTime; + + while (true) + { + // Update acceleration for this frame. Note: rockets don't get gravity applied when in non-rotating reference frames. + Vector3 simAcceleration = FlightGlobals.RefFrameIsRotating ? FlightGlobals.getGeeForceAtPosition(simCurrPos) : Vector3.zero; + if (simTime <= thrustTime) simAcceleration += thrust / rocketMass * rocketDirection; + Vector3 simVelocityActual = simVelocity + BDKrakensbane.FrameVelocityV3f; // We want the actual velocity for aero-stabilisation. + if (BDArmorySettings.BULLET_WATER_DRAG) + { + if (FlightGlobals.getAltitudeAtPos(simCurrPos) < 0) + simAcceleration += -(0.5f * 1 * simVelocityActual.sqrMagnitude * 0.5f * (Mathf.PI * caliber * caliber * 0.25f / 1000000)) / rocketMass * rocketDirection;//this is going to throw off aiming code, but you aren't going to hit anything with rockets underwater anyway + } + + if (atmosMultiplier > 0) + { + // Rotation (aero stabilize). + var atmosFactor = atmosMultiplier * 0.5f * 0.012f * simVelocityActual.sqrMagnitude * simDeltaTime; + simRotation = Quaternion.RotateTowards(simRotation, Quaternion.LookRotation(simVelocityActual, fireTransform.up), atmosFactor); // Using Vector3.RotateTowards isn't accurate enough. + rocketDirection = simRotation * simInvInitialDirection; + } + + // No longer thrusting, finish up with a ballistic sim. + if (simTime > thrustTime) + { + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, simTime); + var timeRemaining = maxTime - simTime; + timeToCPA = AIUtils.TimeToCPA(targetPredictedPosition - simCurrPos, targetVelocity - simVelocity, targetAcceleration - simAcceleration, timeRemaining); // For aiming, we want the closest approach to refine our aim. + closestPointOfApproach = AIUtils.PredictPosition(simCurrPos, simVelocity, simAcceleration, timeToCPA); + if (!hitDetected) bulletPrediction = closestPointOfApproach; + if (BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS && !hitDetected) + { + if (FlightGlobals.RefFrameIsRotating) + { + simDeltaTime = Mathf.Clamp(Mathf.Min((closestPointOfApproach - simCurrPos).magnitude / simVelocity.magnitude, timeRemaining) / 8f, Time.fixedDeltaTime, Time.fixedDeltaTime * BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER); // Use 8 steps for better visuals. + BallisticTrajectorySimulation(ref simCurrPos, simVelocity, timeRemaining, simDeltaTime, simTime, FlightGlobals.getAltitudeAtPos(targetPosition) < 0, resetTrajectoryPoints: false); // For visuals, we want the trajectory sim with collision detection. Note: this is done after to avoid messing with simCurrPos. + bulletPrediction = simCurrPos; + } + else + { + // simAcceleration is zero, so, it's just a straight line to the CPA. + if (Physics.Raycast(new Ray(simCurrPos, simVelocity), out RaycastHit hit, simVelocity.magnitude * timeRemaining, layerMask1) && hit.collider != null && hit.collider.gameObject != null && hit.collider.gameObject.GetComponentInParent() != part) // Any hit other than the part firing the rocket. + { bulletPrediction = hit.point; } + } + } + simTime += timeToCPA; + break; + } + + // Symplectic Euler velocity update. (Unity's integrator is closer to Symplectic Euler than LeapFrog.) + simVelocity += simDeltaTime * simAcceleration; + + // Check for collisions within the next update. (Note: this is only relevant against static objects and isn't used for aiming.) + if (!inOrbit && !hitDetected && !aiControlled && !slaved) + { + if (Physics.Raycast(simCurrPos, simVelocity, out RaycastHit hit, simDeltaTime * simVelocity.magnitude, layerMask1) && hit.collider != null && hit.collider.gameObject != null && hit.collider.gameObject.GetComponentInParent() != part) // Any hit other than the part firing the rocket. + { + bulletPrediction = hit.point; + hitDetected = true; + Part hitPart; + KerbalEVA hitEVA; + try + { + hitPart = hit.collider.gameObject.GetComponentInParent(); + hitEVA = hit.collider.gameObject.GetComponentUpwards(); + if (hitEVA != null) + { + hitPart = hitEVA.part; + } + if (hitPart == null) + { + autoFire = false; + autoFireFailReason = "Null target"; + } + } + catch (NullReferenceException e) + { + Debug.Log("[BDArmory.ModuleWeapon]:NullReferenceException for Ballistic Hit: " + e.Message); + } + } + // else if (FlightGlobals.getAltitudeAtPos(simCurrPos) < 0) // Note: this prevents aiming below sea-level. + // { + // bulletPrediction = simCurrPos; + // break; + // } + } + + // Check for closest approach within the next update. + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, simTime); + var targetPredictedVelocity = targetVelocity + simTime * targetAcceleration; + timeToCPA = AIUtils.TimeToCPA(targetPredictedPosition - simCurrPos, targetPredictedVelocity - simVelocity, targetAcceleration - simAcceleration, simDeltaTime); + if (timeToCPA < 0) // No longer approaching. + { + if (!hitDetected) bulletPrediction = closestPointOfApproach; + break; // No longer approaching. + } + else if (timeToCPA < simDeltaTime) // CPA within the next frame. + { + closestPointOfApproach = AIUtils.PredictPosition(simCurrPos, simVelocity, simAcceleration, timeToCPA); + if (!hitDetected) bulletPrediction = closestPointOfApproach; + simTime += timeToCPA; + break; + } + else // CPA beyond the next frame. + { + closestPointOfApproach = simCurrPos; + } + + // Symplectic Euler position update. (Unity's integrator is closer to Symplectic Euler than LeapFrog.) + simCurrPos += simDeltaTime * simVelocity; + + // Update the current sim time. + simTime += simDeltaTime; + + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && !hitDetected) + trajectoryPoints.Add(simCurrPos); + + // Book-keeping and max time checks. + if (simTime > maxTime) + { + if (!hitDetected) bulletPrediction = simCurrPos; + break; + } + } + + // Visuals + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + { + trajectoryPoints.Add(bulletPrediction); + // Debug.Log($"DEBUG traj d {simTime}s Δ {trajectoryPoints.Count - 1}: {(trajectoryPoints[trajectoryPoints.Count - 1] - trajectoryPoints[trajectoryPoints.Count - 2]).magnitude}"); + trajectoryRenderer = gameObject.GetComponent(); + if (trajectoryRenderer == null) + { + trajectoryRenderer = gameObject.AddComponent(); + trajectoryRenderer.startWidth = .1f; + trajectoryRenderer.endWidth = .1f; + } + 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 + offset); + ++i; + } + } + + predictedFlightTime = simTime; + Vector3 pointingPos = startPos + targetDistance * fireTransform.forward; + if (!FlightGlobals.RefFrameIsRotating) // Compensate for gravity in non-rotating reference frames. + pointingPos -= 0.5f * predictedFlightTime * predictedFlightTime * (Vector3)FlightGlobals.getGeeForceAtPosition(pointingPos); + trajectoryOffset = pointingPos - closestPointOfApproach; + } + } + } + + public enum SimulationStage { Normal, Refining, Final }; + /// + /// Use the leapfrog numerical integrator for a ballistic trajectory simulation under the influence of just gravity. + /// The leapfrog integrator is a second-order symplectic method. + /// + /// Note: Use this to see the trajectory with collision detection, but use BallisticTrajectoryClosestApproachSimulation instead for targeting purposes. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public float BallisticTrajectorySimulation(ref Vector3 position, Vector3 velocity, float maxTime, float timeStep, float startTime = 0, bool ignoreWater = false, SimulationStage stage = SimulationStage.Normal, bool resetTrajectoryPoints = true) + { + float elapsedTime = 0f; + var startPosition = position; + if (FlightGlobals.getAltitudeAtPos(position) < 0) ignoreWater = true; + bool inOrbit = vessel.InOrbit(); + var gravity = (Vector3)FlightGlobals.getGeeForceAtPosition(position); + velocity += 0.5f * timeStep * gravity; // Boot-strap velocity calculation. + Ray ray = new(); + RaycastHit hit = new(); + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && stage == SimulationStage.Normal) + { + trajectoryPoints ??= []; + 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 deltaPosition = timeStep * velocity; + var altitude = FlightGlobals.getAltitudeAtPos(position + deltaPosition); + if (!inOrbit // Raycast detection isn't meaningful in orbit as it assumes stationary targets. Use CPA instead. + && Physics.Raycast(ray, out hit, deltaPosition.magnitude, layerMask1) && hit.collider != null && hit.collider.gameObject != null && hit.collider.gameObject.GetComponentInParent() != part + || (!ignoreWater && altitude < 0) // Underwater + || (stage == SimulationStage.Normal && elapsedTime + timeStep > maxTime) // Out of time + ) + { + switch (stage) + { + case SimulationStage.Normal: + { + if (elapsedTime + timeStep > maxTime) // Final time amount. + { + velocity -= 0.5f * timeStep * gravity; // Correction to final velocity. + var finalTime = BallisticTrajectorySimulation(ref position, velocity, maxTime - elapsedTime, (maxTime - elapsedTime) / 4f, startTime + elapsedTime, 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, timeStep, timeStep / 4f, startTime + elapsedTime, ignoreWater, timeStep > 5f * Time.fixedDeltaTime ? SimulationStage.Refining : SimulationStage.Final, false); + elapsedTime += finalTime; + break; + } + case SimulationStage.Final: + { + 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 if (!inOrbit) // Collision + { + elapsedTime += (hit.point - position).magnitude / velocity.magnitude; + position = hit.point; + if (hit.collider != null && hit.collider.gameObject != null) + { + Part hitPart; + KerbalEVA hitEVA; + try + { + hitPart = hit.collider.gameObject.GetComponentInParent(); + hitEVA = hit.collider.gameObject.GetComponentUpwards(); + if (hitEVA != null) + { + hitPart = hitEVA.part; + } + if (hitPart == null) + { + autoFire = false; + autoFireFailReason = "Null target"; + } + } + catch (NullReferenceException e) + { + Debug.Log("[BDArmory.ModuleWeapon]:NullReferenceException for Ballistic Hit: " + e.Message); + } + } + } + break; + } + } + break; + } + { // CPA check + var timeToCPA = AIUtils.TimeToCPA( + AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, elapsedTime) - position, + targetVelocity + elapsedTime * targetAcceleration - velocity, + targetAcceleration - gravity, + timeStep); + if (timeToCPA < timeStep) + { + position = AIUtils.PredictPosition(position, velocity, gravity, timeToCPA); + elapsedTime += timeToCPA; + break; + } + } + position += deltaPosition; + gravity = (Vector3)FlightGlobals.getGeeForceAtPosition(position); + velocity += timeStep * gravity; + elapsedTime += timeStep; + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + { + trajectoryPoints.Add(position); + } + } + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && resetTrajectoryPoints) + { + trajectoryPoints.Add(position); + trajectoryRenderer = gameObject.GetComponent(); + if (trajectoryRenderer == null) + { + trajectoryRenderer = gameObject.AddComponent(); + trajectoryRenderer.startWidth = .1f; + trajectoryRenderer.endWidth = .1f; + } + 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 + offset); + ++i; + } + } + return elapsedTime; + } + + /// + /// Solve the closest time to CPA via simulation for ballistic projectiles over long distances to account for varying gravity. + /// + /// Both the bullet and target positions are integrated with leap-frog. + /// This is consistent with how bullets are moved in PooledBullet.cs and, since it is second-order, is more accurate for larger timesteps than semi-implicit Euler (which is what Unity appears to be using). + /// + /// The bullet's position. + /// The bullet's velocity. + /// Whether the bullet is affected by gravity or not. + /// The target's position. + /// The target's velocity. + /// The target's acceleration (combined gravitational and local forces). + /// Whether the target is supported (in which case gravitational forces are ignored). + /// The timestep to use initially. + /// The max time to run for. + /// The type of closest approach (earliest, latest, closest). + /// Tracker for the elapsed time. + /// Tracker for the simulation stage. + /// The position of the bullet at the CPA, position of the target at the CPA and the time to the CPA. + public (Vector3, Vector3, float) BallisticTrajectoryClosestApproachSimulation(Vector3 position, Vector3 velocity, bool bulletDrop, Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, bool targetIsSupported, float timeStep, float maxTime, AIUtils.CPAType cpaType = AIUtils.CPAType.Earliest, float elapsedTime = 0, SimulationStage stage = SimulationStage.Normal) + { + Vector3 initialPosition = position, initialTargetPosition = targetPosition; + Vector3 lastPosition, lastTargetPosition; + + Vector3 gravity = bulletDrop ? FlightGlobals.getGeeForceAtPosition(position) : Vector3.zero; + Vector3 targetGravity = targetIsSupported ? Vector3.zero : FlightGlobals.getGeeForceAtPosition(targetPosition); // Supported targets (landed, splashed, VM) aren't affected by gravity due to contact forces. + targetAcceleration -= targetGravity; // Separate the target's acceleration into a gravity component (varying with position) and a constant component (local thrust). + velocity += 0.5f * timeStep * gravity; // Leap-frog boot-strapping for the bullet. + targetVelocity += 0.5f * timeStep * (targetAcceleration + targetGravity); // Leap-frog boot-strapping for the target. + + bool closing = (stage != SimulationStage.Normal) || Vector3.Dot(targetPosition - position, targetVelocity - velocity) < 0f; // For the Normal stage, we need to wait until we've started closing or are certain we never will. + var simStartTime = Time.realtimeSinceStartup; + while (elapsedTime < maxTime && Time.realtimeSinceStartup - simStartTime < 0.1f) // Allow 0.1s of real-time for the simulation. This ought to be plenty. + { + lastPosition = position; + lastTargetPosition = targetPosition; + + position += timeStep * velocity; // Leap-frog for the bullet's position. + targetPosition += timeStep * targetVelocity; // Leap-frog for the target's position. + + // Check whether we've passed through the CPA. This has to behave similarly to AIUtils.TimeToCPA. + // TODO It should support the different CPATypes, but that can be left for later as we only need Earliest for now. + if (!closing) + { + closing = Vector3.Dot(targetPosition - position, targetVelocity - velocity) < 0; // Check if we've started closing. + if (!closing && Vector3.Dot(targetVelocity - velocity, targetGravity + targetAcceleration - gravity) > 0) // Check if they're accelerating away from each other => never going to meet (without performing a full orbit...). + { + return (initialPosition, initialTargetPosition, 0); // timeToCPA is negative + } + } + if (closing && Vector3.Dot(targetPosition - position, targetVelocity - velocity) >= 0f) // Step went beyond the CPA. + { + velocity -= 0.5f * timeStep * gravity; // Undo the last leap-frog step for the bullet's velocity. + targetVelocity -= 0.5f * timeStep * (targetAcceleration + targetGravity); // Undo the last leap-frog step for the target's velocity. + switch (stage) + { + case SimulationStage.Normal: + case SimulationStage.Refining: // Perform a more accurate final step for the collision. + return BallisticTrajectoryClosestApproachSimulation( + lastPosition, + velocity, + bulletDrop, + lastTargetPosition, + targetVelocity, + targetAcceleration + targetGravity, + targetIsSupported, + timeStep / 4f, + maxTime, + cpaType, + elapsedTime, + timeStep > 5f * Time.fixedDeltaTime ? SimulationStage.Refining : SimulationStage.Final + ); + case SimulationStage.Final: + // Perform the last step analytically + var timeToCPA = AIUtils.TimeToCPA(lastPosition - lastTargetPosition, velocity - targetVelocity, gravity - (targetAcceleration + targetGravity), timeStep); + position = AIUtils.PredictPosition(lastPosition, velocity, gravity, timeToCPA); + targetPosition = AIUtils.PredictPosition(lastTargetPosition, targetVelocity, targetAcceleration + targetGravity, timeToCPA); + elapsedTime += timeToCPA; + return (position, targetPosition, elapsedTime); + } + } + gravity = bulletDrop ? FlightGlobals.getGeeForceAtPosition(position) : Vector3.zero; + if (!targetIsSupported) targetGravity = FlightGlobals.getGeeForceAtPosition(targetPosition); + velocity += timeStep * gravity; + targetVelocity += timeStep * (targetAcceleration + targetGravity); + elapsedTime += timeStep; + } + if (elapsedTime < maxTime) Debug.LogWarning("[BDArmory.ModuleWeapon]: Ballistic trajectory closest approach simulation timed out."); + return (position, targetPosition, elapsedTime); // Was heading to a CPA, but didn't reach it in time. + } + + //more organization, grouping like with like + public Vector3 GetLeadOffset() + { + return fixedLeadOffset; + } + + void UpdateOffsetWeapon() + { + if (fireTransforms == null || fireTransforms.Length == 0) return; // Empty fireTransforms can happen as a race condition when MissileFire.Start() calls this, which should be harmless. + Vector3 weaponPosition = fireTransforms[0].position; + Vector3 weaponDirection = fireTransforms[0].forward; + if (part.symmetryCounterparts.Count > 0) + { + foreach (var part in part.symmetryCounterparts) + { + weaponPosition += part.transform.position; + if (part.GetComponent().fireTransforms != null) + weaponDirection += part.GetComponent().fireTransforms[0].forward; + } + weaponPosition /= 1 + part.symmetryCounterparts.Count; + weaponDirection /= 1 + part.symmetryCounterparts.Count; + } + + offsetWeaponPosition = weaponPosition - vessel.ReferenceTransform.position; + offsetWeaponDirection = vessel.ReferenceTransform.InverseTransformDirection(weaponDirection); + } + + public float targetCosAngle; + public bool safeToFire; + void CheckAIAutofire() + { + //autofiring with AI + if (targetAcquired && aiControlled) + { + Transform fireTransform = fireTransforms[0]; + if (eWeaponType == WeaponTypes.Rocket && rocketPod) + { + fireTransform = rockets[0].parent; // support for legacy RLs + } + + Vector3 targetRelPos = finalAimTarget - fireTransform.position; + Vector3 aimDirection = fireTransform.forward; + targetCosAngle = Vector3.Dot(aimDirection, targetRelPos.normalized); + // var maxAutoFireCosAngle2 = targetAdjustedMaxCosAngle; + safeToFire = CheckForFriendlies(fireTransform); //TODO - test why APS returning safeToFire = false + if (BDArmorySettings.BULLET_WATER_DRAG && eWeaponType == WeaponTypes.Ballistic && FlightGlobals.getAltitudeAtPos(fireTransforms[0].position) < 0) + safeToFire = false; //don't fire guns underwater + + if (safeToFire) + { + if (eWeaponType == WeaponTypes.Ballistic || eWeaponType == WeaponTypes.Laser) + { + autoFire = targetCosAngle >= targetAdjustedMaxCosAngle; + autoFireFailReason = autoFire ? "" : "Not on target"; + } + else // Rockets + { + autoFire = (targetCosAngle >= targetAdjustedMaxCosAngle) && ((finalAimTarget - fireTransform.position).sqrMagnitude > (blastRadius * blastRadius) * 2); + autoFireFailReason = autoFire ? "" : "Not on target"; + } + + if (autoFire && VectorUtils.Angle(targetPosition - fireTransform.position, aimDirection) < 5) //check LoS for direct-fire weapons + { + if (RadarUtils.TerrainCheck(fireTransform.position, eWeaponType == WeaponTypes.Laser ? targetPosition : fireTransform.position + (fireTransform.forward * Mathf.Min(1500, (targetPosition - fireTransform.position).magnitude)))) //kerbin curvature is going to start returning raycast terrain hits at about 1.8km for tanks + { + autoFire = false; + autoFireFailReason = "Terrain check"; + } + } + } + else + { + autoFire = false; + autoFireFailReason = "Not safe"; + } + var wm = WeaponManager; + if (autoFire && wm.staleTarget && (lastVisualTargetVessel != null && lastVisualTargetVessel.LandedOrSplashed && vessel.LandedOrSplashed)) + { + autoFire = false; //ground Vee engaging another ground Vee which has ducked out of sight, don't fire + // won't catch cloaked tanks, but oh well. + autoFireFailReason = "Stale target"; + } + + // 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(wm, 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(wm, 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 + { + autoFire = false; + autoFireFailReason = aiControlled ? "No target" : "Not AI controlled"; + } + + //disable autofire after burst length + if (autoFire && (!BurstOverride && Time.time - autoFireTimer > autoFireLength) || (BurstOverride && autofireShotCount >= fireBurstLength)) + { + autoFire = false; + autoFireFailReason = "Disabled after burst"; + //visualTargetVessel = null; + //visualTargetPart = null; + //tgtShell = null; + //tgtRocket = null; + if (SpoolUpTime > 0) + { + roundsPerMinute = baseRPM / 10; + spooltime = 0; + } + if (eWeaponType == WeaponTypes.Laser && LaserGrowTime > 0) + { + projectileColorC = GUIUtils.ParseColor255(projectileColor); + startColorS = startColor.Split(","[0]); + laserDamage = baseLaserdamage; + tracerStartWidth = tracerBaseSWidth; + tracerEndWidth = tracerBaseEWidth; + Offset = 0; + } + } + if (isAPS) + { + float threatDirectionFactor = (fireTransforms[0].position - targetPosition).DotNormalized(targetVelocity - part.rb.velocity); + if (threatDirectionFactor < 0.9f) + { + autoFire = false; //within 28 degrees in front, else ignore, target likely not on intercept vector + autoFireFailReason = "APS threat direction"; + } + } + } + + /// + /// 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) + { + var wm = WeaponManager; + if (wm == null || wm.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 == wm.vessel) continue; + var wmf = friendly.Current.ActiveController().WM; + if (wmf == null || wmf.Team != wm.Team) continue; + var friendlyRelativePosition = friendly.Current.CoM - fireTransform.position; + var (friendlyDistance, friendlyDirection) = friendlyRelativePosition.MagNorm(); + var theta = friendly.Current.GetRadius() / friendlyDistance; + 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, friendlyDirection) > 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 + BDKrakensbane.FrameVelocityV3f + (eWeaponType == WeaponTypes.Rocket ? (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 == wm.vessel) continue; + var wmf = friendly.Current.ActiveController().WM; + if (wmf == null || wmf.Team != wm.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 (fireConditionCheck) + { + 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)); //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 && timeSinceFired > beamDuration)) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(false); + } + } + } + } + } + } + else + { + if (SpoolUpTime > 0) + { + if (spooltime < 1) + { + spooltime += TimeWarp.deltaTime / SpoolUpTime; + spooltime = Mathf.Clamp01(spooltime); + roundsPerMinute = Mathf.Lerp((baseRPM / 10), baseRPM, spooltime); + } + } + 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; + } + if (BurstFire && RoundsRemaining > 0 && RoundsRemaining < RoundsPerMag) + { + finalFire = true; + } + if (eWeaponType == WeaponTypes.Laser) + { + if (LaserGrowTime > 0) + { + laserDamage = Mathf.Lerp(laserDamage, laserMaxDamage, 0.02f / LaserGrowTime); + tracerStartWidth = Mathf.Lerp(tracerStartWidth, tracerMaxStartWidth, 0.02f / LaserGrowTime); + tracerEndWidth = Mathf.Lerp(tracerEndWidth, tracerMaxEndWidth, 0.02f / LaserGrowTime); + if (DynamicBeamColor) + { + startColorS[0] = Mathf.Lerp(float.Parse(startColorS[0]), float.Parse(endColorS[0]), 0.02f / LaserGrowTime).ToString(); + startColorS[1] = Mathf.Lerp(float.Parse(startColorS[1]), float.Parse(endColorS[1]), 0.02f / LaserGrowTime).ToString(); + startColorS[2] = Mathf.Lerp(float.Parse(startColorS[2]), float.Parse(endColorS[2]), 0.02f / LaserGrowTime).ToString(); + startColorS[3] = Mathf.Lerp(float.Parse(startColorS[3]), float.Parse(endColorS[3]), 0.02f / LaserGrowTime).ToString(); + } + for (int i = 0; i < 4; i++) + { + projectileColorC[i] = float.Parse(startColorS[i]) / 255; + } + } + UpdateLaserSpecifics(DynamicBeamColor, dynamicFX, LaserGrowTime > 0, beamScrollRate != 0); + } + } + } + else + { + if (WeaponManager != null && WeaponManager.GetRippleIndex(WeaponName) == rippleIndex) + { + StartCoroutine(IncrementRippleIndex(0)); + isRippleFiring = false; + } + if (eWeaponType == WeaponTypes.Laser) + { + if (LaserGrowTime > 0) + { + projectileColorC = GUIUtils.ParseColor255(projectileColor); + startColorS = startColor.Split(","[0]); + laserDamage = baseLaserdamage; + tracerStartWidth = tracerBaseSWidth; + tracerEndWidth = tracerBaseEWidth; + Offset = 0; + } + if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && timeSinceFired > beamDuration)) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(false); + } + } + } + } + //if (!pulseLaser || !oneShotSound) + //{ + // audioSource.Stop(); + //} + } + if (SpoolUpTime > 0) + { + if (spooltime > 0) + { + spooltime -= TimeWarp.deltaTime / SpoolUpTime; + spooltime = Mathf.Clamp01(spooltime); + roundsPerMinute = Mathf.Lerp(baseRPM, (baseRPM / 10), spooltime); + } + } + if (hasCharged) + { + if (hasChargeHoldAnimation) + { + chargeHoldState.enabled = true; //play chargedHold anim while weapon is charged but not firing - spooled gatling gun spin anims, etc + if (chargeHoldState.normalizedTime > 1) + chargeHoldState.normalizedTime = 0; + chargeHoldState.speed = fireAnimSpeed; + } + else if (hasChargeAnimation) + { + chargeState.enabled = true; + chargeState.speed = 0; + chargeState.normalizedTime = 1; //else use final frame of the chargeAnim if no hold anim, so weapon doesn't immediately revert to default state moment firing stops + } + + if (ChargeTime > 0 && timeSinceFired > chargeHoldLength && !isReloading) + { + hasCharged = false; + if (electricResource) RoundsRemaining = 0; //reset rounds fired if prematurely running out of EC + if (hasChargeAnimation && hasChargeHoldAnimation || postFireChargeAnim) chargeRoutine = StartCoroutine(ChargeRoutine(true)); + } + } + } + } + + void AimAndFire() + { + // This runs in the FashionablyLate timing phase of FixedUpdate before Krakensbane corrections have been applied. + if (!(aimAndFireIfPossible || aimOnly)) return; + if (this == null || vessel == null || !vessel.loaded || !gameObject.activeInHierarchy || FlightGlobals.currentMainBody == null) return; + var wm = WeaponManager; + if (wm == null) return; + + if (isAPS || (wm.guardMode && dualModeAPS)) //prioritize APS as APS if AI using dualmode units for engaging standard targets + { + if (isAPS) TrackIncomingProjectile(); + if (wm.guardMode && dualModeAPS && !TrackIncomingProjectile()) UpdateTargetVessel(); + } + else + { + UpdateTargetVessel(); + } + if (targetAcquired) + { + bool reset = (lastTargetAcquisitionType != targetAcquisitionType) || (targetAcquisitionType == TargetAcquisitionType.Visual && lastVisualTargetVessel != visualTargetVessel); + SmoothTargetKinematics(targetPosition, targetVelocity, targetAcceleration, targetIsLandedOrSplashed, reset); + if (reset && BDArmorySettings.AIMING_VISUAL_MALUS > 0) + { + shotsFiredSinceAcquiringTarget = (targetAcquisitionType == TargetAcquisitionType.Radar || targetAcquisitionType == TargetAcquisitionType.Slaved) ? 4 : 0; // Radar/slaved gives an initial starting bonus + targetAcquisitionTime = Time.time; + float size = malusSightingAccuracy * (targetPosition - transform.position).magnitude * ((smoothedPartVelocity - targetVelocity).OneNorm() + 1f) + (smoothedPartAcceleration - targetAcceleration).OneNorm(); + kinematicAimMalusDelta = 0.1f * size * UnityEngine.Random.insideUnitSphere; + kinematicAimMalus = 0.1f * size * UnityEngine.Random.insideUnitSphere; // This is about the typical size that the malus peaks at during the bounded slow random walk. + // rangeAimMalus = UnityEngine.Random.Range(-0.01f, 0.01f); + } + } + + Aim(); + if (aimAndFireIfPossible) + { + CheckWeaponSafety(); + CheckAIAutofire(); + CheckFinalFire(); + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log("DEBUG " + vessel.vesselName + " targeting visualTargetVessel: " + visualTargetVessel + ", finalFire: " + finalFire + ", pointingAtSelf: " + pointingAtSelf + ", targetDistance: " + targetDistance); + + if (finalFire) + { + if (ChargeTime > 0 && !hasCharged) + { + if (!isCharging) + { + if (chargeRoutine != null) + { + StopCoroutine(chargeRoutine); + chargeRoutine = null; + } + chargeRoutine = StartCoroutine(ChargeRoutine()); + } + else + { + aimAndFireIfPossible = false; + aimOnly = false; + } + } + else + { + switch (eWeaponType) + { + case WeaponTypes.Laser: + if (FireLaser()) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = true; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(true); + } + } + } + if (isAPS && (tgtShell != null || tgtRocket != null)) + { + StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); + } + } + else + { + if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && timeSinceFired > beamDuration)) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(false); + } + } + } + } + //if (!pulseLaser || !oneShotSound) + //{ + // audioSource.Stop(); + //} + } + break; + case WeaponTypes.Ballistic: + Fire(); + break; + case WeaponTypes.Rocket: + FireRocket(); + break; + } + ++shotsFiredSinceAcquiringTarget; // Overflow shouldn't be an issue. + } + } + } + + aimAndFireIfPossible = false; + aimOnly = false; + } + + void DrawAlignmentIndicator() + { + if (fireTransforms == null || fireTransforms[0] == null) return; + + Part rootPart = EditorLogic.RootPart; + if (rootPart == null) return; + + Transform refTransform = rootPart.GetReferenceTransform(); + if (!refTransform) return; + + Vector3 fwdPos = fireTransforms[0].position + (5 * fireTransforms[0].forward); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, fwdPos, useThisWeaponForAim ? 8 : 4, useThisWeaponForAim ? Color.blue : Color.green); + + Vector3 referenceDirection = refTransform.up; + Vector3 refUp = -refTransform.forward; + Vector3 refRight = refTransform.right; + + Vector3 refFwdPos = fireTransforms[0].position + (5 * referenceDirection); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, refFwdPos, 2, Color.white); + + GUIUtils.DrawLineBetweenWorldPositions(fwdPos, refFwdPos, 2, XKCDColors.Orange); + + string blocker = ""; + if (Physics.Raycast(new Ray(fireTransforms[0].position, fireTransforms[0].forward), out RaycastHit hit, 1000f, (int)LayerMasks.Parts)) + { + var hitPart = hit.collider.gameObject.GetComponentInParent(); + var hitEVA = hit.collider.gameObject.GetComponentUpwards(); + if (hitEVA != null) hitPart = hitEVA.part; + if (hitPart != null) + { + blocker = hitPart.partInfo.title; + GUIUtils.DrawTextureOnWorldPos(hit.point, BDArmorySetup.Instance.redDotTexture, new Vector2(16, 16), 0); + } + } + + Vector2 guiPos; + if (GUIUtils.WorldToGUIPos(fwdPos, out guiPos)) + { + Rect angleRect = new Rect(guiPos.x, guiPos.y, 100, 200); + + Vector3 pitchVector = (5 * fireTransforms[0].forward.ProjectOnPlanePreNormalized(refRight)); + Vector3 yawVector = (5 * fireTransforms[0].forward.ProjectOnPlanePreNormalized(refUp)); + + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + pitchVector, fwdPos, 3, + Color.white); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + yawVector, fwdPos, 3, Color.white); + + float pitch = VectorUtils.Angle(pitchVector, referenceDirection); + float yaw = VectorUtils.Angle(yawVector, referenceDirection); + + string convergeDistance; + + Vector3 projAxis = Vector3.Project(refTransform.position - fireTransforms[0].transform.position, + refRight); + float xDist = projAxis.magnitude; + float convergeAngle = 90 - VectorUtils.Angle(yawVector, refTransform.up); + if (Vector3.Dot(fireTransforms[0].forward, projAxis) > 0) + { + convergeDistance = $"Converge: {Mathf.Round((xDist * Mathf.Tan(convergeAngle * Mathf.Deg2Rad))).ToString()} m"; + } + else + { + convergeDistance = "Diverging"; + } + + string xAngle = $"X: {VectorUtils.Angle(fireTransforms[0].forward, pitchVector):0.00}"; + string yAngle = $"Y: {VectorUtils.Angle(fireTransforms[0].forward, yawVector):0.00}"; + + string label = $"{xAngle}\n{yAngle}\n{convergeDistance}"; + if (!string.IsNullOrEmpty(blocker)) + { + angleRect.width += 6 * blocker.Length; + label += $"\nBlocked: {blocker}"; + } + GUI.Label(angleRect, label); + } + } + + #endregion Targeting + + #region Updates + void CheckCrewed() + { + if (!gunnerSeatLookedFor) // Only find the module once. + { + var kerbalSeats = part.Modules.OfType(); + if (kerbalSeats.Count() > 0) + gunnerSeat = kerbalSeats.First(); + else + gunnerSeat = null; + gunnerSeatLookedFor = true; + } + if ((gunnerSeat == null || gunnerSeat.Occupant == null) && part.protoModuleCrew.Count <= 0) //account for both lawn chairs and internal cabins + { + hasGunner = false; + } + else + { + hasGunner = true; + } + } + void UpdateHeat() + { + if (heat > maxHeat && !isOverheated) + { + isOverheated = true; + autoFire = false; + autoFireFailReason = "Overheated"; + hasCharged = false; + if (hasChargeAnimation) chargeRoutine = StartCoroutine(ChargeRoutine(postFireChargeAnim)); + if (!oneShotSound) audioSource.Stop(); + wasFiring = false; + if (spinDownAnimation) spinningDown = true; + audioSource2.PlayOneShot(overheatSound); + var wm = WeaponManager; + if (wm) wm.ResetGuardInterval(); + } + heat = Mathf.Clamp(heat - heatLoss * TimeWarp.fixedDeltaTime, 0, Mathf.Infinity); + if (heat < maxHeat / 3 && isOverheated) //reset on cooldown + { + isOverheated = false; + autofireShotCount = 0; + RoundsRemaining = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: AutoFire length: " + autofireShotCount); + } + } + void ReloadWeapon() + { + if (isReloading) + { + ReloadTimer = Mathf.Min(ReloadTimer + TimeWarp.fixedDeltaTime / ReloadTime, 1); + if (hasDeployAnim) + { + AnimTimer = Mathf.Min(AnimTimer + TimeWarp.fixedDeltaTime / (ReloadTime - deployState.length), 1); + } + } + if ((RoundsRemaining >= RoundsPerMag && !isReloading) && (ammoCount > 0 || BDArmorySettings.INFINITE_AMMO)) + { + isReloading = true; + autoFire = false; + autoFireFailReason = "Reloading"; + if (eWeaponType == WeaponTypes.Laser) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(false); + } + } + } + } + wasFiring = false; + var wm = WeaponManager; + if (wm) wm.ResetGuardInterval(); + showReloadMeter = true; + if (hasReloadAnim) + { + if (reloadRoutine != null) + { + StopCoroutine(reloadRoutine); + reloadRoutine = null; + } + reloadRoutine = StartCoroutine(ReloadRoutine()); + } + else + { + if (hasDeployAnim) + { + StopShutdownStartupRoutines(); + shutdownRoutine = StartCoroutine(ShutdownRoutine(true)); + } + if (!oneShotSound) audioSource.Stop(); + if (!string.IsNullOrEmpty(reloadAudioPath)) + { + audioSource2.Stop(); + audioSource2.PlayOneShot(reloadAudioClip); + } + } + } + if (!hasReloadAnim && hasDeployAnim && (AnimTimer >= 1 && isReloading)) + { + if (eWeaponType == WeaponTypes.Rocket && rocketPod) + { + RoundsRemaining = 0; + UpdateRocketScales(); + } + if (weaponState == WeaponStates.Disabled || weaponState == WeaponStates.PoweringDown) + { + } + else + { + StopShutdownStartupRoutines(); //if weapon un-selected while reloading, don't activate weapon + startupRoutine = StartCoroutine(StartupRoutine(true)); + } + } + if (ReloadTimer >= 1 && isReloading) + { + RoundsRemaining = 0; + autofireShotCount = 0; + gauge.UpdateReloadMeter(1); + showReloadMeter = false; + isReloading = false; + ReloadTimer = 0; + AnimTimer = 0; + if (eWeaponType == WeaponTypes.Rocket && rocketPod) + { + UpdateRocketScales(); + } + if (!string.IsNullOrEmpty(reloadCompletePath)) + { + audioSource2.Stop(); + audioSource2.PlayOneShot(reloadCompleteAudioClip); + } + } + } + void UpdateTargetVessel() + { + targetAcquired = false; + targetInVisualRange = false; + slaved = false; + GPSTarget = false; + radarTarget = false; + bool atprWasAcquired = atprAcquired; + atprAcquired = false; + lastTargetAcquisitionType = targetAcquisitionType; + var weaponManager = WeaponManager; + + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) + { + if (Time.time - staleGoodTargetTime > Mathf.Max(BDArmorySettings.FIRE_RATE_OVERRIDE / 60f, weaponManager.targetScanInterval)) + { + targetAcquisitionType = TargetAcquisitionType.None; + } + } + else + { + if (Time.time - staleGoodTargetTime > Mathf.Max(roundsPerMinute / 60f, weaponManager.targetScanInterval)) + { + targetAcquisitionType = TargetAcquisitionType.None; + } + } + + if (weaponManager) + { + if (visualTargetVessel) + { + targetInVisualRange = (visualTargetVessel.transform.position - transform.position).sqrMagnitude < weaponManager.guardRange * weaponManager.guardRange; + } + if (weaponManager.vesselRadarData && weaponManager.vesselRadarData.locked) // && weaponManager.slavedPosition != Vector3.zero) + { + TargetSignatureData targetData = TargetSignatureData.noTarget; + if (weaponManager.multiTargetNum > 1 && turret && (maxPitch != minPitch || yawRange > 0)) //if multi target turrets, get relevant lock + { + List possibleTargets = weaponManager.vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == visualTargetVessel) + { + targetData = possibleTargets[i]; + break; + } + } + } + if (targetData.exists) + { + targetVelocity = targetData.velocity - BDKrakensbane.FrameVelocityV3f; + targetPosition = targetData.predictedPositionWithChaffFactor(targetData.lockedByRadar.radarChaffClutterFactor); + targetRadius = 35; + targetAcceleration = targetData.acceleration; + targetIsLandedOrSplashed = false; + if (targetData.vessel) + { + targetRadius = targetData.vessel.GetRadius(); + targetIsLandedOrSplashed = targetData.vessel.LandedOrSplashed; + } + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.Radar; + radarTarget = true; + slaved = true; + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.ModuleWeapon - {shortName} is tracking target {targetData.vessel.vesselName} via radarlock from {targetData.lockedByRadar.part.partInfo.title}"); + return; + } + else //no lock for our secondary target/fixed gun/no multitargeting? slave weapon to primary lock + { + bool isVessel = weaponManager.slavedTarget.vessel != null; + if (!isVessel || !(targetInVisualRange && RadarUtils.GetVesselChaffFactor(weaponManager.slavedTarget.vessel) < 1f)) + { + if (weaponManager.slavingTurrets) slaved = true; + targetRadius = isVessel ? weaponManager.slavedTarget.vessel.GetRadius() : 35f; + targetPosition = weaponManager.slavedPosition != Vector3.zero ? weaponManager.slavedPosition : weaponManager.vesselRadarData.lockedTargetData.targetData.predictedPositionWithChaffFactor(weaponManager.vesselRadarData.lockedTargetData.detectedByRadar.radarChaffClutterFactor); + targetVelocity = isVessel ? weaponManager.slavedTarget.vessel.rb_velocity : weaponManager.vesselRadarData.lockedTargetData.targetData.velocity - BDKrakensbane.FrameVelocityV3f; + targetAcceleration = isVessel ? weaponManager.slavedAcceleration : weaponManager.vesselRadarData.lockedTargetData.targetData.acceleration; + if (isVessel) targetIsLandedOrSplashed = weaponManager.slavedTarget.vessel.LandedOrSplashed; + else targetIsLandedOrSplashed = false; + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.Slaved; + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.ModuleWeapon - {shortName} had no lock for {(visualTargetVessel != null ? visualTargetVessel.vesselName : "'unknown'")}; isVessel? {isVessel}; slaving to primary lock on {(isVessel ? weaponManager.slavedTarget.vessel.name : weaponManager.vesselRadarData.lockedTargetData.vessel.name)}"); + return; + } + } + } + if (weaponManager.mainTGP != null && ModuleTargetingCamera.windowIsOpen && weaponManager.mainTGP.slaveTurrets && weaponManager.slavedPosition != Vector3.zero) + { + bool isVessel = weaponManager.mainTGP.lockedVessel != null; + slaved = true; + targetRadius = isVessel ? weaponManager.mainTGP.lockedVessel.GetRadius() : 35f; + targetPosition = weaponManager.slavedPosition; + targetVelocity = Vector3.zero; //tgtCam returns 0 for these + targetAcceleration = Vector3.zero; + if (isVessel) targetIsLandedOrSplashed = weaponManager.mainTGP.lockedVessel.LandedOrSplashed; + else targetIsLandedOrSplashed = false; + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.Slaved; + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.ModuleWeapon - {shortName} is tracking target {(isVessel ? weaponManager.mainTGP.lockedVessel.vesselName : "null target")} via tgtCamera"); + return; + } + // within visual range and no radar aiming/need precision visual targeting of specific subsystems + if (aiControlled && visualTargetVessel && targetInVisualRange) + { + //targetRadius = visualTargetVessel.GetRadius(); + + if (visualTargetPart == null || visualTargetPart.vessel != visualTargetVessel) + { + TargetInfo currentTarget = visualTargetVessel.gameObject.GetComponent(); + if (currentTarget == null) + { + 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 + { + if (targetCockpits) + { + for (int i = 0; i < currentTarget.targetCommandList.Count; i++) + { + if (!targetparts.Contains(currentTarget.targetCommandList[i])) + { + targetparts.Add(currentTarget.targetCommandList[i]); + } + } + } + if (targetEngines) + { + for (int i = 0; i < currentTarget.targetEngineList.Count; i++) + { + if (!targetparts.Contains(currentTarget.targetEngineList[i])) + { + targetparts.Add(currentTarget.targetEngineList[i]); + } + } + } + if (targetWeapons) + { + for (int i = 0; i < currentTarget.targetWeaponList.Count; i++) + { + if (!targetparts.Contains(currentTarget.targetWeaponList[i])) + { + targetparts.Add(currentTarget.targetWeaponList[i]); + } + } + } + if (targetMass) + { + for (int i = 0; i < currentTarget.targetMassList.Count; i++) + { + if (!targetparts.Contains(currentTarget.targetMassList[i])) + { + targetparts.Add(currentTarget.targetMassList[i]); + } + } + } + if (targetRandom && currentTarget.Vessel != null) + { + 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++) + { + if (!targetparts.Contains(currentTarget.targetMassList[i])) + { + targetparts.Add(currentTarget.targetMassList[i]); + } + } + } + targetparts = targetparts.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 + targetparts.Reverse(); //Order by mass is lightest to heaviest. We want H>L + //targetparts.Shuffle(); //alternitively, increase the random range from maxtargetnum to targetparts.count, otherwise edge cases where lots of one thing (targeting command/mass) will be pulled before lighter things (weapons, maybe engines) if both selected + if (turret && (yawRange > 0 || maxPitch > minPitch)) + { + targetID = (int)UnityEngine.Random.Range(0, Mathf.Min(targetparts.Count, weaponManager.multiTargetNum)); + } + else //make fixed guns all get the same target part + { + targetID = 0; + } + if (targetparts.Count == 0) + { + 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 + } + } + } + else + { + if (targetCOM) + { + targetPosition = visualTargetVessel.CoM; + visualTargetPart = null; //make sure these get reset + targetRadius = visualTargetVessel.GetRadius(average: true); + } + else + { + targetPosition = visualTargetPart.transform.position; + targetRadius = 5; + } + } + targetVelocity = visualTargetVessel.rb_velocity; + targetAcceleration = visualTargetVessel.acceleration; + targetIsLandedOrSplashed = visualTargetVessel.LandedOrSplashed; + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.Visual; + 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; + targetIsLandedOrSplashed = true; + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.GPS; + return; + } + + //auto proxy tracking + if (vessel.isActiveVessel && (autoProxyTrackRange > 0 || MouseAimFlight.IsMouseAimActive)) // Allow better auto-proxy tracking when using MouseAimFlight. + { + if (++aptrTicker < 20) + { + if (atprWasAcquired) + { + targetAcquired = true; + atprAcquired = true; + } + } + else + { + aptrTicker = 0; + Vessel tgt = null; + float closestSqrDist = autoProxyTrackRange * autoProxyTrackRange; + if (MouseAimFlight.IsMouseAimActive) closestSqrDist = Mathf.Max(closestSqrDist, maxEffectiveDistance * maxEffectiveDistance); + using (var v = BDATargetManager.LoadedVessels.GetEnumerator()) + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded || VesselModuleRegistry.IgnoredVesselTypes.Contains(v.Current.vesselType)) continue; + if (!v.Current.IsControllable) continue; + if (v.Current == vessel) continue; + Vector3 targetVector = v.Current.CoM - part.transform.position; + var turretInRange = turret && turret.TargetInRange(v.Current.CoM, maxEffectiveDistance, 20); + if (!(turretInRange || Vector3.Dot(targetVector, fireTransforms[0].forward) > 0)) continue; + float sqrDist = (v.Current.CoM - part.transform.position).sqrMagnitude; + if (sqrDist > closestSqrDist) continue; + if (!(turretInRange || VectorUtils.Angle(targetVector, fireTransforms[0].forward) < 20)) continue; + tgt = v.Current; + closestSqrDist = sqrDist; + } + + if (tgt != null) + { + targetAcquired = true; + atprAcquired = true; + targetRadius = tgt.GetRadius(average: true); + targetPosition = tgt.CoM; + targetVelocity = tgt.rb_velocity; + targetAcceleration = tgt.acceleration; + targetIsLandedOrSplashed = tgt.LandedOrSplashed; + atprTargetPosition = targetPosition; + } + } + if (targetAcquired) + { + targetPosition = atprTargetPosition; + targetAcquisitionType = TargetAcquisitionType.AutoProxy; + return; + } + } + } + + if (!targetAcquired) + { + targetVelocity = Vector3.zero; + targetAcceleration = Vector3.zero; + targetIsLandedOrSplashed = false; + } + } + + + bool TrackIncomingProjectile() + { + targetAcquired = false; + atprAcquired = false; + slaved = false; + radarTarget = false; + GPSTarget = false; + lastTargetAcquisitionType = targetAcquisitionType; + closestTarget = Vector3.zero; + var wm = WeaponManager; + if (!wm || Time.time - staleGoodTargetTime > Mathf.Max(roundsPerMinute / 60f, wm.targetScanInterval)) + { + targetAcquisitionType = TargetAcquisitionType.None; + } + if (wm && weaponState == WeaponStates.Enabled) + { + if (tgtShell != null || tgtRocket != null || visualTargetPart != null) + { + visualTargetVessel = null; + if (tgtShell != null) + { + targetVelocity = tgtShell.currentVelocity - BDKrakensbane.FrameVelocityV3f; // Local frame velocity. + targetPosition = tgtShell.previousPosition; // Bullets have been moved already, but aiming logic is based on pre-move positions. + targetRadius = 0.25f; + } + if (tgtRocket != null) + { + targetVelocity = tgtRocket.currentVelocity - BDKrakensbane.FrameVelocityV3f; + targetPosition = tgtRocket.currentPosition; + targetRadius = 0.25f; + } + if (visualTargetPart != null) + { + targetVelocity = visualTargetPart.vessel.rb_velocity; + targetPosition = visualTargetPart.transform.position; + visualTargetVessel = visualTargetPart.vessel; + TargetInfo currentTarget = (visualTargetVessel != null ? visualTargetVessel.gameObject.GetComponent() : null); + targetRadius = currentTarget != null ? visualTargetVessel.GetRadius(fireTransforms[0].forward, currentTarget.bounds) : 0; + } + + if (visualTargetPart != null && visualTargetPart.vessel != null) + { + targetAcceleration = (Vector3)visualTargetPart.vessel.acceleration; + targetIsLandedOrSplashed = visualTargetPart.vessel.LandedOrSplashed; + } + else + { + targetAcceleration = Vector3.zero; + targetIsLandedOrSplashed = false; + } + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.Visual; + if (wm.slavingTurrets && turret) slaved = false; + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.ModuleWeapon] tgtVelocity: " + tgtVelocity + "; tgtPosition: " + targetPosition + "; tgtAccel: " + targetAcceleration); + Debug.Log($"[BDArmory.ModuleWeapon - {(vessel != null ? vessel.GetName() : "null")}] Lead Offset: {fixedLeadOffset}, FinalAimTgt: {finalAimTarget}, tgt CosAngle {targetCosAngle}, wpn CosAngle {targetAdjustedMaxCosAngle}, Wpn Autofire: {autoFire}"); + } + return true; + } + else + { + if (turret && visualTargetVessel == null) turret.ReturnTurret(); //reset turret if no target + //visualTargetPart = null; + //tgtShell = null; + //tgtRocket = null; + + for (int i = 0; i < customTurret.Count; i++) + { + if (customTurret[i] == null) continue; + if (customTurret[i].vessel != vessel) continue; + customTurret[i].ReturnTurret(); + } + } + } + return false; + } + + IEnumerator KillIncomingProjectile(PooledBullet shell, PooledRocket rocket) + { + //So, uh, this is fine for simgle shot APS; what about conventional CIWS type AMS using rotary cannon for dakka vs accuracy? + //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 incoming proj + if (shell != null || rocket != null) + { + delayTime = -1; + if (baseDeviation > 0.05 && (eWeaponType == WeaponTypes.Ballistic || (eWeaponType == WeaponTypes.Laser && pulseLaser))) //if using rotary cannon/CIWS for APS + { + if (UnityEngine.Random.Range(0, (targetDistance - (Mathf.Cos(baseDeviation) * targetDistance))) > 1) + { + yield break; //simulate inaccuracy, decreasing as incoming projectile gets closer + } + } + delayTime = eWeaponType == WeaponTypes.Ballistic ? (targetDistance / (bulletVelocity + (targetVelocity - part.rb.velocity).magnitude)) : (eWeaponType == WeaponTypes.Rocket ? (targetDistance / ((targetDistance / predictedFlightTime) + (targetVelocity - part.rb.velocity).magnitude)) : -1); + if (delayTime < 0) + { + 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 more 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, null, default, -1, false, shell.bulletMass, -1, 1, sourceVelocity: shell.currentVelocity); + shell.KillBullet(); + tgtShell = null; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon] {part.partInfo.name} on {vessel.vesselName} Detonated Incoming Projectile!"); + } + else + { + if (eWeaponType == WeaponTypes.Laser) + { + shell.KillBullet(); + tgtShell = null; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon] {part.partInfo.name} on {vessel.vesselName} Vaporized Incoming Projectile!"); + } + else + { + 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] {part.partInfo.name} on {vessel.vesselName} Exploded Incoming Projectile!"); + } + } + } + } + else + { + 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, null, default, -1, false, rocket.rocketMass * 1000, -1, 1, sourceVelocity: rocket.currentVelocity); + } + rocket.gameObject.SetActive(false); + tgtRocket = null; + } + } + else + { + //Debug.Log("[BDArmory.ModuleWeapon] KillIncomingProjectile called on null object!"); + } + } + + /// + /// Apply Brown's double exponential smoothing to the target velocity and acceleration values to smooth out noise. + /// The smoothing factor depends on the distance to the target. + /// The smoothed velocity components are corrected for the Krakensbane velocity frame and may suffer loss of precision at extreme speeds. + /// + /// + /// + /// + /// + void SmoothTargetKinematics(Vector3 position, Vector3 velocity, Vector3 acceleration, bool landedOrSplashed, bool reset = false) + { + // Floating objects need vertical smoothing. + float altitude = (float)FlightGlobals.currentMainBody.GetAltitude(position); + if (altitude < 12 && altitude > -10) + acceleration = acceleration.ProjectOnPlanePreNormalized(VectorUtils.GetUpDirection(position)); + + var distance = Vector3.Distance(position, part.transform.position); + var alpha = Mathf.Max(1f - BDAMath.Sqrt(distance) / (landedOrSplashed ? 256f : 512f), 0.1f); // Landed targets have various "corrections" that cause significant noise in their acceleration values. + var beta = alpha * alpha; + if (!reset) + { + // To smooth velocities, we need to use a consistent reference frame. + targetVelocitySmoothing.Update(velocity + BDKrakensbane.FrameVelocityV3f, alpha); + targetVelocity = targetVelocitySmoothing.Value - BDKrakensbane.FrameVelocityV3f; + targetAccelerationSmoothing.Update(acceleration, beta); + targetAcceleration = targetAccelerationSmoothing.Value; + partVelocitySmoothing.Update(part.rb.velocity + BDKrakensbane.FrameVelocityV3f, alpha); + smoothedPartVelocity = partVelocitySmoothing.Value - BDKrakensbane.FrameVelocityV3f; + partAccelerationSmoothing.Update(part.vessel.acceleration_immediate); + smoothedPartAcceleration = partAccelerationSmoothing.Value; + } + else + { + targetVelocitySmoothing.Reset(velocity + BDKrakensbane.FrameVelocityV3f); + targetVelocity = velocity; + targetAccelerationSmoothing.Reset(acceleration); + targetAcceleration = acceleration; + partVelocitySmoothing.Reset(part.rb.velocity + BDKrakensbane.FrameVelocityV3f); + smoothedPartVelocity = part.rb.velocity; + partAccelerationSmoothing.Reset(part.vessel.acceleration_immediate); + smoothedPartAcceleration = partAccelerationSmoothing.Value; + lastTimeToCPA = -1; + } + } + + void UpdateGUIWeaponState() + { + guiStatusString = weaponState.ToString(); + } + + IEnumerator StartupRoutine(bool calledByReload = false, bool secondaryFiring = false) + { + if (hasReloadAnim && isReloading) //wait for reload to finish before shutting down + { + yield return new WaitWhileFixed(() => reloadState.normalizedTime < 1); + } + if (!calledByReload) + { + weaponState = WeaponStates.PoweringUp; + UpdateGUIWeaponState(); + } + if (hasDeployAnim && deployState) + { + 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 (!calledByReload) + { + if (!secondaryFiring) + weaponState = WeaponStates.Enabled; + else + weaponState = WeaponStates.EnabledForSecondaryFiring; + } + UpdateGUIWeaponState(); + UpdateOffsetWeapon(); // Re-calculate offset/non-centerline weapon corrections on weapon selection + 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 (BurstFire && RoundsRemaining > 0 && RoundsRemaining < RoundsPerMag) //if we're in the middle of a burst and the weapon is deselected, finish burst + { + yield return new WaitWhileFixed(() => RoundsRemaining < RoundsPerMag); + ReloadWeapon(); + } + if (hasReloadAnim && isReloading) //wait for reload to finish before shutting down + { + yield return new WaitWhileFixed(() => reloadState.normalizedTime < 1); //why is this not registering when in Guardmode? + } + if (!calledByReload) //allow isreloading to co-opt the startup/shutdown anim without disabling weapon in the process + { + weaponState = WeaponStates.PoweringDown; + UpdateGUIWeaponState(); + } + else + { + guiStatusString = "Reloading"; + } + BDArmorySetup.Instance.UpdateCursorState(); + if (turret) + { + yield return new WaitForSecondsFixed(0.2f); + yield return new WaitWhileFixed(() => !turret.ReturnTurret()); //wait till turret has returned + } + if (customTurret.Count > 0) + { + yield return new WaitForSecondsFixed(0.2f); + for (int i = 0; i < customTurret.Count; i++) + { + if (customTurret[i] == null) continue; + if (customTurret[i].vessel != vessel) continue; + yield return new WaitWhileFixed(() => !customTurret[i].ReturnTurret()); //wait till turret has returned + } + } + if (hasCharged) + { + if (hasChargeAnimation && postFireChargeAnim) + yield return chargeRoutine = StartCoroutine(ChargeRoutine(true)); + } + if (hasDeployAnim && 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 (!calledByReload) + { + weaponState = WeaponStates.Disabled; + UpdateGUIWeaponState(); + } + } + IEnumerator ReloadRoutine() + { + guiStatusString = "Reloading"; + hasCharged = false; + float timeGap = 60 / roundsPerMinute * TimeWarp.CurrentRate; + float netReloadTime = ReloadTime - timeGap - (postFireChargeAnim && ChargeTime > 0 ? ChargeTime : 0); + if (hasFireAnimation) yield return new WaitForSecondsFixed(Mathf.Min(timeGap, fireState[0].length / fireAnimSpeed)); //wait for fire anim to finish. + for (int i = 0; i < fireState.Length; i++) + { + fireState[i].normalizedTime = 1; + fireState[i].speed = 0; + fireState[i].enabled = false; + //if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon]: packing Fire Anim, i = " + i + "; fire anim " + fireState[i].name + "normalizedTime: " + fireState[i].normalizedTime); + } + if (hasChargeAnimation && postFireChargeAnim) + { + chargeRoutine = StartCoroutine(ChargeRoutine(true)); + yield return new WaitWhileFixed(() => chargeState.normalizedTime > 0); //wait for animation here + } + if (!oneShotSound) audioSource.Stop(); + if (!string.IsNullOrEmpty(reloadAudioPath)) + { + audioSource2.Stop(); + audioSource2.PlayOneShot(reloadAudioClip); + } + reloadState.normalizedTime = 0; + reloadState.enabled = true; + reloadState.speed = (reloadState.length / netReloadTime);//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(bool discharge = false) + { + isCharging = true; + guiStatusString = "Charging"; + if (discharge) + { + if (hasChargeHoldAnimation) chargeHoldState.enabled = false; + if (hasChargeAnimation) chargeState.enabled = false; + hasCharged = false; + } + if (!string.IsNullOrEmpty(chargeSoundPath) && !discharge) + { + audioSource.Stop(); + audioSource.PlayOneShot(chargeSound); + } + if (hasChargeAnimation) + { + chargeState.normalizedTime = discharge ? 1 : 0; + chargeState.enabled = true; + chargeState.speed = (chargeState.length / ChargeTime) * (discharge ? -1 : 1);//ensure reload anim is not longer than reload time + yield return new WaitWhileFixed(() => discharge ? chargeState.normalizedTime > 0 : chargeState.normalizedTime < 1); //wait for animation here + chargeState.normalizedTime = discharge ? 0 : 1; + chargeState.speed = 0; + chargeState.enabled = false; + } + else + { + yield return new WaitForSecondsFixed(ChargeTime); + } + UpdateGUIWeaponState(); + isCharging = false; + if (!discharge) + { + if (!ChargeEachShot) hasCharged = true; + switch (eWeaponType) + { + case WeaponTypes.Laser: + if (FireLaser()) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = true; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(true); + } + } + } + } + else + { + if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && timeSinceFired > beamDuration)) + { + if (!conicAoE) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + else + { + if (beamCone != null) + { + for (int c = 0; c < beamCone.Length; c++) + { + beamCone[c].SetActive(false); + } + } + } + } + } + break; + case WeaponTypes.Ballistic: + Fire(); + break; + case WeaponTypes.Rocket: + FireRocket(); + break; + } + } + } + IEnumerator StandbyRoutine() + { + yield return StartupRoutine(true); + weaponState = WeaponStates.Standby; + UpdateGUIWeaponState(); + BDArmorySetup.Instance.UpdateCursorState(); + } + void StopShutdownStartupRoutines() + { + if (shutdownRoutine != null) + { + StopCoroutine(shutdownRoutine); + shutdownRoutine = null; + } + + if (startupRoutine != null) + { + StopCoroutine(startupRoutine); + startupRoutine = null; + } + + if (standbyRoutine != null) + { + StopCoroutine(standbyRoutine); + standbyRoutine = null; + } + } + + #endregion Updates + + #region Bullets + + void ParseBulletDragType() + { + bulletDragTypeName = bulletDragTypeName.ToLower(); + + switch (bulletDragTypeName) + { + case "none": + bulletDragType = BulletDragTypes.None; + break; + + case "numericalintegration": + bulletDragType = BulletDragTypes.NumericalIntegration; + break; + + case "analyticestimate": + bulletDragType = BulletDragTypes.AnalyticEstimate; + break; + default: + bulletDragType = BulletDragTypes.AnalyticEstimate; + break; + } + } + + void ParseAPSType(string type) + { + type = type.ToLower(); + switch (type) + { + case "ballistic": + eAPSType = APSTypes.Ballistic; + break; + case "missile": + eAPSType = APSTypes.Missile; + break; + case "omni": + eAPSType = APSTypes.Omni; + break; + default: + eAPSType = APSTypes.None; + break; + } + } + + public void SetupBulletPool() + { + if (bulletPool != null) return; + GameObject templateBullet = new GameObject("Bullet"); + templateBullet.AddComponent(); + templateBullet.SetActive(false); + bulletPool = ObjectPool.CreateObjectPool(templateBullet, 100, true, true); + } + + void SetupShellPool() + { + GameObject templateShell = GameDatabase.Instance.GetModel("BDArmory/Models/shell/model"); + templateShell.SetActive(false); + templateShell.AddComponent(); + shellPool = ObjectPool.CreateObjectPool(templateShell, 50, true, true); + } + + public void SetupRocketPool(string name, string modelpath) + { + var key = name; + if (!rocketPool.ContainsKey(key) || rocketPool[key] == null) + { + var RocketTemplate = GameDatabase.Instance.GetModel(modelpath); + if (RocketTemplate == null) + { + Debug.LogError("[BDArmory.ModuleWeapon]: model '" + modelpath + "' not found. Expect exceptions if trying to use this rocket."); + return; + } + RocketTemplate.SetActive(false); + RocketTemplate.AddComponent(); + rocketPool[key] = ObjectPool.CreateObjectPool(RocketTemplate, 10, true, true); + } + } + + public void SetupAmmo(BaseField field, object obj) + { + if (useCustomBelt && customAmmoBelt.Count > 0) + { + currentType = customAmmoBelt[AmmoIntervalCounter].ToString(); + currentTypeIndex = customAmmoBeltIndexes[AmmoIntervalCounter]; + } + else + { + ammoList = BDAcTools.ParseNames(bulletType); + if (ammoTypeIndex >= ammoList.Count) + { + Debug.LogWarning($"[BDArmory.ModuleWeapon]: AmmoTypeNum {AmmoTypeNum} is not valid for {WeaponName}. Resetting to 1."); + AmmoTypeNum = 1; // For weapons where the ammo types have changed such that the old AmmoTypeNum is no longer valid. + currentTypeIndex = 0; + } + currentType = ammoList[ammoTypeIndex].ToString(); + currentTypeIndex = ammoTypeIndex; + } + ParseAmmoStats(); + } + public void ParseAmmoStats() + { + if (eWeaponType == WeaponTypes.Ballistic) + { + bulletInfo = bulletInfoList[currentTypeIndex]; + guiAmmoTypeString = ""; //reset name + maxDeviation = baseDeviation; //reset modified deviation + caliber = bulletInfo.caliber; + bulletVelocity = bulletInfo.bulletVelocity; + bulletMass = bulletInfo.bulletMass; + ProjectileCount = bulletInfo.projectileCount; + //bulletDragTypeName = bulletInfo.bulletDragTypeName; // deprecated, do not need to set it + projectileColorC = bulletInfo.projectileColorC; + startColorC = bulletInfo.startColorC; + //fadeColor = bulletInfo.fadeColor; // deprecated + //ParseBulletDragType(); + //bulletDragType = bulletInfo.bulletDragType; // deprecated + //ParseBulletFuzeType(bulletInfo.fuzeType, bulletInfo.tntMass, bulletInfo.beehive); + eFuzeType = bulletInfo.eFuzeType; + //ParseBulletHEType(bulletInfo.explosive); + //eHEType = bulletInfo.eHEType; // deprecated + tntMass = bulletInfo.tntMass; + beehive = bulletInfo.beehive; + Impulse = bulletInfo.impulse; + massAdjustment = bulletInfo.massMod; + if (!tracerOverrideWidth) + { + tracerStartWidth = caliber / 300; + tracerEndWidth = caliber / 750; + nonTracerWidth = caliber / 500; + } + SelectedAmmoType = ammoList[currentTypeIndex]; //store selected ammo name as string for retrieval by web orc filter/later GUI implementation + if (!useCustomBelt) + { + baseBulletVelocity = bulletVelocity; + if (bulletInfo.projectileCount > 1) + { + 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 || bulletInfo.sabot) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_AP") + " "; + } + else if (bulletInfo.apBulletMod < 1.1 && bulletInfo.apBulletMod > 0.8f) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_SAP") + " "; + } + if (bulletInfo.nuclear) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Nuclear") + " "; + } + if (bulletInfo.tntMass > 0 && !bulletInfo.nuclear) + { + if (eFuzeType == BulletFuzeTypes.Timed || eFuzeType == BulletFuzeTypes.Proximity || eFuzeType == BulletFuzeTypes.Flak) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Flak") + " "; + } + else if (bulletInfo.eHEType == PooledBulletTypes.Shaped) + { + if (bulletInfo.projectileCount > 1) + { + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Shot") + " " + + StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; + } + else + { + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; + } + } + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Explosive") + " "; + } + if (bulletInfo.incendiary) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Incendiary") + " "; + } + if (bulletInfo.EMP && !bulletInfo.nuclear) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_EMP") + " "; + } + if (bulletInfo.beehive) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Beehive") + " "; + } + if (bulletInfo.tntMass <= 0 && bulletInfo.apBulletMod <= 0.8) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); + } + } + else + { + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Multiple"); + if (baseBulletVelocity < 0) + { + baseBulletVelocity = bulletInfoList[customAmmoBeltIndexes[0]].bulletVelocity; + } + } + electroLaser = bulletInfo.EMP; //borrowing electrolaser bool, should really rename it empWeapon + } + if (eWeaponType == WeaponTypes.Rocket) + { + rocketInfo = RocketInfo.rockets[currentType]; + guiAmmoTypeString = ""; //reset name + rocketMass = rocketInfo.rocketMass; + caliber = rocketInfo.caliber; + thrust = rocketInfo.thrust; + thrustTime = rocketInfo.thrustTime; + ProjectileCount = rocketInfo.projectileCount; + 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.projectileCount > 1) + { + 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.nuclear) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Nuclear") + " "; + } + else + { + if (rocketInfo.explosive) + { + if (rocketInfo.flak) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Flak") + " "; + eFuzeType = BulletFuzeTypes.Flak; //fix rockets not getting detonation range slider + } + else if (rocketInfo.shaped) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; + } + if (rocketInfo.EMP || rocketInfo.choker || rocketInfo.impulse) + { + if (rocketInfo.EMP) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_EMP") + " "; + } + if (rocketInfo.choker) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Choker") + " "; + } + if (rocketInfo.impulse) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Impulse") + " "; + } + } + else + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_HE") + " "; + } + if (rocketInfo.incendiary) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Incendiary") + " "; + } + if (rocketInfo.gravitic) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Gravitic") + " "; + } + } + else + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Kinetic"); + } + } + if (rocketInfo.flak) + { + proximityDetonation = true; + } + else + { + proximityDetonation = false; + } + graviticWeapon = rocketInfo.gravitic; + impulseWeapon = rocketInfo.impulse; + electroLaser = rocketInfo.EMP; //borrowing electrolaser bool, should really rename it empWeapon + choker = rocketInfo.choker; + incendiary = rocketInfo.incendiary; + SetupRocketPool(currentType, rocketModelPath); + } + PAWRefresh(); + SetInitialDetonationDistance(); + } + protected void SetInitialDetonationDistance() + { + if (detonationRange == -1) + { + if (eWeaponType == WeaponTypes.Ballistic && bulletInfo.tntMass > 0f && (eFuzeType == BulletFuzeTypes.Proximity || eFuzeType == BulletFuzeTypes.Flak)) + { + blastRadius = BlastPhysicsUtils.CalculateBlastRange(bulletInfo.tntMass); //reporting as two so blastradius can be handed over to PooledRocket for detonation/safety stuff + detonationRange = beehive ? 100 : blastRadius * 0.666f; + } + else if (eWeaponType == WeaponTypes.Rocket && rocketInfo.tntMass > 0f) //don't fire rockets at point blank + { + blastRadius = BlastPhysicsUtils.CalculateBlastRange(rocketInfo.tntMass); + detonationRange = beehive ? 100 : blastRadius * 0.666f; + } + } + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.ModuleWeapon]: DetonationDistance = : " + detonationRange); + } + } + + #endregion Bullets + + #region RMB Info + + public override string GetInfo() + { + ammoList = BDAcTools.ParseNames(bulletType); + if (BulletInfo.bullets == null || RocketInfo.rockets == null) + { + Debug.LogError($"[BDArmory.ModuleWeapon]: BDArmory hasn't loaded properly.\n This is typically a symptom of having installed BDArmory (or other mods) incorrectly.\n Check the 'AssemblyLoader' section at the start of the KSP.log for duplicate, missing or misplaced mod dlls or other errors/exceptions."); + // Let the exception happen and break loading KSP so users can fix their install. + } + StringBuilder output = new StringBuilder(); + output.Append(Environment.NewLine); + output.AppendLine($"Weapon Type: {weaponType}"); + + if (weaponType == "laser") + { + if (secondaryAmmoPerShot == 0) secondaryAmmoPerShot = ECPerShot; + if (electroLaser) + { + if (pulseLaser) + { + output.AppendLine($"Electrolaser EMP damage: {laserDamage}/shot"); + } + else + { + output.AppendLine($"Electrolaser EMP damage: {laserDamage}/s"); + } + output.AppendLine($"{(electricResource ? "Power" : secondaryAmmoName)} required: {secondaryAmmoPerShot * (pulseLaser ? roundsPerMinute / 60 : 50)}/s"); + } + else + { + output.AppendLine($"Laser damage: {laserDamage}"); + if (LaserGrowTime > 0) + { + output.AppendLine($"-Laser takes: {LaserGrowTime} seconds to reach max power"); + output.AppendLine($"-Maximum output: {laserMaxDamage} damage"); + } + if (secondaryAmmoPerShot > 0 || ECPerShot > 0) + { + if (pulseLaser) + { + output.AppendLine($"{secondaryAmmoName} required per shot: {secondaryAmmoPerShot}"); + } + else + { + output.AppendLine($"{secondaryAmmoName}: {secondaryAmmoPerShot}/s"); + } + } + if (requestResourceAmount > 0) + { + if (pulseLaser) + { + output.AppendLine($"{ammoName} required per shot: {requestResourceAmount}"); + } + else + { + output.AppendLine($"{ammoName}: {requestResourceAmount}/s"); + } + } + } + if (conicAoE) + { + output.AppendLine($"Conic beam weapon"); + output.AppendLine($"- Beam FOV: {beamFOV} deg."); + output.AppendLine($"- Will not hit friendlies in AoE: {!friendlyFire}"); + } + if (pulseLaser) + { + output.AppendLine($"Rounds Per Minute: {roundsPerMinute * (fireTransforms?.Length ?? 1)}"); + if (SpoolUpTime > 0) output.AppendLine($"Weapon requires {SpoolUpTime} seconds to come to max RPM"); + output.AppendLine($"Shot Deviation: {Mathf.Tan(Mathf.Deg2Rad * maxDeviation) * 1000 * (1.285f / 2) * 2:F2} mrad, 80% hit"); + if (HEpulses) + { + output.AppendLine($"Blast:"); + output.AppendLine($"- tnt mass: {Math.Round((laserDamage / 10000), 2)} kg"); + output.AppendLine($"- radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(laserDamage / 10000), 2)} m"); + } + } + + } + else + { + output.AppendLine($"Rounds Per Minute: {roundsPerMinute * (fireTransforms?.Length ?? 1)}"); + if (SpoolUpTime > 0) output.AppendLine($"Weapon requires {SpoolUpTime} second" + (SpoolUpTime > 1 ? "s" : "") + " to come to max RPM"); + output.AppendLine(); + output.AppendLine($"Ammunition: {ammoName}"); + if (secondaryAmmoPerShot > 0 || ECPerShot > 0) + { + if (secondaryAmmoPerShot == 0) secondaryAmmoPerShot = ECPerShot; + output.AppendLine($"{secondaryAmmoName} required per shot: {secondaryAmmoPerShot}"); + } + output.AppendLine($"Max Range: {maxEffectiveDistance} m"); + if (minSafeDistance > 0) + { + output.AppendLine($"Min Range: {minSafeDistance} m"); + } + if (weaponType == "ballistic") + { + output.AppendLine($"Shot Deviation: {Mathf.Tan(Mathf.Deg2Rad * maxDeviation) * 1000 * (1.285f / 2) * 2:F2} mrad, 80% hit"); + for (int i = 0; i < ammoList.Count; i++) + { + BulletInfo binfo = BulletInfo.bullets[ammoList[i].ToString()]; + if (binfo == null) + { + Debug.LogError("[BDArmory.ModuleWeapon]: The requested bullet type (" + ammoList[i].ToString() + ") does not exist."); + output.AppendLine($"Bullet type: {ammoList[i]} - MISSING"); + output.AppendLine(""); + continue; + } + // HE and fuze types parsed here previously despite not needing to be parsed? + 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"); + //output.AppendLine($"Explosive: {binfo.explosive}"); + if (binfo.projectileCount > 1) + { + output.AppendLine($"Cannister Round"); + output.AppendLine($" - Submunition count: {binfo.projectileCount}"); + } + float tempPenDepth = ProjectileUtils.CalculatePenetration(binfo.caliber, binfo.bulletVelocity, binfo.bulletMass, binfo.apBulletMod, muParam1: binfo.sabot ? 0.9470311374f : 0.656060636f, muParam2: binfo.sabot ? 1.555757746f : 1.20190930f, muParam3: binfo.sabot ? 2.753715499f : 1.77791929f, sabot: binfo.sabot); + output.AppendLine($"Estimated Penetration: {tempPenDepth:F2} mm"); + if ((binfo.tntMass > 0) && !binfo.nuclear) + { + output.AppendLine($"Blast:"); + output.AppendLine($"- tnt mass: {Math.Round(binfo.tntMass, 3)} kg"); + output.AppendLine($"- fuze type: {binfo.eFuzeType switch + { + BulletFuzeTypes.None => "None", + BulletFuzeTypes.Impact => "Impact", + BulletFuzeTypes.Timed => "Timed", + BulletFuzeTypes.Proximity => "Proximity", + BulletFuzeTypes.Flak => "Flak", + BulletFuzeTypes.Delay => "Delayed", + BulletFuzeTypes.Penetrating => "Penetrating", + _ => "Unknown" + }}"); + if (binfo.eFuzeType == BulletFuzeTypes.Penetrating) + output.AppendLine($"- Min thickness to arm fuze: {(binfo.fuzeSensitivity > 0 ? binfo.fuzeSensitivity : tempPenDepth * 0.666f):F2} mm"); + if (binfo.eFuzeType == BulletFuzeTypes.Penetrating || binfo.eFuzeType == BulletFuzeTypes.Delay) + output.AppendLine($"- Fuze delay: {(1000f * (binfo.fuzeDelay > 0 ? binfo.fuzeDelay : 1f / 30f)):F2} ms"); + output.AppendLine($"- radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(binfo.tntMass), 2)} m"); + + if (binfo.eFuzeType == BulletFuzeTypes.Timed || binfo.eFuzeType == BulletFuzeTypes.Proximity || binfo.eFuzeType == BulletFuzeTypes.Flak) + { + output.AppendLine($"Air detonation: True"); + output.AppendLine($"- auto timing: {(binfo.eFuzeType != BulletFuzeTypes.Proximity)}"); + output.AppendLine($"- max range: {maxTargetingRange} m"); + } + else + { + output.AppendLine($"Air detonation: False"); + } + + if (binfo.eHEType == PooledBulletTypes.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) + { + output.AppendLine($"Nuclear Shell:"); + output.AppendLine($"- yield: {Math.Round(binfo.tntMass, 3)} kT"); + if (binfo.EMP) + { + output.AppendLine($"- generates EMP"); + } + } + if (binfo.EMP && !binfo.nuclear) + { + output.AppendLine($"BlueScreen:"); + output.AppendLine($"- EMP buildup per hit:{binfo.caliber * Mathf.Clamp(binfo.bulletMass - binfo.tntMass, 0.1f, 100)}"); + } + if (binfo.impulse != 0) + { + output.AppendLine($"Concussive:"); + output.AppendLine($"- Impulse to target:{binfo.impulse}"); + } + if (binfo.massMod != 0) + { + output.AppendLine($"Gravitic:"); + output.AppendLine($"- weight added per hit:{binfo.massMod * 1000} kg"); + } + if (binfo.incendiary) + { + output.AppendLine($"Incendiary"); + } + if (binfo.beehive) + { + output.AppendLine($"Beehive Shell:"); + string[] subMunitionData = binfo.subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + if (subMunitionData.Length < 2 || !int.TryParse(subMunitionData[1], out int count)) count = 1; + BulletInfo sinfo = BulletInfo.bullets[projType]; + output.AppendLine($"- deploys {count}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); + } + if (binfo.guidanceDPS > 0) + { + output.AppendLine($"Guidance:"); + output.AppendLine($"- Guidance Rate: {Math.Round(binfo.guidanceDPS, 3)} deg/s"); + if (binfo.guidanceRange > 0) + output.AppendLine($"- Guidance Range: {Math.Round(binfo.guidanceRange)} m"); + else + output.AppendLine($"- Guidance Range: Unlimited"); + } + } + } + if (weaponType == "rocket") + { + for (int i = 0; i < ammoList.Count; i++) + { + RocketInfo rinfo = RocketInfo.rockets[ammoList[i].ToString()]; + if (rinfo == null) + { + Debug.LogError("[BDArmory.ModuleWeapon]: The requested rocket type (" + ammoList[i].ToString() + ") does not exist."); + output.AppendLine($"Rocket type: {ammoList[i]} - MISSING"); + output.AppendLine(""); + continue; + } + 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 && !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.projectileCount > 1) + { + output.AppendLine($"Cluster Rocket"); + output.AppendLine($" - Submunition count: {rinfo.projectileCount}"); + } + if (impulseWeapon || graviticWeapon || choker || electroLaser || incendiary) + { + output.AppendLine($"Special Weapon:"); + if (impulseWeapon) + { + output.AppendLine($"Concussion warhead:"); + output.AppendLine($"- Impulse to target:{Impulse}"); + } + if (graviticWeapon) + { + output.AppendLine($"Gravitic warhead:"); + output.AppendLine($"- Mass added per part hit:{massAdjustment * 1000} kg"); + } + if (electroLaser && !rinfo.nuclear) + { + output.AppendLine($"EMP warhead:"); + output.AppendLine($"- can temporarily shut down targets"); + } + if (choker) + { + output.AppendLine($"Atmospheric Deprivation Warhead:"); + output.AppendLine($"- Will temporarily knock out air intakes"); + } + if (incendiary) + { + output.AppendLine($"Incendiary:"); + output.AppendLine($"- Covers targets in inferno gel"); + } + if (rinfo.beehive) + { + output.AppendLine($"Cluster Rocket:"); + string[] subMunitionData = rinfo.subMunitionType.Split(new char[] { ';' }); + string projType = subMunitionData[0]; + if (subMunitionData.Length < 2 || !int.TryParse(subMunitionData[1], out int count)) count = 1; + if (BulletInfo.bulletNames.Contains(projType)) + { + BulletInfo sinfo = BulletInfo.bullets[projType]; + output.AppendLine($"- deploys {count}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); + } + else if (RocketInfo.rocketNames.Contains(projType)) + { + RocketInfo sinfo = RocketInfo.rockets[projType]; + output.AppendLine($"- deploys {count}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); + } + } + } + + + } + if (externalAmmo) + { + output.AppendLine($"Uses External Ammo"); + } + + } + } + output.AppendLine(""); + if (BurstFire) + { + output.AppendLine($"Burst Fire Weapon"); + output.AppendLine($" - Rounds Per Burst: {RoundsPerMag}"); + } + if (!BeltFed && !BurstFire) + { + output.AppendLine($" Reloadable"); + output.AppendLine($" - Shots before Reload: {RoundsPerMag}"); + output.AppendLine($" - Reload Time: {ReloadTime}"); + } + if (crewserved) + { + 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(); + } + + #endregion RMB Info + } + + #region UI //borrowing code from ModularMissile GUI + + [KSPAddon(KSPAddon.Startup.EditorAny, false)] + public class WeaponGroupWindow : MonoBehaviour + { + internal static EventVoid OnActionGroupEditorOpened = new EventVoid("OnActionGroupEditorOpened"); + internal static EventVoid OnActionGroupEditorClosed = new EventVoid("OnActionGroupEditorClosed"); + + private static GUIStyle unchanged; + private static GUIStyle changed; + private static GUIStyle greyed; + private static GUIStyle overfull; + + private static WeaponGroupWindow instance; + + private bool ActionGroupMode; + + private Rect guiWindowRect = new Rect(0, 0, 0, 0); + + private ModuleWeapon WPNmodule; + + [KSPField] public int offsetGUIPos = -1; + + private Vector2 scrollPos; + + [KSPField(isPersistant = false, guiActiveEditor = true, guiActive = false, guiName = "#LOC_BDArmory_ShowGroupEditor"), UI_Toggle(enabledText = "#LOC_BDArmory_ShowGroupEditor_enabledText", disabledText = "#LOC_BDArmory_ShowGroupEditor_disabledText")][NonSerialized] public bool showRFGUI;//Show Group Editor--close Group GUI--open Group GUI + + private bool styleSetup; + + private string txtName = string.Empty; + + public static void HideGUI() + { + // Doing it this way prevents OnGUI events from below the window from being triggered by the window disappearing. + if (instance != null) instance.StartCoroutine(instance.HideGUIAtEndOfFrame()); + else GUIUtils.PreventClickThrough(default, "BD_MN_GUILock", true); + } + bool waitingForEndOfFrame = false; + IEnumerator HideGUIAtEndOfFrame() + { + if (waitingForEndOfFrame) yield break; + waitingForEndOfFrame = true; + yield return new WaitForEndOfFrame(); + waitingForEndOfFrame = false; + if (instance != null && instance.WPNmodule != null) + { + instance.WPNmodule.WeaponDisplayName = instance.WPNmodule.shortName; + instance.WPNmodule = null; + instance.applyWeaponGroupTo = null; + instance.UpdateGUIState(); + } + GUIUtils.PreventClickThrough(default, "BD_MN_GUILock", true); + } + + public static void ShowGUI(ModuleWeapon WPNmodule) + { + if (instance != null) + { + 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() + { + enabled = WPNmodule != null; + } + + private IEnumerator CheckActionGroupEditor() + { + while (EditorLogic.fetch == null) + { + yield return null; + } + EditorLogic editor = EditorLogic.fetch; + while (EditorLogic.fetch != null) + { + if (editor.editorScreen == EditorScreen.Actions) + { + if (!ActionGroupMode) + { + HideGUI(); + OnActionGroupEditorOpened.Fire(); + } + EditorActionGroups age = EditorActionGroups.Instance; + if (WPNmodule && !age.GetSelectedParts().Contains(WPNmodule.part)) + { + HideGUI(); + } + ActionGroupMode = true; + } + else + { + if (ActionGroupMode) + { + HideGUI(); + OnActionGroupEditorClosed.Fire(); + } + ActionGroupMode = false; + } + yield return null; + } + } + + private void Awake() + { + enabled = false; + instance = this; + } + + private void OnDestroy() + { + instance = null; + GUIUtils.PreventClickThrough(guiWindowRect, "BD_MN_GUILock", true); + } + + public void OnGUI() + { + if (!styleSetup) + { + styleSetup = true; + Styles.InitStyles(); + } + + EditorLogic editor = EditorLogic.fetch; + if (!HighLogic.LoadedSceneIsEditor || !editor) + { + return; + } + + int posMult = 0; + if (offsetGUIPos != -1) + { + posMult = offsetGUIPos; + } + if (ActionGroupMode) + { + if (guiWindowRect.width == 0) + { + guiWindowRect = new Rect(430 * posMult, 365, 438, 50); + } + new Rect(guiWindowRect.xMin + 440, Screen.height - Input.mousePosition.y - 5, 300, 20); + } + else + { + if (guiWindowRect.width == 0) + { + //guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, (Screen.height - 365)); + guiWindowRect = new Rect(Screen.width - 8 - 430 * (posMult + 1), 365, 438, 50); + } + new Rect(guiWindowRect.xMin - (230 - 8), Screen.height - Input.mousePosition.y - 5, 220, 20); + } + if (BDArmorySettings.UI_SCALE_ACTUAL != 1) GUIUtility.ScaleAroundPivot(BDArmorySettings.UI_SCALE_ACTUAL * Vector2.one, guiWindowRect.position); + guiWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), guiWindowRect, GUIWindow, StringUtils.Localize("#LOC_BDArmory_WeaponGroup"), Styles.styleEditorPanel); + } + + string[] applyWeaponGroupTo; + string _applyWeaponGroupTo; + int _applyWeaponGroupToIndex = 0; + public void GUIWindow(int windowID) + { + GUIUtils.PreventClickThrough(guiWindowRect, "BD_MN_GUILock"); + InitializeStyles(); + + GUILayout.BeginVertical(); + GUILayout.Space(20); + + GUILayout.BeginHorizontal(); + + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_WeaponGroup")} "); + + txtName = GUILayout.TextField(txtName); + + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_saveClose"))) + { + string newName = string.IsNullOrEmpty(txtName.Trim()) ? WPNmodule.OriginalShortName : txtName.Trim(); + + 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.isAPS && !wpn.dualModeAPS) 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($"{StringUtils.Localize("#LOC_BDArmory_applyTo")} {_applyWeaponGroupTo}"); + if (_applyWeaponGroupToIndex != (_applyWeaponGroupToIndex = Mathf.RoundToInt(GUILayout.HorizontalSlider(_applyWeaponGroupToIndex, 0, 4, GUILayout.Width(150))))) _applyWeaponGroupTo = applyWeaponGroupTo[_applyWeaponGroupToIndex]; + GUILayout.EndHorizontal(); + + scrollPos = GUILayout.BeginScrollView(scrollPos); + + GUILayout.EndScrollView(); + + GUILayout.EndVertical(); + + GUI.DragWindow(); + GUIUtils.RepositionWindow(ref guiWindowRect); + } + + private static void InitializeStyles() + { + if (unchanged == null) + { + if (GUI.skin == null) + { + unchanged = new GUIStyle(); + changed = new GUIStyle(); + greyed = new GUIStyle(); + overfull = new GUIStyle(); + } + else + { + unchanged = new GUIStyle(GUI.skin.textField); + changed = new GUIStyle(GUI.skin.textField); + greyed = new GUIStyle(GUI.skin.textField); + overfull = new GUIStyle(GUI.skin.label); + } + + unchanged.normal.textColor = Color.white; + unchanged.active.textColor = Color.white; + unchanged.focused.textColor = Color.white; + unchanged.hover.textColor = Color.white; + + changed.normal.textColor = Color.yellow; + changed.active.textColor = Color.yellow; + changed.focused.textColor = Color.yellow; + changed.hover.textColor = Color.yellow; + + greyed.normal.textColor = Color.gray; + + overfull.normal.textColor = Color.red; + } + } + } + #endregion UI //borrowing code from ModularMissile GUI +} 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 ordnance.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac ordinance.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac ordnance.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/Misc/BDAExtensions.cs b/BDArmory/_unused_old_code/Misc/BDAExtensions.cs similarity index 99% rename from BDArmory/Misc/BDAExtensions.cs rename to BDArmory/_unused_old_code/Misc/BDAExtensions.cs index 01e0b5767..0fc13dda0 100644 --- a/BDArmory/Misc/BDAExtensions.cs +++ b/BDArmory/_unused_old_code/Misc/BDAExtensions.cs @@ -9,8 +9,6 @@ namespace BDArmory.Misc { public static class BDAExtensions { - - public static IEnumerable BDAParts(this List parts) { return (from avPart in parts.Where(p => p.partPrefab) diff --git a/BDArmory/Misc/BahaTurretBullet.cs b/BDArmory/_unused_old_code/Misc/BahaTurretBullet.cs similarity index 100% rename from BDArmory/Misc/BahaTurretBullet.cs rename to BDArmory/_unused_old_code/Misc/BahaTurretBullet.cs 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/BDACategoryModule.cs b/BDArmory/_unused_old_code/Modules/BDACategoryModule.cs similarity index 100% rename from BDArmory/Modules/BDACategoryModule.cs rename to BDArmory/_unused_old_code/Modules/BDACategoryModule.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 97% rename from BDArmory.Core/PerformanceLogger.cs rename to BDArmory/_unused_old_code/PerformanceLogger.cs index e82bdb26a..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 @@ -47,7 +47,7 @@ private void OnDestroy() if (PerformanceEntries.Count == 0) return; - Debug.Log("PerformanceLogger.OnDestroy"); + Debug.Log("[BDArmory.PerformanceLogger]: OnDestroy"); var sb = new StringBuilder(); foreach (var performanceEntry in PerformanceEntries.OrderByDescending(x => x.Value.TotalTicks)) 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/BDArmory/UI/BDAPersistantSettingsField.cs b/BDArmory/_unused_old_code/UI/BDAPersistantSettingsField.cs similarity index 100% rename from BDArmory/UI/BDAPersistantSettingsField.cs rename to BDArmory/_unused_old_code/UI/BDAPersistantSettingsField.cs diff --git a/Notes.md b/Notes.md new file mode 100644 index 000000000..4bc1ed2ac --- /dev/null +++ b/Notes.md @@ -0,0 +1,86 @@ +### Building / Debugging +- Based on https://forum.kerbalspaceprogram.com/topic/102909-ksp-plugin-debugging-and-profiling-for-visual-studio-and-monodevelop-on-all-os/ +- Create a folder `_LocalDev` above the cloned repository, e.g., in Linux: + ``` + |— _LocalDev/ + | |— ksp_dir.txt + | |— KSPRefs → /KSP_Data/Managed + |— BDArmory/ + | |— .git/ + | |— BDArmory/ + |— OtherMods + | |— ... + ``` +- Add paths to KSP installations in `ksp_dir.txt`. E.g., + ``` + /home/user/Games/KSP + /home/user/Games/KSP-copy + ``` + In Windows, the additional files `pdb2mdb_exe.txt`, `7za_exe.txt` and `dist_dir.txt` may need creating with paths to the appropriate executables and folder. +- BDArmory should then be able to be built with: + ```bash + export FrameWorkPathOverride=/usr/lib/mono/4.8-api/ # I recommend putting this into a .envrc file and using direnv. + dotnet build --configuration Debug # Use "--configuration Release" for a release build. + ``` +- Install UnityHub and install the `2019.4.18f1` editor. Then copy the playback engine to the KSP folder and create a symlink to it to replace the default playback engine. E.g., + ```bash + cd ~/Games/KSP + mv UnityPlayer.so UnityPlayer.so.orig + cp ~/Unity/Hub/Editors/2019.4.18f1/Editor/Data/PlaybackEngines/LinuxStandaloneSupport/Variations/linux64_withgfx_development_mono/UnityPlayer.so UnityPlayer.so.debug + ln -sf UnityPlayer.so.debug UnityPlayer.so + ``` + Reverting to the non-development playback engine can be done by switching the symlink: + ```bash + ln -sf UnityPlayer.so.orig UnityPlayer.so + ``` +- Logged exceptions and errors should then give a stack trace with line numbers. +- Profiling can be achieved by creating a project in UnityHub, launching the profiling window and connecting it to a running instance of KSP. + +### 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. +- Intel and AMD have hardware support for sqrt, but M1 Macs don't, so we do need to avoid using sqrt in frequently used functions. + +- 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 + +### Shader Compilation +- Shaders should be compiled using Unity 2018.4.36f1 to be compatible with KSP 1.9.1. +- To compile a shader bundle: + 1. Install AssetBundle Browser: https://docs.unity3d.com/Manual/AssetBundles-Browser.html + 2. Open a Unity project (an empty one is fine). + 3. Import the shaders (if not already done) via "Assets->Import New Asset...". + 4. Go to File->Build Settings. Pick Windows/Mac/Linux based on what bundle you plan to make. + 5. Go to "Window->AssetBundle Browser". + 6. Drag the 4 shader assets from the "Project" tab in the main Unity window into the AssetBundle Browser window. + 7. Rename the asset bundle to match the build target for loading in BDAShaderLoader.cs (e.g., "bdarmoryshaders_linux"). + 8. In the build tab select Standalone Windows/Standalone OSX Universal/Standalone Linux 64 (match your build settings). + 9. Hit build. + 10. Repeat 4, 7, 8 and 9 for the remaining Windows/Mac/Linux bundles. + 11. Copy them from `~/Unity//AssetBundles` (or equivalent on the OS you're using) to `Distribution/GameData/BDArmory/AssetBundles`. \ No newline at end of file diff --git a/README.md b/README.md index bb0d458ca..ea466636f 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,38 @@ -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) +- [Bill Nye](https://github.com/BillNyeTheIE) + +Contributors: +- [Aubranium](https://github.com/agoodman) (Remote Orchestration) +- [Halban](https://github.com/Halbann/) (Code Adapted from Kessler (formerly Kerbal Combat Suite)) +- [Spartwo](https://github.com/Spartwo/) (Code Adapted from Kessler (formerly Kerbal Combat Suite)) +- [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: - [PapaJoe](https://github.com/PapaJoesSoup) - [jrodrigv](https://github.com/jrodrigv) - [SpannerMonkey](https://github.com/SpannerMonkey) @@ -38,4 +61,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/Round4_tournament_generation.py b/Round4_tournament_generation.py deleted file mode 100644 index fa09ac330..000000000 --- a/Round4_tournament_generation.py +++ /dev/null @@ -1,95 +0,0 @@ -# Standard library imports -import argparse -import random -import json -import queue -from pathlib import Path -from typing import List - -parser = argparse.ArgumentParser(description="A tournament file generator for season 2 round 4", formatter_class=argparse.ArgumentDefaultsHelpFormatter) -parser.add_argument('folder', type=str, help="The folder (under AutoSpawn) where the craft files are organised into teams.") -parser.add_argument('rounds', type=int, help="The number of rounds to generate.") -parser.add_argument('perTeam', type=int, help="The number of vessels per team per heat.") -args = parser.parse_args() - -folder = Path(args.folder) -if not folder.exists(): - raise ValueError(f"The folder ({folder}) doesn't exist.") -teams = {f.name: [str(c.resolve()) for c in f.glob("*.craft")] for f in folder.iterdir() if f.is_dir()} # Find all the teams -# teams = {f.name: [str(c.resolve()) for c in f.glob("*.craft")] for f in folder.iterdir() if f.is_dir() and f.name[0] != '_'} # Find all the teams -# individuals = [str(c.resolve()) for f in folder.iterdir() if f.is_dir() and f.name[0] == '_' for c in f.glob("*.craft")] # Find all the individuals -print(f"Found {len(teams)} teams:") -# print(f"Found {len(teams)} teams and {len(individuals)} of individuals:") -for team in teams: - print(f" - {team} has {len(teams[team])} players,") -# print(f" - and {len(individuals)} individual players.") -print(f"Generating tournament.state file for {args.rounds} rounds with {len(teams)*(len(teams)-1)//2} heats per round (each team against another) and {args.perTeam} vessels per team per heat.") - -tournamentHeader = json.dumps({ - "tournamentID": 4, # Has to be a number - "craftFiles": [craftFile for team in teams for craftFile in teams[team]] # + individuals -}, separators=(',', ':')) - -roundConfig = {"latitude": -0.04762, "longitude": -74.8593, "altitude": 5000.0, "distance": 20.0, "absDistanceOrFactor": False, "easeInSpeed": 0.7, "killEverythingFirst": True, "assignTeams": False, "folder": "", "round": 2, "heat": 1, "completed": False} - -teamQueues = {team: queue.Queue() for team in teams} -individualsQueue = queue.Queue() - - -def getTeamSelection(team: str, N: int) -> List[str]: - global teamQueues, teams - selection = [] - while len(selection) < N: - while teamQueues[team].qsize() < 1: # Not enough in the queue, extend it with randomised ordering of craft in the team. - random.shuffle(teams[team]) - residue = [] - for craftFile in teams[team]: - if craftFile not in selection: # Avoid duplicates in the same heat. - teamQueues[team].put(craftFile) - else: - residue.append(craftFile) - for craftFile in residue: # Add crafts that had already been selected to the queue last. - teamQueues[team].put(craftFile) - # if len(selection) < len(teams[team]): # Still a craft in the queue that we haven't used yet. - # selection.append(teamQueues[team].get()) - # else: - # fillWithIndividuals(selection, N) - selection.append(teamQueues[team].get()) - return selection - - -# def fillWithIndividuals(selection: List[str], N: int): -# global individualsQueue, individuals -# while len(selection) < N: -# while individualsQueue.qsize() < 1: # Not enough in the queue, extend it with randomised ordering of craft in the team. -# random.shuffle(individuals) -# residue = [] -# for craftFile in individuals: -# if craftFile not in selection: # Avoid duplicates in the same heat. -# individualsQueue.put(craftFile) -# else: -# residue.append(craftFile) -# for craftFile in residue: # Add crafts that had already been selected to the queue last. -# individualsQueue.put(craftFile) -# selection.append(individualsQueue.get()) - - -with open('tournament.state', 'w') as f: - f.write(tournamentHeader) - - teamNames = list(teams) - for round in range(args.rounds): - heats = [] - for i, team1 in enumerate(teamNames): - for team2 in teamNames[i + 1:]: - roundConfig.update({"craftFiles": getTeamSelection(team1, args.perTeam) + getTeamSelection(team2, args.perTeam)}) - heats.append({k: v for k, v in roundConfig.items()}) - - for i in range(10): # Shuffle doesn't seem especially random, so do it 10 times. - random.shuffle(heats) # Shuffle the heat order to avoid watching the same team play too much in a row. - for heat, heatConfig in enumerate(heats): - heatConfig.update({"round": round, "heat": heat}) - - # Write the round to the tournament.state file. - for heat in heats: - f.write('\n' + json.dumps(heat, separators=(',', ':'))) diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..5aea3d230 --- /dev/null +++ b/TODO.md @@ -0,0 +1,107 @@ +### Bugs (are these still valid?) +- RemoveAllVessels can sometimes get stuck if the Kraken breaks the view frustrum due to SoI changes. Add a timeout and warning, possibly also an option to auto-quit if it breaks. +- 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 +- Taking off with the global 'P' button for two VTOL craft on the runway disables their engines! +- WM without AI or with stationary ground AI sometimes just sits there without attacking valid targets. +- Check whether `base.OnStart(state);` in `MissileLauncher.OnStart` can be re-enabled and some common stuff be moved to MissileBase.cs. +- Improve custom turret aiming. + +- Clean up invalid UTF-8 chars by searching for `[^\x00-\x7f±°ñ—α→θψφρqω₀π²·δ↔∫•∈"γgμν‽Δ↕]` (add more to exclude as necessary). Ignore localisation files. + +### TODO (smaller items and specific requests / higher priority) +- Fix bugs + - Sometimes the field toggles in ModuleWeapon (and elsewhere) throw InvalidCast exceptions on startup. Suspect a race condition. +- Finish Gauntlet tournament heats if only opponent craft are left as only relative ranking of variants is relevant. +- Resource stealing of integer amounts should consider integer amounts per container, not overall. +- Cts spawn with NPCs + +- Wiki entries + - Auto-Tuning + +- Requests from discord: + - Formation Flying https://discord.com/channels/720416076571082863/720423078533791854/1260742190418624633 + - More options for formations + - Altitude stagger + - ? 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" + - Entering evasion. + - Smart part that can trigger an action group when one of the specified parts gets below X% HP. + - Would have to work similarly to the KAL to remember which parts it should affect/monitor. + - Scope view for aiming tank turrets (similar to the targeting pod, but more direct), maybe holding a button adjusts the camera zoom based on the distance to the target? + - Team continuous spawning (could be done in multiple ways — requires UI settings to configure it): + - NvN...vN where each team replenishes craft from their pool. + - NvN...vN where a new team spawns once one team is dead. + +- Ilya_G requests: + - Omni-radars to include a radiation pattern so that they don't see well along the dipole axis. + - Switchable ammo types in-the-field for large gun types + - include a significant delay when doing so (e.g., 2-5x the reload time for reconfiguring them) + - would require manual intervention, unless some good way for the AI to decide to switch ammo is added + - Seismic charges - needs Concodroid's model. + - Difficulty settings possibilities: + - Makes it easier for human vs AI combat. + - Adds "measurement" noise to the target's position, velocity and acceleration + - AI lacking acceleration info for targeting. + - Precision reduction option in aiming guns/guiding missiles. + - Laser turrets will still be deadly accurate (increasing maxDeviation would amount to the same thing as targeting jitter). + - Add noise (fn of game time, not proper random) to targeting info. + - Multiply pos, vel, acc by 1+sin(t)/X for X=10, 100, etc. to simulate sampling noise. It doesn't need to be game time, but something related to the vessel (e.g., speed + time) + +- Improve Immelmann angle / target behind logic. +- Add tooltips to settings. +- Fix the piñata spawning logic - spawn the piñata(s) separately after circular spawning has occured. +- Add NPC and piñata support for single competitions as well (currently they're only supported in tournaments) + - Add "role" option in the VM for specifying PC, NPC, piñata, etc. +- Figure out why bullet hole decals are frequently offset behind the craft. - krakensbane or flightintegrator at time of decal attachment? +- Inertial correction to pitch, roll, yaw errors for PID calcuations. Rotate the vessel reference transform first, computing debugPos2 from the top +- Low altitude AI setting should be aware of killer GM low altitude. +- Memory for AI state so that it can resume once finished extending/evading instead of just scanning for new targets. +- 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) +- 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 + - 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 ordnance 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 +- 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/_Other Stuff/CreateCanvas.cs b/_Other Stuff/CreateCanvas.cs new file mode 100644 index 000000000..316d575f9 --- /dev/null +++ b/_Other Stuff/CreateCanvas.cs @@ -0,0 +1,406 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +public class CreateCanvas : MonoBehaviour +{ + public bool addCanvas = false; + private bool addedCanvas = false; + + private GameObject canvasObject; + private GridLayoutGroup grid; + private GameObject groupObject; + private GameObject resourcesPanel; + private List textObjects; + + private static Material textMat; + + public Sprite backgroundSprite; + public float panelSpacing = 7f; + public float cornerPadding = 12f; + + public float resourceMultiplier = 0.01f; + + //public struct Text + //{ + // public string text; + // public Color colour; + // public TextAlignmentOptions alignment; + // public int fontSize; + //} + + // Ship info. + + public string competitionText = "Namae Y-21 24-G was shot down by Nagai X-12 42-F"; + public string vesselName = "Nagai X-12 42-F"; + public float altitude = 15000; + public Vector3 velocity = new Vector3(0,1200f,0); + public string action = "is shooting at Namae Y-21 24-G"; + public Resource[] resources = new Resource[6]; + public Color teamColour = Desaturate(Color.cyan, 0.5f); + + public static Color Desaturate(Color colour, float sat) + { + Color.RGBToHSV(colour, out float h, out float s, out float v); + return Color.HSVToRGB(h, sat * s, v); + } + + public struct Resource + { + public string name; + public float amount; + public float maxAmount; + + public static Color full = Color.white; + public static Color good = Desaturate(Color.green, 0.7f); + public static Color half = Desaturate(Color.yellow, 0.7f); + public static Color low = Desaturate(Color.red, 0.7f); + public static Color empty = Color.grey; + + public float Percentage + { + get { return amount / maxAmount * 100; } + } + + public string Text + { + get { + return $"{name}: {amount:N0}/{maxAmount:N0}"; + } + } + + public string Amount + { + get + { + return $"{amount:N0}/{maxAmount:N0}"; + } + } + + public Color Colour + { + get + { + switch (Percentage) + { + case float n when (n >= 100): + return full; + case float n when (n >= 50): + return good; + case float n when (n >= 25): + return half; + case float n when (n > 0): + return low; + default: + return empty; + } + } + } + + public Resource(string name, float amount, float maxAmount) + { + this.name = name; + this.amount = amount; + this.maxAmount = maxAmount; + } + } + + + + //private Vector2 _gridSize = new Vector2(200, 20); + //public Vector2 GridSize + //{ + // get { return _gridSize; } + // set + // { + // _gridSize = value; + // grid.cellSize = _gridSize; + // } + //} + + // Start is called before the first frame update + void Start() + { + textMat = new Material(Shader.Find("TextMeshPro/Distance Field")); + textMat.EnableKeyword("UNDERLAY_ON"); + textMat.SetFloat("_UnderlaySoftness", 0.15f); + textMat.SetFloat("_UnderlayOffsetX", 1); + textMat.SetFloat("_UnderlayOffsetY", -1); + + resources[0] = new Resource("CMFlare", 252, 264); + resources[1] = new Resource("Electric Charge", 200, 200); + resources[2] = new Resource("CMChaff", 132, 162); + resources[3] = new Resource("25x137 Ammo", 500, 625); + resources[4] = new Resource("Intake Atm", 5.7f, 5.7f); + resources[5] = new Resource("Liquid Fuel", 193, 270); + } + + // Update is called once per frame + void Update() + { + if (addCanvas != addedCanvas) + { + if (addCanvas) + AddTestCanvas(Camera.main); + else + Destroy(canvasObject); + + addedCanvas = addCanvas; + } + + if (addedCanvas) + { + // Update the canvas. + //UpdateValues(); + + if (resources.Length != textObjects.Count / 2) + RefreshResources(); + + UpdateResources(); + } + + for (int i = 0; i < resources.Length; i++) + { + resources[i].amount -= Time.deltaTime * resourceMultiplier * (resources[i].maxAmount / 10); + resources[i].amount = Mathf.Clamp(resources[i].amount, 0, resources[i].maxAmount); + } + } + + //public void UpdateValues() + //{ + // groupObject.GetComponent().spacing = panelSpacing; + // groupObject.GetComponent().anchoredPosition = new Vector2(cornerPadding, -cornerPadding); + //} + + public void AddTestCanvas(Camera camera) + { + // Add a canvas to the individual camera. + canvasObject = new GameObject("Capture Tools BD Canvas", typeof(Canvas), typeof(CanvasScaler)); + + // Set the canvas to render to the camera. + var canvas = canvasObject.GetComponent(); + canvas.renderMode = RenderMode.ScreenSpaceCamera; + canvas.worldCamera = camera; + canvas.planeDistance = 1f; + canvas.transform.SetParent(camera.transform, false); + + // Scale canvas with screen size. + var scaler = canvasObject.GetComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + + // Create the vessel group. + var vesselGroup = CreateGroup("Vessel", canvasObject, Vector2.up, + new Vector2(cornerPadding, -cornerPadding), TextAnchor.UpperLeft, Vector2.up); + + // Create the pilot panel. + var pilotPanel = CreatePanel(vesselGroup); + var pilotLayout = pilotPanel.GetComponent(); + pilotLayout.childAlignment = TextAnchor.MiddleCenter; + + var vesselNameT = CreateText(pilotPanel, vesselName, teamColour, 20); + vesselNameT.overflowMode = TextOverflowModes.Ellipsis; + vesselNameT.alignment = TextAlignmentOptions.Center; + + var actionText = CreateText(pilotPanel, action, Color.white, 14); + actionText.overflowMode = TextOverflowModes.Ellipsis; + actionText.alignment = TextAlignmentOptions.Center; + + // Create the stats panel. + var statsPanel = CreatePanel(vesselGroup); + + CreateText(statsPanel, $"Speed: {velocity.magnitude:N0} m/s", Color.white, 14); + CreateText(statsPanel, $"Altitude: {altitude:N0} m", Color.white, 14); + + + // Create the group situated in the upper right. + var compGroup = CreateGroup("Competition", canvasObject, Vector2.one, + new Vector2(-cornerPadding, -cornerPadding), TextAnchor.UpperRight, Vector2.up); + + // Create the competition panel. + var compPanel = CreatePanel(compGroup); + + // Create comp text. + var compText = CreateText(compPanel, competitionText, Color.white, 16); + compText.overflowMode = TextOverflowModes.Overflow; + compText.alignment = TextAlignmentOptions.Center; + + + // Create the resources panel. + var resourcesGroup = CreateGroup("Resources", canvasObject, Vector2.zero, + new Vector2(cornerPadding, cornerPadding), TextAnchor.LowerLeft, Vector2.zero); + + var resFitter = resourcesGroup.AddComponent(); + resFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + resFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + resourcesPanel = CreatePanel(resourcesGroup); + //var resRect = resourcesPanel.GetComponent(); + //resRect.anchorMin = Vector2.zero; + //resRect.anchorMax = Vector2.zero; + //resRect.pivot = Vector2.zero; + //resRect.anchoredPosition = new Vector2(cornerPadding, cornerPadding); + + textObjects = new List(); + RefreshResources(); + } + + public void RefreshResources() + { + // Destroy all children. + //foreach (var child in textObjects) + // Destroy(child.gameObject); + + // Destroy all children of the resources panel. + foreach (Transform child in resourcesPanel.transform) + Destroy(child.gameObject); + + textObjects.Clear(); + + var horizontalGroup = CreateLayoutGroup("Horizontal", false, resourcesPanel, TextAnchor.MiddleLeft, false, 10); + var nameColumn = CreateLayoutGroup("Name", true, horizontalGroup, TextAnchor.MiddleRight, false, 10); + var amountColumn = CreateLayoutGroup("Amount", true, horizontalGroup, TextAnchor.MiddleLeft, false, 10); + + // Create the resources. + foreach (var resource in resources) + { + //var text = CreateText(resourcesPanel, resource.text, resource.colour, 14); + var text = CreateText(nameColumn, resource.name, Color.white, 14); + text.alignment = TextAlignmentOptions.Right; + text.gameObject.name = "Name"; + text.overflowMode = TextOverflowModes.Overflow; + + var amount = CreateText(amountColumn, resource.Amount, Color.white, 14); + amount.gameObject.name = resource.name; + amount.alignment = TextAlignmentOptions.Left; + amount.overflowMode = TextOverflowModes.Overflow; + + textObjects.Add(text); + textObjects.Add(amount); + } + } + + public static GameObject CreateLayoutGroup(string name, bool vertical, GameObject parent, TextAnchor alignment, bool controlChildren, float spacing = 0) + { + var go = new GameObject(name, typeof(RectTransform), vertical ? typeof(VerticalLayoutGroup) : typeof(HorizontalLayoutGroup)); + go.transform.SetParent(parent.transform, false); + + var layout = go.GetComponent(vertical ? "VerticalLayoutGroup" : "HorizontalLayoutGroup") as HorizontalOrVerticalLayoutGroup; + layout.childAlignment = alignment; + + if (!controlChildren) + { + layout.childControlHeight = false; + layout.childControlWidth = false; + layout.childForceExpandHeight = false; + layout.childForceExpandWidth = false; + } + + layout.spacing = spacing; + + var fitter = go.AddComponent(); + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + return go; + } + + void UpdateResources() + { + // Update resource text and colours. + + foreach (var child in textObjects) + { + var resource = resources.ToList().Find(r => r.name == child.gameObject.name); + + if (resource.name == null) + continue; + + child.text = resource.Amount; + //text.color = resource.colour; + } + } + + public GameObject CreateGroup(string name, GameObject parent, Vector2 anchor, Vector2 padding, TextAnchor alignment, Vector2 pivot) + { + // Create the group situated in the upper right. + var groupObject = new GameObject(name, typeof(RectTransform), typeof(VerticalLayoutGroup)); + var groupRect = groupObject.GetComponent(); + groupRect.SetParent(parent.transform, false); + + groupRect.anchorMin = anchor; + groupRect.anchorMax = anchor; + + groupRect.pivot = pivot; + groupRect.anchoredPosition = padding; + + groupRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 0); + groupRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 0); + + // Group spacing. + var group = groupObject.GetComponent(); + group.spacing = panelSpacing; + group.childAlignment = alignment; + + return groupObject; + } + + private GameObject CreatePanel(GameObject groupObject) + { + // Create a panel + var panelObject = new GameObject("Panel", typeof(Image), typeof(CanvasGroup)); + panelObject.transform.SetParent(groupObject.transform, false); + var panel = panelObject.GetComponent(); + panel.color = new Color(0, 0, 0, 0.3f); + //panel.rectTransform.pivot = new Vector2(0, 1); + + //rect_round_down_dark_transparent + //var sprite1 = Resources.GetBuiltinResource("unity_builtin_extra/Background"); + //var sprite2 = FindObjectsOfType().ToList().Find(x => x.name == "Background"); + + var image = panelObject.GetComponent(); + image.sprite = backgroundSprite; + image.type = Image.Type.Sliced; + + // Grid layout group. + //grid = panelObject.AddComponent(); + //grid.cellSize = new Vector2(340, 20); + //grid.padding = new RectOffset(20, 20, 12, 12); + + // Vertical layout group. + var vertical = panelObject.AddComponent(); + vertical.padding = new RectOffset(20, 20, 12, 12); + vertical.childControlHeight = false; + vertical.childControlWidth = false; + vertical.childForceExpandHeight = false; + vertical.childForceExpandWidth = false; + vertical.spacing = 4f; + + // Fitter. + var fitter = panelObject.AddComponent(); + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + return panelObject; + } + + private TextMeshProUGUI CreateText(GameObject panel, string text, Color colour, int size = 14) + { + var textObject = new GameObject("Text", typeof(TextMeshProUGUI)); + textObject.transform.SetParent(panel.transform, false); + + var textM = textObject.GetComponent(); + textM.fontSize = size; + textM.alignment = TextAlignmentOptions.Left; + textM.color = colour; + textM.text = text; + textM.overflowMode = TextOverflowModes.Ellipsis; + textM.material = textMat; + textM.richText = true; + + textM.rectTransform.sizeDelta = new Vector2(textM.preferredWidth, textM.preferredHeight); + return textM; + } +} 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/Score_weight_analysis_tournaments.tar.bz2 b/_Other Stuff/Score_weight_analysis_tournaments.tar.bz2 new file mode 100644 index 000000000..08af4d89d Binary files /dev/null and b/_Other Stuff/Score_weight_analysis_tournaments.tar.bz2 differ diff --git a/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterMainScreen.png b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterMainScreen.png new file mode 100644 index 000000000..8709a56be Binary files /dev/null and b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterMainScreen.png differ diff --git a/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot1.png b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot1.png new file mode 100644 index 000000000..50b91be18 Binary files /dev/null and b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot1.png differ diff --git a/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot2.png b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot2.png new file mode 100644 index 000000000..863ebe605 Binary files /dev/null and b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot2.png differ diff --git a/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot3.png b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot3.png new file mode 100644 index 000000000..83fd25f33 Binary files /dev/null and b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot3.png differ diff --git a/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot4.png b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot4.png new file mode 100644 index 000000000..0b184b11e Binary files /dev/null and b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot4.png differ diff --git a/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot5.png b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot5.png new file mode 100644 index 000000000..d06de9cdd Binary files /dev/null and b/_Other Stuff/Wiki Images/BDArmoryMissileManeuverEnvelopePlotterPlot5.png differ diff --git a/_Other Stuff/Wiki Images/KappaTerminationConditions.png b/_Other Stuff/Wiki Images/KappaTerminationConditions.png new file mode 100644 index 000000000..c572bb5b5 Binary files /dev/null and b/_Other Stuff/Wiki Images/KappaTerminationConditions.png differ diff --git a/_Other Stuff/Wiki Images/LoftGuidanceFlightpath.png b/_Other Stuff/Wiki Images/LoftGuidanceFlightpath.png new file mode 100644 index 000000000..77a7b8080 Binary files /dev/null and b/_Other Stuff/Wiki Images/LoftGuidanceFlightpath.png differ diff --git a/_Other Stuff/Wiki Images/LoftRangeFac.png b/_Other Stuff/Wiki Images/LoftRangeFac.png new file mode 100644 index 000000000..00c70af7c Binary files /dev/null and b/_Other Stuff/Wiki Images/LoftRangeFac.png differ diff --git a/_Other Stuff/dmg_analysis.txt b/_Other Stuff/dmg_analysis.txt new file mode 100644 index 000000000..836ebb291 --- /dev/null +++ b/_Other Stuff/dmg_analysis.txt @@ -0,0 +1,76 @@ +To destroy a 13400HP Pollux SRB at 100% damage multiplier. + Old: New: expl:hits +4x browning: ~270 hits in 5.5s ~500 hits in 16s 1:1 vs 1:1 +3x browning: ~270 hits in 10.9s ~500 hits in 20s 1:1 vs 1:1 +1x browning: ~120 hits in 13s ~240 hits in 30s 1:1 (old) vs 1:1 (new) + +4x vulcan: ~600 hits in 4.7s ~225 hits in 1.0s 1:6 vs 1:1 +2x vulcan: ~310 hits in 3.9s ~240 hits in 1.5s 1:3 vs 1:1 +1x vulcan: ~260 hits in 9s ~360 hits in 13s 3:5 vs 1:1 + +2x gau: ~125 hits in 1.0s ~60 hits in 0.5s 1:4 vs 1:1 +1x gau: ~90 hits in 5s ~110 hits in 5s 3:5 vs 1:1 + + +with vulcan and gau at 30% +3x browning: 490 in 19.1s +4x browning: 498 in 14s +1x gau: 145 in 9.0s (2 cool-downs) +2x vulcan: 404 in 4.8s (1 cool-down) +4x vulcan: 408 in 1.2s +2x gau: 147 in 1.2s + +at 10% damage, new dlls +3x browning: 4869 hits in 231s +4x browning: 4974 hits in 173s +1x gau: 1428 hits in 107s +2x vulcan: 3966 hits in 93s +3x vulcan: 4005 hits in 55s +2x gau: 1434 hits in 54s +4x vulcan: 3999 hits in 42s + +expected values if using 25% for 20mm and 35% for 30mm +3x browning: 231s +4x browning: 173s +2x vulcan: 112s +1x gau: 92s +3x vulcan: 66s +4x vulcan: 50s +2x gau: 46s + +at 10% damage, old dlls with original tnt amounts +3x browning: 2708 hits in 124s +4x browning: 2711 hits in 91s +2x vulcan: 3152 hits in 66s +3x vulcan: 4044 hits in 58s +4x vulcan: 4248 hits in 45s +1x gau: 559 hits in 42s +2x gau: 988 hits in 36s + +at 10% damage, old dlls with 25%, 35% +3x browning: 2710 hits in 123s +4x browning: 2689 hits in 121s +2x vulcan: 4228 hits in 131s +1x gau: 961 hits in 68s +3x vulcan: 5766 hits in 82s +4x vulcan: 5540 hits in 58s +2x gau: 1483 hits in 50s + +new DLLs, with 10% (0.00625), 35.4% (0.0900) and 1.125 EXP_DMG_MOD_BALLISTIC +3x browning: 2703 hits in 124s +4x browning: 2710 hits in 92s +2x vulcan: 3116 hits in 66s +3x vulcan: 3120 hits in 43s +4x vulcan: 3116 hits in 31s +1x gau: 731 hits in 52s +2x gau: 728 hits in 26s + +new DLLs, with 10% (0.00625), 35.4% (0.0900) and 0.65 EXP_DMG_MOD_BALLISTIC +3x browning: 4142 hits in 249s (124s) +4x browning: 4141 hits in 169s (91s) +2x vulcan: 4744 hits in 100s (66s) +3x vulcan: 4713 hits in 66s (58s) +4x vulcan: 4712 hits in 49s (45s) +1x gau: 1131 hits in 83s (42s) +2x gau: 1140 hits in 40s (36s) + 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') diff --git a/_Other Stuff/localisation_organisation_sync.py b/_Other Stuff/localisation_organisation_sync.py new file mode 100644 index 000000000..9c36feaa3 --- /dev/null +++ b/_Other Stuff/localisation_organisation_sync.py @@ -0,0 +1,79 @@ +# Sync organisation of localisation between languages (based on en-us.cfg) to arrange entries similarly and list mismatching entries at the end. +# Note: This overwrites the other localisation files. +# +# Run this from the BDArmory/BDArmory folder as: "python3 ../_Other\ Stuff/localisation_organisation_sync.py" +# Then edit the other localisation files for entries that have values of ??? (and are commented out) or unknown keys (at the end). + +from pathlib import Path + +# Get dictionaries of LOC keys with line numbers, localisations, indentation and other (comments/structure). +locPath = Path('Distribution/GameData/BDArmory/Localization/UI') +with open(locPath / "en-us.cfg", 'r') as f: + en = [l.strip('\n') for l in f.readlines()] +enkv = {line: (key.strip(), loc.strip()) for key, loc, line in ((*l.split('=', 1), i) for i, l in enumerate(l.strip() for l in en) if l.startswith("#LOC"))} +enk = {key for _, (key, _) in enkv.items()} +other = {line: text for line, text in enumerate(l.strip() for l in en) if not text.startswith("#LOC")} +indents = {line: len(text) - len(text.lstrip()) for line, text in enumerate(en)} +lang_id_line = next(i for i, v in other.items() if v.strip() == "en-us") + +for lang in ("de-de", "ja", "ru", "zh-cn"): + file = f"{lang}.cfg" + with open(locPath / file, 'r') as f: + l = [l.strip('\n') for l in f.readlines()] + kv = {key.strip(): loc.strip() for key, loc in (l.split('=', 1) for l in (l.strip() for l in l) if l.startswith("#LOC"))} + new = [] + for line in range(len(en)): + if line == lang_id_line: + new.append(f"{' ' * indents[line]}{lang}") + elif line in other: # line is either in other or enkv. + new.append(f"{' ' * indents[line]}{other[line]}") + elif enkv[line][0] in kv: + new.append(f"{' ' * indents[line]}{enkv[line][0]} = {kv[enkv[line][0]]}") + else: + new.append(f"{' ' * indents[line]}//{enkv[line][0]} = ??? {enkv[line][1]}") + unknown = [f"{' ' * 4}//{key} = {kv[key]}" for key in kv if key not in enk] + index = next((i for i, text in enumerate(l) if text == '// Unknown keys:'), -1) # Copy old unknown keys too. + if index > -1: + unknown.extend(l[index + 1:]) + if len(unknown) > 0: + new.append("// Unknown keys:") + new.extend(unknown) + + with open(locPath / file, 'w') as f: + f.writelines(l + '\n' for l in new) + +# Do the same for the weapon localisation files in Distribution/GameData/BDArmory/Localization +locPath = Path('Distribution/GameData/BDArmory/Localization') +with open(locPath / "localization-en-us.cfg", 'r') as f: + en = [l.strip('\n') for l in f.readlines()] +enkv = {line: (key.strip(), loc.strip()) for key, loc, line in ((*l.split('=', 1), i) for i, l in enumerate(l.strip() for l in en) if l.startswith("#loc"))} +enk = {key for _, (key, _) in enkv.items()} +other = {line: text for line, text in enumerate(l.strip() for l in en) if not text.startswith("#loc")} +indents = {line: len(text) - len(text.lstrip()) for line, text in enumerate(en)} +lang_id_line = next(i for i, v in other.items() if v.strip() == "en-us") + +for lang in ("de-de", "ja", "ru", "zh-cn"): + file = f"localization-{lang}.cfg" + with open(locPath / file, 'r') as f: + l = [l.strip('\n') for l in f.readlines()] + kv = {key.strip(): loc.strip() for key, loc in (l.split('=', 1) for l in (l.strip() for l in l) if l.startswith("#loc"))} + new = [] + for line in range(len(en)): + if line == lang_id_line: + new.append(f"{'\t' * indents[line]}{lang}") + elif line in other: # line is either in other or enkv. + new.append(f"{'\t' * indents[line]}{other[line]}") + elif enkv[line][0] in kv: + new.append(f"{'\t' * indents[line]}{enkv[line][0]} = {kv[enkv[line][0]]}") + else: + new.append(f"{'\t' * indents[line]}//{enkv[line][0]} = ??? {enkv[line][1]}") + unknown = [f"{'\t' * 4}//{key} = {kv[key]}" for key in kv if key not in enk] + index = next((i for i, text in enumerate(l) if text == '// Unknown keys:'), -1) # Copy old unknown keys too. + if index > -1: + unknown.extend(l[index + 1:]) + if len(unknown) > 0: + new.append("// Unknown keys:") + new.extend(unknown) + + with open(locPath / file, 'w') as f: + f.writelines(l + '\n' for l in new) \ No newline at end of file diff --git a/_Other Stuff/optimise_vessels_per_heat.py b/_Other Stuff/optimise_vessels_per_heat.py new file mode 100644 index 000000000..3c757b988 --- /dev/null +++ b/_Other Stuff/optimise_vessels_per_heat.py @@ -0,0 +1,29 @@ +import argparse +import math +from typing import Tuple + + +def OptimiseVesselsPerHeat(count: int, limits: Tuple[int, int] = (6, 10)) -> Tuple[int, int]: + options = reversed(list(range(limits[1]//2, limits[1] + 1))) if count > limits[1] and count < 2*limits[0]-1 else reversed(list(range(limits[0], limits[1] + 1))) + for val in options: + if count % val == 0: + return val, 0 + result = OptimiseVesselsPerHeat(count + 1, limits) + return result[0], result[1] + 1 + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Optimise the number of vessels per heat.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("count", type=int, help="The number of craft in the tournament.") + parser.add_argument("-l", "--limits", type=int, nargs=2, default=(6, 10), help="Limits on the number of vessels.") + args = parser.parse_args() + + result = OptimiseVesselsPerHeat(args.count, args.limits) + full_heats = max(int(math.ceil(args.count/result[0]) - result[1]), 0) + if full_heats > 0: + string = f"Optimal is {full_heats} heats with {result[0]} vessels" + if result[1] > 0: + string += f" and {result[1]} heat{'s' if result[1]>1 else ''} with {result[0]-1} vessels" + else: + string = f"Optimal is a single heat with {args.count} vessels" + print(string) diff --git a/_Other Stuff/score_weight_analysis.txt b/_Other Stuff/score_weight_analysis.txt new file mode 100644 index 000000000..323954fbe --- /dev/null +++ b/_Other Stuff/score_weight_analysis.txt @@ -0,0 +1,35 @@ +Kerbal Space Program/GameData/BDArmory/Logs$ ../parse_tournament_log_files_v1.13.7.py -q * && python3 ../score_weights.py * +{'deaths': -0.78, 'kills': 2.97, 'assists': 0.74, 'hit': 0.06, 'dmg': 2.96, 'dmgIn': 0.65, 'Mdmg': 0.02, 'MdmgIn': 0.01, 'ram': 0.06} Tournament 41-rapid-fire-tank-turrents +{'deaths': -0.77, 'kills': 1.49, 'assists': 1.8, 'hit': 0.51, 'dmg': 2.2, 'dmgIn': 0.45, 'Rstr': 0.04, 'Rhit': 0.04, 'Rdmg': 0.13, 'RdmgIn': 0.01, 'ram': 0.46} Tournament 42-large-planes +{'deaths': -0.7, 'kills': 1.77, 'assists': 1.21, 'hit': 0.16, 'dmg': 0.94, 'dmgIn': 0.25, 'ram': 0.33} Tournament 43-rocket-engines +{'deaths': -0.78, 'kills': 2.74, 'assists': 0.89, 'hit': 0.47, 'dmg': 1.33, 'dmgIn': 0.31, 'ram': 0.65} Tournament 47-replicas +{'deaths': -0.81, 'kills': 2.79, 'assists': 1.02, 'hit': 0.5, 'dmg': 1.54, 'dmgIn': 0.33, 'ram': 0.62} Tournament 47-replicas-Doc-testing +{'deaths': -0.79, 'kills': 3.26, 'assists': 0.76, 'hit': 0.44, 'dmg': 1.13, 'dmgIn': 0.21, 'ram': 0.55} Tournament 47-replicas-EzBro-testing +{'deaths': -0.65, 'kills': 1.27, 'assists': 0.75, 'Rstr': 0.4, 'Rhit': 0.22, 'Rdmg': 1.27, 'RdmgIn': 0.34, 'Mstr': 0.04, 'Mhit': 0.02, 'Mdmg': 0.12, 'MdmgIn': 0.07, 'ram': 0.01} Tournament 66594635-rockets-4-planes +{'deaths': -0.77, 'kills': 2.55, 'assists': 0.62, 'hit': 0.12, 'dmg': 0.29, 'dmgIn': 0.05, 'Rdmg': 0.01, 'Mstr': 0.22, 'Mhit': 0.14, 'Mdmg': 0.82, 'MdmgIn': 0.3, 'ram': 0.3} Tournament 66606544-missiles++ +{'deaths': -0.73, 'kills': 2.53, 'assists': 0.79, 'hit': 0.13, 'dmg': 0.32, 'dmgIn': 0.07, 'Rdmg': 0.01, 'Mstr': 0.28, 'Mhit': 0.17, 'Mdmg': 0.94, 'MdmgIn': 0.2, 'ram': 0.21} Tournament 66611739-missiles++ +{'deaths': -0.72, 'kills': 1.83, 'assists': 0.76, 'Mstr': 0.3, 'Mhit': 0.18, 'Mdmg': 1.03, 'MdmgIn': 0.26, 'ram': 0.88} Tournament 66642576-missiles +{'deaths': -0.8, 'kills': 1.09, 'assists': 1.98, 'hit': 0.17, 'dmg': 0.29, 'dmgIn': 0.03, 'ram': 0.21} Tournament 66651588-micro-planes +{'deaths': -0.9, 'kills': 0.71, 'assists': 0.66, 'ram': 2.66} Tournament 66667819-ramming +{'deaths': -0.62, 'kills': 2.88, 'assists': 0.72, 'hit': 0.34, 'dmg': 0.71, 'dmgIn': 0.09, 'Mstr': 0.03, 'Mhit': 0.01, 'Mdmg': 0.16, 'MdmgIn': 0.04, 'ram': 0.11} Tournament 66681234-mix-5-planes +{'deaths': -0.72, 'kills': 2.38, 'assists': 0.64, 'hit': 0.27, 'dmg': 0.54, 'dmgIn': 0.09, 'RdmgIn': 0.01, 'Mstr': 0.05, 'Mhit': 0.03, 'Mdmg': 0.23, 'MdmgIn': 0.07, 'ram': 0.2} Tournament 66687119-mix-12-planes +{'deaths': -0.73, 'kills': 1.32, 'assists': 1.09, 'Mstr': 0.3, 'Mhit': 0.2, 'Mdmg': 1.05, 'MdmgIn': 0.27, 'ram': 0.7} Tournament 66698855-missiles +{'deaths': -0.69, 'kills': 1.12, 'assists': 1.01, 'Rstr': 0.42, 'Rhit': 0.23, 'Rdmg': 1.3, 'RdmgIn': 0.35, 'Mstr': 0.03, 'Mhit': 0.01, 'Mdmg': 0.08, 'MdmgIn': 0.03, 'ram': 0.23} Tournament 66733324-rockets-8-planes + +Average kda / hd: 1.114 +2.93 / 3.76 = 0.7793 : Tournament 41-rapid-fire-tank-turrents +2.52 / 3.84 = 0.6562 : Tournament 42-large-planes +2.28 / 1.68 = 1.357 : Tournament 43-rocket-engines +2.85 / 2.76 = 1.033 : Tournament 47-replicas + 3 / 2.99 = 1.003 : Tournament 47-replicas-Doc-testing +3.23 / 2.33 = 1.386 : Tournament 47-replicas-EzBro-testing +1.37 / 2.49 = 0.5502 : Tournament 66594635-rockets-4-planes + 2.4 / 2.25 = 1.067 : Tournament 66606544-missiles++ +2.59 / 2.33 = 1.112 : Tournament 66611739-missiles++ +1.87 / 2.65 = 0.7057 : Tournament 66642576-missiles +2.27 / 0.7 = 3.243 : Tournament 66651588-micro-planes +0.47 / 2.66 = 0.1767 : Tournament 66667819-ramming +2.98 / 1.49 = 2 : Tournament 66681234-mix-5-planes (smallish) + 2.3 / 1.49 = 1.544 : Tournament 66687119-mix-12-planes (smallish) +1.68 / 2.52 = 0.6667 : Tournament 66698855-missiles +1.44 / 2.68 = 0.5373 : Tournament 66733324-rockets-8-planes diff --git a/_Other Stuff/score_weights.py b/_Other Stuff/score_weights.py new file mode 100644 index 000000000..7c494f0ca --- /dev/null +++ b/_Other Stuff/score_weights.py @@ -0,0 +1,62 @@ +# Script for examining the balance between the scoring weights of tournaments. +# +# Usage: +# Run a bunch of tournaments emphasising different weapon configurations, then in the BDArmory/Logs folder run: +# ../parse_tournament_log_files_v1.13.7.py -q * && python3 ../weights.py * +# using the same weights here as in the parser. +# +# Proposed weights give: +# dmg ~= 4*dmgIn ~= 3*hits for medium guns ~= 2*hits for small guns ~= 4*hits for large guns +# dmg ~= 4*dmgIn ~= 2*(str+hits) for rockets and missiles +# k+a-d = α * (dmg+hits+rams), where α ~= 1 for normal planes, α ~= 0.7 for tanky planes, α ~= 2 for small planes and α ~= 3 for micro planes. +# +# Tournaments run to get the proposed weights: +# S4R1, S4R2, S4R3, S4R7 (x3), tmp/tmp10 (missiles) (x2 with other weapons, x2 without), tmp/tmp6 (rockets/missiles) (x2), tmp/tmp9 (micro), tmp/tmp12 (ramming), tmp/tmp (mixed) (x2) +# Total: 352 rounds, 800 heats. + +import argparse +import json +import sys +from pathlib import Path + +parser = argparse.ArgumentParser() +parser.add_argument('filenames', nargs='*', type=str, help="Tournament folders containing summary.csv files.") +parser.add_argument('-f', '--full', action='store_true', help="Use the full complement of craft instead of just the top half.") +parser.add_argument('-sw', '--show-weights', action='store_true', help="Show the weights, then quit.") +args = parser.parse_args() + +if len(args.filenames) == 0: + print(f"Please provide some tournament folders, e.g., Tournament*") + exit(0) + +with open(Path(args.filenames[0]) / 'summary.json', 'r') as f: + meta = json.load(f) +weights = list(meta['meta']['score weights'].values()) # Read the weights used to parse the first tournament and assume that it's the same for all. +wIndex = {0: 0, 3: 3, 12: 6, 17: 7, 18: 8, 20: 10, 21: 11, 22: 12, 24: 14, 26: 16, 27: 17, 28: 18, 30: 20, 32: 22, 33: 23, 34: 24} # Convert between summary.csv indexing and score parser weight indexing. +entries = {'wins': 0, 'deaths': 3, 'kills': 12, 'assists': 17, 'hit': 18, 'dmg': 20, 'dmgIn': 21, 'Rstr': 22, 'Rhit': 24, 'Rdmg': 26, 'RdmgIn': 27, 'Mstr': 28, 'Mhit': 30, 'Mdmg': 32, 'MdmgIn': 33, 'ram': 34} + +if args.show_weights: + for k, v in entries.items(): + print(k, weights[wIndex[v]]) + sys.exit() +proposed = {} +for filename in args.filenames: + with open(Path(filename) / 'summary.csv', 'r') as f: + data = f.readlines() + craft_count = data.index('\n') - 1 + scores = [[float(v) for v in row.strip().split(',')[2:-8]] for row in data[1:craft_count + 1]] + rnds = len([l for l in data if "Per Round" in l][0].strip().split(',')) - 1 + prop = {k: [] for k in entries} + # Only take the top half (unless specified not to), assuming that the lower half has poor statistics due to low counts in the various fields. Including fewer increases the ratios and vice-versa. + for score in scores[:len(scores) // (1 if args.full else 2)]: + for k, v in entries.items(): + prop[k].append(score[v] * weights[wIndex[v]] / rnds) + proposed[filename] = {k: round(sum(v) / len(v), 2) for k, v in prop.items()} +for filename in proposed: + print({k: v for k, v in proposed[filename].items() if v != 0}, filename) +for filename in proposed: + proposed[filename]['wkda'] = round(sum(proposed[filename][k] for k in ('wins', 'kills', 'deaths', 'assists')), 3) + proposed[filename]['hd'] = round(sum(proposed[filename][k] for k in ('hit', 'dmg', 'dmgIn', 'Rstr', 'Rhit', 'Rdmg', 'RdmgIn', 'Mstr', 'Mhit', 'Mdmg', 'MdmgIn', 'ram')), 3) + proposed[filename]['wkda/hd'] = proposed[filename]['wkda'] / proposed[filename]['hd'] +print(f"\nAverage wkda / hd: {sum(proposed[filename]['wkda/hd'] for filename in proposed) / len(proposed):.3f}") +print("\n".join(f"{v['wkda']:4g} / {v['hd']:4g} = {v['wkda/hd']:6.4g} : {k}" for k, v in proposed.items()))