Tip of the Day
Being overencumbered only slows down your activities, but doesn't affect travel.

MediaWiki:Gadget-DryCalc.js: Difference between revisions

From Walkscape Walkthrough
Created page with "global mw, $: (function () { 'use strict'; function clampNumber(n, min, max) { if (!isFinite(n)) return NaN; return Math.min(Math.max(n, min), max); } function formatPercent(x) { if (!isFinite(x)) return '—'; return (x * 100).toFixed(2) + '%'; } function formatNumber(x, decimals) { if (!isFinite(x)) return '—'; var d = (typeof decimals === 'number') ? decimals : 2; return x.toFixed(d); } function calc(p, t) { /..."
 
mNo edit summary
Line 20: Line 20:


   function calc(p, t) {
   function calc(p, t) {
    // p: probability per trial, t: trials
     var q = 1 - p;
     var q = 1 - p;
     var p0 = Math.pow(q, t);       // P(0 drops)
     var p0 = Math.pow(q, t);
     var pge1 = 1 - p0;             // P(>=1)
     var pge1 = 1 - p0;
     var mu = t * p;               // expected drops
     var mu = t * p;
     return { p0: p0, pge1: pge1, mu: mu };
     return { p0: p0, pge1: pge1, mu: mu };
   }
   }


   function trialsForConfidence(p, c) {
   function trialsForConfidence(p, c) {
    // smallest t such that P(>=1) >= c
    // 1 - (1-p)^t >= c  => (1-p)^t <= 1-c
    // t >= ln(1-c)/ln(1-p)
     if (!(p > 0 && p < 1)) return NaN;
     if (!(p > 0 && p < 1)) return NaN;
     if (!(c > 0 && c < 1)) return NaN;
     if (!(c > 0 && c < 1)) return NaN;
     var t = Math.log(1 - c) / Math.log(1 - p);
     var t = Math.log(1 - c) / Math.log(1 - p);
     return Math.ceil(t);
     return Math.ceil(t);
  }
  // Convert user-selected input mode -> effective per-step probability.
  // Returns: { pStep, wear, baseStepsPerDrop, warnings: [] }
  function normalizeToPerStep(mode, wearInput, oneInX, percent, baseStepsPerAction) {
    var warnings = [];
    if (mode === 'wear') {
      var wear = clampNumber(wearInput, 1, 1e18);
      if (!(wear >= 1)) return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Invalid WEAR.'] };
      return { pStep: 1 / wear, wear: wear, baseStepsPerDrop: NaN, warnings: warnings };
    }
    // Actions / Percent are per-action odds; to express as per-step, we need base steps per action.
    var sBase = clampNumber(baseStepsPerAction, 1, 1e18);
    if (!(sBase >= 1)) {
      warnings.push('Base steps per action is required to convert action odds into steps.');
      return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: warnings };
    }
    var pAction;
    if (mode === 'actions') {
      var x = clampNumber(oneInX, 1, 1e18);
      if (!(x >= 1)) return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Invalid “1 in X”.'] };
      pAction = 1 / x;
    } else if (mode === 'percent') {
      var pct = clampNumber(percent, 0, 100);
      if (!(pct > 0 && pct <= 100)) return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Invalid percent.'] };
      pAction = pct / 100;
    } else {
      return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Unknown mode.'] };
    }
    // “Base expected steps per drop” (unmodified) = stepsPerAction / pAction
    var baseStepsPerDrop = sBase / pAction;
    // If the user also happened to provide WEAR elsewhere (not in this mode), you could compare.
    // For now, we treat this as base-only and label it as such.
    var pStepBase = 1 / baseStepsPerDrop;
    return { pStep: pStepBase, wear: NaN, baseStepsPerDrop: baseStepsPerDrop, warnings: warnings };
   }
   }


   function buildUI($host) {
   function buildUI($host) {
    var defaultMode = ($host.data('default-mode') || 'actions').toString().toLowerCase();
    if (defaultMode !== 'steps') defaultMode = 'actions';
     var $wrap = $('<div>').addClass('ws-drycalc-card');
     var $wrap = $('<div>').addClass('ws-drycalc-card');
    var $title = $('<div>').addClass('ws-drycalc-title').text('Drop Chance Calculator');


     var $title = $('<div>').addClass('ws-drycalc-title').text('Drop Chance Calculator');
     // Mode radios
     var $modeRow = $('<div>').addClass('ws-drycalc-row');
     var $modeRow = $('<div>').addClass('ws-drycalc-row');
    var $modeLabel = $('<div>').addClass('ws-drycalc-label').text('Input mode');
    function radio(id, value, text) {
      var $r = $('<input>').attr({ type: 'radio', name: 'ws-drycalc-mode', id: id, value: value });
      var $l = $('<label>').attr('for', id).addClass('ws-drycalc-radio-label').text(text);
      return $('<div>').addClass('ws-drycalc-radio').append($r, $l);
    }
    var $mWear = radio('wsdc-wear', 'wear', 'WEAR (steps per item)');
    var $mAct  = radio('wsdc-actions', 'actions', 'Actions (1 in X)');
    var $mPct  = radio('wsdc-percent', 'percent', 'Percent (%)');


     var $modeLabel = $('<label>').addClass('ws-drycalc-label').text('Mode');
     $modeRow.append($modeLabel, $('<div>').addClass('ws-drycalc-radio-wrap').append($mWear, $mAct, $mPct));
    var $mode = $('<select>').addClass('ws-drycalc-input')
      .append($('<option>').val('actions').text('Actions'))
      .append($('<option>').val('steps').text('Steps'))
      .val(defaultMode);


     $modeRow.append($modeLabel, $mode);
     // Inputs
    var $wearRow = $('<div>').addClass('ws-drycalc-row');
    var $wearLabel = $('<label>').addClass('ws-drycalc-label').text('WEAR (steps per drop)');
    var $wear = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '1', step: '1', value: '62745' });
    $wearRow.append($wearLabel, $wear);


     var $rateRow = $('<div>').addClass('ws-drycalc-row');
     var $oneInRow = $('<div>').addClass('ws-drycalc-row');
     var $rateLabel = $('<label>').addClass('ws-drycalc-label').text('Drop rate');
     var $oneInLabel = $('<label>').addClass('ws-drycalc-label').text('Rate (1 in X)');
    var $ratePrefix = $('<span>').addClass('ws-drycalc-inline').text('1 in');
     var $oneIn = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '1', step: '1', value: '980' });
     var $rateN = $('<input>')
    $oneInRow.append($oneInLabel, $oneIn);
      .addClass('ws-drycalc-input')
      .attr({ type: 'number', min: '1', step: '1', value: '128' });


     $rateRow.append($rateLabel, $('<div>').addClass('ws-drycalc-inlinewrap').append($ratePrefix, $rateN));
     var $pctRow = $('<div>').addClass('ws-drycalc-row');
    var $pctLabel = $('<label>').addClass('ws-drycalc-label').text('Chance (%)');
    var $pct = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '0', max: '100', step: '0.001', value: '0.102' });
    $pctRow.append($pctLabel, $pct);


     var $countRow = $('<div>').addClass('ws-drycalc-row');
     var $baseStepsRow = $('<div>').addClass('ws-drycalc-row');
     var $countLabel = $('<label>').addClass('ws-drycalc-label');
     var $baseStepsLabel = $('<label>').addClass('ws-drycalc-label').text('Base steps per action');
     var $count = $('<input>')
     var $baseSteps = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '1', step: '1', value: '115' });
      .addClass('ws-drycalc-input')
    $baseStepsRow.append($baseStepsLabel, $baseSteps);
      .attr({ type: 'number', min: '0', step: '1', value: '0' });


     $countRow.append($countLabel, $count);
     var $stepsRow = $('<div>').addClass('ws-drycalc-row');
    var $stepsLabel = $('<label>').addClass('ws-drycalc-label').text('Steps completed');
    var $steps = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '0', step: '1', value: '0' });
    $stepsRow.append($stepsLabel, $steps);


     var $out = $('<div>').addClass('ws-drycalc-out');
     var $out = $('<div>').addClass('ws-drycalc-out');
    function getMode() {
      return $wrap.find('input[name="ws-drycalc-mode"]:checked').val() || 'wear';
    }
    function setVisibility(mode) {
      $wearRow.toggle(mode === 'wear');
      $oneInRow.toggle(mode === 'actions');
      $pctRow.toggle(mode === 'percent');
      // baseSteps is required for actions/percent modes (to convert to steps)
      $baseStepsRow.toggle(mode === 'actions' || mode === 'percent');
    }


     function render() {
     function render() {
       var mode = $mode.val();
       var mode = getMode();
       var n = parseFloat($rateN.val());
      setVisibility(mode);
       var t = parseFloat($count.val());
 
       var stepsDone = clampNumber(parseFloat($steps.val()), 0, 1e18);
 
       var norm = normalizeToPerStep(
        mode,
        parseFloat($wear.val()),
        parseFloat($oneIn.val()),
        parseFloat($pct.val()),
        parseFloat($baseSteps.val())
      );


       n = clampNumber(n, 1, 1e12);
       $out.empty();
      t = clampNumber(t, 0, 1e15);


       var unit = (mode === 'steps') ? 'steps' : 'actions';
       if (norm.warnings && norm.warnings.length) {
      $countLabel.text('Completed (' + unit + ')');
        norm.warnings.forEach(function (w) {
          $out.append($('<div>').addClass('ws-drycalc-warn').text(w));
        });
      }


       if (!(n >= 1) || !(t >= 0)) {
       if (!(stepsDone >= 0) || !(norm.pStep > 0 && norm.pStep < 1)) {
        $out.empty().append(
          $('<div>').addClass('ws-drycalc-warn').text('Please enter a valid drop rate and count.')
        );
         return;
         return;
       }
       }


       var p = 1 / n;
       var r = calc(norm.pStep, stepsDone);
       var r = calc(p, t);
 
      var t50 = trialsForConfidence(norm.pStep, 0.50);
      var t90 = trialsForConfidence(norm.pStep, 0.90);
      var t95 = trialsForConfidence(norm.pStep, 0.95);
       var t99 = trialsForConfidence(norm.pStep, 0.99);
 
      // Derive the “expected steps per drop” that the calculator is using
      var expectedStepsPerDrop = 1 / norm.pStep;
      var multiple = stepsDone / expectedStepsPerDrop;


       // confidence milestones
       $out.append(
      var t50 = trialsForConfidence(p, 0.50);
        $('<div>').addClass('ws-drycalc-metric')
      var t90 = trialsForConfidence(p, 0.90);
          .append($('<div>').addClass('ws-drycalc-k').text('Expected steps per drop (used in calc)'))
      var t95 = trialsForConfidence(p, 0.95);
          .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(expectedStepsPerDrop, 0))),


      $out.empty().append(
         $('<div>').addClass('ws-drycalc-metric')
         $('<div>').addClass('ws-drycalc-metric')
           .append($('<div>').addClass('ws-drycalc-k').text('Expected ' + unit + ' per drop'))
           .append($('<div>').addClass('ws-drycalc-k').text('You are at'))
           .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(n, 0))),
          .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(multiple, 2) + '× expected')),
 
        (isFinite(norm.baseStepsPerDrop) ? $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('Base expected steps per drop'))
           .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(norm.baseStepsPerDrop, 0))) : null),


         $('<div>').addClass('ws-drycalc-metric')
         $('<div>').addClass('ws-drycalc-metric')
