Exemple #1
0
  static double CurvedPanelOutput(Vessel vessel, ProtoPartSnapshot part, Part prefab, Vector3d sun_dir, double sun_dist, double atmo_factor)
  {
    // if, for whatever reason, sun_dist is zero (or negative), we do not return any output
    if (sun_dist <= double.Epsilon) return 0.0;

    // shortcuts
    Quaternion rot = part.rotation;

    // get values from part
    string transform_name = part.partData.GetValue("PanelTransformName");
    float k = Convert.ToSingle(part.partData.GetValue("chargePerTransform"));

    // get components
    Transform[] components = prefab.FindModelTransforms(transform_name);
    if (components.Length == 0) return 0.0;

    // calculate solar flux
    double solar_flux = Sim.SolarFlux(sun_dist);

    // reduce solar flux inside atmosphere
    solar_flux *= atmo_factor;

    // normalize against solar flux at home
    solar_flux /= Sim.SolarFluxAtHome();
    solar_flux *= k;

    // for each one of the components the curved panel is composed of
    double output = 0.0;
    foreach(Transform t in prefab.FindModelTransforms(transform_name))
    {
      double cosine_factor = Math.Max(Vector3d.Dot(sun_dir, (vessel.transform.rotation * rot * t.forward).normalized), 0.0);
      output += cosine_factor * solar_flux;
    }
    return output;
  }
Exemple #2
0
        static void ProcessCurvedPanel(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, PartModule curved_panel, Part part_prefab, vessel_info info, resource_info ec, double elapsed_s)
        {
            // note: we assume deployed, this is a current limitation

            // if in sunlight
            if (info.sunlight > double.Epsilon)
            {
                // get values from module
                string transform_name = Lib.ReflectionValue <string>(curved_panel, "PanelTransformName");
                float  tot_rate       = Lib.ReflectionValue <float>(curved_panel, "TotalEnergyRate");

                // get components
                Transform[] components = part_prefab.FindModelTransforms(transform_name);
                if (components.Length == 0)
                {
                    return;
                }

                // calculate normalized solar flux
                // note: this include fractional sunlight if integrated over orbit
                // note: this include atmospheric absorption if inside an atmosphere
                double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome();

                // calculate rate per component
                double rate = (double)tot_rate / (double)components.Length;

                // calculate world-space part rotation quaternion
                // note: a possible optimization here is to cache the transform lookup (unity was coded by monkeys)
                Quaternion rot = v.transform.rotation * p.rotation;

                // calculate output of all components
                double output = 0.0;
                foreach (Transform t in components)
                {
                    output += rate                                                                       // nominal rate per-component at 1 AU
                              * norm_solar_flux                                                          // normalized solar flux at panel distance from sun
                              * Math.Max(Vector3d.Dot(info.sun_dir, (rot * t.forward).normalized), 0.0); // cosine factor of component orientation
                }

                // produce EC
                ec.Produce(output * elapsed_s);
            }
        }
Exemple #3
0
        static void ProcessPanel(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, ModuleDeployableSolarPanel panel, Vessel_info info, Resource_info ec, double elapsed_s)
        {
            // note: we ignore temperature curve, and make sure it is not relevant in the MM patch
            // note: cylindrical and spherical panels are not supported
            // note: we assume the tracking target is SUN
            // if in sunlight and extended
            if (info.sunlight > double.Epsilon && m.moduleValues.GetValue("deployState") == "EXTENDED")
            {
                // get panel normal/pivot direction in world space
                Transform tr  = panel.part.FindModelComponent <Transform>(panel.pivotName);
                Vector3d  dir = panel.isTracking ? tr.up : tr.forward;
                dir = (v.transform.rotation * p.rotation * dir).normalized;

                float age          = (float)(v.missionTime / (Lib.HoursInDay() * 3600));
                float effic_factor = panel.timeEfficCurve != null?panel.timeEfficCurve.Evaluate(age) : 1.0f;

                // calculate cosine factor
                // - fixed panel: clamped cosine
                // - tracking panel, tracking pivot enabled: around the pivot
                // - tracking panel, tracking pivot disabled: assume perfect alignment
                double cosine_factor =
                    !panel.isTracking
                                  ? Math.Max(Vector3d.Dot(info.sun_dir, dir), 0.0)
                                  : Settings.TrackingPivot
                                  ? Math.Cos(1.57079632679 - Math.Acos(Vector3d.Dot(info.sun_dir, dir)))
                                  : 1.0;

                // calculate normalized solar flux
                // - this include fractional sunlight if integrated over orbit
                // - this include atmospheric absorption if inside an atmosphere
                double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome();

                // calculate output
                double output = panel.resHandler.outputResources[0].rate                              // nominal panel charge rate at 1 AU
                                * norm_solar_flux                                                     // normalized flux at panel distance from sun
                                * cosine_factor                                                       // cosine factor of panel orientation
                                * effic_factor;

                // produce EC
                ec.Produce(output * elapsed_s, "panel");
            }
        }
