Source: stamina.js

const extractStam = '{"recovery":{"points":1,"loWind":0,"hiWind":30,"loTime":5,"hiTime":15},"tiredness":[20,50],"consumption":{"points":{"tack":10,"gybe":10,"sail":20},"winds":{"0":1,"10":1.2,"20":1.5,"30":2},"boats":{"0":1,"5":1.2,"15":1.5,"50":2}},"impact":{"0":2,"100":0.5}}';

const shipParam = [
   {
      name: "Imoca",
      cShip: 1.2,
      tMin: [300, 300, 420],
      tMax: [660, 660, 600]
   },
   {
      name: "Ultim",
      cShip: 1.5,
      tMin: [300, 300, 420],
      tMax: [660, 660, 600]
   },
   {
      name: "Normal",
      cShip: 1.0,
      tMin: [300, 300, 336],
      tMax: [660, 660, 480]
   }
];

/**
 * Compute the penalty in seconds for a manoeuvre type in Virtual Regatta.
 * Depends on true wind speed (tws) and energy. Also computes the stamina coefficient.
 * 
 * @param {number} shipIndex - Index of the ship in ship parameters.
 * @param {number} type - Type of manoeuvre (0, 1, or 2).
 * @param {number} tws - True wind speed in knots.
 * @param {number} energy - Current energy (0 to 100).
 * @param {object} staminaK - stamina coefficient.
 * @returns {number} Penalty time in seconds, or -1 if the type is invalid.
 */
function fPenalty (shipIndex, type, tws, energy, staminaK, fullPack) {
   if (type < 0 || type > 2) {
      console.error(`In fPenalty, type unknown: ${type}`);
      return -1;
   }
   const cShip = shipParam[shipIndex].cShip;
   const tMin = shipParam[shipIndex].tMin[type];
   const tMax = shipParam[shipIndex].tMax[type];
   const fTws = 50.0 - 50.0 * Math.cos(Math.PI * ((Math.max(10.0, Math.min(tws, 30.0)) - 10.0) / 20.0));
   let t = tMin + fTws * (tMax - tMin) / 100.0;
   if (fullPack) t *= 0.8;
   return t * cShip * staminaK;
}

/**
 * Compute the point loss for a manoeuvre in Virtual Regatta.
 * Depends on true wind speed (tws) and whether the full pack is active.
 * 
 * @param {number} shipIndex - Index of the ship in ship parameters.
 * @param {number} type - Type of manoeuvre (0, 1, or 2).
 * @param {number} tws - True wind speed in knots.
 * @param {boolean} fullPack - Whether the full pack option is active.
 * @returns {number} Point loss.
 */
function fPointLoss (shipIndex, type, tws, fullPack) {
   const fPCoeff = fullPack ? 0.8 : 1.0;
   const loss = (type === 2) ? 0.2 : 0.1;
   const cShip = shipParam[shipIndex].cShip;

   const fTws = (tws < 10.0) ? 0.02 * tws + 1.0 :
                (tws <= 20.0) ? 0.03 * tws + 0.9 :
                (tws <= 30.0) ? 0.05 * tws + 0.5 :
                2.0;

  return fPCoeff * loss * cShip * fTws;
}

/**
 * Compute the time in seconds needed to recover one energy point, depending on true wind speed.
 * 
 * @param {number} tws - True wind speed in knots.
 * @returns {number} Time in seconds to recover one energy point.
 */
function fTimeToRecupOnePoint (tws) {
   const timeToRecupLow = 5.0 * 60.0;   // 5 minutes in seconds
   const timeToRecupHigh = 15.0 * 60.0; // 15 minutes in seconds
   const fTws = 1.0 - Math.cos(Math.PI * (Math.min(tws, 30.0) / 30.0));

   return timeToRecupLow + fTws * (timeToRecupHigh - timeToRecupLow) / 2.0;
}

/**
 * Opens a SweetAlert2 modal acting as a live stamina & penalty calculator for Virtual Regatta.
 *
 * This function creates a well-aligned UI with:
 * - A dropdown for boat type.
 * - Sliders for TWS and Energy with value display.
 * - A Full Pack checkbox.
 * - Calculated recovery time.
 * - A table showing Time To Manoeuvre and Energy Point Lossed for Tack, Gybe, and Sail.
 *
 * Updates are live on any user interaction.
 */
