MediaWiki:Gadget-catchcalc.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/*
* Pixelmon catch-rate calculator gadget.
* Mounts into <div id="catch-calculator"> on pages in the calculator category.
* Data (species catch rates + ball bonuses) is provided by the companion
* Gadget-catchcalc-data.js file as window.PixelmonCatchData.
*
* This file is the static UI/logic; it is NOT generated. The data file IS.
*/
( function () {
'use strict';
function el( tag, attrs, children ) {
var node = document.createElement( tag );
attrs = attrs || {};
Object.keys( attrs ).forEach( function ( k ) {
if ( k === 'class' ) { node.className = attrs[ k ]; }
else if ( k === 'text' ) { node.textContent = attrs[ k ]; }
else if ( k === 'html' ) { node.innerHTML = attrs[ k ]; }
else { node.setAttribute( k, attrs[ k ] ); }
} );
( children || [] ).forEach( function ( c ) {
node.appendChild( typeof c === 'string' ? document.createTextNode( c ) : c );
} );
return node;
}
// ── catch maths ────────────────────────────────────────────────
// Status multipliers (Pixelmon / Gen VIII): 2.5x sleep & freeze, 1.5x the rest.
var STATUS = {
none: 1, asleep: 2.5, frozen: 2.5,
paralyzed: 1.5, burned: 1.5, poisoned: 1.5
};
// Effective ball bonus given the chosen ball, target, and context toggles.
function ballBonus( ball, ctx ) {
var base = ball.bonus || 1;
if ( base >= 255 ) { return base; } // guaranteed catch sentinel
var c = ball.cond;
if ( !c ) { return base; }
var t = ctx.target;
switch ( c.kind ) {
case 'type':
return ( t.t || [] ).some( function ( x ) {
return c.types.indexOf( x ) !== -1;
} ) ? c.mult : base;
case 'fast':
return ( t.s != null && t.s >= 100 ) ? c.mult : base;
case 'statusSleep':
return ctx.status === 'asleep' ? c.mult : base;
case 'toggle':
return ctx.flags[ c.flag ] ? c.mult : base;
case 'level': { // Level Ball
var mine = ctx.myLevel, lv = ctx.level;
if ( mine >= 4 * lv ) { return 8; }
if ( mine >= 2 * lv ) { return 4; }
if ( mine > lv ) { return 2; }
return 1;
}
case 'nest': { // Nest Ball
var m = ( 41 - ctx.level ) / 10;
return m > 1 ? m : 1;
}
case 'timer': { // Timer Ball
var m2 = 1 + 0.3 * ( ctx.turn - 1 );
return m2 > 4 ? 4 : m2;
}
default:
return base;
}
}
// Gen VIII Heavy Ball adds a flat amount to the catch rate based on weight (kg).
function heavyBonus( weight ) {
if ( weight == null ) { return 0; }
if ( weight >= 300 ) { return 30; }
if ( weight >= 200 ) { return 20; }
if ( weight <= 100 ) { return -20; }
return 0;
}
// Single-throw catch probability (0..1) for one ball.
function catchChance( ball, ctx ) {
var t = ctx.target;
var rate = t.r;
if ( ball.cond && ball.cond.kind === 'heavy' ) {
rate = Math.max( 1, rate + heavyBonus( t.w ) );
}
var bonus = ballBonus( ball, ctx );
if ( bonus >= 255 ) { return 1; }
var hpFrac = ctx.hpPercent / 100; // 0<frac<=1
var a = ( ( 3 - 2 * hpFrac ) * rate * bonus * STATUS[ ctx.status ] ) / 3;
if ( a >= 255 ) { return 1; }
if ( a <= 0 ) { return 0; }
// Shake check: Gen III/IV uses exponent 1/4, Gen V+ (gen8) uses 3/16.
var exp = ball.method === 'gen4' ? 0.25 : 0.1875;
var b = 65536 / Math.pow( 255 / a, exp );
var per = Math.min( 1, b / 65536 );
return Math.pow( per, 4 );
}
// ── UI ────────────────────────────────────────────────────────
function build( root, data ) {
var byName = {};
data.species.forEach( function ( s ) { byName[ s.n ] = s; } );
var form = el( 'div', { class: 'ccalc-form' } );
// Pokémon picker (datalist autocomplete).
var listId = 'ccalc-species-list';
var datalist = el( 'datalist', { id: listId } );
data.species.forEach( function ( s ) {
datalist.appendChild( el( 'option', { value: s.n } ) );
} );
var species = el( 'input', {
class: 'ccalc-input', list: listId, placeholder: 'e.g. Bulbasaur',
autocomplete: 'off'
} );
// Numeric / select controls.
var hp = el( 'input', { type: 'range', min: '1', max: '100', value: '100', class: 'ccalc-range' } );
var hpOut = el( 'span', { class: 'ccalc-hpval', text: '100%' } );
var status = el( 'select', { class: 'ccalc-input' } );
[ [ 'none', 'None' ], [ 'asleep', 'Asleep' ], [ 'frozen', 'Frozen' ],
[ 'paralyzed', 'Paralyzed' ], [ 'burned', 'Burned' ], [ 'poisoned', 'Poisoned' ] ]
.forEach( function ( o ) { status.appendChild( el( 'option', { value: o[ 0 ], text: o[ 1 ] } ) ); } );
var level = el( 'input', { type: 'number', min: '1', max: '100', value: '50', class: 'ccalc-num' } );
var myLevel = el( 'input', { type: 'number', min: '1', max: '100', value: '50', class: 'ccalc-num' } );
var turn = el( 'input', { type: 'number', min: '1', max: '99', value: '1', class: 'ccalc-num' } );
function field( label, control, extra ) {
var kids = [ el( 'label', { class: 'ccalc-label', text: label } ), control ];
if ( extra ) { kids.push( extra ); }
return el( 'div', { class: 'ccalc-field' }, kids );
}
form.appendChild( field( 'Pokémon', species, datalist ) );
form.appendChild( field( 'Remaining HP', hp, hpOut ) );
form.appendChild( field( 'Status', status ) );
form.appendChild( field( 'Target level', level ) );
form.appendChild( field( 'Your Pokémon level', myLevel ) );
form.appendChild( field( 'Battle turn', turn ) );
// Context toggles (only those that are user-decided, not derivable from data).
var flags = {};
var toggleWrap = el( 'div', { class: 'ccalc-toggles' } );
var toggleDefs = [];
data.balls.forEach( function ( b ) {
if ( b.cond && b.cond.kind === 'toggle' ) {
toggleDefs.push( { flag: b.cond.flag, label: b.cond.label } );
}
} );
toggleDefs.forEach( function ( d ) {
var cb = el( 'input', { type: 'checkbox' } );
flags[ d.flag ] = cb;
toggleWrap.appendChild( el( 'label', { class: 'ccalc-toggle' }, [ cb, ' ' + d.label ] ) );
} );
var results = el( 'div', { class: 'ccalc-results' } );
function recompute() {
hpOut.textContent = hp.value + '%';
var target = byName[ species.value ];
if ( !target ) {
results.innerHTML = '<p class="ccalc-hint">Start typing a Pokémon name above to see catch chances.</p>';
return;
}
var ctx = {
target: target,
hpPercent: Math.max( 1, Math.min( 100, +hp.value || 100 ) ),
status: status.value,
level: Math.max( 1, +level.value || 1 ),
myLevel: Math.max( 1, +myLevel.value || 1 ),
turn: Math.max( 1, +turn.value || 1 ),
flags: {}
};
Object.keys( flags ).forEach( function ( k ) { ctx.flags[ k ] = flags[ k ].checked; } );
var rows = data.balls.map( function ( b ) {
var p = catchChance( b, ctx );
return { ball: b, p: p, bonus: ballBonus( b, ctx ) };
} ).sort( function ( a, b ) { return b.p - a.p; } );
var tbl = el( 'table', { class: 'wikitable sortable ccalc-table' } );
tbl.appendChild( el( 'tr', {}, [
el( 'th', { text: 'Poké Ball' } ),
el( 'th', { text: 'Catch chance / throw' } ),
el( 'th', { text: 'Avg. balls to catch' } ),
el( 'th', { text: 'Effective bonus' } )
] ) );
rows.forEach( function ( r ) {
var pct = ( r.p * 100 );
var pctText = r.p >= 0.99995 ? 'Guaranteed' : pct.toFixed( pct < 10 ? 2 : 1 ) + '%';
var avg = r.p > 0 ? ( 1 / r.p ).toFixed( r.p > 0.2 ? 1 : 0 ) : '—';
var tr = el( 'tr', {}, [
el( 'td', { text: r.ball.name } ),
el( 'td', { 'data-sort-value': pct.toFixed( 4 ), text: pctText } ),
el( 'td', { text: avg } ),
el( 'td', { text: ( r.bonus >= 255 ? '∞' : r.bonus.toFixed( 2 ).replace( /\.00$/, '' ) ) + '×' } )
] );
if ( r.p >= 0.99995 ) { tr.className = 'ccalc-guaranteed'; }
tbl.appendChild( tr );
} );
results.innerHTML = '';
var target_t = ( target.t || [] ).join( ' / ' ).toLowerCase();
results.appendChild( el( 'p', { class: 'ccalc-meta', text:
species.value + ' — base catch rate ' + target.r +
( target_t ? ', ' + target_t + '-type' : '' ) +
( target.w != null ? ', ' + target.w + ' kg' : '' ) } ) );
results.appendChild( tbl );
}
[ species, hp, status, level, myLevel, turn ].forEach( function ( c ) {
c.addEventListener( 'input', recompute );
c.addEventListener( 'change', recompute );
} );
Object.keys( flags ).forEach( function ( k ) {
flags[ k ].addEventListener( 'change', recompute );
} );
root.appendChild( form );
if ( toggleDefs.length ) {
root.appendChild( el( 'div', { class: 'ccalc-toggles-wrap' }, [
el( 'div', { class: 'ccalc-label', text: 'Conditional ball situations' } ),
toggleWrap
] ) );
}
root.appendChild( results );
recompute();
}
function init() {
var root = document.getElementById( 'catch-calculator' );
if ( !root ) { return; }
var data = window.PixelmonCatchData;
if ( !data || !data.species || !data.balls ) {
root.textContent = 'Catch calculator data failed to load.';
return;
}
root.classList.add( 'ccalc-ready' );
root.innerHTML = '';
build( root, data );
}
if ( document.readyState !== 'loading' ) { init(); }
else { document.addEventListener( 'DOMContentLoaded', init ); }
}() );