Exemple #4
0
        static void ProcessPanel(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, ModuleDeployableSolarPanel panel, vessel_info info, resource_info ec, double elapsed_s)
        {
            // note: we ignore temperature curve, and make sure it is not relavant in the MM patch
            // note: we ignore power curve, that is used by no panel as far as I know

            // play nice with BackgroundProcessing
            if (Kerbalism.detected_mods.BackgroundProcessing)
            {
                return;
            }

            // if in sunlight and extended
            if (info.sunlight > double.Epsilon && m.moduleValues.GetValue("stateString") == "EXTENDED")
            {
                // get panel normal direction
                Vector3d normal = panel.part.FindModelComponent <Transform>(panel.raycastTransformName).forward;

                // calculate cosine factor
                // note: for gameplay reasons, we ignore tracking panel pivots
                // note: a possible optimization here is to cache the transform lookup (unity was coded by monkeys)
                double cosine_factor = panel.sunTracking ? 1.0 : Math.Max(Vector3d.Dot(info.sun_dir, (v.transform.rotation * p.rotation * normal).normalized), 0.0);

                // calculate normalized solar flux
                // note: this include fractional sunlight if integrated over orbit
                // note: this include atmospheric absorption if inside an atmosphere
                double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome();

                // calculate output
                double output = panel.chargeRate                            // nominal panel charge rate at 1 AU
                                * norm_solar_flux                           // normalized flux at panel distance from sun
                                * cosine_factor                             // cosine factor of panel orientation
                                * Reliability.Penalty(p, "Panel");          // malfunctioned panel penalty

                // produce EC
                ec.Produce(output * elapsed_s);
            }
        }
Exemple #5
0
 // return normalized natural lighting at specified distance from the sun (1 in the home world)
 public static double NaturalLighting(double sun_dist)
 {
   // return natural lighting
   // note: should be 1 at kerbin
   return Sim.SolarFlux(sun_dist) / Sim.SolarFluxAtHome();
 }
        public void FixedUpdate()
        {
            // do nothing in editor
            if (Lib.IsEditor())
            {
                return;
            }

            // do nothing if there isn't a solar panel
            if (panel == null)
            {
                return;
            }

            // get resource handler
            resource_info ec = ResourceCache.Info(vessel, "ElectricCharge");

            // get vessel data from cache
            vessel_info info = Cache.VesselInfo(vessel);

            // do nothing if vessel is invalid
            if (!info.is_valid)
            {
                return;
            }

            // detect if sunlight is evaluated analytically
            bool analytical_sunlight = info.sunlight > 0.0 && info.sunlight < 1.0;

            // detect occlusion from other vessel parts
            // - we are only interested when the sunlight evaluation is discrete
            var  collider         = panel.hit.collider;
            bool locally_occluded = !analytical_sunlight && collider != null && info.sunlight > 0.0;

            // if panel is enabled and extended, and if sun is not occluded, not even locally
            if (panel.isEnabled && panel.deployState == ModuleDeployablePart.DeployState.EXTENDED && info.sunlight > 0.0 && !locally_occluded)
            {
                // calculate cosine factor
                // - the stock module is already computing the tracking direction
                double cosine_factor = Math.Max(Vector3d.Dot(info.sun_dir, panel.trackingDotTransform.forward), 0.0);

                // calculate normalized solar flux
                // - this include fractional sunlight if integrated over orbit
                // - this include atmospheric absorption if inside an atmosphere
                double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome();

                // calculate output
                double output = rate                    // nominal panel charge rate at 1 AU
                                * norm_solar_flux       // normalized flux at panel distance from sun
                                * cosine_factor;        // cosine factor of panel orientation

                // produce EC
                ec.Produce(output * Kerbalism.elapsed_s);

                // update ui
                field_visibility = info.sunlight * 100.0;
                field_atmosphere = info.atmo_factor * 100.0;
                field_exposure   = cosine_factor * 100.0;
                field_output     = output;
                Fields["field_visibility"].guiActive = analytical_sunlight;
                Fields["field_atmosphere"].guiActive = info.atmo_factor < 1.0;
                Fields["field_exposure"].guiActive   = true;
                Fields["field_output"].guiActive     = true;
            }
            // if panel is disabled, retracted, or in shadow
            else
            {
                // hide ui
                Fields["field_visibility"].guiActive = false;
                Fields["field_atmosphere"].guiActive = false;
                Fields["field_exposure"].guiActive   = false;
                Fields["field_output"].guiActive     = false;
            }

            // update status ui
            field_status = analytical_sunlight
    ? "<color=#ffff22>Integrated over the orbit</color>"
    : locally_occluded
    ? "<color=#ff2222>Occluded by vessel</color>"
    : info.sunlight < 1.0
    ? "<color=#ff2222>Occluded by celestial body</color>"
    : string.Empty;
            Fields["field_status"].guiActive = field_status.Length > 0;
        }
