MediaWiki:Gadget-fa-bid-calculator.js

Revision as of 21:46, 23 September 2026 by Sam (talk | contribs) (Omit blank names, Points wording, copy button)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * 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 MIN_OFFER = 500000,
		MIN_RAISE = 200000,
		MAX_YEARS = 4,
		MAX_YEARS_35 = 2,
		YEAR_POINTS = 1.5;

	function money( n ) {
		return '$' + n.toLocaleString( 'en-US' );
	}

	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 );
		return wrap;
	}

	/** 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';

		function row( labelText, el ) {
			var r = document.createElement( 'div' );
			r.className = 'tc-calc-row';
			var l = document.createElement( 'label' );
			l.textContent = labelText;
			r.appendChild( l );
			r.appendChild( el );
			form.appendChild( r );
			return r;
		}

		var years = document.createElement( 'select' );
		var salary = document.createElement( 'input' );
		salary.type = 'number';
		salary.step = String( MIN_RAISE );
		salary.min = String( MIN_OFFER );
		salary.value = String( MIN_OFFER );

		var age35 = document.createElement( 'input' );
		age35.type = 'checkbox';

		var beat = document.createElement( 'input' );
		beat.type = 'number';
		beat.step = '0.1';
		beat.placeholder = 'e.g. 8.5';

		var name = document.createElement( 'input' );
		name.type = 'text';
		name.placeholder = 'Caldwell Jones';
		name.style.width = '14em';

		var team = document.createElement( 'input' );
		team.type = 'text';
		team.placeholder = 'Suns';
		team.style.width = '14em';

		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();

		row( 'Years', years );
		row( 'Salary per year', salary );
		var ageRow = row( 'Player is 35 or over', age35 );
		ageRow.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 = Number( salary.value ),
				max = age35.checked ? MAX_YEARS_35 : MAX_YEARS;

			out.textContent = '';

			if ( !s || isNaN( s ) ) {
				var empty = document.createElement( 'p' );
				empty.className = 'tc-calc-note';
				empty.textContent = 'Enter a salary.';
				out.appendChild( empty );
				return;
			}

			var total = points( y, s );

			var big = document.createElement( 'div' );
			big.className = 'tc-calc-big';
			big.textContent = pts( total ) + ' points';
			out.appendChild( big );

			var steps = document.createElement( 'ol' );
			steps.className = 'tc-calc-steps';
			[
				y + ' × 1.5 = ' + pts( y * YEAR_POINTS ) + ' points for years',
				money( s ) + ' = ' + pts( s / 1000000 ) + ' points for money',
				'Total ' + pts( total )
			].forEach( function ( t ) {
				var li = document.createElement( 'li' );
				li.textContent = t;
				steps.appendChild( li );
			} );
			out.appendChild( steps );

			// --- 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 or over' : '' ) + '.' );
			}
			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';

				if ( total > target ) {
					box.textContent = 'This already beats ' + pts( target ) + ' points.';
				} else {
					// cheapest money-only raise, in $200,000 steps
					var needed = ( target - total ) * 1000000,
						raises = Math.max( 1, Math.ceil( needed / MIN_RAISE ) ),
						newSalary = s + ( raises * MIN_RAISE );
					while ( points( y, newSalary ) <= target ) {
						newSalary += MIN_RAISE;
					}
					var lines = [
						'To beat ' + pts( target ) + ' points at ' + y +
							( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) +
							' (' + pts( points( y, newSalary ) ) + ' points).'
					];
					if ( y < max ) {
						var extraYear = points( y + 1, s );
						lines.push( 'Or add a year: ' + ( y + 1 ) + ' years at ' + money( s ) +
							' = ' + pts( extraYear ) + ' points' +
							( extraYear > target ? '.' : ' — still short.' ) );
					}
					box.textContent = lines.join( ' ' );
				}
				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 ) );

			var note = document.createElement( 'div' );
			note.className = 'tc-calc-note';
			note.textContent = 'Check this against your cap space before posting — a bid over your ' +
				'space costs 40% of the contract for a year, and you do not get the player.';
			out.appendChild( note );
		}

		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 );
		}
	} );
}() );