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) { | ||
var q = 1 - p; | var q = 1 - p; | ||
var p0 = Math.pow(q, t); | var p0 = Math.pow(q, t); | ||
var pge1 = 1 - p0; | var pge1 = 1 - p0; | ||
var mu = t * p; | 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) { | ||
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 $wrap = $('<div>').addClass('ws-drycalc-card'); | 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 $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 $ | var $oneInRow = $('<div>').addClass('ws-drycalc-row'); | ||
var $ | 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' }); | |||
var $ | $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 $ | var $baseStepsRow = $('<div>').addClass('ws-drycalc-row'); | ||
var $ | var $baseStepsLabel = $('<label>').addClass('ws-drycalc-label').text('Base steps per action'); | ||
var $ | 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'); | 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 = | var mode = getMode(); | ||
var | setVisibility(mode); | ||
var | |||
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 (!( | if (!(stepsDone >= 0) || !(norm.pStep > 0 && norm.pStep < 1)) { | ||
return; | return; | ||
} | } | ||
var | var r = calc(norm.pStep, stepsDone); | ||
var | |||
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') | $('<div>').addClass('ws-drycalc-metric') | ||
.append($('<div>').addClass('ws-drycalc-k').text(' | .append($('<div>').addClass('ws-drycalc-k').text('You are at')) | ||
.append($('<div>').addClass('ws-drycalc-v').text(formatNumber( | .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 : '—') + ' ' | $('<div>').addClass('ws-drycalc-chip').text('50%: ' + (isFinite(t50) ? t50 : '—') + ' steps'), | ||
$('<div>').addClass('ws-drycalc-chip').text('90%: ' + (isFinite(t90) ? t90 : '—') + ' ' | $('<div>').addClass('ws-drycalc-chip').text('90%: ' + (isFinite(t90) ? t90 : '—') + ' steps'), | ||
$('<div>').addClass('ws-drycalc-chip').text('95%: ' + (isFinite(t95) ? t95 : '—') + ' ' + | $('<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); | $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);
})();