Exemple #7
0
  // implement greenhouse mechanics for unloaded vessels
  public static void BackgroundUpdate(Vessel vessel, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, Greenhouse greenhouse, vessel_info info, vessel_resources resources, double elapsed_s)
  {
    // get protomodule data
    bool door_opened = Lib.Proto.GetBool(m, "door_opened");
    double growth = Lib.Proto.GetDouble(m, "growth");
    float lamps = Lib.Proto.GetFloat(m, "lamps");
    double lighting = Lib.Proto.GetDouble(m, "lighting");

    // if lamp is on
    if (lamps > float.Epsilon)
    {
      // get resource handler
      resource_info ec = resources.Info(vessel, "ElectricCharge");

      // consume ec
      ec.Consume(greenhouse.ec_rate * lamps * elapsed_s * Reliability.Penalty(p, "Greenhouse", 2.0));

      // shut down the light if there isn't enough ec
      // note: comparing against amount at previous simulation step
      if (ec.amount <= double.Epsilon) lamps = 0.0f;
    }

    // determine lighting conditions
    // note: we ignore sun direction for gameplay reasons: else the user must reorient the greenhouse as the planets dance over time
    // - natural light depend on: distance from sun, direct sunlight, door status
    // - artificial light depend on: lamps tweakable and ec available, door status
    lighting = info.solar_flux / Sim.SolarFluxAtHome() * (door_opened ? 1.0 : 0.0) + lamps;

    // if can use waste, and there is some lighting
    double waste_perc = 0.0;
    if (greenhouse.waste_name.Length > 0 && lighting > double.Epsilon)
    {
      // get resource handler
      resource_info waste = resources.Info(vessel, greenhouse.waste_name);

      // consume waste
      waste.Consume(greenhouse.waste_rate * elapsed_s);

      // determine waste bonus
      // note: comparing against amount from previous simulation step
      waste_perc = Math.Min(waste.amount / greenhouse.waste_rate, 1.0);
    }

    // determine growth bonus
    double growth_bonus = greenhouse.soil_bonus * (info.landed ? 1.0 : 0.0) + greenhouse.waste_bonus * waste_perc;

    // grow the crop
    double growing = greenhouse.growth_rate * (1.0 + growth_bonus) * lighting;
    growth += elapsed_s * growing;

    // if it is harvest time
    if (growth >= 1.0)
    {
      // reset growth
      growth = 0.0;

      // produce food
      resources.Produce(vessel, greenhouse.resource_name, greenhouse.harvest_size);

      // show a message to the user
      Message.Post(Lib.BuildString("On <color=FFFFFF>", vessel.vesselName, "</color> the crop harvest produced <color=FFFFFF>",
        greenhouse.harvest_size.ToString("F0"), " ", greenhouse.resource_name, "</color>"));

      // record first space harvest
      if (!info.landed && DB.Ready()) DB.Landmarks().space_harvest = 1;
    }

    // store data
    Lib.Proto.Set(m, "growth", growth);
    Lib.Proto.Set(m, "lamps", lamps);
    Lib.Proto.Set(m, "lighting", lighting);
    Lib.Proto.Set(m, "growth_diff", growing);
  }
