MediaWiki:Gadget-spawncalc.js: Difference between revisions

From Pixelrus
Jump to navigation Jump to search
(Create MediaWiki:Gadget-spawncalc.js)
 
m (Update MediaWiki:Gadget-spawncalc.js)
Line 41: Line 41:
return c.toUpperCase();
return c.toUpperCase();
} );
} );
}
function titleCase( s ) {
return s.charAt( 0 ) + s.slice( 1 ).toLowerCase();
}
}


Line 89: Line 93:
return y;
return y;
}
}
function round2( n ) { return Math.round( n * 100 ) / 100; }


// ── controls ──────────────────────────────────────────────────
// ── controls ──────────────────────────────────────────────────
Line 109: Line 115:
var timeSel = el( 'select', { class: 'scalc-input' } );
var timeSel = el( 'select', { class: 'scalc-input' } );
data.times.forEach( function ( t, i ) {
data.times.forEach( function ( t, i ) {
timeSel.appendChild( el( 'option', { value: i, text: t.charAt( 0 ) + t.slice( 1 ).toLowerCase() } ) );
timeSel.appendChild( el( 'option', { value: i, text: titleCase( t ) } ) );
} );
} );


var weatherSel = el( 'select', { class: 'scalc-input' } );
var weatherSel = el( 'select', { class: 'scalc-input' } );
data.weathers.forEach( function ( w, i ) {
data.weathers.forEach( function ( w, i ) {
weatherSel.appendChild( el( 'option', { value: i, text: w.charAt( 0 ) + w.slice( 1 ).toLowerCase() } ) );
weatherSel.appendChild( el( 'option', { value: i, text: titleCase( w ) } ) );
} );
} );


Line 126: Line 132:


var bestBtn = el( 'button', { class: 'scalc-btn', type: 'button',
var bestBtn = el( 'button', { class: 'scalc-btn', type: 'button',
text: 'Find best chance' } );
text: 'Find best chance' } );


var form = el( 'div', { class: 'scalc-form' }, [
var form = el( 'div', { class: 'scalc-form' }, [
Line 133: Line 139:
field( 'Time of day', timeSel ),
field( 'Time of day', timeSel ),
field( 'Weather', weatherSel ),
field( 'Weather', weatherSel ),
field( 'Y-level', ySel ),
field( 'Y-level', ySel )
el( 'div', { class: 'scalc-field scalc-btnfield' }, [
el( 'label', { class: 'scalc-label', text: ' ' } ), bestBtn
] )
] );
] );


Line 150: Line 153:
};
};
}
}
var flagBest = false;


