MediaWiki:Gadget-fa-bid-calculator.js: Difference between revisions
Appearance
Thousands separators in the salary field |
Bid to beat must clear the standing bid by 0.2 points |
||
| Line 11: | Line 11: | ||
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 ) { | ||
| Line 256: | 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 + | box.textContent = 'To beat ' + pts( target ) + ' points at ' + y + | ||
( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) + | ( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) + | ||
Revision as of 21:00, 7 October 2026
/**
* 1TC RealGM — free agency bid calculator.
* 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.
*/
( function () {
'use strict';
var NEW_POST_URL = 'https://2teams1cup.com/forum/index.php?act=post&do=new_post&f=67';
var MIN_OFFER = 500000,
MIN_RAISE = 200000,
MIN_RAISE_POINTS = 0.2,
MAX_YEARS = 4,
MAX_YEARS_35 = 2,
YEAR_POINTS = 1.5;
function money( n ) {
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' );
}
function points( years, salary ) {
return ( years * YEAR_POINTS ) + ( salary / 1000000 );
}
/** Round a points total for display without trailing float noise. */
function pts( n ) {
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 );
}
function build( container ) {
var form = document.createElement( 'div' );
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 ) {
var r = document.createElement( 'div' );
r.className = 'tc-calc-row';
var l = document.createElement( 'label' );
l.textContent = labelText;
var c = document.createElement( 'div' );
c.className = 'tc-calc-controls';
c.appendChild( el );
r.appendChild( l );
r.appendChild( c );
form.appendChild( r );
return c;
}
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' );
salary.type = 'text';
salary.inputMode = 'numeric';
salary.autocomplete = 'off';
salary.className = 'tc-calc-money';
salary.value = MIN_OFFER.toLocaleString( 'en-US' );
salary.addEventListener( 'input', function () {
formatWithCaret( salary );
} );
var age35 = document.createElement( 'input' );
age35.type = 'checkbox';
var beat = document.createElement( 'input' );
beat.type = 'number';
beat.step = '0.1';
beat.placeholder = '8.5';
var name = document.createElement( 'input' );
name.type = 'text';
name.placeholder = 'Caldwell Jones';
name.className = 'tc-calc-wide';
var team = document.createElement( 'input' );
team.type = 'text';
team.placeholder = 'Suns';
team.className = 'tc-calc-wide';
function fillYears() {
var max = age35.checked ? MAX_YEARS_35 : MAX_YEARS,
prev = Number( years.value ) || 1;
years.textContent = '';
for ( var y = 1; y <= max; y++ ) {
var o = document.createElement( 'option' );
o.value = String( y );
o.textContent = y + ( y === 1 ? ' year' : ' years' );
years.appendChild( o );
}
years.value = String( Math.min( prev, max ) );
}
fillYears();
var yearPts = document.createElement( 'span' );
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( 'Your team (optional)', team );
row( 'Player (optional)', name );
var out = document.createElement( 'div' );
out.className = 'tc-calc-out';
form.appendChild( out );
function render() {
var y = Number( years.value ),
s = parseMoney( salary.value ),
max = age35.checked ? MAX_YEARS_35 : MAX_YEARS;
out.textContent = '';
if ( !s || isNaN( s ) ) {
yearPts.textContent = '';
salaryPts.textContent = '';
var empty = document.createElement( 'p' );
empty.className = 'tc-calc-note';
empty.textContent = 'Enter a salary.';
out.appendChild( empty );
return;
}
var total = points( y, s );
yearPts.textContent = pointWord( y * YEAR_POINTS );
salaryPts.textContent = pointWord( s / 1000000 );
var big = document.createElement( 'div' );
big.className = 'tc-calc-big';
big.textContent = pts( total ) + ' points';
out.appendChild( big );
// --- validity ---
var problems = [];
if ( s < MIN_OFFER ) {
problems.push( 'Below the ' + money( MIN_OFFER ) + ' minimum offer.' );
}
if ( y > max ) {
problems.push( 'Maximum length is ' + max + ' years' +
( age35.checked ? ' for a player 35+' : '' ) + '.' );
}
if ( problems.length ) {
var warn = document.createElement( 'div' );
warn.className = 'tc-calc-warn';
warn.textContent = problems.join( ' ' );
out.appendChild( warn );
}
// --- what it takes to beat a standing bid ---
var target = Number( beat.value );
if ( target && !isNaN( target ) ) {
var box = document.createElement( 'div' );
box.className = 'tc-calc-note';
// 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.';
} else {
var salaryTenths = requiredTenths - y * ( YEAR_POINTS * 10 ),
newSalary = Math.max( salaryTenths * 100000, MIN_OFFER );
box.textContent = 'To beat ' + pts( target ) + ' points at ' + y +
( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) +
' (' + 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' );
render();
} );
box.appendChild( use );
}
out.appendChild( box );
}
// --- ready-to-paste post ---
// "Suns 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(),
prefix = tm && pl ? tm + ' offer ' + pl : ( pl || ( tm ? tm + ' offer' : '' ) ),
offer = y + ( y === 1 ? ' year' : ' years' ) + ' @ ' + money( s ) +
' = ' + pts( total ) + ' points';
out.appendChild( postBlock( prefix ? prefix + ' ' + offer : offer ) );
}
age35.addEventListener( 'change', function () { fillYears(); render(); } );
[ years, salary, beat, name, team ].forEach( function ( el ) {
el.addEventListener( 'input', render );
el.addEventListener( 'change', render );
} );
container.appendChild( form );
render();
}
mw.hook( 'wikipage.content' ).add( function () {
var el = document.getElementById( 'tc-fa-bid-calculator' );
if ( el && !el.dataset.tcBuilt ) {
el.dataset.tcBuilt = '1';
build( el );
}
} );
}() );