Exemple #8
0
  // implement greenhouse mechanics
  public void FixedUpdate()
  {
    // set emissive intensity from lamp tweakable
    if (emissive_object.Length > 0)
    {
      foreach(var rdr in part.GetComponentsInChildren<UnityEngine.Renderer>())
      {
        if (rdr.name == emissive_object) { rdr.material.SetColor("_EmissiveColor", new Color(lamps, lamps, lamps, 1.0f)); break; }
      }
    }

    // do nothing else in the editor
    if (HighLogic.LoadedSceneIsEditor) return;

    // get vessel info from the cache
    vessel_info vi = Cache.VesselInfo(vessel);

    // do nothing if vessel is invalid
    if (!vi.is_valid) return;

    // get resource cache
    vessel_resources resources = ResourceCache.Get(vessel);

    // get elapsed time
    double elapsed_s = Kerbalism.elapsed_s * vi.time_dilation;

    // when the greenhouse is assembled using KIS, the growth field is set to NaN
    // at that point it remain NaN forever, so we fix it
    // also, a report indicated -infinity in growth
    if (double.IsNaN(growth) || double.IsInfinity(growth)) growth = 0.0;

    // if lamp is on
    if (lamps > float.Epsilon)
    {
      // get resource handler
      resource_info ec = resources.Info(vessel, "ElectricCharge");

      // consume ec
      ec.Consume(ec_rate * lamps * elapsed_s);

      // shut down the light if there isn't enough ec
      // note: comparing against amount at previous simulation step
      if (ec.amount <= double.Epsilon) lamps = 0.0f;
    }

    // determine lighting conditions
    // note: we ignore sun direction for gameplay reasons: else the user must reorient the greenhouse as the planets dance over time
    lighting = vi.solar_flux / Sim.SolarFluxAtHome() * (door_opened ? 1.0 : 0.0) + lamps;

    // if can use waste, and there is some lighting
    double waste_perc = 0.0;
    if (waste_name.Length > 0 && waste_rate > double.Epsilon && lighting > double.Epsilon)
    {
      // get resource handler
      resource_info waste = resources.Info(vessel, waste_name);

      // consume waste
      waste.Consume(waste_rate * elapsed_s);

      // determine waste bonus
      // note: comparing against amount from previous simulation step
      waste_perc = Math.Min(waste.amount / waste_rate, 1.0);
    }

    // determine growth bonus
    double growth_bonus = soil_bonus * (vi.landed ? 1.0 : 0.0) + waste_bonus * waste_perc;

    // grow the crop
    growing = growth_rate * (1.0 + growth_bonus) * lighting;
    growth += elapsed_s * growing;

    // if it is harvest time
    if (growth >= 1.0)
    {
      // reset growth
      growth = 0.0;

      // produce food
      resources.Produce(vessel, resource_name, harvest_size);

      // show a message to the user
      Message.Post(Lib.BuildString("On <color=FFFFFF>", vessel.vesselName, "</color> the crop harvest produced <color=FFFFFF>",
        harvest_size.ToString("F0"), " ", resource_name, "</color>"));

      // record first space harvest
      if (!vi.landed && DB.Ready()) DB.Landmarks().space_harvest = 1;
    }

    // set rmb ui status
    GrowthStatus = Lib.HumanReadablePerc(growth);
    LightStatus = Lib.HumanReadablePerc(lighting);
    WasteStatus = Lib.HumanReadablePerc(waste_perc);
    SoilStatus = vi.landed ? "yes" : "no";
    TTAStatus = Lib.HumanReadableDuration(growing > double.Epsilon ? (1.0 - growth) / growing : 0.0);


    // enable/disable emergency harvest
    Events["EmergencyHarvest"].active = (growth >= 0.5);
  }
  static double CurvedPanelOutput(Vessel vessel, ProtoPartSnapshot part, Part prefab, PartModule m, Vector3d sun_dir, double sun_dist, double atmo_factor)
  {
    // if, for whatever reason, sun_dist is zero (or negative), we do not return any output
    if (sun_dist <= double.Epsilon) return 0.0;

    // shortcuts
    Quaternion rot = part.rotation;

    // get values from part
    string transform_name = Lib.ReflectionValue<string>(m, "PanelTransformName");
    float tot_rate = Lib.ReflectionValue<float>(m, "TotalEnergyRate");

    // get components
    Transform[] components = prefab.FindModelTransforms(transform_name);
    if (components.Length == 0) return 0.0;

    // calculate solar flux
    double solar_flux = Sim.SolarFlux(sun_dist);

    // reduce solar flux inside atmosphere
    solar_flux *= atmo_factor;

    // for each one of the components the curved panel is composed of
    double output = 0.0;
    foreach(Transform t in components)
    {
      double cosine_factor = Math.Max(Vector3d.Dot(sun_dir, (vessel.transform.rotation * rot * t.forward.normalized).normalized), 0.0);
      output += (double)tot_rate / (double)components.Length * cosine_factor * solar_flux / Sim.SolarFluxAtHome();
    }
    return output;
  }