function stamina() {
   if (! window.matchMedia('(orientation: landscape)').matches) {
      Swal.fire('Stamina Warning', 'Switch to Lay out', 'warning');
      return;
   }

   const shipOptions = shipParam.map((ship, index) => `<option value="${index}">${ship.name}</option>`).join("");

   const htmlContent = `
   <table class="no-lines">
   <tr>
      <td><label for="vrShipSelect" style="min-width: 100px;">Boat</label></td>
      <td><select id="vrShipSelect">${shipOptions}</select></td>
   </tr>

   <tr>
      <td><label for="vrTwsRange" style="min-width: 100px;">TWS</label></td>
      <td><input id="vrTwsRange" type="range" min="0" max="30" value="15"
            style="flex: 1 0 150px; margin: 0 10px; max-width: 150px;"></td>
      <td><span><span id="vrTwsValue">15</span> kts</span></td>
   </tr>

   <tr>
      <td><label for="vrEnergyRange" style="min-width: 100px;">Energy</label></td>
      <td><input id="vrEnergyRange" type="range" min="0" max="100" value="100"
             style="flex: 1 0 150px; margin: 0 10px; max-width: 150px;"></td>
      <td><span><span id="vrEnergyValue" style="min-width: 200px;">100 </span></span></td>
   </tr>

   <tr>
      <td><label for="vrFullPack" style="min-width: 100px;">Full Pack</label></td>
      <td><input id="vrFullPack" type="checkbox" style="margin-right: 5px;"></td>
   </tr>
   <tr>
      <td>Time to recover 1 point:</td> 
      <td><span id="vrRecoverTime"></span></td>
   </tr>
   </table>

   <table style="margin-top: 10px; width: 100%; border-collapse: collapse;">
      <tr>
        <th style="text-align: left;"></th>
        <th>Tack</th>
        <th>Gybe</th>
        <th>Sail</th>
      </tr>
      <tr>
        <td style="text-align: left;">Time To Manoeuvre</td>
        <td id="vrTimeTack"></td>
        <td id="vrTimeGybe"></td>
        <td id="vrTimeSail"></td>
      </tr>
      <tr>
        <td style="text-align: left;">Energy Point Lossed</td>
        <td id="vrLossTack"></td>
        <td id="vrLossGybe"></td>
        <td id="vrLossSail"></td>
      </tr>
   </table>
   `;

   Swal.fire({
      title: "Stamina & Penalty Calculator",
      html: htmlContent,
      customClass: "stamina-popup",  // see css associated
      showCancelButton: true,
      showCloseButton: true,
      focusConfirm: false,
      confirmButtonText: "More",
      didOpen: (popup) => {
         const shipSelect   = popup.querySelector("#vrShipSelect");
         const twsRange     = popup.querySelector("#vrTwsRange");
         const energyRange  = popup.querySelector("#vrEnergyRange");
         const fullPackChk  = popup.querySelector("#vrFullPack");
         const twsValue     = popup.querySelector("#vrTwsValue");
         const energyValue  = popup.querySelector("#vrEnergyValue");
         const recoverTime  = popup.querySelector("#vrRecoverTime");
         const timeCells    = ["#vrTimeTack", "#vrTimeGybe", "#vrTimeSail"].map(id => popup.querySelector(id));
         const lossCells    = ["#vrLossTack", "#vrLossGybe", "#vrLossSail"].map(id => popup.querySelector(id));

         function formatTimeSecToMnS(time) {
            const m = Math.floor(time / 60).toString().padStart(2, "0");
            const s = Math.round(time % 60).toString().padStart(2, "0");
            return `${m} mn ${s} s`;
         }

         function update () {
            const kPenalty = 0.015;
            const shipIndex = parseInt(shipSelect.value);
            const tws = parseFloat(twsRange.value);
            const energy = parseFloat(energyRange.value);
            const fullPack = fullPackChk.checked;
            const staminaOut = 2 - Math.min (energy, 100.0) * kPenalty;

            twsValue.textContent = tws.toFixed(0);
            energyValue.textContent = `${energy.toFixed(2).padStart (3, "0")}% (${staminaOut.toFixed(2)})`;

            const recup = fTimeToRecupOnePoint(tws);
            recoverTime.textContent =  formatTimeSecToMnS (recup);

            for (let i = 0; i < 3; i++) {
               const time = fPenalty (shipIndex, i, tws, energy, staminaOut, fullPack);
               const loss = fPointLoss (shipIndex, i, tws, fullPack);
               timeCells[i].textContent =  formatTimeSecToMnS(time);
               lossCells[i].textContent = (100 * loss).toFixed(2) + " %";
            }
         }

         shipSelect.addEventListener ("change", update);
         twsRange.addEventListener ("input", update);
         energyRange.addEventListener ("input", update);
         fullPackChk.addEventListener ("change", update);

         update ();
      }
   }).then ((result)=>{
      const content = JSON.stringify(JSON.parse(extractStam), null, 2);
      if (result.isConfirmed) {
         Swal.fire ({
            title: 'extractStam',
            html: `<pre style="text-align:left;white-space:pre-wrap;margin:0">${content}</pre>`
         });
      }
   });
}