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) |
||
| (14 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* | /* | ||
* Pixelmon spawn-chance calculator gadget. | * Pixelmon spawn-chance calculator gadget (full-page). | ||
* | * | ||
* Mounts into <div id="spawn-calculator" data-species="..."> on | * Mounts into <div id="spawn-calculator" data-species="..."> on the standalone | ||
* | * Spawn Calculator page. data-species is normally empty; a ?species=Name query | ||
* parameter (the link on each species page) pre-fills the picker. | |||
* | |||
* | * | ||
* | * Left column: pick a species and situation (biome, location type, time of day, | ||
* | * weather, Y-level, light level, moon phase). Only dimensions that constrain the | ||
* chosen species are shown, and each categorical filter lists only values where | |||
* the species can spawn. Each shown field can be locked so "Find best chance" | |||
* leaves it fixed. Find best chance lists the top areas; clicking one fills the | |||
* form and shows that area's full spawn breakdown on the right. | |||
* | * | ||
* Right column: the chosen species' chance, plus every species that can spawn in | |||
* the current situation with its share, the chosen species highlighted. | |||
* | |||
* Data is provided by Gadget-spawncalc-data.js as window.PixelmonSpawnData. | |||
* This file is static UI/logic; it is NOT generated. The data file IS. | * This file is static UI/logic; it is NOT generated. The data file IS. | ||
*/ | */ | ||
| Line 26: | Line 22: | ||
'use strict'; | 'use strict'; | ||
var TIME_TICKS = { | var TIME_TICKS = { | ||
DAWN: 1800, MORNING: 7451, DAY: 12001, MIDDAY: 1001, | DAWN: 1800, MORNING: 7451, DAY: 12001, MIDDAY: 1001, | ||
| Line 48: | Line 41: | ||
} | } | ||
function prettyBiome( id ) { | function prettyBiome( id ) { | ||
var s = id.charAt( 0 ) === '#' ? id.slice( 1 ) : 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( ':' )[ 1 ]; } | ||
if ( s.indexOf( '/' ) !== -1 ) { s = s.split( '/' ).pop(); } | if ( s.indexOf( '/' ) !== -1 ) { s = s.split( '/' ).pop(); } | ||
return s.replace( /_/g, ' ' ).replace( /\b\w/g, function ( c ) { | 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 uniq( arr ) { | |||
var seen = {}, out = []; | |||
arr.forEach( function ( v ) { if ( !seen[ v ] ) { seen[ v ] = 1; out.push( v ); } } ); | |||
return out; | |||
} | |||
function getParam( name ) { | |||
var m = new RegExp( '[?&]' + name + '=([^&#]*)' ).exec( location.search || '' ); | |||
if ( !m ) { return null; } | |||
try { return decodeURIComponent( m[ 1 ].replace( /\+/g, ' ' ) ); } catch ( e ) { return m[ 1 ]; } | |||
} | |||
function articlePath() { | |||
return ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/index.php/$1'; | |||
} | |||
function pageHref( title ) { | |||
return articlePath().replace( '$1', encodeURIComponent( String( title ).replace( / /g, '_' ) ) ); | |||
} | } | ||
function fileHref( filename ) { | |||
function | return articlePath().replace( '$1', 'Special:FilePath/' + encodeURIComponent( filename ) ); | ||
return | |||
} | } | ||
function build( root, data, initialSpecies ) { | |||
function build( root, data, | |||
var entries = data.entries; | var entries = data.entries; | ||
var NT = data.times.length; | var NT = data.times.length; | ||
var timeLen = data.times.map( function ( name ) { return TIME_TICKS[ name ] || 1; } ); | var timeLen = data.times.map( function ( name ) { return TIME_TICKS[ name ] || 1; } ); | ||
var allTimes = data.times.map( function ( _t, i ) { return i; } ); | |||
var allWeathers = data.weathers.map( function ( _w, i ) { return i; } ); | |||
var byMethod = {}; | var byMethod = {}; | ||
entries.forEach( function ( e ) { | entries.forEach( function ( e ) { | ||
e.m.forEach( function ( m ) { | e.m.forEach( function ( m ) { ( byMethod[ m ] = byMethod[ m ] || [] ).push( e ); } ); | ||
} ); | |||
var speciesSet = {}, speciesList = []; | |||
entries.forEach( function ( e ) { | |||
if ( !speciesSet[ e.s ] ) { speciesSet[ e.s ] = true; speciesList.push( e.s ); } | |||
} ); | |||
speciesList.sort( function ( a, b ) { return a.localeCompare( b ); } ); | |||
var species = initialSpecies || ''; | |||
var allSpeciesEntries = []; // every spawn system and form for the chosen species | |||
var modeEntries = []; // entries for the selected spawn system | |||
var speciesEntries = []; // form-filtered entries for options and optimizer | |||
var modes = [], showMode = false, mode = 'standard'; | |||
var forms = [], showForm = false, curForm = 'All'; | |||
var rel = {}; | |||
function modeOf( e ) { return e.i === 1 ? 'legendary' : 'standard'; } | |||
function modeLabel( value ) { return value === 'legendary' ? 'Legendary' : 'Standard wild'; } | |||
function modeOk( e ) { return modeOf( e ) === mode; } | |||
// A spawn entry's form matches the chosen form ('All' accepts any). | |||
function formOk( c ) { return curForm === 'All' || ( c.f || 'Normal' ) === curForm; } | |||
function applyForm() { | |||
speciesEntries = ( curForm === 'All' ) ? modeEntries.slice() | |||
: modeEntries.filter( function ( e ) { return ( e.f || 'Normal' ) === curForm; } ); | |||
function any( fn ) { return speciesEntries.some( fn ); } | |||
function constrained( e, field ) { | |||
return e[ field ] != null || ( e.rm || [] ).some( function ( multiplier ) { | |||
return multiplier[ field ] != null; | |||
} ); | |||
} | |||
rel = { | |||
time: any( function ( e ) { return constrained( e, 't' ); } ), | |||
weather: any( function ( e ) { return constrained( e, 'w' ); } ), | |||
y: any( function ( e ) { return constrained( e, 'y0' ) || constrained( e, 'y1' ); } ), | |||
light: any( function ( e ) { return constrained( e, 'l0' ) || constrained( e, 'l1' ); } ), | |||
moon: any( function ( e ) { return constrained( e, 'mp' ); } ), | |||
struct: any( function ( e ) { return constrained( e, 'st' ); } ) | |||
}; | |||
} | |||
function refreshForms() { | |||
var seen = {}; forms = []; | |||
modeEntries.forEach( function ( e ) { | |||
var f = e.f || 'Normal'; | |||
if ( !seen[ f ] ) { seen[ f ] = 1; forms.push( f ); } | |||
} ); | } ); | ||
} ); | forms.sort( function ( a, b ) { | ||
if ( a === 'Normal' ) { return -1; } | |||
if ( b === 'Normal' ) { return 1; } | |||
return a.localeCompare( b ); | |||
} ); | |||
// Only offer a form picker when more than one form spawns naturally; | |||
// then include "All" to accept any form. | |||
showForm = forms.length > 1; | |||
fillSelect( formSel, ( showForm ? [ 'All' ].concat( forms ) : forms ) | |||
.map( function ( f ) { return { v: f, t: f }; } ) ); | |||
curForm = showForm ? 'All' : ( forms[ 0 ] || 'All' ); | |||
formSel.value = curForm; | |||
applyForm(); | |||
} | |||
function refreshSpecies() { | |||
allSpeciesEntries = entries.filter( function ( e ) { return e.s === species; } ); | |||
var seenModes = {}; modes = []; | |||
allSpeciesEntries.forEach( function ( e ) { | |||
var spawnMode = modeOf( e ); | |||
if ( !seenModes[ spawnMode ] ) { seenModes[ spawnMode ] = 1; modes.push( spawnMode ); } | |||
} ); | |||
modes.sort(); | |||
showMode = modes.length > 1; | |||
mode = modes.indexOf( 'legendary' ) !== -1 && modes.indexOf( 'standard' ) === -1 | |||
? 'legendary' : ( modes[ 0 ] || 'standard' ); | |||
fillSelect( modeSel, modes.map( function ( value ) { | |||
return { v: value, t: modeLabel( value ) }; | |||
} ) ); | |||
modeSel.value = mode; | |||
modeEntries = allSpeciesEntries.filter( modeOk ); | |||
refreshForms(); | |||
} | |||
// ── | // ── core probability ─────────────────────────────────────────── | ||
function | function passes( c, base ) { | ||
if ( c.b && !inList( c.b, base.B ) ) { return false; } | |||
if ( | if ( c.ab && inList( c.ab, base.B ) ) { return false; } | ||
if ( | if ( base.y != null ) { | ||
if ( | if ( c.y0 != null && base.y < c.y0 ) { return false; } | ||
if ( c.y1 != null && base.y > c.y1 ) { return false; } | |||
if ( | |||
if ( | |||
} | } | ||
if ( base.light != null ) { | |||
if ( c.l0 != null && base.light < c.l0 ) { return false; } | |||
if ( c.l1 != null && base.light > c.l1 ) { return false; } | |||
} | |||
if ( base.moon != null && c.mp != null && base.moon !== c.mp ) { return false; } | |||
if ( base.W != null && c.w && !inList( c.w, base.W ) ) { return false; } | |||
// Structure-gated spawns only occur inside a matching structure; with | |||
// no structure chosen ("Outside") they are excluded entirely. | |||
if ( c.st && ( base.struct == null || !inList( c.st, base.struct ) ) ) { return false; } | |||
return true; | |||
} | |||
function multiplierPasses( condition, base, t ) { | |||
if ( condition.b && !inList( condition.b, base.B ) ) { return false; } | |||
if ( condition.t && ( t == null || !inList( condition.t, t ) ) ) { return false; } | |||
if ( condition.w && ( base.W == null || !inList( condition.w, base.W ) ) ) { return false; } | |||
if ( condition.y0 != null && ( base.y == null || base.y < condition.y0 ) ) { return false; } | |||
if ( condition.y1 != null && ( base.y == null || base.y > condition.y1 ) ) { return false; } | |||
if ( condition.l0 != null && ( base.light == null || base.light < condition.l0 ) ) { return false; } | |||
if ( condition.l1 != null && ( base.light == null || base.light > condition.l1 ) ) { return false; } | |||
if ( condition.mp != null && base.moon !== condition.mp ) { return false; } | |||
if ( condition.st && ( base.struct == null || !inList( condition.st, base.struct ) ) ) { return false; } | |||
return true; | return true; | ||
} | |||
function adjustedRarity( c, base, t ) { | |||
var rarity = c.r; | |||
( c.rm || [] ).forEach( function ( multiplier ) { | |||
if ( multiplierPasses( multiplier, base, t ) ) { rarity *= multiplier.x; } | |||
} ); | |||
return rarity; | |||
} | |||
function chanceP8( base ) { | |||
var pool = byMethod[ base.M ] || []; | |||
var num = [], den = []; | |||
for ( var z = 0; z < NT; z++ ) { num.push( 0 ); den.push( 0 ); } | |||
for ( var i = 0; i < pool.length; i++ ) { | |||
var c = pool[ i ]; | |||
if ( !modeOk( c ) || !passes( c, base ) ) { continue; } | |||
var ct = c.t || allTimes; | |||
for ( var j = 0; j < ct.length; j++ ) { | |||
var rarity = adjustedRarity( c, base, ct[ j ] ); | |||
den[ ct[ j ] ] += rarity; | |||
if ( c.s === species && formOk( c ) ) { num[ ct[ j ] ] += rarity; } | |||
} | |||
} | |||
var p8 = []; | |||
for ( var q = 0; q < NT; q++ ) { p8.push( den[ q ] > 0 ? num[ q ] / den[ q ] : 0 ); } | |||
return p8; | |||
} | } | ||
function | function chanceAt( base, t ) { | ||
var pool = byMethod[ base.M ] || []; | |||
var num = 0, den = 0, comp = 0; | var num = 0, den = 0, comp = 0; | ||
for ( var i = 0; i < | for ( var i = 0; i < pool.length; i++ ) { | ||
if ( | var c = pool[ i ]; | ||
if ( !modeOk( c ) || !passes( c, base ) ) { continue; } | |||
if ( | if ( t != null && c.t && !inList( c.t, t ) ) { continue; } | ||
var rarity = adjustedRarity( c, base, t ); | |||
den += rarity; | |||
if ( c.s === species && formOk( c ) ) { num += rarity; } else { comp++; } | |||
} | |||
return { num: num, den: den, comp: comp, p: den > 0 ? num / den : 0 }; | |||
} | |||
// Length-weighted share of every species that can spawn in this area over | |||
// the given block of times. Target share matches the headline chance. | |||
function spawnBreakdown( base, times ) { | |||
var pool = byMethod[ base.M ] || []; | |||
var totals = {}, sp = {}; | |||
for ( var i = 0; i < pool.length; i++ ) { | |||
var c = pool[ i ]; | |||
if ( !modeOk( c ) || !passes( c, base ) ) { continue; } | |||
var ct = c.t || allTimes; | |||
for ( var j = 0; j < ct.length; j++ ) { | |||
var t = ct[ j ]; | |||
if ( times.indexOf( t ) === -1 ) { continue; } | |||
var rarity = adjustedRarity( c, base, t ); | |||
totals[ t ] = ( totals[ t ] || 0 ) + rarity; | |||
if ( c.s === species && !formOk( c ) ) { continue; } | |||
var m = sp[ c.s ] || ( sp[ c.s ] = {} ); | |||
m[ t ] = ( m[ t ] || 0 ) + rarity; | |||
} | } | ||
} | } | ||
var wsum = 0; | |||
for ( var k = 0; k < times.length; k++ ) { wsum += timeLen[ times[ k ] ]; } | |||
var out = []; | |||
Object.keys( sp ).forEach( function ( name ) { | |||
var acc = 0; | |||
times.forEach( function ( t ) { | |||
if ( totals[ t ] > 0 ) { acc += timeLen[ t ] * ( ( sp[ name ][ t ] || 0 ) / totals[ t ] ); } | |||
} ); | |||
out.push( { s: name, share: wsum > 0 ? acc / wsum : 0 } ); | |||
} ); | |||
out.sort( function ( a, b ) { return b.share - a.share; } ); | |||
return out; | |||
} | } | ||
function blockTimes( start, n ) { | function blockTimes( start, n ) { | ||
n = Math.max( 1, Math.min( NT, n ) ); | n = Math.max( 1, Math.min( NT, n ) ); | ||
| Line 110: | Line 268: | ||
return out; | return out; | ||
} | } | ||
function blockWeighted( p8, times ) { | |||
function blockWeighted( | |||
var wsum = 0, acc = 0; | var wsum = 0, acc = 0; | ||
for ( var i = 0; i < times.length; i++ ) { | for ( var i = 0; i < times.length; i++ ) { | ||
wsum += timeLen[ times[ i ] ]; | |||
acc += timeLen[ times[ i ] ] * p8[ times[ i ] ]; | |||
acc += timeLen[ | |||
} | } | ||
return wsum > 0 ? acc / wsum : 0; | return wsum > 0 ? acc / wsum : 0; | ||
} | } | ||
function pickY( e ) { | function pickY( e ) { | ||
var y = 63; | var y = 63; | ||
| Line 129: | Line 282: | ||
return y; | return y; | ||
} | } | ||
function round2( n ) { return Math.round( n * 100 ) / 100; } | function round2( n ) { return Math.round( n * 100 ) / 100; } | ||
function fmtPct( p ) { | function fmtPct( p ) { | ||
if ( !( p > 0 ) ) { return '0%'; } | if ( !( p > 0 ) ) { return '0%'; } | ||
var v = p * 100; | var v = p * 100; | ||
return ( v >= 1 ? v.toFixed( 1 ) : v.toFixed( 2 ) ) + '%'; | return ( v >= 1 ? v.toFixed( 1 ) : v.toFixed( 2 ) ) + '%'; | ||
} | |||
function fmtDuration( minutes ) { | |||
if ( minutes < 60 ) { return Math.round( minutes ) + ' minutes'; } | |||
if ( minutes < 1440 ) { return ( minutes / 60 ).toFixed( minutes < 600 ? 1 : 0 ) + ' hours'; } | |||
return ( minutes / 1440 ).toFixed( 1 ) + ' days'; | |||
} | } | ||
// ── controls | // ── controls ──────────────────────────────────────────────────── | ||
var | var biomeSel = el( 'select', { class: 'scalc-input' } ); | ||
var | var methodSel = el( 'select', { class: 'scalc-input' } ); | ||
var timeSel = el( 'select', { class: 'scalc-input' } ); | |||
var weatherSel = el( 'select', { class: 'scalc-input' } ); | |||
var ySel = el( 'input', { type: 'number', min: '-64', max: '320', value: '63', class: 'scalc-num' } ); | |||
var lightSel = el( 'input', { type: 'number', min: '0', max: '15', value: '7', class: 'scalc-num' } ); | |||
var moonSel = el( 'select', { class: 'scalc-input' } ); | |||
var structSel = el( 'select', { class: 'scalc-input' } ); | |||
var formSel = el( 'select', { class: 'scalc-input' } ); | |||
var modeSel = el( 'select', { class: 'scalc-input' } ); | |||
var playersSel = el( 'input', { type: 'number', min: '1', value: '1', class: 'scalc-num' } ); | |||
function fillSelect( sel, items ) { | |||
data.biomes | sel.innerHTML = ''; | ||
items.forEach( function ( o ) { sel.appendChild( el( 'option', { value: o.v, text: o.t } ) ); } ); | |||
.sort( function ( a, b ) { return a. | } | ||
.forEach( function ( | function allBiomeIdx() { return data.biomes.map( function ( _b, i ) { return i; } ); } | ||
function biomeOptionLabel( i ) { | |||
return prettyBiome( data.biomes[ i ] ); | |||
} | |||
function relevantTimes() { | |||
if ( !speciesEntries.length || speciesEntries.some( function ( e ) { return !e.t; } ) ) { return allTimes; } | |||
var all = []; | |||
speciesEntries.forEach( function ( e ) { all = all.concat( e.t ); } ); | |||
return uniq( all ).sort( function ( a, b ) { return a - b; } ); | |||
} | |||
function relevantWeathers() { | |||
if ( !speciesEntries.length || speciesEntries.some( function ( e ) { return !e.w; } ) ) { return allWeathers; } | |||
var all = []; | |||
speciesEntries.forEach( function ( e ) { all = all.concat( e.w ); } ); | |||
return uniq( all ).sort( function ( a, b ) { return a - b; } ); | |||
} | |||
function relevantMoons() { | |||
if ( !speciesEntries.length || speciesEntries.some( function ( e ) { return e.mp == null; } ) ) { | |||
var r = []; for ( var i = 0; i < 8; i++ ) { r.push( i ); } return r; | |||
} | |||
var all = []; | |||
speciesEntries.forEach( function ( e ) { if ( e.mp != null ) { all.push( e.mp ); } } ); | |||
return uniq( all ).sort( function ( a, b ) { return a - b; } ); | |||
} | |||
function applyNumRange( input, loF, hiF, absLo, absHi ) { | |||
var los = [], his = []; | |||
speciesEntries.forEach( function ( e ) { | |||
if ( e[ loF ] != null ) { los.push( e[ loF ] ); } | |||
if ( e[ hiF ] != null ) { his.push( e[ hiF ] ); } | |||
} ); | } ); | ||
input.min = String( los.length ? Math.min.apply( null, los ) : absLo ); | |||
input.max = String( his.length ? Math.max.apply( null, his ) : absHi ); | |||
} | |||
function repopulate() { | |||
var noSp = !speciesEntries.length; | |||
var biomeIdx = noSp ? allBiomeIdx() : biomeCandidates(); | |||
fillSelect( biomeSel, biomeIdx | |||
.map( function ( i ) { return { v: i, t: biomeOptionLabel( i ) }; } ) | |||
.sort( function ( a, b ) { return a.t.localeCompare( b.t ); } ) ); | |||
var methodIdx = noSp ? data.methods.map( function ( _m, i ) { return i; } ) : methodCandidates(); | |||
fillSelect( methodSel, methodIdx | |||
.map( function ( i ) { return { v: i, t: data.methods[ i ] }; } ) | |||
.sort( function ( a, b ) { return a.t.localeCompare( b.t ); } ) ); | |||
fillSelect( timeSel, relevantTimes().map( function ( i ) { return { v: i, t: titleCase( data.times[ i ] ) }; } ) ); | |||
fillSelect( weatherSel, relevantWeathers().map( function ( i ) { return { v: i, t: titleCase( data.weathers[ i ] ) }; } ) ); | |||
fillSelect( moonSel, relevantMoons().map( function ( i ) { return { v: i, t: 'Phase ' + i }; } ) ); | |||
var sopts = [ { v: 'Outside', t: 'Outside (open)' } ]; | |||
structCandidates().forEach( function ( i ) { sopts.push( { v: i, t: prettyBiome( data.structures[ i ] ) } ); } ); | |||
fillSelect( structSel, sopts ); | |||
applyNumRange( ySel, 'y0', 'y1', -64, 320 ); | |||
applyNumRange( lightSel, 'l0', 'l1', 0, 15 ); | |||
} | |||
var | var consecSel = el( 'input', { | ||
type: 'number', min: '1', max: String( NT ), value: '1', class: 'scalc-num', | |||
title: 'Average a block of this many consecutive times of day, weighting ' | |||
+ 'each by its real length. Set to ' + NT + ' for any time.' | |||
} ); | } ); | ||
var strictChk = el( 'input', { type: 'checkbox', class: 'scalc-check', checked: 'checked' } ); | |||
function lockBox() { | |||
return el( 'input', { type: 'checkbox', class: 'scalc-lock-chk', | |||
title: 'Lock: keep this value when using Find best chance.' } ); | |||
} | |||
var lockBiome = lockBox(), lockMethod = lockBox(), lockTime = lockBox(), | |||
lockWeather = lockBox(), lockY = lockBox(), lockLight = lockBox(), lockMoon = lockBox(), | |||
lockStruct = lockBox(); | |||
var | function field( labelText, control, lock ) { | ||
var lab = el( 'label', { class: 'scalc-label' }, [ document.createTextNode( labelText ) ] ); | |||
if ( lock ) { | |||
lab.appendChild( el( 'label', { class: 'scalc-lock' }, [ lock, document.createTextNode( ' lock' ) ] ) ); | |||
} | |||
return el( 'div', { class: 'scalc-field' }, [ lab, control ] ); | |||
} | |||
var | var speciesInput = el( 'input', { | ||
class: 'scalc-input scalc-species', list: 'scalc-species-list', | |||
placeholder: 'Type a Pokemon name', autocomplete: 'off' | |||
} ); | } ); | ||
var speciesDl = el( 'datalist', { id: 'scalc-species-list' } ); | |||
speciesList.forEach( function ( n ) { speciesDl.appendChild( el( 'option', { value: n } ) ); } ); | |||
function onSpeciesPick() { if ( speciesSet[ speciesInput.value ] ) { setSpecies( speciesInput.value ); } } | |||
speciesInput.addEventListener( 'change', onSpeciesPick ); | |||
speciesInput.addEventListener( 'input', onSpeciesPick ); | |||
var fSpecies = el( 'div', { class: 'scalc-field scalc-field-species' }, [ | |||
el( 'label', { class: 'scalc-label', text: 'Pokemon' } ), speciesInput, speciesDl | |||
] ); | |||
var | var fForm = field( 'Form', formSel ); | ||
formSel.addEventListener( 'change', function () { setForm( formSel.value ); } ); | |||
var fMode = field( 'Spawn system', modeSel ); | |||
modeSel.addEventListener( 'change', function () { setMode( modeSel.value ); } ); | |||
var | var fBiome = field( 'Biome', biomeSel, lockBiome ); | ||
var fMethod = field( 'Location type', methodSel, lockMethod ); | |||
var fTime = field( 'Time of day', timeSel, lockTime ); | |||
var fWeather = field( 'Weather', weatherSel, lockWeather ); | |||
var fY = field( 'Y-level', ySel, lockY ); | |||
var fLight = field( 'Light level', lightSel, lockLight ); | |||
var fMoon = field( 'Moon phase', moonSel, lockMoon ); | |||
var fStruct = field( 'Structure', structSel, lockStruct ); | |||
var fPlayers = field( 'Online players', playersSel ); | |||
var formFields = []; | |||
if ( !initialSpecies ) { formFields.push( fSpecies ); } | |||
formFields.push( fMode, fForm, fBiome, fMethod, fTime, fWeather, fY, fLight, fMoon, fStruct, fPlayers ); | |||
var form = el( 'div', { class: 'scalc-form' }, formFields ); | |||
var | var meta = el( 'div', { class: 'scalc-form scalc-meta' }, [ | ||
el( 'span', { class: 'scalc-meta-label', text: 'Time block' } ), | |||
field( 'Consecutive times', consecSel ), | |||
el( 'div', { class: 'scalc-field' }, [ | |||
el( 'label', { class: 'scalc-label', text: 'Strict block' } ), | |||
el( 'label', { class: 'scalc-check-wrap', | |||
field( ' | title: 'Only accept blocks where the species spawns in every time. ' | ||
+ 'If none exists, the block size is reduced and a warning is shown.' }, [ | |||
strictChk, el( 'span', { class: 'scalc-check-text', text: 'all times must spawn' } ) | |||
] ) | |||
] ) | ] ) | ||
] ); | ] ); | ||
var bestBtn = el( 'button', { class: 'scalc-btn', type: 'button', text: 'Find best chance' } ); | |||
var countSel = el( 'input', { | |||
var | type: 'number', min: '1', value: '5', class: 'scalc-num', | ||
title: 'How many top areas to list when using Find best chance.' | |||
} ); | |||
var actions = el( 'div', { class: 'scalc-actions' }, [ | |||
bestBtn, | |||
el( 'label', { class: 'scalc-count-wrap' }, [ | |||
el( 'span', { class: 'scalc-count-label', text: 'Results' } ), countSel | |||
] ) | ] ) | ||
] ); | ] ); | ||
var | var topWrap = el( 'div', { class: 'scalc-top' } ); | ||
var results = el( 'div', { class: 'scalc-results' } ); | var results = el( 'div', { class: 'scalc-results' } ); | ||
function | function getN() { return Math.max( 1, Math.min( NT, parseInt( consecSel.value, 10 ) || 1 ) ); } | ||
function getResultCount() { return Math.max( 1, parseInt( countSel.value, 10 ) || 5 ); } | |||
function getBase() { | |||
return { | return { | ||
B: +biomeSel.value, | B: +biomeSel.value, | ||
M: +methodSel.value, | M: +methodSel.value, | ||
W: rel.weather ? +weatherSel.value : null, | |||
y: rel.y ? ( ySel.value === '' ? null : +ySel.value ) : null, | |||
y: ySel.value === '' ? null : +ySel.value, | light: rel.light ? ( lightSel.value === '' ? null : +lightSel.value ) : null, | ||
moon: rel.moon ? +moonSel.value : null, | |||
struct: rel.struct && structSel.value !== 'Outside' && structSel.value !== '' | |||
? +structSel.value : null | |||
}; | }; | ||
} | |||
function structCandidates() { | |||
var all = []; | |||
speciesEntries.forEach( function ( e ) { | |||
if ( e.st ) { all = all.concat( e.st ); } | |||
( e.rm || [] ).forEach( function ( multiplier ) { | |||
if ( multiplier.st ) { all = all.concat( multiplier.st ); } | |||
} ); | |||
} ); | |||
return uniq( all ); | |||
} | |||
function curTimes() { return rel.time ? blockTimes( +timeSel.value, getN() ) : allTimes; } | |||
function applyVisibility() { | |||
fMode.style.display = showMode ? '' : 'none'; | |||
fForm.style.display = showForm ? '' : 'none'; | |||
fTime.style.display = rel.time ? '' : 'none'; | |||
fWeather.style.display = rel.weather ? '' : 'none'; | |||
fY.style.display = rel.y ? '' : 'none'; | |||
fLight.style.display = rel.light ? '' : 'none'; | |||
fMoon.style.display = rel.moon ? '' : 'none'; | |||
fStruct.style.display = rel.struct ? '' : 'none'; | |||
fPlayers.style.display = mode === 'legendary' ? '' : 'none'; | |||
meta.style.display = rel.time ? '' : 'none'; | |||
} | |||
var flagBest = false, warnText = null, topAreas = [], activeArea = null, selectedTime = null, topN = 1; | |||
function areaLabel( area ) { | |||
var b = area.base; | |||
var parts = [ biomeOptionLabel( b.B ), data.methods[ b.M ] ]; | |||
if ( rel.time && area.start != null ) { | |||
parts.push( titleCase( data.times[ area.start ] ) + ( topN > 1 ? ' +' + ( topN - 1 ) : '' ) ); | |||
} | |||
if ( rel.weather && b.W != null ) { parts.push( titleCase( data.weathers[ b.W ] ) ); } | |||
if ( rel.y && b.y != null ) { parts.push( 'Y ' + b.y ); } | |||
if ( rel.light && b.light != null ) { parts.push( 'Light ' + b.light ); } | |||
if ( rel.moon && b.moon != null ) { parts.push( 'Moon ' + b.moon ); } | |||
if ( rel.struct ) { parts.push( b.struct != null ? prettyBiome( data.structures[ b.struct ] ) : 'Outside' ); } | |||
return parts.join( ' / ' ); | |||
} | } | ||
function renderTop() { | |||
topWrap.innerHTML = ''; | |||
if ( !topAreas.length ) { return; } | |||
topWrap.appendChild( el( 'div', { class: 'scalc-top-title', text: 'Top areas (click to view)' } ) ); | |||
topAreas.forEach( function ( area, idx ) { | |||
var item = el( 'div', { | |||
class: 'scalc-top-item' + ( activeArea === area ? ' scalc-top-active' : '' ) | |||
}, [ | |||
el( 'span', { class: 'scalc-top-rank', text: '#' + ( idx + 1 ) } ), | |||
el( 'span', { class: 'scalc-top-pct', text: fmtPct( area.p ) } ), | |||
el( 'span', { class: 'scalc-top-params', text: areaLabel( area ) } ) | |||
] ); | |||
item.addEventListener( 'click', function () { applyArea( area ); } ); | |||
topWrap.appendChild( item ); | |||
} ); | |||
} | |||
function recompute() { | function recompute() { | ||
results.innerHTML = ''; | results.innerHTML = ''; | ||
if ( !species ) { | |||
results.appendChild( el( 'div', { class: 'scalc-result' }, [ | |||
el( 'span', { class: 'scalc-note', text: 'Choose a Pokemon to see its spawn chances.' } ) | |||
] ) ); | |||
flagBest = false; warnText = null; return; | |||
} | |||
if ( !speciesEntries.length ) { | if ( !speciesEntries.length ) { | ||
results.appendChild( el( 'div', { class: 'scalc-result scalc-result-miss' }, [ | results.appendChild( el( 'div', { class: 'scalc-result scalc-result-miss' }, [ | ||
el( 'span', { class: 'scalc-note', text: | el( 'span', { class: 'scalc-note', text: | ||
species + ' has no standard overworld spawns | species + ' has no supported standard or Legendary overworld spawns.' } ) | ||
] ) ); | ] ) ); | ||
flagBest = false; | flagBest = false; warnText = null; return; | ||
} | } | ||
var p, | var base = getBase(); | ||
var | var N = getN(); | ||
var | var p8 = []; | ||
if ( | for ( var t = 0; t < NT; t++ ) { p8.push( chanceAt( base, t ).p ); } | ||
var | var times = rel.time ? blockTimes( +timeSel.value, N ) : allTimes; | ||
var showBreak = rel.time && times.length > 1; | |||
detailText = p | var single = rel.time && times.length === 1; | ||
var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times ); | |||
var detailText; | |||
if ( mode === 'legendary' ) { | |||
var legendary = data.legendary || {}; | |||
var players = Math.max( 1, parseInt( playersSel.value, 10 ) || 1 ); | |||
var effectiveTicks = ( legendary.ticks || 25000 ) / | |||
( 1 + ( players - 1 ) * ( legendary.playerMultiplier || 0 ) ); | |||
var attemptChance = ( legendary.chance == null ? 0.3 : legendary.chance ) * p; | |||
var waitMinutes = attemptChance > 0 ? effectiveTicks / 1200 / attemptChance : 0; | |||
detailText = '~1 in ' + Math.round( 1 / p ) + ' eligible Legendary selections. ' | |||
+ 'With the configured ' + fmtPct( legendary.chance == null ? 0.3 : legendary.chance ) | |||
+ ' attempt success chance, the estimated average wait is ' | |||
+ fmtDuration( waitMinutes ) + ', assuming a viable location is found.'; | |||
} else if ( single ) { | |||
var res = chanceAt( base, times[ 0 ] ); | |||
detailText = '~1 in ' + Math.round( 1 / res.p ) + ' spawns - rarity ' | |||
+ round2( res.num ) + ' of ' + round2( res.den ) + ' vs ' + res.comp | |||
+ ' competing spawn' + ( res.comp === 1 ? '' : 's' ) + '.'; | |||
} else { | } else { | ||
detailText = '~1 in ' + Math.round( 1 / p ) + ' spawns, length-weighted across ' | |||
+ ( !rel.time ? 'any time (this Pokemon ignores time of day)' | |||
: ( N >= NT ? 'any time' : N + ' consecutive times' ) ) + '.'; | |||
detailText = | |||
} | } | ||
var hit = p > 0; | var hit = p > 0; | ||
var head = el( 'div', { class: 'scalc-result-head' }, [ | var head = el( 'div', { class: 'scalc-result-head' }, [ | ||
el( 'span', { class: 'scalc-pct', text: | el( 'span', { class: 'scalc-pct', text: fmtPct( hit ? p : 0 ) } ), | ||
el( 'span', { class: 'scalc-pctlabel', text: | el( 'span', { class: 'scalc-pctlabel', text: | ||
'chance a wild spawn here is ' + species | ( mode === 'legendary' ? 'share of eligible Legendary selections for ' : | ||
'chance a wild spawn here is ' ) + species + ( flagBest && hit ? ' (best found)' : '' ) } ) | |||
] ); | ] ); | ||
var detail = hit | var detail = hit | ||
? el( 'span', { class: 'scalc-detail', text: detailText } ) | ? 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 | 'Does not spawn in this situation. Try Find best chance, or change ' | ||
+ ' | + 'the biome / location type / time.' } ); | ||
var children = []; | var children = []; | ||
if ( warnText ) { | if ( warnText ) { children.push( el( 'div', { class: 'scalc-warn', text: warnText } ) ); } | ||
children.push( head, detail ); | children.push( head, detail ); | ||
// | // Which time the breakdown shows: a single block time (chip-selected, | ||
// | // defaulting to the highest-chance time) or the whole block / day. | ||
var bdTimes = times, bdLabel = null; | |||
if ( hit && | if ( hit && showBreak ) { | ||
var chips = | if ( selectedTime == null || times.indexOf( selectedTime ) === -1 ) { | ||
var dead = !( | selectedTime = times[ 0 ]; | ||
times.forEach( function ( t ) { if ( p8[ t ] > p8[ selectedTime ] ) { selectedTime = t; } } ); | |||
el( 'span', { class: 'scalc-chip-time', text: | } | ||
el( 'span', { class: 'scalc-chip-pct', text: fmtPct( | bdTimes = [ selectedTime ]; | ||
bdLabel = titleCase( data.times[ selectedTime ] ); | |||
var deadCount = 0; | |||
var chips = times.map( function ( ti ) { | |||
var pv = p8[ ti ], dead = !( pv > 0 ); | |||
if ( dead ) { deadCount++; } | |||
var chip = el( 'span', { | |||
class: 'scalc-chip scalc-chip-click' + ( dead ? ' scalc-chip-dead' : '' ) | |||
+ ( ti === selectedTime ? ' scalc-chip-active' : '' ), | |||
title: 'Show spawns during ' + titleCase( data.times[ ti ] ) | |||
}, [ | |||
el( 'span', { class: 'scalc-chip-time', text: titleCase( data.times[ ti ] ) } ), | |||
el( 'span', { class: 'scalc-chip-pct', text: fmtPct( pv ) } ) | |||
] ); | ] ); | ||
chip.addEventListener( 'click', function () { selectedTime = ti; recompute(); } ); | |||
return chip; | |||
} ); | } ); | ||
children.push( el( 'div', { class: 'scalc-breakdown' }, chips ) ); | children.push( el( 'div', { class: 'scalc-breakdown' }, chips ) ); | ||
if ( deadCount > 0 ) { | if ( deadCount > 0 ) { | ||
children.push( el( 'div', { class: 'scalc-note', text: | children.push( el( 'div', { class: 'scalc-note', text: | ||
deadCount + ' of ' + | deadCount + ' of ' + times.length + ' times have no ' + species | ||
+ ' spawns here' + ( | + ' spawns here' + ( strictChk.checked ? '' | ||
: '; enable "Strict block" to make the optimizer avoid them' ) | : '; enable "Strict block" to make the optimizer avoid them' ) + '.' } ) ); | ||
} | } | ||
} else if ( single ) { | |||
bdLabel = titleCase( data.times[ times[ 0 ] ] ); | |||
} else if ( !rel.time ) { | |||
bdLabel = 'any time'; | |||
} | } | ||
results.appendChild( el( 'div', { | results.appendChild( el( 'div', { | ||
| Line 321: | Line 625: | ||
+ ( flagBest && hit ? ' scalc-result-best' : '' ) | + ( flagBest && hit ? ' scalc-result-best' : '' ) | ||
}, children ) ); | }, children ) ); | ||
flagBest = false; | |||
// Full spawn breakdown for the selected time (or whole block / day). | |||
var bd = spawnBreakdown( base, bdTimes ); | |||
var card = el( 'div', { class: 'scalc-bd' }, [ | |||
el( 'div', { class: 'scalc-bd-title', | |||
text: ( mode === 'legendary' ? 'Eligible Legendary pool' : 'Spawns in this area' ) | |||
+ ( bdLabel ? ' (' + bdLabel + ')' : '' ) } ) | |||
] ); | |||
if ( !bd.length ) { | |||
card.appendChild( el( 'div', { class: 'scalc-note', text: 'Nothing spawns here.' } ) ); | |||
} else { | |||
var list = el( 'div', { class: 'scalc-bd-list' } ); | |||
bd.slice( 0, 60 ).forEach( function ( r ) { | |||
var isT = r.s === species; | |||
var left = el( 'span', { class: 'scalc-bd-left' } ); | |||
var spriteFile = data.sprites && data.sprites[ r.s ]; | |||
if ( spriteFile ) { | |||
left.appendChild( el( 'img', { class: 'scalc-bd-img', | |||
src: fileHref( spriteFile ), alt: r.s, loading: 'lazy' } ) ); | |||
} | |||
left.appendChild( el( 'a', { class: 'scalc-bd-name', href: pageHref( r.s ), | |||
title: r.s }, [ document.createTextNode( r.s ) ] ) ); | |||
list.appendChild( el( 'div', { class: 'scalc-bd-row' + ( isT ? ' scalc-bd-target' : '' ) }, [ | |||
left, | |||
el( 'span', { class: 'scalc-bd-pct', text: fmtPct( r.share ) } ) | |||
] ) ); | |||
} ); | |||
card.appendChild( list ); | |||
if ( bd.length > 60 ) { | |||
card.appendChild( el( 'div', { class: 'scalc-note', text: 'and ' + ( bd.length - 60 ) + ' more.' } ) ); | |||
} | |||
} | |||
results.appendChild( card ); | |||
flagBest = false; warnText = null; | |||
} | |||
// ── optimizer ──────────────────────────────────────────────────── | |||
function biomeCandidates() { | |||
if ( speciesEntries.some( function ( e ) { return !e.b; } ) ) { return allBiomeIdx(); } | |||
var all = []; | |||
speciesEntries.forEach( function ( e ) { if ( e.b ) { all = all.concat( e.b ); } } ); | |||
return uniq( all ); | |||
} | |||
function methodCandidates() { | |||
var all = []; | |||
speciesEntries.forEach( function ( e ) { all = all.concat( e.m ); } ); | |||
return uniq( all ); | |||
} | |||
function numCandidates( lo, hi, extra ) { | |||
var vals = []; | |||
speciesEntries.forEach( function ( e ) { | |||
if ( e[ lo ] != null ) { vals.push( e[ lo ] ); } | |||
if ( e[ hi ] != null ) { vals.push( e[ hi ] ); } | |||
( e.rm || [] ).forEach( function ( multiplier ) { | |||
if ( multiplier[ lo ] != null ) { vals.push( multiplier[ lo ] ); } | |||
if ( multiplier[ hi ] != null ) { vals.push( multiplier[ hi ] ); } | |||
} ); | |||
} ); | |||
( extra || [] ).forEach( function ( v ) { vals.push( v ); } ); | |||
return uniq( vals ); | |||
} | |||
function moonCandidatesList() { | |||
var vals = []; | |||
speciesEntries.forEach( function ( e ) { | |||
if ( e.mp != null ) { vals.push( e.mp ); } | |||
( e.rm || [] ).forEach( function ( multiplier ) { | |||
if ( multiplier.mp != null ) { vals.push( multiplier.mp ); } | |||
} ); | |||
} ); | |||
return uniq( vals ); | |||
} | |||
function placeKey( b ) { return [ b.B, b.M, b.W, b.y, b.light, b.moon, b.struct ].join( '|' ); } | |||
function applyArea( area ) { | |||
var b = area.base; | |||
biomeSel.value = b.B; | |||
methodSel.value = b.M; | |||
if ( rel.weather && b.W != null ) { weatherSel.value = b.W; } | |||
if ( rel.y && b.y != null ) { ySel.value = b.y; } | |||
if ( rel.light && b.light != null ) { lightSel.value = b.light; } | |||
if ( rel.moon && b.moon != null ) { moonSel.value = b.moon; } | |||
if ( rel.struct ) { structSel.value = ( b.struct != null ? b.struct : 'Outside' ); } | |||
if ( rel.time && area.start != null ) { timeSel.value = area.start; } | |||
activeArea = area; | |||
selectedTime = null; | |||
flagBest = true; | |||
recompute(); | |||
renderTop(); | |||
} | } | ||
function findBest() { | function findBest() { | ||
var | if ( !species || !speciesEntries.length ) { recompute(); return; } | ||
var | var N = getN(), strict = strictChk.checked, lockT = lockTime.checked, curT = +timeSel.value; | ||
var biomeC = lockBiome.checked ? [ +biomeSel.value ] : biomeCandidates(); | |||
var methodC = lockMethod.checked ? [ +methodSel.value ] : methodCandidates(); | |||
var weatherC = !rel.weather ? [ null ] : ( lockWeather.checked ? [ +weatherSel.value ] : allWeathers ); | |||
var yC = !rel.y ? [ null ] : ( lockY.checked ? [ +ySel.value ] : numCandidates( 'y0', 'y1', [ 63 ] ) ); | |||
var lightC = !rel.light ? [ null ] : ( lockLight.checked ? [ +lightSel.value ] : numCandidates( 'l0', 'l1', [ 0, 7, 15 ] ) ); | |||
var moonC = !rel.moon ? [ null ] : ( lockMoon.checked ? [ +moonSel.value ] : moonCandidatesList() ); | |||
var curStruct = ( structSel.value === 'Outside' || structSel.value === '' ? null : +structSel.value ); | |||
var hasOpen = speciesEntries.some( function ( e ) { return !e.st; } ); | |||
var structC = !rel.struct ? [ null ] | |||
: ( lockStruct.checked ? [ curStruct ] | |||
: ( hasOpen ? [ null ] : [] ).concat( structCandidates() ) ); | |||
var situations = []; | var situations = []; | ||
for ( var | for ( var a = 0; a < biomeC.length; a++ ) { | ||
for ( var b = 0; b < methodC.length; b++ ) { | |||
for ( var c = 0; c < weatherC.length; c++ ) { | |||
for ( var d = 0; d < yC.length; d++ ) { | |||
for ( var f = 0; f < lightC.length; f++ ) { | |||
for ( var g = 0; g < moonC.length; g++ ) { | |||
for ( var h = 0; h < structC.length; h++ ) { | |||
var base = { B: biomeC[ a ], M: methodC[ b ], W: weatherC[ c ], | |||
for ( var | y: yC[ d ], light: lightC[ f ], moon: moonC[ g ], struct: structC[ h ] }; | ||
situations.push( { base: base, p8: chanceP8( base ) } ); | |||
} | |||
for ( var | |||
} | } | ||
} | } | ||
} | } | ||
} | } | ||
| Line 373: | Line 751: | ||
} | } | ||
// Best block | // Best block per distinct area (place) for block size n. | ||
function rankAt( n, strictFlag ) { | |||
function | var byPlace = {}; | ||
var | |||
for ( var i = 0; i < situations.length; i++ ) { | for ( var i = 0; i < situations.length; i++ ) { | ||
var | var s = situations[ i ], p8 = s.p8, key = placeKey( s.base ); | ||
for ( var | if ( !rel.time ) { | ||
var pw = blockWeighted( p8, allTimes ); | |||
if ( pw > 0 && ( !byPlace[ key ] || pw > byPlace[ key ].p ) ) { | |||
byPlace[ key ] = { base: s.base, start: null, p: pw }; | |||
} | |||
continue; | |||
} | |||
var starts = lockT ? [ curT ] : allTimes; | |||
for ( var si = 0; si < starts.length; si++ ) { | |||
var start = starts[ si ]; | |||
var times = blockTimes( start, n ); | var times = blockTimes( start, n ); | ||
var allSpawn = true; | var allSpawn = true; | ||
| Line 385: | Line 771: | ||
if ( !( p8[ times[ j ] ] > 0 ) ) { allSpawn = false; break; } | if ( !( p8[ times[ j ] ] > 0 ) ) { allSpawn = false; break; } | ||
} | } | ||
if ( strictFlag && !allSpawn ) { | if ( strictFlag && !allSpawn ) { if ( n >= NT ) { break; } continue; } | ||
var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times ); | var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times ); | ||
if ( p > 0 && ( ! | if ( p > 0 && ( !byPlace[ key ] || p > byPlace[ key ].p ) ) { | ||
byPlace[ key ] = { base: s.base, start: start, p: p }; | |||
} | } | ||
if ( n >= NT ) { break; } | if ( n >= NT ) { break; } | ||
} | } | ||
} | } | ||
return | return Object.keys( byPlace ).map( function ( k ) { return byPlace[ k ]; } ) | ||
.sort( function ( x, y ) { return y.p - x.p; } ); | |||
} | } | ||
var | var ranked = [], effN = N; | ||
if ( strict ) { | if ( !rel.time ) { | ||
ranked = rankAt( N, false ); | |||
} else if ( strict ) { | |||
for ( var n = N; n >= 1; n-- ) { | for ( var n = N; n >= 1; n-- ) { | ||
ranked = rankAt( n, true ); | |||
if ( | if ( ranked.length ) { effN = n; break; } | ||
} | } | ||
} else { | } else { | ||
ranked = rankAt( N, false ); | |||
} | } | ||
if ( ! | if ( !ranked.length ) { topAreas = []; renderTop(); recompute(); return; } | ||
topAreas = ranked.slice( 0, getResultCount() ); | |||
topN = effN; | |||
if ( rel.time && strict && effN < N ) { | |||
if ( strict && effN < N ) { | |||
consecSel.value = effN; | consecSel.value = effN; | ||
warnText = species + ' spawns in at most ' + effN + ' consecutive time' | warnText = species + ' spawns in at most ' + effN + ' consecutive time' | ||
+ ( effN === 1 ? '' : 's' ) + ' in any situation, so the block was ' | + ( effN === 1 ? '' : 's' ) + ' in any situation, so the block was reduced from ' | ||
+ N + ' to ' + effN + '.'; | |||
} | } | ||
applyArea( topAreas[ 0 ] ); | |||
} | } | ||
function setDefaults() { | function setDefaults() { | ||
if ( !speciesEntries.length ) { return; } | if ( !speciesEntries.length ) { return; } | ||
var common = {}; | var common = {}; | ||
[ 'Grass', 'Land', 'Water', 'Surface Water', 'Air', 'Tree Top', | [ '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; | var pick = null; | ||
speciesEntries.forEach( function ( e ) { | speciesEntries.forEach( function ( e ) { | ||
| Line 450: | Line 827: | ||
if ( se.t && se.t.length ) { timeSel.value = se.t[ 0 ]; } | if ( se.t && se.t.length ) { timeSel.value = se.t[ 0 ]; } | ||
ySel.value = pickY( se ); | ySel.value = pickY( se ); | ||
if ( rel.light ) { lightSel.value = ( se.l0 != null ? se.l0 : ( se.l1 != null ? se.l1 : 7 ) ); } | |||
if ( rel.moon && se.mp != null ) { moonSel.value = se.mp; } | |||
if ( rel.struct ) { | |||
var hasOpen = speciesEntries.some( function ( e ) { return !e.st; } ); | |||
if ( hasOpen ) { structSel.value = 'Outside'; } | |||
else { var sc = structCandidates(); if ( sc.length ) { structSel.value = sc[ 0 ]; } } | |||
} | |||
} | |||
function setSpecies( name ) { | |||
species = name; | |||
topAreas = []; activeArea = null; selectedTime = null; | |||
refreshSpecies(); | |||
repopulate(); | |||
applyVisibility(); | |||
renderTop(); | |||
setDefaults(); | |||
recompute(); | |||
} | |||
function setForm( f ) { | |||
curForm = f; | |||
topAreas = []; activeArea = null; selectedTime = null; | |||
applyForm(); | |||
repopulate(); | |||
applyVisibility(); | |||
renderTop(); | |||
setDefaults(); | |||
recompute(); | |||
} | } | ||
[ biomeSel, methodSel, timeSel, weatherSel, ySel, consecSel ].forEach( function ( | function setMode( value ) { | ||
mode = value; | |||
topAreas = []; activeArea = null; selectedTime = null; | |||
modeEntries = allSpeciesEntries.filter( modeOk ); | |||
refreshForms(); | |||
repopulate(); | |||
applyVisibility(); | |||
renderTop(); | |||
setDefaults(); | |||
recompute(); | |||
} | |||
// A manual control change deselects the chosen top area (its conditions no | |||
// longer match) but keeps the list available to re-click. | |||
function userChanged() { | |||
selectedTime = null; | |||
if ( activeArea ) { activeArea = null; renderTop(); } | |||
recompute(); | |||
} | |||
[ biomeSel, methodSel, timeSel, weatherSel, ySel, lightSel, moonSel, structSel, | |||
consecSel, playersSel ] | |||
.forEach( function ( ctl ) { | |||
ctl.addEventListener( 'change', userChanged ); | |||
ctl.addEventListener( 'input', userChanged ); | |||
} ); | |||
bestBtn.addEventListener( 'click', findBest ); | bestBtn.addEventListener( 'click', findBest ); | ||
// ── assemble | // ── assemble (two columns) ─────────────────────────────────────── | ||
var header = el( 'div', { class: 'scalc-header' }, [ | var header = el( 'div', { class: 'scalc-header' }, [ | ||
el( 'span', { class: 'scalc-header-title', text: 'Spawn Chance Calculator' } ) | el( 'span', { class: 'scalc-header-title', text: 'Spawn Chance Calculator' } ), | ||
el( 'span', { class: 'scalc-source', text: 'Effective server data' } ) | |||
] ); | ] ); | ||
var | var leftCol = el( 'div', { class: 'scalc-left' }, [ form, meta, actions, topWrap ] ); | ||
var rightCol = el( 'div', { class: 'scalc-right' }, [ results ] ); | |||
var body = el( 'div', { class: 'scalc-body scalc-cols' }, [ leftCol, rightCol ] ); | |||
root.appendChild( header ); | root.appendChild( header ); | ||
root.appendChild( body ); | root.appendChild( body ); | ||
refreshSpecies(); | |||
repopulate(); | |||
applyVisibility(); | |||
setDefaults(); | setDefaults(); | ||
if ( !initialSpecies ) { | |||
var pre = getParam( 'species' ); | |||
if ( pre && speciesSet[ pre ] ) { speciesInput.value = pre; setSpecies( pre ); } | |||
} | |||
recompute(); | recompute(); | ||
} | } | ||
| Line 473: | Line 912: | ||
if ( !root ) { return; } | if ( !root ) { return; } | ||
var data = window.PixelmonSpawnData; | var data = window.PixelmonSpawnData; | ||
if ( !data || !data.entries ) { | |||
if ( !data || !data.entries | |||
root.textContent = 'Spawn calculator data failed to load.'; | root.textContent = 'Spawn calculator data failed to load.'; | ||
return; | |||
} | |||
if ( data.source !== 'effective-datapacks+server-config' ) { | |||
root.textContent = 'Spawn calculator data is not from the effective datapack and server configuration. A data refresh is required.'; | |||
return; | return; | ||
} | } | ||
root.classList.add( 'scalc-ready' ); | root.classList.add( 'scalc-ready' ); | ||
root.innerHTML = ''; | root.innerHTML = ''; | ||
build( root, data, species ); | build( root, data, root.getAttribute( 'data-species' ) || '' ); | ||
} | } | ||
Latest revision as of 23:02, 15 August 2026
/*
* Pixelmon spawn-chance calculator gadget (full-page).
*
* Mounts into <div id="spawn-calculator" data-species="..."> on the standalone
* Spawn Calculator page. data-species is normally empty; a ?species=Name query
* parameter (the link on each species page) pre-fills the picker.
*
* Left column: pick a species and situation (biome, location type, time of day,
* weather, Y-level, light level, moon phase). Only dimensions that constrain the
* chosen species are shown, and each categorical filter lists only values where
* the species can spawn. Each shown field can be locked so "Find best chance"
* leaves it fixed. Find best chance lists the top areas; clicking one fills the
* form and shows that area's full spawn breakdown on the right.
*
* Right column: the chosen species' chance, plus every species that can spawn in
* the current situation with its share, the chosen species highlighted.
*
* Data is provided by Gadget-spawncalc-data.js as window.PixelmonSpawnData.
* This file is static UI/logic; it is NOT generated. The data file IS.
*/
( function () {
'use strict';
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;
}
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 uniq( arr ) {
var seen = {}, out = [];
arr.forEach( function ( v ) { if ( !seen[ v ] ) { seen[ v ] = 1; out.push( v ); } } );
return out;
}
function getParam( name ) {
var m = new RegExp( '[?&]' + name + '=([^&#]*)' ).exec( location.search || '' );
if ( !m ) { return null; }
try { return decodeURIComponent( m[ 1 ].replace( /\+/g, ' ' ) ); } catch ( e ) { return m[ 1 ]; }
}
function articlePath() {
return ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/index.php/$1';
}
function pageHref( title ) {
return articlePath().replace( '$1', encodeURIComponent( String( title ).replace( / /g, '_' ) ) );
}
function fileHref( filename ) {
return articlePath().replace( '$1', 'Special:FilePath/' + encodeURIComponent( filename ) );
}
function build( root, data, initialSpecies ) {
var entries = data.entries;
var NT = data.times.length;
var timeLen = data.times.map( function ( name ) { return TIME_TICKS[ name ] || 1; } );
var allTimes = data.times.map( function ( _t, i ) { return i; } );
var allWeathers = data.weathers.map( function ( _w, i ) { return i; } );
var byMethod = {};
entries.forEach( function ( e ) {
e.m.forEach( function ( m ) { ( byMethod[ m ] = byMethod[ m ] || [] ).push( e ); } );
} );
var speciesSet = {}, speciesList = [];
entries.forEach( function ( e ) {
if ( !speciesSet[ e.s ] ) { speciesSet[ e.s ] = true; speciesList.push( e.s ); }
} );
speciesList.sort( function ( a, b ) { return a.localeCompare( b ); } );
var species = initialSpecies || '';
var allSpeciesEntries = []; // every spawn system and form for the chosen species
var modeEntries = []; // entries for the selected spawn system
var speciesEntries = []; // form-filtered entries for options and optimizer
var modes = [], showMode = false, mode = 'standard';
var forms = [], showForm = false, curForm = 'All';
var rel = {};
function modeOf( e ) { return e.i === 1 ? 'legendary' : 'standard'; }
function modeLabel( value ) { return value === 'legendary' ? 'Legendary' : 'Standard wild'; }
function modeOk( e ) { return modeOf( e ) === mode; }
// A spawn entry's form matches the chosen form ('All' accepts any).
function formOk( c ) { return curForm === 'All' || ( c.f || 'Normal' ) === curForm; }
function applyForm() {
speciesEntries = ( curForm === 'All' ) ? modeEntries.slice()
: modeEntries.filter( function ( e ) { return ( e.f || 'Normal' ) === curForm; } );
function any( fn ) { return speciesEntries.some( fn ); }
function constrained( e, field ) {
return e[ field ] != null || ( e.rm || [] ).some( function ( multiplier ) {
return multiplier[ field ] != null;
} );
}
rel = {
time: any( function ( e ) { return constrained( e, 't' ); } ),
weather: any( function ( e ) { return constrained( e, 'w' ); } ),
y: any( function ( e ) { return constrained( e, 'y0' ) || constrained( e, 'y1' ); } ),
light: any( function ( e ) { return constrained( e, 'l0' ) || constrained( e, 'l1' ); } ),
moon: any( function ( e ) { return constrained( e, 'mp' ); } ),
struct: any( function ( e ) { return constrained( e, 'st' ); } )
};
}
function refreshForms() {
var seen = {}; forms = [];
modeEntries.forEach( function ( e ) {
var f = e.f || 'Normal';
if ( !seen[ f ] ) { seen[ f ] = 1; forms.push( f ); }
} );
forms.sort( function ( a, b ) {
if ( a === 'Normal' ) { return -1; }
if ( b === 'Normal' ) { return 1; }
return a.localeCompare( b );
} );
// Only offer a form picker when more than one form spawns naturally;
// then include "All" to accept any form.
showForm = forms.length > 1;
fillSelect( formSel, ( showForm ? [ 'All' ].concat( forms ) : forms )
.map( function ( f ) { return { v: f, t: f }; } ) );
curForm = showForm ? 'All' : ( forms[ 0 ] || 'All' );
formSel.value = curForm;
applyForm();
}
function refreshSpecies() {
allSpeciesEntries = entries.filter( function ( e ) { return e.s === species; } );
var seenModes = {}; modes = [];
allSpeciesEntries.forEach( function ( e ) {
var spawnMode = modeOf( e );
if ( !seenModes[ spawnMode ] ) { seenModes[ spawnMode ] = 1; modes.push( spawnMode ); }
} );
modes.sort();
showMode = modes.length > 1;
mode = modes.indexOf( 'legendary' ) !== -1 && modes.indexOf( 'standard' ) === -1
? 'legendary' : ( modes[ 0 ] || 'standard' );
fillSelect( modeSel, modes.map( function ( value ) {
return { v: value, t: modeLabel( value ) };
} ) );
modeSel.value = mode;
modeEntries = allSpeciesEntries.filter( modeOk );
refreshForms();
}
// ── core probability ───────────────────────────────────────────
function passes( c, base ) {
if ( c.b && !inList( c.b, base.B ) ) { return false; }
if ( c.ab && inList( c.ab, base.B ) ) { return false; }
if ( base.y != null ) {
if ( c.y0 != null && base.y < c.y0 ) { return false; }
if ( c.y1 != null && base.y > c.y1 ) { return false; }
}
if ( base.light != null ) {
if ( c.l0 != null && base.light < c.l0 ) { return false; }
if ( c.l1 != null && base.light > c.l1 ) { return false; }
}
if ( base.moon != null && c.mp != null && base.moon !== c.mp ) { return false; }
if ( base.W != null && c.w && !inList( c.w, base.W ) ) { return false; }
// Structure-gated spawns only occur inside a matching structure; with
// no structure chosen ("Outside") they are excluded entirely.
if ( c.st && ( base.struct == null || !inList( c.st, base.struct ) ) ) { return false; }
return true;
}
function multiplierPasses( condition, base, t ) {
if ( condition.b && !inList( condition.b, base.B ) ) { return false; }
if ( condition.t && ( t == null || !inList( condition.t, t ) ) ) { return false; }
if ( condition.w && ( base.W == null || !inList( condition.w, base.W ) ) ) { return false; }
if ( condition.y0 != null && ( base.y == null || base.y < condition.y0 ) ) { return false; }
if ( condition.y1 != null && ( base.y == null || base.y > condition.y1 ) ) { return false; }
if ( condition.l0 != null && ( base.light == null || base.light < condition.l0 ) ) { return false; }
if ( condition.l1 != null && ( base.light == null || base.light > condition.l1 ) ) { return false; }
if ( condition.mp != null && base.moon !== condition.mp ) { return false; }
if ( condition.st && ( base.struct == null || !inList( condition.st, base.struct ) ) ) { return false; }
return true;
}
function adjustedRarity( c, base, t ) {
var rarity = c.r;
( c.rm || [] ).forEach( function ( multiplier ) {
if ( multiplierPasses( multiplier, base, t ) ) { rarity *= multiplier.x; }
} );
return rarity;
}
function chanceP8( base ) {
var pool = byMethod[ base.M ] || [];
var num = [], den = [];
for ( var z = 0; z < NT; z++ ) { num.push( 0 ); den.push( 0 ); }
for ( var i = 0; i < pool.length; i++ ) {
var c = pool[ i ];
if ( !modeOk( c ) || !passes( c, base ) ) { continue; }
var ct = c.t || allTimes;
for ( var j = 0; j < ct.length; j++ ) {
var rarity = adjustedRarity( c, base, ct[ j ] );
den[ ct[ j ] ] += rarity;
if ( c.s === species && formOk( c ) ) { num[ ct[ j ] ] += rarity; }
}
}
var p8 = [];
for ( var q = 0; q < NT; q++ ) { p8.push( den[ q ] > 0 ? num[ q ] / den[ q ] : 0 ); }
return p8;
}
function chanceAt( base, t ) {
var pool = byMethod[ base.M ] || [];
var num = 0, den = 0, comp = 0;
for ( var i = 0; i < pool.length; i++ ) {
var c = pool[ i ];
if ( !modeOk( c ) || !passes( c, base ) ) { continue; }
if ( t != null && c.t && !inList( c.t, t ) ) { continue; }
var rarity = adjustedRarity( c, base, t );
den += rarity;
if ( c.s === species && formOk( c ) ) { num += rarity; } else { comp++; }
}
return { num: num, den: den, comp: comp, p: den > 0 ? num / den : 0 };
}
// Length-weighted share of every species that can spawn in this area over
// the given block of times. Target share matches the headline chance.
function spawnBreakdown( base, times ) {
var pool = byMethod[ base.M ] || [];
var totals = {}, sp = {};
for ( var i = 0; i < pool.length; i++ ) {
var c = pool[ i ];
if ( !modeOk( c ) || !passes( c, base ) ) { continue; }
var ct = c.t || allTimes;
for ( var j = 0; j < ct.length; j++ ) {
var t = ct[ j ];
if ( times.indexOf( t ) === -1 ) { continue; }
var rarity = adjustedRarity( c, base, t );
totals[ t ] = ( totals[ t ] || 0 ) + rarity;
if ( c.s === species && !formOk( c ) ) { continue; }
var m = sp[ c.s ] || ( sp[ c.s ] = {} );
m[ t ] = ( m[ t ] || 0 ) + rarity;
}
}
var wsum = 0;
for ( var k = 0; k < times.length; k++ ) { wsum += timeLen[ times[ k ] ]; }
var out = [];
Object.keys( sp ).forEach( function ( name ) {
var acc = 0;
times.forEach( function ( t ) {
if ( totals[ t ] > 0 ) { acc += timeLen[ t ] * ( ( sp[ name ][ t ] || 0 ) / totals[ t ] ); }
} );
out.push( { s: name, share: wsum > 0 ? acc / wsum : 0 } );
} );
out.sort( function ( a, b ) { return b.share - a.share; } );
return out;
}
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;
}
function blockWeighted( p8, times ) {
var wsum = 0, acc = 0;
for ( var i = 0; i < times.length; i++ ) {
wsum += timeLen[ times[ i ] ];
acc += timeLen[ times[ i ] ] * p8[ times[ i ] ];
}
return wsum > 0 ? acc / wsum : 0;
}
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; }
function fmtPct( p ) {
if ( !( p > 0 ) ) { return '0%'; }
var v = p * 100;
return ( v >= 1 ? v.toFixed( 1 ) : v.toFixed( 2 ) ) + '%';
}
function fmtDuration( minutes ) {
if ( minutes < 60 ) { return Math.round( minutes ) + ' minutes'; }
if ( minutes < 1440 ) { return ( minutes / 60 ).toFixed( minutes < 600 ? 1 : 0 ) + ' hours'; }
return ( minutes / 1440 ).toFixed( 1 ) + ' days';
}
// ── controls ────────────────────────────────────────────────────
var biomeSel = el( 'select', { class: 'scalc-input' } );
var methodSel = el( 'select', { class: 'scalc-input' } );
var timeSel = el( 'select', { class: 'scalc-input' } );
var weatherSel = el( 'select', { class: 'scalc-input' } );
var ySel = el( 'input', { type: 'number', min: '-64', max: '320', value: '63', class: 'scalc-num' } );
var lightSel = el( 'input', { type: 'number', min: '0', max: '15', value: '7', class: 'scalc-num' } );
var moonSel = el( 'select', { class: 'scalc-input' } );
var structSel = el( 'select', { class: 'scalc-input' } );
var formSel = el( 'select', { class: 'scalc-input' } );
var modeSel = el( 'select', { class: 'scalc-input' } );
var playersSel = el( 'input', { type: 'number', min: '1', value: '1', class: 'scalc-num' } );
function fillSelect( sel, items ) {
sel.innerHTML = '';
items.forEach( function ( o ) { sel.appendChild( el( 'option', { value: o.v, text: o.t } ) ); } );
}
function allBiomeIdx() { return data.biomes.map( function ( _b, i ) { return i; } ); }
function biomeOptionLabel( i ) {
return prettyBiome( data.biomes[ i ] );
}
function relevantTimes() {
if ( !speciesEntries.length || speciesEntries.some( function ( e ) { return !e.t; } ) ) { return allTimes; }
var all = [];
speciesEntries.forEach( function ( e ) { all = all.concat( e.t ); } );
return uniq( all ).sort( function ( a, b ) { return a - b; } );
}
function relevantWeathers() {
if ( !speciesEntries.length || speciesEntries.some( function ( e ) { return !e.w; } ) ) { return allWeathers; }
var all = [];
speciesEntries.forEach( function ( e ) { all = all.concat( e.w ); } );
return uniq( all ).sort( function ( a, b ) { return a - b; } );
}
function relevantMoons() {
if ( !speciesEntries.length || speciesEntries.some( function ( e ) { return e.mp == null; } ) ) {
var r = []; for ( var i = 0; i < 8; i++ ) { r.push( i ); } return r;
}
var all = [];
speciesEntries.forEach( function ( e ) { if ( e.mp != null ) { all.push( e.mp ); } } );
return uniq( all ).sort( function ( a, b ) { return a - b; } );
}
function applyNumRange( input, loF, hiF, absLo, absHi ) {
var los = [], his = [];
speciesEntries.forEach( function ( e ) {
if ( e[ loF ] != null ) { los.push( e[ loF ] ); }
if ( e[ hiF ] != null ) { his.push( e[ hiF ] ); }
} );
input.min = String( los.length ? Math.min.apply( null, los ) : absLo );
input.max = String( his.length ? Math.max.apply( null, his ) : absHi );
}
function repopulate() {
var noSp = !speciesEntries.length;
var biomeIdx = noSp ? allBiomeIdx() : biomeCandidates();
fillSelect( biomeSel, biomeIdx
.map( function ( i ) { return { v: i, t: biomeOptionLabel( i ) }; } )
.sort( function ( a, b ) { return a.t.localeCompare( b.t ); } ) );
var methodIdx = noSp ? data.methods.map( function ( _m, i ) { return i; } ) : methodCandidates();
fillSelect( methodSel, methodIdx
.map( function ( i ) { return { v: i, t: data.methods[ i ] }; } )
.sort( function ( a, b ) { return a.t.localeCompare( b.t ); } ) );
fillSelect( timeSel, relevantTimes().map( function ( i ) { return { v: i, t: titleCase( data.times[ i ] ) }; } ) );
fillSelect( weatherSel, relevantWeathers().map( function ( i ) { return { v: i, t: titleCase( data.weathers[ i ] ) }; } ) );
fillSelect( moonSel, relevantMoons().map( function ( i ) { return { v: i, t: 'Phase ' + i }; } ) );
var sopts = [ { v: 'Outside', t: 'Outside (open)' } ];
structCandidates().forEach( function ( i ) { sopts.push( { v: i, t: prettyBiome( data.structures[ i ] ) } ); } );
fillSelect( structSel, sopts );
applyNumRange( ySel, 'y0', 'y1', -64, 320 );
applyNumRange( lightSel, 'l0', 'l1', 0, 15 );
}
var consecSel = el( 'input', {
type: 'number', min: '1', max: String( NT ), value: '1', class: 'scalc-num',
title: 'Average a block of this many consecutive times of day, weighting '
+ 'each by its real length. Set to ' + NT + ' for any time.'
} );
var strictChk = el( 'input', { type: 'checkbox', class: 'scalc-check', checked: 'checked' } );
function lockBox() {
return el( 'input', { type: 'checkbox', class: 'scalc-lock-chk',
title: 'Lock: keep this value when using Find best chance.' } );
}
var lockBiome = lockBox(), lockMethod = lockBox(), lockTime = lockBox(),
lockWeather = lockBox(), lockY = lockBox(), lockLight = lockBox(), lockMoon = lockBox(),
lockStruct = lockBox();
function field( labelText, control, lock ) {
var lab = el( 'label', { class: 'scalc-label' }, [ document.createTextNode( labelText ) ] );
if ( lock ) {
lab.appendChild( el( 'label', { class: 'scalc-lock' }, [ lock, document.createTextNode( ' lock' ) ] ) );
}
return el( 'div', { class: 'scalc-field' }, [ lab, control ] );
}
var speciesInput = el( 'input', {
class: 'scalc-input scalc-species', list: 'scalc-species-list',
placeholder: 'Type a Pokemon name', autocomplete: 'off'
} );
var speciesDl = el( 'datalist', { id: 'scalc-species-list' } );
speciesList.forEach( function ( n ) { speciesDl.appendChild( el( 'option', { value: n } ) ); } );
function onSpeciesPick() { if ( speciesSet[ speciesInput.value ] ) { setSpecies( speciesInput.value ); } }
speciesInput.addEventListener( 'change', onSpeciesPick );
speciesInput.addEventListener( 'input', onSpeciesPick );
var fSpecies = el( 'div', { class: 'scalc-field scalc-field-species' }, [
el( 'label', { class: 'scalc-label', text: 'Pokemon' } ), speciesInput, speciesDl
] );
var fForm = field( 'Form', formSel );
formSel.addEventListener( 'change', function () { setForm( formSel.value ); } );
var fMode = field( 'Spawn system', modeSel );
modeSel.addEventListener( 'change', function () { setMode( modeSel.value ); } );
var fBiome = field( 'Biome', biomeSel, lockBiome );
var fMethod = field( 'Location type', methodSel, lockMethod );
var fTime = field( 'Time of day', timeSel, lockTime );
var fWeather = field( 'Weather', weatherSel, lockWeather );
var fY = field( 'Y-level', ySel, lockY );
var fLight = field( 'Light level', lightSel, lockLight );
var fMoon = field( 'Moon phase', moonSel, lockMoon );
var fStruct = field( 'Structure', structSel, lockStruct );
var fPlayers = field( 'Online players', playersSel );
var formFields = [];
if ( !initialSpecies ) { formFields.push( fSpecies ); }
formFields.push( fMode, fForm, fBiome, fMethod, fTime, fWeather, fY, fLight, fMoon, fStruct, fPlayers );
var form = el( 'div', { class: 'scalc-form' }, formFields );
var meta = el( 'div', { class: 'scalc-form scalc-meta' }, [
el( 'span', { class: 'scalc-meta-label', text: 'Time block' } ),
field( 'Consecutive times', consecSel ),
el( 'div', { class: 'scalc-field' }, [
el( 'label', { class: 'scalc-label', text: 'Strict block' } ),
el( 'label', { class: 'scalc-check-wrap',
title: 'Only accept blocks where the species spawns in every time. '
+ 'If none exists, the block size is reduced and a warning is shown.' }, [
strictChk, el( 'span', { class: 'scalc-check-text', text: 'all times must spawn' } )
] )
] )
] );
var bestBtn = el( 'button', { class: 'scalc-btn', type: 'button', text: 'Find best chance' } );
var countSel = el( 'input', {
type: 'number', min: '1', value: '5', class: 'scalc-num',
title: 'How many top areas to list when using Find best chance.'
} );
var actions = el( 'div', { class: 'scalc-actions' }, [
bestBtn,
el( 'label', { class: 'scalc-count-wrap' }, [
el( 'span', { class: 'scalc-count-label', text: 'Results' } ), countSel
] )
] );
var topWrap = el( 'div', { class: 'scalc-top' } );
var results = el( 'div', { class: 'scalc-results' } );
function getN() { return Math.max( 1, Math.min( NT, parseInt( consecSel.value, 10 ) || 1 ) ); }
function getResultCount() { return Math.max( 1, parseInt( countSel.value, 10 ) || 5 ); }
function getBase() {
return {
B: +biomeSel.value,
M: +methodSel.value,
W: rel.weather ? +weatherSel.value : null,
y: rel.y ? ( ySel.value === '' ? null : +ySel.value ) : null,
light: rel.light ? ( lightSel.value === '' ? null : +lightSel.value ) : null,
moon: rel.moon ? +moonSel.value : null,
struct: rel.struct && structSel.value !== 'Outside' && structSel.value !== ''
? +structSel.value : null
};
}
function structCandidates() {
var all = [];
speciesEntries.forEach( function ( e ) {
if ( e.st ) { all = all.concat( e.st ); }
( e.rm || [] ).forEach( function ( multiplier ) {
if ( multiplier.st ) { all = all.concat( multiplier.st ); }
} );
} );
return uniq( all );
}
function curTimes() { return rel.time ? blockTimes( +timeSel.value, getN() ) : allTimes; }
function applyVisibility() {
fMode.style.display = showMode ? '' : 'none';
fForm.style.display = showForm ? '' : 'none';
fTime.style.display = rel.time ? '' : 'none';
fWeather.style.display = rel.weather ? '' : 'none';
fY.style.display = rel.y ? '' : 'none';
fLight.style.display = rel.light ? '' : 'none';
fMoon.style.display = rel.moon ? '' : 'none';
fStruct.style.display = rel.struct ? '' : 'none';
fPlayers.style.display = mode === 'legendary' ? '' : 'none';
meta.style.display = rel.time ? '' : 'none';
}
var flagBest = false, warnText = null, topAreas = [], activeArea = null, selectedTime = null, topN = 1;
function areaLabel( area ) {
var b = area.base;
var parts = [ biomeOptionLabel( b.B ), data.methods[ b.M ] ];
if ( rel.time && area.start != null ) {
parts.push( titleCase( data.times[ area.start ] ) + ( topN > 1 ? ' +' + ( topN - 1 ) : '' ) );
}
if ( rel.weather && b.W != null ) { parts.push( titleCase( data.weathers[ b.W ] ) ); }
if ( rel.y && b.y != null ) { parts.push( 'Y ' + b.y ); }
if ( rel.light && b.light != null ) { parts.push( 'Light ' + b.light ); }
if ( rel.moon && b.moon != null ) { parts.push( 'Moon ' + b.moon ); }
if ( rel.struct ) { parts.push( b.struct != null ? prettyBiome( data.structures[ b.struct ] ) : 'Outside' ); }
return parts.join( ' / ' );
}
function renderTop() {
topWrap.innerHTML = '';
if ( !topAreas.length ) { return; }
topWrap.appendChild( el( 'div', { class: 'scalc-top-title', text: 'Top areas (click to view)' } ) );
topAreas.forEach( function ( area, idx ) {
var item = el( 'div', {
class: 'scalc-top-item' + ( activeArea === area ? ' scalc-top-active' : '' )
}, [
el( 'span', { class: 'scalc-top-rank', text: '#' + ( idx + 1 ) } ),
el( 'span', { class: 'scalc-top-pct', text: fmtPct( area.p ) } ),
el( 'span', { class: 'scalc-top-params', text: areaLabel( area ) } )
] );
item.addEventListener( 'click', function () { applyArea( area ); } );
topWrap.appendChild( item );
} );
}
function recompute() {
results.innerHTML = '';
if ( !species ) {
results.appendChild( el( 'div', { class: 'scalc-result' }, [
el( 'span', { class: 'scalc-note', text: 'Choose a Pokemon to see its spawn chances.' } )
] ) );
flagBest = false; warnText = null; return;
}
if ( !speciesEntries.length ) {
results.appendChild( el( 'div', { class: 'scalc-result scalc-result-miss' }, [
el( 'span', { class: 'scalc-note', text:
species + ' has no supported standard or Legendary overworld spawns.' } )
] ) );
flagBest = false; warnText = null; return;
}
var base = getBase();
var N = getN();
var p8 = [];
for ( var t = 0; t < NT; t++ ) { p8.push( chanceAt( base, t ).p ); }
var times = rel.time ? blockTimes( +timeSel.value, N ) : allTimes;
var showBreak = rel.time && times.length > 1;
var single = rel.time && times.length === 1;
var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times );
var detailText;
if ( mode === 'legendary' ) {
var legendary = data.legendary || {};
var players = Math.max( 1, parseInt( playersSel.value, 10 ) || 1 );
var effectiveTicks = ( legendary.ticks || 25000 ) /
( 1 + ( players - 1 ) * ( legendary.playerMultiplier || 0 ) );
var attemptChance = ( legendary.chance == null ? 0.3 : legendary.chance ) * p;
var waitMinutes = attemptChance > 0 ? effectiveTicks / 1200 / attemptChance : 0;
detailText = '~1 in ' + Math.round( 1 / p ) + ' eligible Legendary selections. '
+ 'With the configured ' + fmtPct( legendary.chance == null ? 0.3 : legendary.chance )
+ ' attempt success chance, the estimated average wait is '
+ fmtDuration( waitMinutes ) + ', assuming a viable location is found.';
} else if ( single ) {
var res = chanceAt( base, times[ 0 ] );
detailText = '~1 in ' + Math.round( 1 / res.p ) + ' spawns - rarity '
+ round2( res.num ) + ' of ' + round2( res.den ) + ' vs ' + res.comp
+ ' competing spawn' + ( res.comp === 1 ? '' : 's' ) + '.';
} else {
detailText = '~1 in ' + Math.round( 1 / p ) + ' spawns, length-weighted across '
+ ( !rel.time ? 'any time (this Pokemon ignores time of day)'
: ( N >= NT ? 'any time' : N + ' consecutive times' ) ) + '.';
}
var hit = p > 0;
var head = el( 'div', { class: 'scalc-result-head' }, [
el( 'span', { class: 'scalc-pct', text: fmtPct( hit ? p : 0 ) } ),
el( 'span', { class: 'scalc-pctlabel', text:
( mode === 'legendary' ? 'share of eligible Legendary selections for ' :
'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.' } );
var children = [];
if ( warnText ) { children.push( el( 'div', { class: 'scalc-warn', text: warnText } ) ); }
children.push( head, detail );
// Which time the breakdown shows: a single block time (chip-selected,
// defaulting to the highest-chance time) or the whole block / day.
var bdTimes = times, bdLabel = null;
if ( hit && showBreak ) {
if ( selectedTime == null || times.indexOf( selectedTime ) === -1 ) {
selectedTime = times[ 0 ];
times.forEach( function ( t ) { if ( p8[ t ] > p8[ selectedTime ] ) { selectedTime = t; } } );
}
bdTimes = [ selectedTime ];
bdLabel = titleCase( data.times[ selectedTime ] );
var deadCount = 0;
var chips = times.map( function ( ti ) {
var pv = p8[ ti ], dead = !( pv > 0 );
if ( dead ) { deadCount++; }
var chip = el( 'span', {
class: 'scalc-chip scalc-chip-click' + ( dead ? ' scalc-chip-dead' : '' )
+ ( ti === selectedTime ? ' scalc-chip-active' : '' ),
title: 'Show spawns during ' + titleCase( data.times[ ti ] )
}, [
el( 'span', { class: 'scalc-chip-time', text: titleCase( data.times[ ti ] ) } ),
el( 'span', { class: 'scalc-chip-pct', text: fmtPct( pv ) } )
] );
chip.addEventListener( 'click', function () { selectedTime = ti; recompute(); } );
return chip;
} );
children.push( el( 'div', { class: 'scalc-breakdown' }, chips ) );
if ( deadCount > 0 ) {
children.push( el( 'div', { class: 'scalc-note', text:
deadCount + ' of ' + times.length + ' times have no ' + species
+ ' spawns here' + ( strictChk.checked ? ''
: '; enable "Strict block" to make the optimizer avoid them' ) + '.' } ) );
}
} else if ( single ) {
bdLabel = titleCase( data.times[ times[ 0 ] ] );
} else if ( !rel.time ) {
bdLabel = 'any time';
}
results.appendChild( el( 'div', {
class: 'scalc-result ' + ( hit ? 'scalc-result-hit' : 'scalc-result-miss' )
+ ( flagBest && hit ? ' scalc-result-best' : '' )
}, children ) );
// Full spawn breakdown for the selected time (or whole block / day).
var bd = spawnBreakdown( base, bdTimes );
var card = el( 'div', { class: 'scalc-bd' }, [
el( 'div', { class: 'scalc-bd-title',
text: ( mode === 'legendary' ? 'Eligible Legendary pool' : 'Spawns in this area' )
+ ( bdLabel ? ' (' + bdLabel + ')' : '' ) } )
] );
if ( !bd.length ) {
card.appendChild( el( 'div', { class: 'scalc-note', text: 'Nothing spawns here.' } ) );
} else {
var list = el( 'div', { class: 'scalc-bd-list' } );
bd.slice( 0, 60 ).forEach( function ( r ) {
var isT = r.s === species;
var left = el( 'span', { class: 'scalc-bd-left' } );
var spriteFile = data.sprites && data.sprites[ r.s ];
if ( spriteFile ) {
left.appendChild( el( 'img', { class: 'scalc-bd-img',
src: fileHref( spriteFile ), alt: r.s, loading: 'lazy' } ) );
}
left.appendChild( el( 'a', { class: 'scalc-bd-name', href: pageHref( r.s ),
title: r.s }, [ document.createTextNode( r.s ) ] ) );
list.appendChild( el( 'div', { class: 'scalc-bd-row' + ( isT ? ' scalc-bd-target' : '' ) }, [
left,
el( 'span', { class: 'scalc-bd-pct', text: fmtPct( r.share ) } )
] ) );
} );
card.appendChild( list );
if ( bd.length > 60 ) {
card.appendChild( el( 'div', { class: 'scalc-note', text: 'and ' + ( bd.length - 60 ) + ' more.' } ) );
}
}
results.appendChild( card );
flagBest = false; warnText = null;
}
// ── optimizer ────────────────────────────────────────────────────
function biomeCandidates() {
if ( speciesEntries.some( function ( e ) { return !e.b; } ) ) { return allBiomeIdx(); }
var all = [];
speciesEntries.forEach( function ( e ) { if ( e.b ) { all = all.concat( e.b ); } } );
return uniq( all );
}
function methodCandidates() {
var all = [];
speciesEntries.forEach( function ( e ) { all = all.concat( e.m ); } );
return uniq( all );
}
function numCandidates( lo, hi, extra ) {
var vals = [];
speciesEntries.forEach( function ( e ) {
if ( e[ lo ] != null ) { vals.push( e[ lo ] ); }
if ( e[ hi ] != null ) { vals.push( e[ hi ] ); }
( e.rm || [] ).forEach( function ( multiplier ) {
if ( multiplier[ lo ] != null ) { vals.push( multiplier[ lo ] ); }
if ( multiplier[ hi ] != null ) { vals.push( multiplier[ hi ] ); }
} );
} );
( extra || [] ).forEach( function ( v ) { vals.push( v ); } );
return uniq( vals );
}
function moonCandidatesList() {
var vals = [];
speciesEntries.forEach( function ( e ) {
if ( e.mp != null ) { vals.push( e.mp ); }
( e.rm || [] ).forEach( function ( multiplier ) {
if ( multiplier.mp != null ) { vals.push( multiplier.mp ); }
} );
} );
return uniq( vals );
}
function placeKey( b ) { return [ b.B, b.M, b.W, b.y, b.light, b.moon, b.struct ].join( '|' ); }
function applyArea( area ) {
var b = area.base;
biomeSel.value = b.B;
methodSel.value = b.M;
if ( rel.weather && b.W != null ) { weatherSel.value = b.W; }
if ( rel.y && b.y != null ) { ySel.value = b.y; }
if ( rel.light && b.light != null ) { lightSel.value = b.light; }
if ( rel.moon && b.moon != null ) { moonSel.value = b.moon; }
if ( rel.struct ) { structSel.value = ( b.struct != null ? b.struct : 'Outside' ); }
if ( rel.time && area.start != null ) { timeSel.value = area.start; }
activeArea = area;
selectedTime = null;
flagBest = true;
recompute();
renderTop();
}
function findBest() {
if ( !species || !speciesEntries.length ) { recompute(); return; }
var N = getN(), strict = strictChk.checked, lockT = lockTime.checked, curT = +timeSel.value;
var biomeC = lockBiome.checked ? [ +biomeSel.value ] : biomeCandidates();
var methodC = lockMethod.checked ? [ +methodSel.value ] : methodCandidates();
var weatherC = !rel.weather ? [ null ] : ( lockWeather.checked ? [ +weatherSel.value ] : allWeathers );
var yC = !rel.y ? [ null ] : ( lockY.checked ? [ +ySel.value ] : numCandidates( 'y0', 'y1', [ 63 ] ) );
var lightC = !rel.light ? [ null ] : ( lockLight.checked ? [ +lightSel.value ] : numCandidates( 'l0', 'l1', [ 0, 7, 15 ] ) );
var moonC = !rel.moon ? [ null ] : ( lockMoon.checked ? [ +moonSel.value ] : moonCandidatesList() );
var curStruct = ( structSel.value === 'Outside' || structSel.value === '' ? null : +structSel.value );
var hasOpen = speciesEntries.some( function ( e ) { return !e.st; } );
var structC = !rel.struct ? [ null ]
: ( lockStruct.checked ? [ curStruct ]
: ( hasOpen ? [ null ] : [] ).concat( structCandidates() ) );
var situations = [];
for ( var a = 0; a < biomeC.length; a++ ) {
for ( var b = 0; b < methodC.length; b++ ) {
for ( var c = 0; c < weatherC.length; c++ ) {
for ( var d = 0; d < yC.length; d++ ) {
for ( var f = 0; f < lightC.length; f++ ) {
for ( var g = 0; g < moonC.length; g++ ) {
for ( var h = 0; h < structC.length; h++ ) {
var base = { B: biomeC[ a ], M: methodC[ b ], W: weatherC[ c ],
y: yC[ d ], light: lightC[ f ], moon: moonC[ g ], struct: structC[ h ] };
situations.push( { base: base, p8: chanceP8( base ) } );
}
}
}
}
}
}
}
// Best block per distinct area (place) for block size n.
function rankAt( n, strictFlag ) {
var byPlace = {};
for ( var i = 0; i < situations.length; i++ ) {
var s = situations[ i ], p8 = s.p8, key = placeKey( s.base );
if ( !rel.time ) {
var pw = blockWeighted( p8, allTimes );
if ( pw > 0 && ( !byPlace[ key ] || pw > byPlace[ key ].p ) ) {
byPlace[ key ] = { base: s.base, start: null, p: pw };
}
continue;
}
var starts = lockT ? [ curT ] : allTimes;
for ( var si = 0; si < starts.length; si++ ) {
var start = starts[ si ];
var times = blockTimes( start, n );
var allSpawn = true;
for ( var j = 0; j < times.length; j++ ) {
if ( !( p8[ times[ j ] ] > 0 ) ) { allSpawn = false; break; }
}
if ( strictFlag && !allSpawn ) { if ( n >= NT ) { break; } continue; }
var p = times.length === 1 ? p8[ times[ 0 ] ] : blockWeighted( p8, times );
if ( p > 0 && ( !byPlace[ key ] || p > byPlace[ key ].p ) ) {
byPlace[ key ] = { base: s.base, start: start, p: p };
}
if ( n >= NT ) { break; }
}
}
return Object.keys( byPlace ).map( function ( k ) { return byPlace[ k ]; } )
.sort( function ( x, y ) { return y.p - x.p; } );
}
var ranked = [], effN = N;
if ( !rel.time ) {
ranked = rankAt( N, false );
} else if ( strict ) {
for ( var n = N; n >= 1; n-- ) {
ranked = rankAt( n, true );
if ( ranked.length ) { effN = n; break; }
}
} else {
ranked = rankAt( N, false );
}
if ( !ranked.length ) { topAreas = []; renderTop(); recompute(); return; }
topAreas = ranked.slice( 0, getResultCount() );
topN = effN;
if ( rel.time && strict && effN < N ) {
consecSel.value = effN;
warnText = species + ' spawns in at most ' + effN + ' consecutive time'
+ ( effN === 1 ? '' : 's' ) + ' in any situation, so the block was reduced from '
+ N + ' to ' + effN + '.';
}
applyArea( topAreas[ 0 ] );
}
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 );
if ( rel.light ) { lightSel.value = ( se.l0 != null ? se.l0 : ( se.l1 != null ? se.l1 : 7 ) ); }
if ( rel.moon && se.mp != null ) { moonSel.value = se.mp; }
if ( rel.struct ) {
var hasOpen = speciesEntries.some( function ( e ) { return !e.st; } );
if ( hasOpen ) { structSel.value = 'Outside'; }
else { var sc = structCandidates(); if ( sc.length ) { structSel.value = sc[ 0 ]; } }
}
}
function setSpecies( name ) {
species = name;
topAreas = []; activeArea = null; selectedTime = null;
refreshSpecies();
repopulate();
applyVisibility();
renderTop();
setDefaults();
recompute();
}
function setForm( f ) {
curForm = f;
topAreas = []; activeArea = null; selectedTime = null;
applyForm();
repopulate();
applyVisibility();
renderTop();
setDefaults();
recompute();
}
function setMode( value ) {
mode = value;
topAreas = []; activeArea = null; selectedTime = null;
modeEntries = allSpeciesEntries.filter( modeOk );
refreshForms();
repopulate();
applyVisibility();
renderTop();
setDefaults();
recompute();
}
// A manual control change deselects the chosen top area (its conditions no
// longer match) but keeps the list available to re-click.
function userChanged() {
selectedTime = null;
if ( activeArea ) { activeArea = null; renderTop(); }
recompute();
}
[ biomeSel, methodSel, timeSel, weatherSel, ySel, lightSel, moonSel, structSel,
consecSel, playersSel ]
.forEach( function ( ctl ) {
ctl.addEventListener( 'change', userChanged );
ctl.addEventListener( 'input', userChanged );
} );
bestBtn.addEventListener( 'click', findBest );
// ── assemble (two columns) ───────────────────────────────────────
var header = el( 'div', { class: 'scalc-header' }, [
el( 'span', { class: 'scalc-header-title', text: 'Spawn Chance Calculator' } ),
el( 'span', { class: 'scalc-source', text: 'Effective server data' } )
] );
var leftCol = el( 'div', { class: 'scalc-left' }, [ form, meta, actions, topWrap ] );
var rightCol = el( 'div', { class: 'scalc-right' }, [ results ] );
var body = el( 'div', { class: 'scalc-body scalc-cols' }, [ leftCol, rightCol ] );
root.appendChild( header );
root.appendChild( body );
refreshSpecies();
repopulate();
applyVisibility();
setDefaults();
if ( !initialSpecies ) {
var pre = getParam( 'species' );
if ( pre && speciesSet[ pre ] ) { speciesInput.value = pre; setSpecies( pre ); }
}
recompute();
}
function init() {
var root = document.getElementById( 'spawn-calculator' );
if ( !root ) { return; }
var data = window.PixelmonSpawnData;
if ( !data || !data.entries ) {
root.textContent = 'Spawn calculator data failed to load.';
return;
}
if ( data.source !== 'effective-datapacks+server-config' ) {
root.textContent = 'Spawn calculator data is not from the effective datapack and server configuration. A data refresh is required.';
return;
}
root.classList.add( 'scalc-ready' );
root.innerHTML = '';
build( root, data, root.getAttribute( 'data-species' ) || '' );
}
if ( document.readyState !== 'loading' ) { init(); }
else { document.addEventListener( 'DOMContentLoaded', init ); }
}() );