Exemple #10
0
  // return solar panel EC output
  // note: we ignore temperature curve, and make sure it is not relavant in the MM patch
  static double PanelOutput(Vessel vessel, ProtoPartSnapshot part, ModuleDeployableSolarPanel panel, Vector3d sun_dir, double sun_dist, double atmo_factor)
  {
    // if, for whatever reason, sun_dist is zero (or negative), we do not return any output
    if (sun_dist <= double.Epsilon) return 0.0;

    // shortcuts
    Quaternion rot = part.rotation;
    Vector3d normal = panel.part.FindModelComponent<Transform>(panel.raycastTransformName).forward;

    // calculate cosine factor
    // note: for gameplay reasons, we ignore tracking panel pivots
    double cosine_factor = panel.sunTracking ? 1.0 : Math.Max(Vector3d.Dot(sun_dir, (vessel.transform.rotation * rot * normal).normalized), 0.0);

    // calculate solar flux
    double solar_flux = Sim.SolarFlux(sun_dist);

    // reduce solar flux inside atmosphere
    solar_flux *= atmo_factor;

    // finally, calculate output
    return panel.chargeRate * cosine_factor * (panel.useCurve ? panel.powerCurve.Evaluate((float)sun_dist) : solar_flux / Sim.SolarFluxAtHome());
  }