Line 120: Line 200:


         $('<div>').addClass('ws-drycalc-milestones').append(
         $('<div>').addClass('ws-drycalc-milestones').append(
           $('<div>').addClass('ws-drycalc-chip').text('50%: ' + (isFinite(t50) ? t50 : '—') + ' ' + unit),
           $('<div>').addClass('ws-drycalc-chip').text('50%: ' + (isFinite(t50) ? t50 : '—') + ' steps'),
           $('<div>').addClass('ws-drycalc-chip').text('90%: ' + (isFinite(t90) ? t90 : '—') + ' ' + unit),
           $('<div>').addClass('ws-drycalc-chip').text('90%: ' + (isFinite(t90) ? t90 : '—') + ' steps'),
           $('<div>').addClass('ws-drycalc-chip').text('95%: ' + (isFinite(t95) ? t95 : '—') + ' ' + unit)
           $('<div>').addClass('ws-drycalc-chip').text('95%: ' + (isFinite(t95) ? t95 : '—') + ' steps'),
          $('<div>').addClass('ws-drycalc-chip').text('99%: ' + (isFinite(t99) ? t99 : '—') + ' steps')
         )
         )
       );
       );
     }
     }


     $mode.on('change', render);
    // Default mode
     $rateN.on('input', render);
     $mWear.find('input').prop('checked', true);
     $count.on('input', render);
 
     $wrap.on('change input', 'input, select', render);
 
     $wrap.append(
      $title,
      $modeRow,
      $wearRow,
      $oneInRow,
      $pctRow,
      $baseStepsRow,
      $stepsRow,
      $out
    );


    $wrap.append($title, $modeRow, $rateRow, $countRow, $out);
     $host.empty().append($wrap);
     $host.empty().append($wrap);
     render();
     render();
   }
   }