function recompute() {
function recompute() {
var s = getInputs();
var res = chance( s.B, s.M, s.T, s.W, s.y );
results.innerHTML = '';
results.innerHTML = '';
if ( !speciesEntries.length ) {
if ( !speciesEntries.length ) {
results.appendChild( el( 'p', { class: 'scalc-note', text:
results.appendChild( el( 'div', { class: 'scalc-result scalc-result-miss' }, [
species + ' has no standard overworld spawns (it may be a '
el( 'p', { class: 'scalc-note', text:
+ 'legendary, boss, fossil, evolution-only or event Pokemon).' } ) );
species + ' has no standard overworld spawns. It may be a '
+ 'legendary, boss, fossil, evolution-only or event Pokemon.' } )
] ) );
flagBest = false;
return;
return;
}
}
var pctText = res.p <= 0 ? '0%'
var s = getInputs();
var res = chance( s.B, s.M, s.T, s.W, s.y );
var hit = res.p > 0;
var pctText = !hit ? '0%'
: ( res.p * 100 >= 1 ? ( res.p * 100 ).toFixed( 1 ) : ( res.p * 100 ).toFixed( 2 ) ) + '%';
: ( res.p * 100 >= 1 ? ( res.p * 100 ).toFixed( 1 ) : ( res.p * 100 ).toFixed( 2 ) ) + '%';
var big = el( 'div', { class: 'scalc-big' }, [
 
el( 'span', { class: 'scalc-pct', text: pctText } ),
var kids = [];
el( 'span', { class: 'scalc-pctlabel', text: ' chance per spawn selection' } )
if ( flagBest && hit ) {
] );
kids.push( el( 'div', { class: 'scalc-badge', text: '✨ Best odds found' } ) );
results.appendChild( big );
}
if ( res.p <= 0 ) {
kids.push( el( 'div', { class: 'scalc-pct', text: pctText } ) );
results.appendChild( el( 'p', { class: 'scalc-note', text:
kids.push( el( 'div', { class: 'scalc-pctlabel', text:
species + ' does not spawn in this exact situation. Try "Find '
'chance a wild spawn here is ' + species } ) );
+ 'best chance", or change the biome / location type / time.' } ) );
if ( hit ) {
kids.push( el( 'div', { class: 'scalc-detail', text:
'About 1 in ' + Math.round( 1 / res.p ) + ' spawns. '
+ species + ' rarity ' + round2( res.num ) + ' of ' + round2( res.den )
+ ' total, against ' + res.competitors + ' competing spawn'
+ ( res.competitors === 1 ? '' : 's' ) + ' here.' } ) );
} else {
} else {
results.appendChild( el( 'p', { class: 'scalc-detail', text:
kids.push( el( 'div', { class: 'scalc-note', text:
species + ' rarity ' + round2( res.num ) + ' of ' + round2( res.den )
species + ' does not spawn in this exact situation. Try '
+ ' total across ' + res.competitors + ' competing spawn'
+ '“Find best chance”, or change the biome, location '
+ ( res.competitors === 1 ? '' : 's' ) + ' here'
+ 'type or time.' } ) );
+ ( res.p > 0 ? ' (about 1 in ' + Math.round( 1 / res.p ) + ' spawns).' : '.' ) } ) );
}
}
results.appendChild( el( 'div', {
class: 'scalc-result ' + ( hit ? 'scalc-result-hit' : 'scalc-result-miss' )
}, kids ) );
flagBest = false;
}
}
function round2( n ) { return Math.round( n * 100 ) / 100; }


// ── feature 2: search the species' situations for the best P ────
// ── feature 2: search the species' situations for the best P ────
Line 210: Line 225:
for ( var wi = 0; wi < weathers.length; wi++ ) {
for ( var wi = 0; wi < weathers.length; wi++ ) {
var W = weathers[ wi ];
var W = weathers[ wi ];
var num = 0, den = 0, comp = 0;
var num = 0, den = 0;
for ( var k = 0; k < sub.length; k++ ) {
for ( var k = 0; k < sub.length; k++ ) {
var c = sub[ k ];
var c = sub[ k ];
Line 217: Line 232:
den += c.r;
den += c.r;
if ( c.s === species ) { num += c.r; }
if ( c.s === species ) { num += c.r; }
else { comp++; }
}
}
var p = den > 0 ? num / den : 0;
var p = den > 0 ? num / den : 0;
Line 229: Line 243:
}
}
if ( !best ) { recompute(); return; }
if ( !best ) { recompute(); return; }
// reflect the winning situation in the form, then recompute.
biomeSel.value = best.B;
biomeSel.value = best.B;
methodSel.value = best.M;
methodSel.value = best.M;
Line 235: Line 248:
weatherSel.value = best.W;
weatherSel.value = best.W;
ySel.value = best.y;
ySel.value = best.y;
flagBest = true;
recompute();
recompute();
results.insertBefore(
el( 'p', { class: 'scalc-best', text:
'Best situation found: highest chance to encounter ' + species
+ ' is shown above for these settings.' } ),
results.firstChild
);
}
}