Exemple #11
0
  public static ec_data analyze_ec(List<Part> parts, environment_data env, crew_data crew, food_data food, oxygen_data oxygen, signal_data signal)
  {
    // store data
    ec_data ec = new ec_data();

    // calculate climate cost
    ec.consumed = (double)crew.count * env.temp_diff * Settings.ElectricChargePerSecond;

    // scan the parts
    foreach(Part p in parts)
    {
      // accumulate EC storage
      ec.storage += Lib.GetResourceAmount(p, "ElectricCharge");

      // remember if we already considered a resource converter module
      // rationale: we assume only the first module in a converter is active
      bool first_converter = true;

      // for each module
      foreach(PartModule m in p.Modules)
      {
        // command
        if (m.moduleName == "ModuleCommand")
        {
          ModuleCommand mm = (ModuleCommand)m;
          foreach(ModuleResource res in mm.inputResources)
          {
            if (res.name == "ElectricCharge")
            {
              ec.consumed += res.rate;
            }
          }
        }
        // solar panel
        else if (m.moduleName == "ModuleDeployableSolarPanel")
        {
          ModuleDeployableSolarPanel mm = (ModuleDeployableSolarPanel)m;
          double solar_k = (mm.useCurve ? mm.powerCurve.Evaluate((float)env.sun_dist) : env.sun_flux / Sim.SolarFluxAtHome());
          double generated = mm.chargeRate * solar_k * env.atmo_factor;
          ec.generated_sunlight += generated;
          ec.best_ec_generator = Math.Max(ec.best_ec_generator, generated);
        }
        // generator
        else if (m.moduleName == "ModuleGenerator")
        {
          // skip launch clamps, that include a generator
          if (p.partInfo.name == "launchClamp1") continue;

          ModuleGenerator mm = (ModuleGenerator)m;
          foreach(ModuleResource res in mm.inputList)
          {
            if (res.name == "ElectricCharge")
            {
              ec.consumed += res.rate;
            }
          }
          foreach(ModuleResource res in mm.outputList)
          {
            if (res.name == "ElectricCharge")
            {
              ec.generated_shadow += res.rate;
              ec.generated_sunlight += res.rate;
              ec.best_ec_generator = Math.Max(ec.best_ec_generator, res.rate);
            }
          }
        }
        // converter
        // note: only electric charge is considered for resource converters
        // note: we only consider the first resource converter in a part, and ignore the rest
        else if (m.moduleName == "ModuleResourceConverter" && first_converter)
        {
          ModuleResourceConverter mm = (ModuleResourceConverter)m;
          foreach(ResourceRatio rr in mm.inputList)
          {
            if (rr.ResourceName == "ElectricCharge")
            {
              ec.consumed += rr.Ratio;
            }
          }
          foreach(ResourceRatio rr in mm.outputList)
          {
            if (rr.ResourceName == "ElectricCharge")
            {
              ec.generated_shadow += rr.Ratio;
              ec.generated_sunlight += rr.Ratio;
              ec.best_ec_generator = Math.Max(ec.best_ec_generator, rr.Ratio);
            }
          }
          first_converter = false;
        }
        // harvester
        // note: only electric charge is considered for resource harvesters
        else if (m.moduleName == "ModuleResourceHarvester")
        {
          ModuleResourceHarvester mm = (ModuleResourceHarvester)m;
          foreach(ResourceRatio rr in mm.inputList)
          {
            if (rr.ResourceName == "ElectricCharge")
            {
              ec.consumed += rr.Ratio;
            }
          }
        }
        // active radiators
        else if (m.moduleName == "ModuleActiveRadiator")
        {
          ModuleActiveRadiator mm = (ModuleActiveRadiator)m;
          if (mm.IsCooling)
          {
            foreach(var rr in mm.inputResources)
            {
              if (rr.name == "ElectricCharge")
              {
                ec.consumed += rr.rate;
              }
            }
          }
        }
        // wheels
        else if (m.moduleName == "ModuleWheelMotor")
        {
          ModuleWheelMotor mm = (ModuleWheelMotor)m;
          if (mm.motorEnabled && mm.inputResource.name == "ElectricCharge")
          {
            ec.consumed += mm.inputResource.rate;
          }
        }
        else if (m.moduleName == "ModuleWheelMotorSteering")
        {
          ModuleWheelMotorSteering mm = (ModuleWheelMotorSteering)m;
          if (mm.motorEnabled && mm.inputResource.name == "ElectricCharge")
          {
            ec.consumed += mm.inputResource.rate;
          }
        }
        // SCANsat support
        else if (m.moduleName == "SCANsat" || m.moduleName == "ModuleSCANresourceScanner")
        {
          // include it in ec consumption, if deployed
          if (SCANsat.isDeployed(p, m)) ec.consumed += Lib.ReflectionValue<float>(m, "power");
        }
        // NearFutureSolar support
        // note: assume half the components are in sunlight, and average inclination is half
        else if (m.moduleName == "ModuleCurvedSolarPanel")
        {
          // get total rate
          double tot_rate = Lib.ReflectionValue<float>(m, "TotalEnergyRate");

          // get number of components
          int components = p.FindModelTransforms(Lib.ReflectionValue<string>(m, "PanelTransformName")).Length;

          // approximate output
          // 0.7071: average clamped cosine
          ec.generated_sunlight += 0.7071 * tot_rate;
        }
      }
    }

    // include cost from greenhouses artificial lighting
    ec.consumed += food.greenhouse_cost;

    // include cost from scrubbers
    ec.consumed += oxygen.scrubber_cost;

    // include relay cost for the best relay antenna
    ec.consumed += signal.relay_cost;

    // finally, calculate life expectancy of ec
    ec.life_expectancy_sunlight = ec.storage / Math.Max(ec.consumed - ec.generated_sunlight, 0.0);
    ec.life_expectancy_shadow = ec.storage / Math.Max(ec.consumed - ec.generated_shadow, 0.0);

    // return data
    return ec;
  }