Tip of the day
You can find achievements and obtained collectible items by tapping your character at the top-left.

MediaWiki:Gadget-DryCalc.js: Difference between revisions

From Walkscape Walkthrough
mNo edit summary
mNo edit summary
 
(One intermediate revision by the same user not shown)
Line 119: Line 119:
       "Malik, foraging on the glacier.",
       "Malik, foraging on the glacier.",
       "Agile, scrambling along the ridge.",
       "Agile, scrambling along the ridge.",
      "Ravaha, trying to get anything.",
       "You, doing whatever you're doing."
       "You, doing whatever you're doing."
     ]},
     ]},
Line 227: Line 228:
     var $out = $('<div>').addClass('ws-dropcalc-out');
     var $out = $('<div>').addClass('ws-dropcalc-out');
// Flavour render state (prevents API spam + out-of-order updates)
// Flavour render state
var lastFlavourKey = null;
var flavourRenderToken = 0;
var flavourRenderToken = 0;
var flavourHtmlCache = {};


     // Default mode
     // Default mode
Line 322: Line 323:
        
        
  // ----- Flavour text (parsed wikitext) -----
  // ----- Flavour text (parsed wikitext) -----
  // NOTE: This uses OSRS-style metric: "chance you'd have had ≥1 drop by now"
// NOTE: This uses OSRS-style metric: "chance you'd have had ≥1 drop by now"
  var wouldHaveDropPercent = r.pge1 * 100;
var wouldHaveDropPercent = r.pge1 * 100;
  var lines = getFlavourText(wouldHaveDropPercent);
var lines = getFlavourText(wouldHaveDropPercent);
 
  // Create a stable key so we only parse when message changes
var flavourKey = (lines && lines.length) ? lines.join('\n') : '';
  var flavourKey = (lines && lines.length) ? lines.join('\n') : '';
var $flavour = $('<div>').addClass('ws-dropcalc-flavour');
  var $flavour = $('<div>').addClass('ws-dropcalc-flavour');
$out.append($flavour);
  $out.append($flavour);
 
if (!flavourKey) return;
  if (!flavourKey) return;
 
// Use cached parsed HTML if this exact message was already parsed.
  // If message hasn't changed, render as plain text quickly (no API call)
if (flavourHtmlCache[flavourKey]) {
  if (flavourKey === lastFlavourKey) {
  $flavour.html(flavourHtmlCache[flavourKey]);
    lines.forEach(function (line) {
  return;
      // Keep last rendered HTML; if you want plain text fallback:
}
      $flavour.append($('<div>').text(line));
    });
// Async parse guard
var myToken = ++flavourRenderToken;
var parsedHtml = [];
// Parse each line and append HTML in order
(function parseNext(i) {
  if (i >= lines.length) {
    if (myToken !== flavourRenderToken) return;
    var finalHtml = parsedHtml.join('');
    flavourHtmlCache[flavourKey] = finalHtml;
    $flavour.html(finalHtml);
    return;
    return;
  }
  }
  lastFlavourKey = flavourKey;
   
  parseWikitext(lines[i]).then(function (html) {
  // Async parse (guard against stale renders)
    if (myToken !== flavourRenderToken) return;
  var myToken = ++flavourRenderToken;
 
    parsedHtml.push(
  // Parse each line and append HTML in order
      $('<div>').html(html).prop('outerHTML')
  (function parseNext(i) {
    );
    if (i >= lines.length) return;
 
    parseNext(i + 1);
    parseWikitext(lines[i]).then(function (html) {
  });
      if (myToken !== flavourRenderToken) return; // stale render, ignore
})(0);
      $flavour.append($('<div>').html(html));
      parseNext(i + 1);
    });
  })(0);
     }
     }



Latest revision as of 11:46, 29 May 2026