Revision as of 01:34, 29 December 2025

/* global mw, $ */
(function () {
  'use strict';

  function clampNumber(n, min, max) {
    if (!isFinite(n)) return NaN;
    return Math.min(Math.max(n, min), max);
  }

  function formatPercent(x) {
    if (!isFinite(x)) return '—';
    return (x * 100).toFixed(2) + '%';
  }

  function formatNumber(x, decimals) {
    if (!isFinite(x)) return '—';
    var d = (typeof decimals === 'number') ? decimals : 2;
    return x.toFixed(d);
  }

  function calc(p, t) {
    var q = 1 - p;
    var p0 = Math.pow(q, t);
    var pge1 = 1 - p0;
    var mu = t * p;
    return { p0: p0, pge1: pge1, mu: mu };
  }

  function trialsForConfidence(p, c) {
    if (!(p > 0 && p < 1)) return NaN;
    if (!(c > 0 && c < 1)) return NaN;
    var t = Math.log(1 - c) / Math.log(1 - p);
    return Math.ceil(t);
  }

  // Convert user-selected input mode -> effective per-step probability.
  // Returns: { pStep, wear, baseStepsPerDrop, warnings: [] }
  function normalizeToPerStep(mode, wearInput, oneInX, percent, baseStepsPerAction) {
    var warnings = [];

    if (mode === 'wear') {
      var wear = clampNumber(wearInput, 1, 1e18);
      if (!(wear >= 1)) return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Invalid WEAR.'] };
      return { pStep: 1 / wear, wear: wear, baseStepsPerDrop: NaN, warnings: warnings };
    }

    // Actions / Percent are per-action odds; to express as per-step, we need base steps per action.
    var sBase = clampNumber(baseStepsPerAction, 1, 1e18);
    if (!(sBase >= 1)) {
      warnings.push('Base steps per action is required to convert action odds into steps.');
      return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: warnings };
    }

    var pAction;
    if (mode === 'actions') {
      var x = clampNumber(oneInX, 1, 1e18);
      if (!(x >= 1)) return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Invalid “1 in X”.'] };
      pAction = 1 / x;
    } else if (mode === 'percent') {
      var pct = clampNumber(percent, 0, 100);
      if (!(pct > 0 && pct <= 100)) return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Invalid percent.'] };
      pAction = pct / 100;
    } else {
      return { pStep: NaN, wear: NaN, baseStepsPerDrop: NaN, warnings: ['Unknown mode.'] };
    }

    // “Base expected steps per drop” (unmodified) = stepsPerAction / pAction
    var baseStepsPerDrop = sBase / pAction;

    // If the user also happened to provide WEAR elsewhere (not in this mode), you could compare.
    // For now, we treat this as base-only and label it as such.
    var pStepBase = 1 / baseStepsPerDrop;

    return { pStep: pStepBase, wear: NaN, baseStepsPerDrop: baseStepsPerDrop, warnings: warnings };
  }

  function buildUI($host) {
    var $wrap = $('<div>').addClass('ws-drycalc-card');
    var $title = $('<div>').addClass('ws-drycalc-title').text('Drop Chance Calculator');

    // Mode radios
    var $modeRow = $('<div>').addClass('ws-drycalc-row');
    var $modeLabel = $('<div>').addClass('ws-drycalc-label').text('Input mode');

    function radio(id, value, text) {
      var $r = $('<input>').attr({ type: 'radio', name: 'ws-drycalc-mode', id: id, value: value });
      var $l = $('<label>').attr('for', id).addClass('ws-drycalc-radio-label').text(text);
      return $('<div>').addClass('ws-drycalc-radio').append($r, $l);
    }

    var $mWear = radio('wsdc-wear', 'wear', 'WEAR (steps per item)');
    var $mAct  = radio('wsdc-actions', 'actions', 'Actions (1 in X)');
    var $mPct  = radio('wsdc-percent', 'percent', 'Percent (%)');

    $modeRow.append($modeLabel, $('<div>').addClass('ws-drycalc-radio-wrap').append($mWear, $mAct, $mPct));

    // Inputs
    var $wearRow = $('<div>').addClass('ws-drycalc-row');
    var $wearLabel = $('<label>').addClass('ws-drycalc-label').text('WEAR (steps per drop)');
    var $wear = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '1', step: '1', value: '62745' });
    $wearRow.append($wearLabel, $wear);

    var $oneInRow = $('<div>').addClass('ws-drycalc-row');
    var $oneInLabel = $('<label>').addClass('ws-drycalc-label').text('Rate (1 in X)');
    var $oneIn = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '1', step: '1', value: '980' });
    $oneInRow.append($oneInLabel, $oneIn);

    var $pctRow = $('<div>').addClass('ws-drycalc-row');
    var $pctLabel = $('<label>').addClass('ws-drycalc-label').text('Chance (%)');
    var $pct = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '0', max: '100', step: '0.001', value: '0.102' });
    $pctRow.append($pctLabel, $pct);

    var $baseStepsRow = $('<div>').addClass('ws-drycalc-row');
    var $baseStepsLabel = $('<label>').addClass('ws-drycalc-label').text('Base steps per action');
    var $baseSteps = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '1', step: '1', value: '115' });
    $baseStepsRow.append($baseStepsLabel, $baseSteps);

    var $stepsRow = $('<div>').addClass('ws-drycalc-row');
    var $stepsLabel = $('<label>').addClass('ws-drycalc-label').text('Steps completed');
    var $steps = $('<input>').addClass('ws-drycalc-input').attr({ type: 'number', min: '0', step: '1', value: '0' });
    $stepsRow.append($stepsLabel, $steps);

    var $out = $('<div>').addClass('ws-drycalc-out');

    function getMode() {
      return $wrap.find('input[name="ws-drycalc-mode"]:checked').val() || 'wear';
    }

    function setVisibility(mode) {
      $wearRow.toggle(mode === 'wear');
      $oneInRow.toggle(mode === 'actions');
      $pctRow.toggle(mode === 'percent');
      // baseSteps is required for actions/percent modes (to convert to steps)
      $baseStepsRow.toggle(mode === 'actions' || mode === 'percent');
    }

    function render() {
      var mode = getMode();
      setVisibility(mode);

      var stepsDone = clampNumber(parseFloat($steps.val()), 0, 1e18);

      var norm = normalizeToPerStep(
        mode,
        parseFloat($wear.val()),
        parseFloat($oneIn.val()),
        parseFloat($pct.val()),
        parseFloat($baseSteps.val())
      );

      $out.empty();

      if (norm.warnings && norm.warnings.length) {
        norm.warnings.forEach(function (w) {
          $out.append($('<div>').addClass('ws-drycalc-warn').text(w));
        });
      }

      if (!(stepsDone >= 0) || !(norm.pStep > 0 && norm.pStep < 1)) {
        return;
      }

      var r = calc(norm.pStep, stepsDone);

      var t50 = trialsForConfidence(norm.pStep, 0.50);
      var t90 = trialsForConfidence(norm.pStep, 0.90);
      var t95 = trialsForConfidence(norm.pStep, 0.95);
      var t99 = trialsForConfidence(norm.pStep, 0.99);

      // Derive the “expected steps per drop” that the calculator is using
      var expectedStepsPerDrop = 1 / norm.pStep;
      var multiple = stepsDone / expectedStepsPerDrop;

      $out.append(
        $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('Expected steps per drop (used in calc)'))
          .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(expectedStepsPerDrop, 0))),

        $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('You are at'))
          .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(multiple, 2) + '× expected')),

        (isFinite(norm.baseStepsPerDrop) ? $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('Base expected steps per drop'))
          .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(norm.baseStepsPerDrop, 0))) : null),

        $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('Expected drops so far'))
          .append($('<div>').addClass('ws-drycalc-v').text(formatNumber(r.mu, 2))),

        $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('Chance of at least 1 drop by now'))
          .append($('<div>').addClass('ws-drycalc-v').text(formatPercent(r.pge1))),

        $('<div>').addClass('ws-drycalc-metric')
          .append($('<div>').addClass('ws-drycalc-k').text('Chance you would be dry (0 drops)'))
          .append($('<div>').addClass('ws-drycalc-v').text(formatPercent(r.p0))),

        $('<div>').addClass('ws-drycalc-subtitle').text('Milestones (chance of ≥1 drop)'),

        $('<div>').addClass('ws-drycalc-milestones').append(
          $('<div>').addClass('ws-drycalc-chip').text('50%: ' + (isFinite(t50) ? t50 : '—') + ' steps'),
          $('<div>').addClass('ws-drycalc-chip').text('90%: ' + (isFinite(t90) ? t90 : '—') + ' steps'),
          $('<div>').addClass('ws-drycalc-chip').text('95%: ' + (isFinite(t95) ? t95 : '—') + ' steps'),
          $('<div>').addClass('ws-drycalc-chip').text('99%: ' + (isFinite(t99) ? t99 : '—') + ' steps')
        )
      );
    }

    // Default mode
    $mWear.find('input').prop('checked', true);

    $wrap.on('change input', 'input, select', render);

    $wrap.append(
      $title,
      $modeRow,
      $wearRow,
      $oneInRow,
      $pctRow,
      $baseStepsRow,
      $stepsRow,
      $out
    );

    $host.empty().append($wrap);
    render();
  }

  function init($content) {
    $content.find('.ws-drycalc').each(function () {
      buildUI($(this));
    });
  }

  mw.hook('wikipage.content').add(init);
})();