MediaWiki:Gadget-catchcalc.js: Difference between revisions

From Pixelrus
Jump to navigation Jump to search
(Create MediaWiki:Gadget-catchcalc.js)
 
m (Update MediaWiki:Gadget-catchcalc.js)
Line 247: Line 247:
else { document.addEventListener( 'DOMContentLoaded', init ); }
else { document.addEventListener( 'DOMContentLoaded', init ); }
}() );
}() );
[[Category:Auto-generated]]

Revision as of 08:38, 15 June 2026

/*
 * 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 ); }
}() );