Line 256: Line 264:
if ( idx !== -1 ) { common[ idx ] = true; }
if ( idx !== -1 ) { common[ idx ] = true; }
} );
} );
var best = null;
var pick = null;
speciesEntries.forEach( function ( e ) {
speciesEntries.forEach( function ( e ) {
var isCommon = e.m.some( function ( m ) { return common[ m ]; } );
var isCommon = e.m.some( function ( m ) { return common[ m ]; } );
var score = ( isCommon ? 1e6 : 0 ) + e.r;
var score = ( isCommon ? 1e6 : 0 ) + e.r;
if ( !best || score > best.score ) { best = { e: e, score: score }; }
if ( !pick || score > pick.score ) { pick = { e: e, score: score }; }
} );
} );
var se = best.e;
var se = pick.e;
var m0 = se.m.filter( function ( m ) { return common[ m ]; } )[ 0 ];
var m0 = se.m.filter( function ( m ) { return common[ m ]; } )[ 0 ];
methodSel.value = ( m0 != null ? m0 : se.m[ 0 ] );
methodSel.value = ( m0 != null ? m0 : se.m[ 0 ] );
Line 276: Line 284:
bestBtn.addEventListener( 'click', findBest );
bestBtn.addEventListener( 'click', findBest );


root.appendChild( el( 'p', { class: 'scalc-intro', text:
// ── assemble the card ──────────────────────────────────────────
'Estimate the chance that a single wild spawn near you is ' + species
var header = el( 'div', { class: 'scalc-header' }, [
+ ' in a given situation, then let the calculator search for the best '
el( 'span', { class: 'scalc-header-icon', text: '🧭' } ),
+ 'settings.' } ) );
el( 'span', { class: 'scalc-header-title', text: 'Spawn Chance Calculator' } )
root.appendChild( form );
] );
root.appendChild( results );
var conditions = el( 'div', { class: 'scalc-section' }, [
el( 'div', { class: 'scalc-section-label', text: 'Conditions' } ),
form
] );
var actions = el( 'div', { class: 'scalc-actions' }, [
bestBtn,
el( 'span', { class: 'scalc-actions-hint', text:
'Searches every biome, time and method for the highest odds.' } )
] );
var body = el( 'div', { class: 'scalc-body' }, [
el( 'p', { class: 'scalc-intro', text:
'Estimate the chance that a single wild spawn near you is ' + species
+ ', then auto-find the settings with the best odds.' } ),
conditions,
actions,
results
] );
root.appendChild( header );
root.appendChild( body );
setDefaults();
setDefaults();
recompute();
recompute();

Revision as of 07:05, 17 June 2026

