MediaWiki:Gadget-fa-bid-calculator.js: Difference between revisions
FA bid calculator |
The league is NBA RealGM |
||
| (11 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | /** | ||
* | * NBA RealGM — free agency bid calculator. | ||
* Implements the points system on [[NBA:Free Agency]]. If the two ever disagree, | * Implements the points system on [[NBA:Free Agency]]. If the two ever disagree, | ||
* the wiki page is the rule and this script is the bug. | * the wiki page is the rule and this script is the bug. | ||
| Line 6: | Line 6: | ||
( function () { | ( function () { | ||
'use strict'; | 'use strict'; | ||
var NEW_POST_URL = 'https://2teams1cup.com/forum/index.php?act=post&do=new_post&f=67'; | |||
var MIN_OFFER = 500000, | var MIN_OFFER = 500000, | ||
MIN_RAISE = 200000, | MIN_RAISE = 200000, | ||
MIN_RAISE_POINTS = 0.2, | |||
MAX_YEARS = 4, | MAX_YEARS = 4, | ||
MAX_YEARS_35 = 2, | MAX_YEARS_35 = 2, | ||
YEAR_POINTS = 1.5; | YEAR_POINTS = 1.5; | ||
function money( n ) { | function money( n ) { | ||
return '$' + n.toLocaleString( 'en-US' ); | return '$' + n.toLocaleString( 'en-US' ); | ||
} | |||
/** Digits only, so "4,000,000" and "4000000" both read as 4000000. */ | |||
function parseMoney( v ) { | |||
var digits = String( v ).replace( /[^0-9]/g, '' ); | |||
return digits === '' ? NaN : Number( digits ); | |||
} | |||
/** | |||
* Reformat a text input with thousands separators, keeping the caret where | |||
* the typist left it. A plain reformat would send it to the end, which makes | |||
* editing the middle of a figure unusable. | |||
*/ | |||
function formatWithCaret( el ) { | |||
var before = el.value.slice( 0, el.selectionStart ), | |||
digitsBefore = before.replace( /[^0-9]/g, '' ).length, | |||
n = parseMoney( el.value ); | |||
el.value = isNaN( n ) ? '' : n.toLocaleString( 'en-US' ); | |||
// Walk forward past the same number of digits to place the caret. | |||
var pos = 0, seen = 0; | |||
while ( pos < el.value.length && seen < digitsBefore ) { | |||
if ( /[0-9]/.test( el.value[ pos ] ) ) { | |||
seen++; | |||
} | |||
pos++; | |||
} | |||
el.setSelectionRange( pos, pos ); | |||
} | |||
function pointWord( n ) { | |||
return '= ' + pts( n ) + ( pts( n ) === 1 ? ' point' : ' points' ); | |||
} | } | ||
| Line 24: | Line 61: | ||
function pts( n ) { | function pts( n ) { | ||
return Math.round( n * 100 ) / 100; | return Math.round( n * 100 ) / 100; | ||
} | |||
/** A monospace post block with a copy button. */ | |||
function postBlock( text ) { | |||
var wrap = document.createElement( 'div' ); | |||
wrap.className = 'tc-calc-postwrap'; | |||
var pre = document.createElement( 'div' ); | |||
pre.className = 'tc-calc-post'; | |||
pre.textContent = text; | |||
wrap.appendChild( pre ); | |||
var btn = document.createElement( 'button' ); | |||
btn.className = 'tc-calc-copy'; | |||
btn.type = 'button'; | |||
btn.textContent = 'Copy'; | |||
btn.addEventListener( 'click', function () { | |||
function done( ok ) { | |||
btn.textContent = ok ? 'Copied' : 'Press \u2318C / Ctrl+C'; | |||
setTimeout( function () { btn.textContent = 'Copy'; }, 2000 ); | |||
} | |||
if ( navigator.clipboard && navigator.clipboard.writeText ) { | |||
navigator.clipboard.writeText( text ).then( function () { done( true ); }, | |||
function () { selectFallback( pre ); done( false ); } ); | |||
} else { | |||
selectFallback( pre ); | |||
done( false ); | |||
} | |||
} ); | |||
wrap.appendChild( btn ); | |||
var actions = document.createElement( 'div' ); | |||
actions.className = 'tc-calc-actions'; | |||
var link = document.createElement( 'a' ); | |||
link.href = NEW_POST_URL; | |||
link.target = '_blank'; | |||
link.rel = 'noopener'; | |||
link.textContent = 'Copy and start a new topic in NBA: Free Agency'; | |||
// IPB has no parameter for seeding the post body, so the next best thing | |||
// is to put it on the clipboard on the way out. | |||
link.addEventListener( 'click', function () { | |||
if ( navigator.clipboard && navigator.clipboard.writeText ) { | |||
navigator.clipboard.writeText( text ); | |||
} | |||
} ); | |||
actions.appendChild( link ); | |||
var outer = document.createElement( 'div' ); | |||
outer.appendChild( wrap ); | |||
outer.appendChild( actions ); | |||
return outer; | |||
} | |||
/** Clipboard API unavailable or refused: select the text so the reader can copy it. */ | |||
function selectFallback( el ) { | |||
var range = document.createRange(); | |||
range.selectNodeContents( el ); | |||
var sel = window.getSelection(); | |||
sel.removeAllRanges(); | |||
sel.addRange( range ); | |||
} | } | ||
| Line 30: | Line 128: | ||
form.className = 'tc-calc'; | form.className = 'tc-calc'; | ||
/** | |||
* One labelled row. Controls live in their own flex wrapper so that on a | |||
* narrow screen the label can stack above them while they stay in a row. | |||
* Returns the wrapper, so callers can append a readout beside the control. | |||
*/ | |||
function row( labelText, el ) { | function row( labelText, el ) { | ||
var r = document.createElement( 'div' ); | var r = document.createElement( 'div' ); | ||
| Line 35: | Line 138: | ||
var l = document.createElement( 'label' ); | var l = document.createElement( 'label' ); | ||
l.textContent = labelText; | l.textContent = labelText; | ||
var c = document.createElement( 'div' ); | |||
c.className = 'tc-calc-controls'; | |||
c.appendChild( el ); | |||
r.appendChild( l ); | r.appendChild( l ); | ||
r.appendChild( | r.appendChild( c ); | ||
form.appendChild( r ); | form.appendChild( r ); | ||
return | return c; | ||
} | } | ||
var years = document.createElement( 'select' ); | var years = document.createElement( 'select' ); | ||
// A number input cannot display separators — browsers reject a comma as | |||
// invalid number syntax — so this is a text input formatted by hand. | |||
var salary = document.createElement( 'input' ); | var salary = document.createElement( 'input' ); | ||
salary.type = ' | salary.type = 'text'; | ||
salary. | salary.inputMode = 'numeric'; | ||
salary. | salary.autocomplete = 'off'; | ||
salary. | salary.className = 'tc-calc-money'; | ||
salary.value = MIN_OFFER.toLocaleString( 'en-US' ); | |||
salary.addEventListener( 'input', function () { | |||
formatWithCaret( salary ); | |||
} ); | |||
var age35 = document.createElement( 'input' ); | var age35 = document.createElement( 'input' ); | ||
| Line 54: | Line 166: | ||
beat.type = 'number'; | beat.type = 'number'; | ||
beat.step = '0.1'; | beat.step = '0.1'; | ||
beat.placeholder = ' | beat.placeholder = '8.5'; | ||
var name = document.createElement( 'input' ); | var name = document.createElement( 'input' ); | ||
name.type = 'text'; | name.type = 'text'; | ||
name.placeholder = 'Caldwell Jones'; | name.placeholder = 'Caldwell Jones'; | ||
name. | name.className = 'tc-calc-wide'; | ||
var team = document.createElement( 'input' ); | var team = document.createElement( 'input' ); | ||
team.type = 'text'; | team.type = 'text'; | ||
team.placeholder = ' | team.placeholder = 'Sonics'; | ||
team. | team.className = 'tc-calc-wide'; | ||
function fillYears() { | function fillYears() { | ||
| Line 80: | Line 192: | ||
fillYears(); | fillYears(); | ||
row( ' | var yearPts = document.createElement( 'span' ); | ||
row( 'Salary | yearPts.className = 'tc-calc-inline'; | ||
row( 'Contract length', years ).appendChild( yearPts ); | |||
var salaryPts = document.createElement( 'span' ); | |||
salaryPts.className = 'tc-calc-inline'; | |||
row( 'Salary', salary ).appendChild( salaryPts ); | |||
row( 'Player is 35+', age35 ).parentNode | |||
.querySelector( 'label' ).style.fontWeight = 'normal'; | |||
row( 'Bid to beat (optional)', beat ); | row( 'Bid to beat (optional)', beat ); | ||
row( 'Your team (optional)', team ); | row( 'Your team (optional)', team ); | ||
| Line 94: | Line 211: | ||
function render() { | function render() { | ||
var y = Number( years.value ), | var y = Number( years.value ), | ||
s = | s = parseMoney( salary.value ), | ||
max = age35.checked ? MAX_YEARS_35 : MAX_YEARS; | max = age35.checked ? MAX_YEARS_35 : MAX_YEARS; | ||
| Line 100: | Line 217: | ||
if ( !s || isNaN( s ) ) { | if ( !s || isNaN( s ) ) { | ||
yearPts.textContent = ''; | |||
salaryPts.textContent = ''; | |||
var empty = document.createElement( 'p' ); | var empty = document.createElement( 'p' ); | ||
empty.className = 'tc-calc-note'; | empty.className = 'tc-calc-note'; | ||
| Line 108: | Line 227: | ||
var total = points( y, s ); | var total = points( y, s ); | ||
yearPts.textContent = pointWord( y * YEAR_POINTS ); | |||
salaryPts.textContent = pointWord( s / 1000000 ); | |||
var big = document.createElement( 'div' ); | var big = document.createElement( 'div' ); | ||
| Line 114: | Line 235: | ||
out.appendChild( big ); | out.appendChild( big ); | ||
// --- validity --- | // --- validity --- | ||
| Line 134: | Line 243: | ||
if ( y > max ) { | if ( y > max ) { | ||
problems.push( 'Maximum length is ' + max + ' years' + | problems.push( 'Maximum length is ' + max + ' years' + | ||
( age35.checked ? ' for a player 35 | ( age35.checked ? ' for a player 35+' : '' ) + '.' ); | ||
} | } | ||
if ( problems.length ) { | if ( problems.length ) { | ||
| Line 149: | Line 258: | ||
box.className = 'tc-calc-note'; | box.className = 'tc-calc-note'; | ||
if ( total > | // A bid must clear the standing one by at least MIN_RAISE_POINTS, so | ||
// the bar is target + 0.2, not merely more than target. Everything | |||
// here is counted in tenths of a point: 0.1 points is exactly | |||
// $100,000 and a year is exactly 15 tenths, so integer arithmetic | |||
// gets the answer that 0.2 + 2.0 in binary floating point does not. | |||
var requiredTenths = Math.round( target * 10 ) + ( MIN_RAISE_POINTS * 10 ); | |||
if ( Math.round( total * 10 ) >= requiredTenths ) { | |||
box.textContent = 'This already beats ' + pts( target ) + ' points.'; | box.textContent = 'This already beats ' + pts( target ) + ' points.'; | ||
} else { | } else { | ||
var salaryTenths = requiredTenths - y * ( YEAR_POINTS * 10 ), | |||
var | newSalary = Math.max( salaryTenths * 100000, MIN_OFFER ); | ||
box.textContent = 'To beat ' + pts( target ) + ' points at ' + y + | |||
( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) + | ( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) + | ||
' (' + pts( points( y, newSalary ) ) + ' points).' | ' (' + pts( points( y, newSalary ) ) + ' points). '; | ||
var use = document.createElement( 'button' ); | |||
use.type = 'button'; | |||
use.className = 'tc-calc-use'; | |||
use.textContent = 'Use ' + money( newSalary ); | |||
use.addEventListener( 'click', function () { | |||
} | salary.value = newSalary.toLocaleString( 'en-US' ); | ||
box. | render(); | ||
} ); | |||
box.appendChild( use ); | |||
} | } | ||
out.appendChild( box ); | out.appendChild( box ); | ||
| Line 176: | Line 288: | ||
// --- ready-to-paste post --- | // --- ready-to-paste post --- | ||
// "Sonics offer Caldwell Jones 2 years @ $500,000 = 3.5 points". | |||
// Whatever of team/player is missing simply drops out. | |||
var tm = team.value.trim(), | |||
pl = name.value.trim(), | |||
y + ' | prefix = tm && pl ? tm + ' offer ' + pl : ( pl || ( tm ? tm + ' offer' : '' ) ), | ||
out.appendChild( | offer = y + ( y === 1 ? ' year' : ' years' ) + ' @ ' + money( s ) + | ||
' = ' + pts( total ) + ' points'; | |||
out.appendChild( postBlock( prefix ? prefix + ' ' + offer : offer ) ); | |||
} | } | ||