using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace Aerostats { public class ModuleBalloonCase : PartModule { public enum BalloonState { Packed, Deployed, Detached } /// /// Maximum rate at which gas can be injected in the balloon to fill it, in m^3/s at stp /// [KSPField(isPersistant = false, guiActive = false)] public float MaxGasFillRate = 100; /// /// Maximum rate at which gas can be removed from the balloon to deflate it, in m^3/s at stp /// [KSPField(isPersistant = false, guiActive = false)] public float MaxGasVentRate = 500; /// /// Weight, in kilograms, of 1m^3 of gas at 100kPa and 0°C /// Helium is 179g/m^3 /// Hydrogen is 90g/m^3 /// [KSPField(isPersistant = false, guiActive = false)] public float GasDensity = 0.179f; /// /// Quantity of gas that the system is trying to get inside the balloon, in m^3 at 100kPa and 0°C. If the balloon contains more gas, some will be vented, otherwise, gas will be injected. /// If the balloon can not store the target amount of gas (because the maximum volume has been reached), the system won't try to inject more gas, to avoid venting through the security valve of the balloon. /// [KSPField(isPersistant = true, guiActive = true)] [UI_FloatRange(minValue = 0, maxValue = 20000.0f, stepIncrement = 100.0f)] public float LiftingGasTargetQuantity = 100.0f; /// /// Minimum amount of gas that will be used to inflate the balloon at the beginning. The balloon can not be deflated below this amount. /// [KSPField(isPersistant = false, guiActive = false)] public float MinimumFillQuantity = 10.0f; /// /// Length of spring at rest (no force applied under this length) /// [KSPField(isPersistant = false, guiActive = false)] public float SpringRestLength = 10.0f; /// /// Length at which the spring breaks /// [KSPField(isPersistant = false, guiActive = false)] public float SpringBreakLength = 50.0f; /// /// Force applied by the spring between the balloon and the vessel, proportional to spring extension, in newton per meter /// [KSPField(isPersistant = false, guiActive = false)] public float SpringHardness = 5000.0f; [KSPField(isPersistant = false, guiActive = true)] public string Status; [KSPField(isPersistant = true, guiActive = false)] public BalloonState State; /// /// Gas quantity currently inside the balloon /// [KSPField(guiName = "Gas quantity", isPersistant = true, guiActive = true)] public float LiftingGasQuantity; [KSPField(guiName = "Volume", isPersistant = false, guiActive = true)] public string VolumeStatus; private ModuleBalloon Balloon; private LineRenderer Spring; [KSPEvent(guiActive = true, active = true, externalToEVAOnly = false, guiActiveUnfocused = true, guiName = "Deploy balloon", unfocusedRange = 5)] public void Deploy() { if (State != BalloonState.Packed) return; State = BalloonState.Deployed; UnityEngine.Debug.Log("Aerostats: deploying"); Util.PostDebugScreenMessage("deploying"); AvailablePart avPart = PartLoader.Instance.parts.Single(p => p.name == "heliumBalloon"); var balloonPart = (Part)UnityEngine.Object.Instantiate(avPart.partPrefab); var balloonObject = balloonPart.gameObject; balloonObject.transform.position = part.Rigidbody.position + part.Rigidbody.transform.up * 1.0f; balloonObject.transform.rotation = part.Rigidbody.rotation; balloonPart.SetHierarchyRoot(balloonPart); balloonPart.vessel = vessel; balloonPart.setParent(part); Balloon = balloonObject.GetComponent(); Balloon.part = balloonPart; balloonObject.SetActive(true); GameEvents.onVesselWasModified.Fire(vessel); Balloon.part.decouple(); // after that, the balloon becomes an independent vessel Balloon.part.vessel.vesselName = vessel.vesselName + " balloon"; float initialGasQuantity = Mathf.Min(0.1f, part.RequestResource("Helium", MinimumFillQuantity)); Balloon.Initialize(this, (Vector3d)part.rigidbody.velocity + Krakensbane.GetFrameVelocity(), GasDensity, initialGasQuantity); LiftingGasQuantity = Balloon.LiftingGasQuantity; Spring = part.gameObject.AddComponent(); Spring.useWorldSpace = true; Spring.material = new Material(Shader.Find("VertexLit")); Spring.SetColors(Color.black, Color.black); Spring.SetWidth(0.2f, 0.2f); Spring.SetVertexCount(2); part.OnJustAboutToBeDestroyed += OnPartDestroyed; } [KSPEvent(guiActive = true, active = true, externalToEVAOnly = false, guiActiveUnfocused = true, guiName = "Cut rope", unfocusedRange = 5)] public void Cut() { if (State != BalloonState.Deployed) return; if(Balloon != null) Balloon.OnDetached(); Balloon = null; if(Spring != null) Destroy(Spring); Spring = null; part.OnJustAboutToBeDestroyed -= OnPartDestroyed; State = BalloonState.Detached; } public override void OnStart(PartModule.StartState state) { if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) { return; } Util.PostDebugScreenMessage("Aerostat loaded"); part.stagingIcon = "PARACHUTES"; } private void OnPartDestroyed() { Cut(); } private void FixedUpdate() { if (State == BalloonState.Deployed && Balloon == null) Cut(); if (State == BalloonState.Detached) { Status = "separated"; VolumeStatus = "-"; return; } if (GameSettings.LAUNCH_STAGES.GetKeyDown() && vessel.isActiveVessel && (part.inverseStage == Staging.CurrentStage - 1 || Staging.CurrentStage == 0)) { Deploy(); } if (State == BalloonState.Deployed) { float currentMaxQuantity = Balloon.GetCurrentMaxQuantity(); LiftingGasTargetQuantity = Math.Max(MinimumFillQuantity, LiftingGasTargetQuantity); if(LiftingGasTargetQuantity > Balloon.LiftingGasQuantity) { // inflating balloon float stepFinalQuantity = Math.Min(Balloon.LiftingGasQuantity + MaxGasFillRate * Time.fixedDeltaTime, Math.Min(LiftingGasTargetQuantity, currentMaxQuantity)); float step = Math.Max(stepFinalQuantity - Balloon.LiftingGasQuantity, 0.0f); float stepResource = part.RequestResource("Helium", step); LiftingGasQuantity = Balloon.InjectGas(stepResource); if(step > 0.0f) { Status = stepResource > 0.0f ? "inflating" : "out-of-gas"; } else { Status = "nominal"; } } else { // deflating balloon Status = LiftingGasTargetQuantity == Balloon.LiftingGasQuantity ? "nominal" : "deflating"; float quantityAfterVenting = Math.Max(Balloon.LiftingGasQuantity - MaxGasVentRate * Time.fixedDeltaTime, LiftingGasTargetQuantity); LiftingGasQuantity = Balloon.InjectGas(quantityAfterVenting - Balloon.LiftingGasQuantity); } if(Balloon.IsVenting) { Status = "full (venting)"; } VolumeStatus = Mathf.Round(Balloon.CurrentVolume) + " / " + Mathf.Round(Balloon.MaxVolume); // spring between balloon and base float restLength = SpringRestLength + Balloon.Radius; var springVec = Balloon.rigidbody.position - part.rigidbody.position; float springLength = springVec.magnitude; float springForceMag = 0; var balloonAttachPoint = Balloon.rigidbody.position - Balloon.transform.up.normalized * Balloon.Radius; if(springLength > SpringBreakLength) { Cut(); FlightLogger.eventLog.Add("The balloon cable broke due to extreme extension"); } else if(springLength > restLength) { float tensingLength = springLength - restLength; springForceMag = tensingLength * SpringHardness; var springForce = springVec * (springForceMag / springLength * 0.001f); part.rigidbody.AddForce(springForce, ForceMode.Force); Balloon.rigidbody.AddForceAtPosition(-springForce, balloonAttachPoint, ForceMode.Force); } Util.PostDebugSingleScreenMessage("spring force", "Spring force = " + springForceMag + "N"); } else { Status = "packed"; VolumeStatus = "-"; } } private void LateUpdate() { if (State == BalloonState.Deployed && Balloon == null) Cut(); if (State != BalloonState.Deployed) return; var balloonAttachPoint = Balloon.rigidbody.position - Balloon.transform.up.normalized * Balloon.Radius; Spring.SetPosition(0, part.transform.position); Spring.SetPosition(1, balloonAttachPoint); } } }