/*
 * Pixelmon spawn-chance calculator gadget.
 *
 * Mounts into <div id="spawn-calculator" data-species="..."> on a species page.
 * Two features:
 *   1. Pick a situation (biome, time, weather, location type, Y-level) and see
 *      the chance that a single Better-Spawner selection is this species:
 *        P = sum(rarity of this species' matching spawns)
 *            / sum(rarity of all matching spawns)
 *   2. "Find best chance" searches the situations where this species can spawn
 *      for the combination giving the highest P, and fills the form with it.
 *
 * Data (every species' spawn entries) is provided by the companion
 * Gadget-spawncalc-data.js file as window.PixelmonSpawnData.
 *
 * This file is 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 { node.setAttribute( k, attrs[ k ] ); }
		} );
		( children || [] ).forEach( function ( c ) {
			node.appendChild( typeof c === 'string' ? document.createTextNode( c ) : c );
		} );
		return node;
	}

	// "minecraft:plains" -> "Plains", "#pixelmon:spawning/mesas" -> "Mesas".
	function prettyBiome( id ) {
		var s = id.charAt( 0 ) === '#' ? id.slice( 1 ) : id;
		if ( s.indexOf( ':' ) !== -1 ) { s = s.split( ':' )[ 1 ]; }
		if ( s.indexOf( '/' ) !== -1 ) { s = s.split( '/' ).pop(); }
		return s.replace( /_/g, ' ' ).replace( /\b\w/g, function ( c ) {
			return c.toUpperCase();
		} );
	}

	function titleCase( s ) {
		return s.charAt( 0 ) + s.slice( 1 ).toLowerCase();
	}

	function inList( list, v ) { return list && list.indexOf( v ) !== -1; }

	function build( root, data, species ) {
		var entries = data.entries;
		var speciesEntries = entries.filter( function ( e ) { return e.s === species; } );

		// Pre-bucket entries by method id for the "find best" search.
		var byMethod = {};
		entries.forEach( function ( e ) {
			e.m.forEach( function ( m ) {
				( byMethod[ m ] = byMethod[ m ] || [] ).push( e );
			} );
		} );

		// ── situation match + probability ─────────────────────────────
		function matches( e, B, M, T, W, y ) {
			if ( !inList( e.m, M ) ) { return false; }
			if ( e.b && !inList( e.b, B ) ) { return false; }
			if ( e.t && !inList( e.t, T ) ) { return false; }
			if ( e.w && !inList( e.w, W ) ) { return false; }
			if ( y !== null ) {
				if ( e.y0 != null && y < e.y0 ) { return false; }
				if ( e.y1 != null && y > e.y1 ) { return false; }
			}
			return true;
		}

		function chance( B, M, T, W, y ) {
			var num = 0, den = 0, comp = 0;
			for ( var i = 0; i < entries.length; i++ ) {
				if ( matches( entries[ i ], B, M, T, W, y ) ) {
					den += entries[ i ].r;
					if ( entries[ i ].s === species ) { num += entries[ i ].r; }
					else { comp++; }
				}
			}
			return { num: num, den: den, p: den > 0 ? num / den : 0, competitors: comp };
		}

		// Default Y inside an entry's range (else 63).
		function pickY( e ) {
			var y = 63;
			if ( e.y0 != null && y < e.y0 ) { y = e.y0; }
			if ( e.y1 != null && y > e.y1 ) { y = e.y1; }
			return y;
		}

		function round2( n ) { return Math.round( n * 100 ) / 100; }

		// ── controls ──────────────────────────────────────────────────
		var allTimes = data.times.map( function ( _t, i ) { return i; } );
		var allWeathers = data.weathers.map( function ( _w, i ) { return i; } );

		var biomeSel = el( 'select', { class: 'scalc-input' } );
		data.biomes
			.map( function ( id, i ) { return { i: i, name: prettyBiome( id ) }; } )
			.sort( function ( a, b ) { return a.name.localeCompare( b.name ); } )
			.forEach( function ( o ) {
				biomeSel.appendChild( el( 'option', { value: o.i, text: o.name } ) );
			} );

		var methodSel = el( 'select', { class: 'scalc-input' } );
		data.methods.forEach( function ( m, i ) {
			methodSel.appendChild( el( 'option', { value: i, text: m } ) );
		} );

		var timeSel = el( 'select', { class: 'scalc-input' } );
		data.times.forEach( function ( t, i ) {
			timeSel.appendChild( el( 'option', { value: i, text: titleCase( t ) } ) );
		} );

		var weatherSel = el( 'select', { class: 'scalc-input' } );
		data.weathers.forEach( function ( w, i ) {
			weatherSel.appendChild( el( 'option', { value: i, text: titleCase( w ) } ) );
		} );

		var ySel = el( 'input', { type: 'number', min: '-64', max: '320', value: '63', class: 'scalc-num' } );

		function field( label, control ) {
			return el( 'div', { class: 'scalc-field' }, [
				el( 'label', { class: 'scalc-label', text: label } ), control
			] );
		}

		var bestBtn = el( 'button', { class: 'scalc-btn', type: 'button',
			text: '✨ Find best chance' } );

		var form = el( 'div', { class: 'scalc-form' }, [
			field( 'Biome', biomeSel ),
			field( 'Location type', methodSel ),
			field( 'Time of day', timeSel ),
			field( 'Weather', weatherSel ),
			field( 'Y-level', ySel )
		] );

		var results = el( 'div', { class: 'scalc-results' } );

		function getInputs() {
			return {
				B: +biomeSel.value,
				M: +methodSel.value,
				T: +timeSel.value,
				W: +weatherSel.value,
				y: ySel.value === '' ? null : +ySel.value
			};
		}

		var flagBest = false;

		function recompute() {
			results.innerHTML = '';
			if ( !speciesEntries.length ) {
				results.appendChild( el( 'div', { class: 'scalc-result scalc-result-miss' }, [
					el( 'p', { class: 'scalc-note', text:
						species + ' has no standard overworld spawns. It may be a '
						+ 'legendary, boss, fossil, evolution-only or event Pokemon.' } )
				] ) );
				flagBest = false;
				return;
			}
			var s = getInputs();
			var res = chance( s.B, s.M, s.T, s.W, s.y );
			var hit = res.p > 0;
			var pctText = !hit ? '0%'
				: ( res.p * 100 >= 1 ? ( res.p * 100 ).toFixed( 1 ) : ( res.p * 100 ).toFixed( 2 ) ) + '%';

			var kids = [];
			if ( flagBest && hit ) {
				kids.push( el( 'div', { class: 'scalc-badge', text: '✨ Best odds found' } ) );
			}
			kids.push( el( 'div', { class: 'scalc-pct', text: pctText } ) );
			kids.push( el( 'div', { class: 'scalc-pctlabel', text:
				'chance a wild spawn here is ' + species } ) );
			if ( hit ) {
				kids.push( el( 'div', { class: 'scalc-detail', text:
					'About 1 in ' + Math.round( 1 / res.p ) + ' spawns. '
					+ species + ' rarity ' + round2( res.num ) + ' of ' + round2( res.den )
					+ ' total, against ' + res.competitors + ' competing spawn'
					+ ( res.competitors === 1 ? '' : 's' ) + ' here.' } ) );
			} else {
				kids.push( el( 'div', { class: 'scalc-note', text:
					species + ' does not spawn in this exact situation. Try '
					+ '“Find best chance”, or change the biome, location '
					+ 'type or time.' } ) );
			}
			results.appendChild( el( 'div', {
				class: 'scalc-result ' + ( hit ? 'scalc-result-hit' : 'scalc-result-miss' )
			}, kids ) );
			flagBest = false;
		}

		// ── feature 2: search the species' situations for the best P ────
		function findBest() {
			var best = null;
			for ( var i = 0; i < speciesEntries.length; i++ ) {
				var se = speciesEntries[ i ];
				var y = pickY( se );
				var biomes = se.b || data.biomes.map( function ( _b, k ) { return k; } );
				var times = se.t || allTimes;
				var weathers = se.w || allWeathers;
				for ( var mi = 0; mi < se.m.length; mi++ ) {
					var M = se.m[ mi ];
					var pool = byMethod[ M ] || [];
					for ( var bi = 0; bi < biomes.length; bi++ ) {
						var B = biomes[ bi ];
						// biome- and y-filtered competitor subset for this (B, M)
						var sub = [];
						for ( var pj = 0; pj < pool.length; pj++ ) {
							var e = pool[ pj ];
							if ( e.b && !inList( e.b, B ) ) { continue; }
							if ( e.y0 != null && y < e.y0 ) { continue; }
							if ( e.y1 != null && y > e.y1 ) { continue; }
							sub.push( e );
						}
						for ( var ti = 0; ti < times.length; ti++ ) {
							var T = times[ ti ];
							for ( var wi = 0; wi < weathers.length; wi++ ) {
								var W = weathers[ wi ];
								var num = 0, den = 0;
								for ( var k = 0; k < sub.length; k++ ) {
									var c = sub[ k ];
									if ( c.t && !inList( c.t, T ) ) { continue; }
									if ( c.w && !inList( c.w, W ) ) { continue; }
									den += c.r;
									if ( c.s === species ) { num += c.r; }
								}
								var p = den > 0 ? num / den : 0;
								if ( p > 0 && ( !best || p > best.p ) ) {
									best = { B: B, M: M, T: T, W: W, y: y, p: p };
								}
							}
						}
					}
				}
			}
			if ( !best ) { recompute(); return; }
			biomeSel.value = best.B;
			methodSel.value = best.M;
			timeSel.value = best.T;
			weatherSel.value = best.W;
			ySel.value = best.y;
			flagBest = true;
			recompute();
		}

		// Default the form to a representative situation where the species
		// spawns: prefer a common overworld method (Grass/Land/Water/...) and the
		// highest-rarity such entry, so the landing view is not a niche 100%
		// (e.g. a single Curry flavour).
		function setDefaults() {
			if ( !speciesEntries.length ) { return; }
			var common = {};
			[ 'Grass', 'Land', 'Water', 'Surface Water', 'Air', 'Tree Top',
			  'Underground', 'Seafloor' ].forEach( function ( name ) {
				var idx = data.methods.indexOf( name );
				if ( idx !== -1 ) { common[ idx ] = true; }
			} );
			var pick = null;
			speciesEntries.forEach( function ( e ) {
				var isCommon = e.m.some( function ( m ) { return common[ m ]; } );
				var score = ( isCommon ? 1e6 : 0 ) + e.r;
				if ( !pick || score > pick.score ) { pick = { e: e, score: score }; }
			} );
			var se = pick.e;
			var m0 = se.m.filter( function ( m ) { return common[ m ]; } )[ 0 ];
			methodSel.value = ( m0 != null ? m0 : se.m[ 0 ] );
			if ( se.b && se.b.length ) { biomeSel.value = se.b[ 0 ]; }
			if ( se.t && se.t.length ) { timeSel.value = se.t[ 0 ]; }
			ySel.value = pickY( se );
		}

		[ biomeSel, methodSel, timeSel, weatherSel, ySel ].forEach( function ( c ) {
			c.addEventListener( 'change', recompute );
			c.addEventListener( 'input', recompute );
		} );
		bestBtn.addEventListener( 'click', findBest );

		// ── assemble the card ──────────────────────────────────────────
		var header = el( 'div', { class: 'scalc-header' }, [
			el( 'span', { class: 'scalc-header-icon', text: '🧭' } ),
			el( 'span', { class: 'scalc-header-title', text: 'Spawn Chance Calculator' } )
		] );
		var conditions = el( 'div', { class: 'scalc-section' }, [
			el( 'div', { class: 'scalc-section-label', text: 'Conditions' } ),
			form
		] );
		var actions = el( 'div', { class: 'scalc-actions' }, [
			bestBtn,
			el( 'span', { class: 'scalc-actions-hint', text:
				'Searches every biome, time and method for the highest odds.' } )
		] );
		var body = el( 'div', { class: 'scalc-body' }, [
			el( 'p', { class: 'scalc-intro', text:
				'Estimate the chance that a single wild spawn near you is ' + species
				+ ', then auto-find the settings with the best odds.' } ),
			conditions,
			actions,
			results
		] );
		root.appendChild( header );
		root.appendChild( body );
		setDefaults();
		recompute();
	}

	function init() {
		var root = document.getElementById( 'spawn-calculator' );
		if ( !root ) { return; }
		var data = window.PixelmonSpawnData;
		var species = root.getAttribute( 'data-species' );
		if ( !data || !data.entries || !species ) {
			root.textContent = 'Spawn calculator data failed to load.';
			return;
		}
		root.classList.add( 'scalc-ready' );
		root.innerHTML = '';
		build( root, data, species );
	}

	if ( document.readyState !== 'loading' ) { init(); }
	else { document.addEventListener( 'DOMContentLoaded', init ); }
}() );