MediaWiki:Gadget-spawncalc.js: Difference between revisions
Jump to navigation
Jump to search
m (Update MediaWiki:Gadget-spawncalc.js) |
m (Update MediaWiki:Gadget-spawncalc.js) |
||
| Line 3: | Line 3: | ||
* | * | ||
* Mounts into <div id="spawn-calculator" data-species="..."> on a species page. | * Mounts into <div id="spawn-calculator" data-species="..."> on a species page. | ||
* | * Features: | ||
* 1. Pick a situation (biome, time, weather, location type, Y-level) and see | * 1. Pick a situation (biome, time, weather, location type, Y-level) and see | ||
* the chance that a single Better-Spawner selection is this species: | * the chance that a single Better-Spawner selection is this species: | ||
* P = sum(rarity of this species' matching spawns) | * P = sum(rarity of this species' matching spawns) | ||
* / sum(rarity of all matching spawns) | * / sum(rarity of all matching spawns) | ||
* 2. "Find best chance" searches the situations | * 2. "Consecutive times": evaluate a block of N consecutive times of day | ||
* | * (cyclic; 1..all). With N > 1 the chance is the per-time chance averaged | ||
* over the block, weighting each time by its real in-game length so longer | |||
* times (e.g. Night) count more than short ones (e.g. Midnight). | |||
* 3. "Find best chance" searches the situations (and, for N > 1, the best | |||
* consecutive time block) giving the highest weighted chance. | |||
* | * | ||
* Data (every species' spawn entries) is provided by the companion | * Data (every species' spawn entries) is provided by the companion | ||
| Line 18: | Line 22: | ||
( function () { | ( function () { | ||
'use strict'; | 'use strict'; | ||
// Active in-game duration (ticks out of 24000) of each WorldTime, derived | |||
// from the Pixelmon WorldTime SEGMENTS partition. Used to weight a block of | |||
// consecutive times by relative length. | |||
var TIME_TICKS = { | |||
DAWN: 1800, MORNING: 7451, DAY: 12001, MIDDAY: 1001, | |||
AFTERNOON: 6000, DUSK: 1800, NIGHT: 10550, MIDNIGHT: 1001 | |||
}; | |||
function el( tag, attrs, children ) { | function el( tag, attrs, children ) { | ||
| Line 52: | Line 64: | ||
var entries = data.entries; | var entries = data.entries; | ||
var speciesEntries = entries.filter( function ( e ) { return e.s === species; } ); | var speciesEntries = entries.filter( function ( e ) { return e.s === species; } ); | ||
var NT = data.times.length; | |||
var timeLen = data.times.map( function ( name ) { return TIME_TICKS[ name ] || 1; } ); | |||
// Pre-bucket entries by method id for the "find best" search. | // Pre-bucket entries by method id for the "find best" search. | ||
| Line 84: | Line 98: | ||
} | } | ||
return { num: num, den: den, p: den > 0 ? num / den : 0, competitors: comp }; | return { num: num, den: den, p: den > 0 ? num / den : 0, competitors: comp }; | ||
} | |||
// Cyclic block of n time indices starting at `start`. | |||
function blockTimes( start, n ) { | |||
n = Math.max( 1, Math.min( NT, n ) ); | |||
var out = []; | |||
for ( var k = 0; k < n; k++ ) { out.push( ( start + k ) % NT ); } | |||
return out; | |||
} | |||
// Length-weighted average of per-time probabilities over a block. | |||
function blockWeighted( pByTime, times ) { | |||
var wsum = 0, acc = 0; | |||
for ( var i = 0; i < times.length; i++ ) { | |||
var t = times[ i ]; | |||
wsum += timeLen[ t ]; | |||
acc += timeLen[ t ] * pByTime[ t ]; | |||
} | |||
return wsum > 0 ? acc / wsum : 0; | |||
} | } | ||
| Line 121: | Line 154: | ||
data.weathers.forEach( function ( w, i ) { | data.weathers.forEach( function ( w, i ) { | ||
weatherSel.appendChild( el( 'option', { value: i, text: titleCase( w ) } ) ); | weatherSel.appendChild( el( 'option', { value: i, text: titleCase( w ) } ) ); | ||
} ); | |||
var consecSel = el( 'input', { | |||
type: 'number', min: '1', max: String( NT ), value: String( Math.min( 3, NT ) ), | |||
class: 'scalc-num', | |||
title: 'Evaluate a block of this many consecutive times of day, starting ' | |||
+ 'at the chosen time. Each time is weighted by its real length, so ' | |||
+ 'longer times (Night) count more than short ones (Midnight). Set to ' | |||
+ NT + ' for any time.' | |||
} ); | } ); | ||
| Line 138: | Line 180: | ||
field( 'Location type', methodSel ), | field( 'Location type', methodSel ), | ||
field( 'Time of day', timeSel ), | field( 'Time of day', timeSel ), | ||
field( 'Consecutive times', consecSel ), | |||
field( 'Weather', weatherSel ), | field( 'Weather', weatherSel ), | ||
field( 'Y-level', ySel ), | field( 'Y-level', ySel ), | ||
el( 'div', { class: 'scalc-field scalc-field-btn' }, [ | el( 'div', { class: 'scalc-field scalc-field-btn' }, [ | ||
el( 'span', { class: 'scalc-label', text: ' | el( 'span', { class: 'scalc-label', text: ' ' } ), bestBtn | ||
] ) | ] ) | ||
] ); | ] ); | ||
| Line 153: | Line 196: | ||
T: +timeSel.value, | T: +timeSel.value, | ||
W: +weatherSel.value, | W: +weatherSel.value, | ||
y: ySel.value === '' ? null : +ySel.value | y: ySel.value === '' ? null : +ySel.value, | ||
N: Math.max( 1, Math.min( NT, parseInt( consecSel.value, 10 ) || 1 ) ) | |||
}; | }; | ||
} | } | ||
| Line 171: | Line 215: | ||
} | } | ||
var s = getInputs(); | var s = getInputs(); | ||
var res = chance( s.B, s.M, s. | var times = blockTimes( s.T, s.N ); | ||
var hit = | var single = times.length === 1; | ||
var p, detailText; | |||
if ( single ) { | |||
var res = chance( s.B, s.M, times[ 0 ], s.W, s.y ); | |||
p = res.p; | |||
detailText = p > 0 | |||
? '~1 in ' + Math.round( 1 / p ) + ' spawns - rarity ' | |||
+ round2( res.num ) + ' of ' + round2( res.den ) + ' vs ' | |||
+ res.competitors + ' competing spawn' | |||
+ ( res.competitors === 1 ? '' : 's' ) + '.' | |||
: ''; | |||
} else { | |||
var p8 = []; | |||
for ( var t = 0; t < NT; t++ ) { p8.push( chance( s.B, s.M, t, s.W, s.y ).p ); } | |||
p = blockWeighted( p8, times ); | |||
var names = times.map( function ( ti ) { return titleCase( data.times[ ti ] ); } ); | |||
detailText = p > 0 | |||
? '~1 in ' + Math.round( 1 / p ) + ' spawns, length-weighted across ' | |||
+ ( s.N >= NT ? 'any time' : names.join( ', ' ) ) + '.' | |||
: ''; | |||
} | |||
var hit = p > 0; | |||
var pctText = !hit ? '0%' | var pctText = !hit ? '0%' | ||
: ( | : ( p * 100 >= 1 ? ( p * 100 ).toFixed( 1 ) : ( p * 100 ).toFixed( 2 ) ) + '%'; | ||
var head = el( 'div', { class: 'scalc-result-head' }, [ | var head = el( 'div', { class: 'scalc-result-head' }, [ | ||
| Line 183: | Line 250: | ||
] ); | ] ); | ||
var detail = hit | var detail = hit | ||
? el( 'span', { class: 'scalc-detail', text: | ? el( 'span', { class: 'scalc-detail', text: detailText } ) | ||
: el( 'span', { class: 'scalc-note', text: | : el( 'span', { class: 'scalc-note', text: | ||
'Does not spawn in this situation. Try "Find best chance" or ' | 'Does not spawn in this situation. Try "Find best chance" or ' | ||
| Line 198: | Line 261: | ||
} | } | ||
// ── | // ── find best: situation (and, for N > 1, best time block) ────── | ||
function findBest() { | function findBest() { | ||
var N = getInputs().N; | |||
var best = null; | var best = null; | ||
for ( var i = 0; i < speciesEntries.length; i++ ) { | for ( var i = 0; i < speciesEntries.length; i++ ) { | ||
| Line 205: | Line 269: | ||
var y = pickY( se ); | var y = pickY( se ); | ||
var biomes = se.b || data.biomes.map( function ( _b, k ) { return k; } ); | var biomes = se.b || data.biomes.map( function ( _b, k ) { return k; } ); | ||
var weathers = se.w || allWeathers; | var weathers = se.w || allWeathers; | ||
for ( var mi = 0; mi < se.m.length; mi++ ) { | for ( var mi = 0; mi < se.m.length; mi++ ) { | ||
| Line 212: | Line 275: | ||
for ( var bi = 0; bi < biomes.length; bi++ ) { | for ( var bi = 0; bi < biomes.length; bi++ ) { | ||
var B = biomes[ bi ]; | var B = biomes[ bi ]; | ||
var sub = []; | var sub = []; | ||
for ( var pj = 0; pj < pool.length; pj++ ) { | for ( var pj = 0; pj < pool.length; pj++ ) { | ||
| Line 221: | Line 283: | ||
sub.push( e ); | sub.push( e ); | ||
} | } | ||
for ( var | for ( var wi = 0; wi < weathers.length; wi++ ) { | ||
var | var W = weathers[ wi ]; | ||
for ( var | // per-time num/den over this (B, M, W, y) subset in one pass | ||
var num = [], den = []; | |||
for ( var z = 0; z < NT; z++ ) { num.push( 0 ); den.push( 0 ); } | |||
for ( var k = 0; k < sub.length; k++ ) { | |||
var c = sub[ k ]; | |||
if ( c.w && !inList( c.w, W ) ) { continue; } | |||
var ct = c.t || allTimes; | |||
den += c.r; | for ( var ti = 0; ti < ct.length; ti++ ) { | ||
if ( c.s === species ) { num += c.r; } | var tt = ct[ ti ]; | ||
den[ tt ] += c.r; | |||
if ( c.s === species ) { num[ tt ] += c.r; } | |||
} | } | ||
} | |||
var p8 = []; | |||
for ( var q = 0; q < NT; q++ ) { p8.push( den[ q ] > 0 ? num[ q ] / den[ q ] : 0 ); } | |||
for ( var start = 0; start < NT; start++ ) { | |||
var times = blockTimes( start, N ); | |||
var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times ); | |||
if ( p > 0 && ( !best || p > best.p ) ) { | if ( p > 0 && ( !best || p > best.p ) ) { | ||
best = { B: B, M: M | best = { B: B, M: M, W: W, y: y, start: start, p: p }; | ||
} | } | ||
if ( N >= NT ) { break; } // whole-day block: start is irrelevant | |||
} | } | ||
} | } | ||
| Line 245: | Line 315: | ||
biomeSel.value = best.B; | biomeSel.value = best.B; | ||
methodSel.value = best.M; | methodSel.value = best.M; | ||
weatherSel.value = best.W; | weatherSel.value = best.W; | ||
ySel.value = best.y; | ySel.value = best.y; | ||
timeSel.value = best.start; | |||
flagBest = true; | flagBest = true; | ||
recompute(); | recompute(); | ||
| Line 278: | Line 348: | ||
} | } | ||
[ biomeSel, methodSel, timeSel, weatherSel, ySel ].forEach( function ( c ) { | [ biomeSel, methodSel, timeSel, weatherSel, ySel, consecSel ].forEach( function ( c ) { | ||
c.addEventListener( 'change', recompute ); | c.addEventListener( 'change', recompute ); | ||
c.addEventListener( 'input', recompute ); | c.addEventListener( 'input', recompute ); | ||
Revision as of 07:24, 17 June 2026
/*
* Pixelmon spawn-chance calculator gadget.
*
* Mounts into <div id="spawn-calculator" data-species="..."> on a species page.
* 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. "Consecutive times": evaluate a block of N consecutive times of day
* (cyclic; 1..all). With N > 1 the chance is the per-time chance averaged
* over the block, weighting each time by its real in-game length so longer
* times (e.g. Night) count more than short ones (e.g. Midnight).
* 3. "Find best chance" searches the situations (and, for N > 1, the best
* consecutive time block) giving the highest weighted chance.
*
* 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';
// Active in-game duration (ticks out of 24000) of each WorldTime, derived
// from the Pixelmon WorldTime SEGMENTS partition. Used to weight a block of
// consecutive times by relative length.
var TIME_TICKS = {
DAWN: 1800, MORNING: 7451, DAY: 12001, MIDDAY: 1001,
AFTERNOON: 6000, DUSK: 1800, NIGHT: 10550, MIDNIGHT: 1001
};
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; } );
var NT = data.times.length;
var timeLen = data.times.map( function ( name ) { return TIME_TICKS[ name ] || 1; } );
// 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 };
}
// Cyclic block of n time indices starting at `start`.
function blockTimes( start, n ) {
n = Math.max( 1, Math.min( NT, n ) );
var out = [];
for ( var k = 0; k < n; k++ ) { out.push( ( start + k ) % NT ); }
return out;
}
// Length-weighted average of per-time probabilities over a block.
function blockWeighted( pByTime, times ) {
var wsum = 0, acc = 0;
for ( var i = 0; i < times.length; i++ ) {
var t = times[ i ];
wsum += timeLen[ t ];
acc += timeLen[ t ] * pByTime[ t ];
}
return wsum > 0 ? acc / wsum : 0;
}
// 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 consecSel = el( 'input', {
type: 'number', min: '1', max: String( NT ), value: String( Math.min( 3, NT ) ),
class: 'scalc-num',
title: 'Evaluate a block of this many consecutive times of day, starting '
+ 'at the chosen time. Each time is weighted by its real length, so '
+ 'longer times (Night) count more than short ones (Midnight). Set to '
+ NT + ' for any time.'
} );
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( 'Consecutive times', consecSel ),
field( 'Weather', weatherSel ),
field( 'Y-level', ySel ),
el( 'div', { class: 'scalc-field scalc-field-btn' }, [
el( 'span', { class: 'scalc-label', text: ' ' } ), bestBtn
] )
] );
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,
N: Math.max( 1, Math.min( NT, parseInt( consecSel.value, 10 ) || 1 ) )
};
}
var flagBest = false;
function recompute() {
results.innerHTML = '';
if ( !speciesEntries.length ) {
results.appendChild( el( 'div', { class: 'scalc-result scalc-result-miss' }, [
el( 'span', { class: 'scalc-note', text:
species + ' has no standard overworld spawns (legendary, boss, '
+ 'fossil, evolution-only or event Pokemon).' } )
] ) );
flagBest = false;
return;
}
var s = getInputs();
var times = blockTimes( s.T, s.N );
var single = times.length === 1;
var p, detailText;
if ( single ) {
var res = chance( s.B, s.M, times[ 0 ], s.W, s.y );
p = res.p;
detailText = p > 0
? '~1 in ' + Math.round( 1 / p ) + ' spawns - rarity '
+ round2( res.num ) + ' of ' + round2( res.den ) + ' vs '
+ res.competitors + ' competing spawn'
+ ( res.competitors === 1 ? '' : 's' ) + '.'
: '';
} else {
var p8 = [];
for ( var t = 0; t < NT; t++ ) { p8.push( chance( s.B, s.M, t, s.W, s.y ).p ); }
p = blockWeighted( p8, times );
var names = times.map( function ( ti ) { return titleCase( data.times[ ti ] ); } );
detailText = p > 0
? '~1 in ' + Math.round( 1 / p ) + ' spawns, length-weighted across '
+ ( s.N >= NT ? 'any time' : names.join( ', ' ) ) + '.'
: '';
}
var hit = p > 0;
var pctText = !hit ? '0%'
: ( p * 100 >= 1 ? ( p * 100 ).toFixed( 1 ) : ( p * 100 ).toFixed( 2 ) ) + '%';
var head = el( 'div', { class: 'scalc-result-head' }, [
el( 'span', { class: 'scalc-pct', text: pctText } ),
el( 'span', { class: 'scalc-pctlabel', text:
'chance a wild spawn here is ' + species
+ ( flagBest && hit ? ' (best found)' : '' ) } )
] );
var detail = hit
? el( 'span', { class: 'scalc-detail', text: detailText } )
: el( 'span', { class: 'scalc-note', text:
'Does not spawn in this situation. Try "Find best chance" or '
+ 'change the biome / location type / time.' } );
results.appendChild( el( 'div', {
class: 'scalc-result ' + ( hit ? 'scalc-result-hit' : 'scalc-result-miss' )
+ ( flagBest && hit ? ' scalc-result-best' : '' )
}, [ head, detail ] ) );
flagBest = false;
}
// ── find best: situation (and, for N > 1, best time block) ──────
function findBest() {
var N = getInputs().N;
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 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 ];
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 wi = 0; wi < weathers.length; wi++ ) {
var W = weathers[ wi ];
// per-time num/den over this (B, M, W, y) subset in one pass
var num = [], den = [];
for ( var z = 0; z < NT; z++ ) { num.push( 0 ); den.push( 0 ); }
for ( var k = 0; k < sub.length; k++ ) {
var c = sub[ k ];
if ( c.w && !inList( c.w, W ) ) { continue; }
var ct = c.t || allTimes;
for ( var ti = 0; ti < ct.length; ti++ ) {
var tt = ct[ ti ];
den[ tt ] += c.r;
if ( c.s === species ) { num[ tt ] += c.r; }
}
}
var p8 = [];
for ( var q = 0; q < NT; q++ ) { p8.push( den[ q ] > 0 ? num[ q ] / den[ q ] : 0 ); }
for ( var start = 0; start < NT; start++ ) {
var times = blockTimes( start, N );
var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times );
if ( p > 0 && ( !best || p > best.p ) ) {
best = { B: B, M: M, W: W, y: y, start: start, p: p };
}
if ( N >= NT ) { break; } // whole-day block: start is irrelevant
}
}
}
}
}
if ( !best ) { recompute(); return; }
biomeSel.value = best.B;
methodSel.value = best.M;
weatherSel.value = best.W;
ySel.value = best.y;
timeSel.value = best.start;
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, consecSel ].forEach( function ( c ) {
c.addEventListener( 'change', recompute );
c.addEventListener( 'input', recompute );
} );
bestBtn.addEventListener( 'click', findBest );
// ── assemble the card (compact: header, controls row, result row) ──
var header = el( 'div', { class: 'scalc-header' }, [
el( 'span', { class: 'scalc-header-title', text: 'Spawn Chance Calculator' } )
] );
var body = el( 'div', { class: 'scalc-body' }, [ form, 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 ); }
}() );