/* 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 formatInt(x) {
    if (!isFinite(x)) return '—';
    return Math.round(x).toLocaleString(mw.config.get('wgUserLanguage') || 'en-US');
  }

  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);
  }
  
  function parseWikitext(wikitext) {
    var api = new mw.Api();
    return api.post({
      action: 'parse',
      format: 'json',
      contentmodel: 'wikitext',
      prop: 'text',
      text: wikitext
    }).then(function (data) {
      return (data && data.parse && data.parse.text && data.parse.text['*']) ? data.parse.text['*'] : '';
    }, function () {
      return '';
    });
  }

  // -----------------------------
  // Flavour text by dryness (%)
  // -----------------------------
  var FLAVOUR_TEXTS = [
    { min: -1,   max: 1,    lines: [
      "You're dancing with the Syrenthians.",
      "You are extremely not dry."
    ]},
    { min: 1,    max: 10,   lines: [
      "Your spoon is too big!",
      "🥄 🥄 🥄",
      "if you have a drop by now..."
    ]},
    { min: 10,   max: 20,   lines: [
      "Stay hydrated, you’ve a long way to go."
    ]},
    { min: 20,   max: 30,   lines: [
      "🥄 Spooned 🥄",
      "Just kidding. No drops."
    ]},
    { min: 30,   max: 40,   lines: [
      "Your friends would be jealous.",
      "…If you had any drops."
    ]},
    { min: 40,   max: 49,   lines: [
      "You're quite the lucker, aren't you?",
      "Or not, since you got no drops."
    ]},
    { min: 49,   max: 51,   lines: [
      "A perfect mix of dry and undry, as all things should be."
    ]},
    { min: 51,   max: 61,   lines: [
      "Nothing interesting happens.",
      "Over half of players would have had a drop by now."
    ]},
    { min: 61,   max: 65,   lines: [
      "An unenlightened being would say:",
      "“1 in X means I should have it by now.”"
    ]},
    { min: 65,   max: 73,   lines: [
      "Nothing interesting happens.",
      "Still no drops."
    ]},
    { min: 73,   max: 74,   lines: [
      "😂😂😂"
    ]},
    { min: 74,   max: 85,   lines: [
      "Oof."
    ]},
    { min: 85,   max: 90,   lines: [
      "A national emergency has been declared about an ongoing drought."
    ]},
    { min: 90,   max: 95,   lines: [
      "Right. Time to post on Discord."
    ]},
    { min: 95,   max: 99,   lines: [
      "You after being this dry:",
      "💀 💀 💀"
    ]},
    { min: 99,   max: 99.5, lines: [
      "You rn: [[File:Unidentified remains.svg|32px]]"
    ]},
    { min: 99.5, max: 99.9, lines: [
      "Malik, foraging on the glacier.",
      "Agile, scrambling along the ridge.",
      "Ravaha, trying to get anything.",
      "You, doing whatever you're doing."
    ]},
    { min: 99.9, max: 1000, lines: [
      "Did you forget to start the activity?"
    ]}
  ];
  
  function getFlavourText(dryPercent) {
    for (var i = 0; i < FLAVOUR_TEXTS.length; i++) {
      var f = FLAVOUR_TEXTS[i];
      if (dryPercent >= f.min && dryPercent < f.max) {
        return f.lines;
      }
    }
    return null;
  }

  function normalize(mode, wearInput, oneInX, percent, count) {
    var warnings = [];
    var t = clampNumber(count, 0, 1e18);

    if (!(t >= 0)) {
      return { p: NaN, t: NaN, unit: '—', perDrop: NaN, warnings: ['Please enter a valid number.'] };
    }

    if (mode === 'wear') {
      var wear = clampNumber(wearInput, 1, 1e18);
      if (!(wear >= 1)) return { p: NaN, t: NaN, unit: 'steps', perDrop: NaN, warnings: ['Enter a valid WEAR (steps per drop).'] };
      return { p: 1 / wear, t: t, unit: 'steps', perDrop: wear, warnings: warnings };
    }

    if (mode === 'actions') {
      var x = clampNumber(oneInX, 1, 1e18);
      if (!(x >= 1)) return { p: NaN, t: NaN, unit: 'actions', perDrop: NaN, warnings: ['Enter a valid “1 in X” value.'] };
      return { p: 1 / x, t: t, unit: 'actions', perDrop: x, warnings: warnings };
    }

    if (mode === 'percent') {
      var pct = clampNumber(percent, 0, 100);
      if (!(pct > 0 && pct <= 100)) return { p: NaN, t: NaN, unit: 'actions', perDrop: NaN, warnings: ['Enter a valid percent (0 < % ≤ 100).'] };
      var p = pct / 100;
      return { p: p, t: t, unit: 'actions', perDrop: 1 / p, warnings: warnings };
    }

    return { p: NaN, t: NaN, unit: '—', perDrop: NaN, warnings: ['Unknown mode.'] };
  }

  var CONF_LEVELS = [
    { c: 0.50, label: '50%' },
    { c: 0.90, label: '90%' },
    { c: 0.95, label: '95%' },
    { c: 0.99, label: '99%' }
  ];

  function buildCalculator($host) {
    var $card = $('<div>').addClass('ws-dropcalc-card');

    var $title = $('<div>').addClass('ws-dropcalc-title').text('Drop Chance Calculator');

    // Mode radios
    var groupName = 'ws-dropcalc-mode-' + Math.random().toString(36).slice(2);

    function makeRadio(value, label) {
      var id = groupName + '-' + value;
      var $label = $('<label>').addClass('ws-dropcalc-radio').attr('for', id);
      var $input = $('<input>').attr({ type: 'radio', id: id, name: groupName, value: value });
      var $text = $('<span>').text(label);
      $label.append($input, $text);
      return $label;
    }

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

    var $radioWrap = $('<div>').addClass('ws-dropcalc-radio-wrap').append(
      makeRadio('wear', 'WEAR (steps per item)'),
      makeRadio('actions', 'Actions (1 in X)'),
      makeRadio('percent', 'Percent (%)')
    );

    $modeRow.append($modeLabel, $radioWrap);

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

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

    var $pctRow = $('<div>').addClass('ws-dropcalc-row');
    var $pctLabel = $('<label>').addClass('ws-dropcalc-label').text('Chance (%)');
    var $pct = $('<input>').addClass('ws-dropcalc-input').attr({ type: 'number', min: '0', max: '100', step: '0.001' }).val('0.0');
    var $pctHint = $('<div>').addClass('ws-dropcalc-hint').text('Example: 0.102 means 0.102%');
    var $pctCol = $('<div>').addClass('ws-dropcalc-col').append($pct, $pctHint);
    $pctRow.append($pctLabel, $pctCol);

    var $countRow = $('<div>').addClass('ws-dropcalc-row');
    var $countLabel = $('<label>').addClass('ws-dropcalc-label');
    var $count = $('<input>').addClass('ws-dropcalc-input').attr({ type: 'number', min: '0', step: '1' }).val('0');
    $countRow.append($countLabel, $count);

    // Output
    var $out = $('<div>').addClass('ws-dropcalc-out');
	
	// Flavour render state
	var flavourRenderToken = 0;
	var flavourHtmlCache = {};

    // Default mode
    $card.find('input[name="' + groupName + '"][value="wear"]').prop('checked', true);

    function getMode() {
      return $card.find('input[name="' + groupName + '"]:checked').val() || 'wear';
    }

    function setVisibility(mode) {
      $wearRow.toggle(mode === 'wear');
      $oneInRow.toggle(mode === 'actions');
      $pctRow.toggle(mode === 'percent');
    }

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

      var unit = (mode === 'wear') ? 'steps' : 'actions';
      $countLabel.text('Completed (' + unit + ')');

      var norm = normalize(
        mode,
        parseFloat($wear.val()),
        parseFloat($oneIn.val()),
        parseFloat($pct.val()),
        parseFloat($count.val())
      );

      $out.empty();

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

      if (!(norm.p > 0 && norm.p < 1) || !(norm.t >= 0)) return;

      var r = calc(norm.p, norm.t);

      var expectedLabel;
      var expectedValue;

      if (mode === 'wear') {
        expectedLabel = 'WEAR (steps per drop)';
        expectedValue = formatInt(norm.perDrop) + ' ' + unit;
      } else if (mode === 'actions') {
        expectedLabel = 'Expected actions per drop';
        expectedValue = formatInt(norm.perDrop) + ' ' + unit;
      } else {
        expectedLabel = 'Expected actions per drop';
        expectedValue = formatNumber(norm.perDrop, 2) + ' ' + unit;
      }

      var multiple = norm.t / norm.perDrop;

      $out.append(
        $('<div>').addClass('ws-dropcalc-metric')
          .append($('<div>').addClass('ws-dropcalc-k').text(expectedLabel))
          .append($('<div>').addClass('ws-dropcalc-v').text(expectedValue)),

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

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

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

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

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

      var $milestones = $('<div>').addClass('ws-dropcalc-milestones');
      CONF_LEVELS.forEach(function (it) {
        var needed = trialsForConfidence(norm.p, it.c);
        $milestones.append(
          $('<div>').addClass('ws-dropcalc-chip')
            .text(it.label + ': ' + (isFinite(needed) ? formatInt(needed) : '—') + ' ' + unit)
        );
      });
      $out.append($milestones);
      
	  // ----- Flavour text (parsed wikitext) -----
	// NOTE: This uses OSRS-style metric: "chance you'd have had ≥1 drop by now"
	var wouldHaveDropPercent = r.pge1 * 100;
	var lines = getFlavourText(wouldHaveDropPercent);
	
	var flavourKey = (lines && lines.length) ? lines.join('\n') : '';
	var $flavour = $('<div>').addClass('ws-dropcalc-flavour');
	$out.append($flavour);
	
	if (!flavourKey) return;
	
	// Use cached parsed HTML if this exact message was already parsed.
	if (flavourHtmlCache[flavourKey]) {
	  $flavour.html(flavourHtmlCache[flavourKey]);
	  return;
	}
	
	// Async parse guard
	var myToken = ++flavourRenderToken;
	var parsedHtml = [];
	
	// Parse each line and append HTML in order
	(function parseNext(i) {
	  if (i >= lines.length) {
	    if (myToken !== flavourRenderToken) return;
	
	    var finalHtml = parsedHtml.join('');
	    flavourHtmlCache[flavourKey] = finalHtml;
	    $flavour.html(finalHtml);
	    return;
	  }
	
	  parseWikitext(lines[i]).then(function (html) {
	    if (myToken !== flavourRenderToken) return;
	
	    parsedHtml.push(
	      $('<div>').html(html).prop('outerHTML')
	    );
	
	    parseNext(i + 1);
	  });
	})(0);
    }

    $card.on('change input', 'input', render);

    // Layout
    $card.append(
      $title,
      $modeRow,
      $wearRow,
      $oneInRow,
      $pctRow,
      $countRow,
      $out
    );

    // Initial visibility + compute
    setVisibility('wear');
    render();

    $host.empty().append($card);
  }

  function init($content) {
    // Dedicated page target (preferred)
    var $page = $content.find('.ws-dropcalc-page').first();
    if ($page.length) {
      buildCalculator($page);
      return;
    }

    // Fallback: allow embedding elsewhere
    $content.find('.ws-dropcalc').each(function () {
      buildCalculator($(this));
    });
  }

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