Jump to content

MediaWiki:Gadget-resign-calculator.js: Difference between revisions

From 1TC Rules
Default the first season to the upcoming season
The league is NBA RealGM
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * 1TC RealGM — re-sign salary calculator.
  * NBA RealGM — re-sign salary calculator.
  * Implements the formula on [[NBA:Re-Signing]]. If the two ever disagree,
  * Implements the formula on [[NBA:Re-Signing]]. 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 9: Line 9:
// The league numbers a season by the year it ends in, so the season being
// The league numbers a season by the year it ends in, so the season being
// signed for is always the next one up from the current calendar year.
// signed for is always the next one up from the current calendar year.
var NEW_POST_URL = 'https://2teams1cup.com/forum/index.php?act=post&do=new_post&f=70';
var UPCOMING_SEASON = new Date().getFullYear() + 1,
var UPCOMING_SEASON = new Date().getFullYear() + 1,
MAX = 11500000,
MAX = 11500000,
Line 66: Line 68:
} );
} );
wrap.appendChild( btn );
wrap.appendChild( btn );
return wrap;
 
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: Re-Signing';
// 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;
}
}


Line 83: Line 105:
var form = document.createElement( 'div' );
var form = document.createElement( 'div' );
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.
*/
function row( labelText, controls, extraClass ) {
var r = document.createElement( 'div' );
r.className = 'tc-calc-row' + ( extraClass ? ' ' + extraClass : '' );
var l = document.createElement( 'label' );
l.textContent = labelText;
var c = document.createElement( 'div' );
c.className = 'tc-calc-controls';
controls.forEach( function ( el ) { c.appendChild( el ); } );
r.appendChild( l );
r.appendChild( c );
form.appendChild( r );
return c;
}


// --- Fantrax scores ---
// --- Fantrax scores ---
var scoreRow = document.createElement( 'div' );
scoreRow.className = 'tc-calc-row tc-scores';
var scoreLabel = document.createElement( 'label' );
scoreLabel.textContent = 'Fantrax scores';
scoreRow.appendChild( scoreLabel );
for ( var i = 0; i < 3; i++ ) {
for ( var i = 0; i < 3; i++ ) {
var input = document.createElement( 'input' );
var input = document.createElement( 'input' );
Line 96: Line 131:
input.min = '0';
input.min = '0';
input.placeholder = [ 'season 1', 'season 2', 'season 3' ][ i ];
input.placeholder = [ 'season 1', 'season 2', 'season 3' ][ i ];
scoreRow.appendChild( input );
scoreInputs.push( input );
scoreInputs.push( input );
}
}
form.appendChild( scoreRow );
row( 'Fantrax scores', scoreInputs, 'tc-scores' );
 
var hint = document.createElement( 'div' );
hint.className = 'tc-calc-note';
hint.textContent = 'Only seasons with 40+ games played count. If he has never had three, ' +
'fill in the ones he did reach 40 games in and leave the rest blank.';
form.appendChild( hint );


// --- term ---
// --- term ---
var termRow = document.createElement( 'div' );
termRow.className = 'tc-calc-row';
var termLabel = document.createElement( 'label' );
termLabel.textContent = 'Contract length';
var term = document.createElement( 'select' );
var term = document.createElement( 'select' );
[ 3, 4, 5 ].forEach( function ( y ) {
[ 3, 4, 5 ].forEach( function ( y ) {
Line 119: Line 143:
term.appendChild( o );
term.appendChild( o );
} );
} );
termRow.appendChild( termLabel );
row( 'Contract length', [ term ] );
termRow.appendChild( term );
form.appendChild( termRow );


// --- optional first season, to generate the contract line ---
// --- first season, to generate the contract line ---
var seasonRow = document.createElement( 'div' );
seasonRow.className = 'tc-calc-row';
var seasonLabel = document.createElement( 'label' );
seasonLabel.textContent = 'First season';
var season = document.createElement( 'input' );
var season = document.createElement( 'input' );
season.type = 'number';
season.type = 'number';
Line 133: Line 151:
season.placeholder = String( UPCOMING_SEASON );
season.placeholder = String( UPCOMING_SEASON );
season.value = String( UPCOMING_SEASON );
season.value = String( UPCOMING_SEASON );
seasonRow.appendChild( seasonLabel );
row( 'First season', [ season ] );
seasonRow.appendChild( season );
 
form.appendChild( seasonRow );
// --- team and player, for the post block ---
var team = document.createElement( 'input' );
team.type = 'text';
team.placeholder = 'Sonics';
team.className = 'tc-calc-wide';
row( 'Your team (optional)', [ team ] );


// --- player name, for the post block ---
var nameRow = document.createElement( 'div' );
nameRow.className = 'tc-calc-row';
var nameLabel = document.createElement( 'label' );
nameLabel.textContent = 'Player (optional)';
var name = document.createElement( 'input' );
var name = document.createElement( 'input' );
name.type = 'text';
name.type = 'text';
name.placeholder = 'Larry Bird';
name.placeholder = 'Larry Bird';
name.style.width = '14em';
name.className = 'tc-calc-wide';
nameRow.appendChild( nameLabel );
row( 'Player (optional)', [ name ] );
nameRow.appendChild( name );
form.appendChild( nameRow );


var out = document.createElement( 'div' );
var out = document.createElement( 'div' );
Line 218: Line 234:
var endYear = first + years - 1;
var endYear = first + years - 1;


var lines = [];
// "Sonics offer Larry Bird 5 years" — team and player drop out if blank.
if ( who ) {
var tm = team.value.trim(),
lines.push( who );
term_ = years + ( years === 1 ? ' year' : ' years' ),
}
prefix = tm && who ? tm + ' offer ' + who :
( who || ( tm ? tm + ' offer' : '' ) );
 
var lines = [ prefix ? prefix + ' ' + term_ : term_ ];
lines.push( '(' + scores.join( ', ' ) + ' = ' +
lines.push( '(' + scores.join( ', ' ) + ' = ' +
( Number.isInteger( r.average ) ? r.average : r.average.toFixed( 2 ) ) + ')' );
( Number.isInteger( r.average ) ? r.average : r.average.toFixed( 2 ) ) + ')' );
lines.push( money( r.salary ) + ' (' + endYear + ')' );
lines.push( money( r.salary ) + ' (' + endYear + ')' );
out.appendChild( postBlock( lines.join( '\n' ) ) );
out.appendChild( postBlock( lines.join( '\n' ) ) );
var note = document.createElement( 'div' );
note.className = 'tc-calc-note';
note.textContent = years + ' years, ' + first + ' through ' + endYear +
'. Post this in the NBA: Re-Signing forum.';
out.appendChild( note );
}
}


[].concat( scoreInputs, [ term, season, name ] ).forEach( function ( el ) {
[].concat( scoreInputs, [ term, season, team, name ] ).forEach( function ( el ) {
el.addEventListener( 'input', render );
el.addEventListener( 'input', render );
el.addEventListener( 'change', render );
el.addEventListener( 'change', render );

Latest revision as of 21:42, 8 October 2026

/**
 * NBA RealGM — re-sign salary calculator.
 * Implements the formula on [[NBA:Re-Signing]]. If the two ever disagree,
 * the wiki page is the rule and this script is the bug.
 */
( function () {
	'use strict';

	// The league numbers a season by the year it ends in, so the season being
	// signed for is always the next one up from the current calendar year.
	var NEW_POST_URL = 'https://2teams1cup.com/forum/index.php?act=post&do=new_post&f=70';

	var UPCOMING_SEASON = new Date().getFullYear() + 1,
		MAX = 11500000,
		DISCOUNT = 1500000,
		CUTOFF = 85;

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

	/** The seven steps, in the order the rules state them. */
	function calculate( scores ) {
		var avg = scores.reduce( function ( a, b ) { return a + b; }, 0 ) / scores.length,
			raw = ( avg / 100 ) * MAX,
			rounded = Math.round( raw / 100000 ) * 100000,
			discounted = avg < CUTOFF ? rounded - DISCOUNT : rounded,
			capped = Math.min( discounted, MAX );

		return {
			average: avg,
			raw: raw,
			rounded: rounded,
			discountApplied: avg < CUTOFF,
			salary: capped,
			cappedAtMax: discounted > MAX,
			nonPositive: capped <= 0
		};
	}


	/** 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: Re-Signing';
		// 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 scoreInputs = [];

		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.
		 */
		function row( labelText, controls, extraClass ) {
			var r = document.createElement( 'div' );
			r.className = 'tc-calc-row' + ( extraClass ? ' ' + extraClass : '' );
			var l = document.createElement( 'label' );
			l.textContent = labelText;
			var c = document.createElement( 'div' );
			c.className = 'tc-calc-controls';
			controls.forEach( function ( el ) { c.appendChild( el ); } );
			r.appendChild( l );
			r.appendChild( c );
			form.appendChild( r );
			return c;
		}

		// --- Fantrax scores ---
		for ( var i = 0; i < 3; i++ ) {
			var input = document.createElement( 'input' );
			input.type = 'number';
			input.step = '0.01';
			input.min = '0';
			input.placeholder = [ 'season 1', 'season 2', 'season 3' ][ i ];
			scoreInputs.push( input );
		}
		row( 'Fantrax scores', scoreInputs, 'tc-scores' );

		// --- term ---
		var term = document.createElement( 'select' );
		[ 3, 4, 5 ].forEach( function ( y ) {
			var o = document.createElement( 'option' );
			o.value = String( y );
			o.textContent = y + ' years';
			term.appendChild( o );
		} );
		row( 'Contract length', [ term ] );

		// --- first season, to generate the contract line ---
		var season = document.createElement( 'input' );
		season.type = 'number';
		season.step = '1';
		season.placeholder = String( UPCOMING_SEASON );
		season.value = String( UPCOMING_SEASON );
		row( 'First season', [ season ] );

		// --- team and player, for the post block ---
		var team = document.createElement( 'input' );
		team.type = 'text';
		team.placeholder = 'Sonics';
		team.className = 'tc-calc-wide';
		row( 'Your team (optional)', [ team ] );

		var name = document.createElement( 'input' );
		name.type = 'text';
		name.placeholder = 'Larry Bird';
		name.className = 'tc-calc-wide';
		row( 'Player (optional)', [ name ] );

		var out = document.createElement( 'div' );
		out.className = 'tc-calc-out';
		form.appendChild( out );

		function render() {
			var scores = scoreInputs
				.map( function ( el ) { return el.value.trim() === '' ? null : Number( el.value ); } )
				.filter( function ( v ) { return v !== null && !isNaN( v ); } );

			out.textContent = '';

			if ( !scores.length ) {
				var empty = document.createElement( 'p' );
				empty.className = 'tc-calc-note';
				empty.textContent = 'Enter at least one Fantrax score.';
				out.appendChild( empty );
				return;
			}

			var r = calculate( scores );

			var big = document.createElement( 'div' );
			big.className = 'tc-calc-big';
			big.textContent = money( r.salary );
			out.appendChild( big );

			var steps = document.createElement( 'ol' );
			steps.className = 'tc-calc-steps';
			[
				'Average of ' + scores.length + ' score' + ( scores.length === 1 ? '' : 's' ) +
					': ' + r.average.toFixed( 2 ),
				'× ' + money( MAX ) + ' = ' + money( Math.round( r.raw ) ),
				'Rounded to nearest $100,000 = ' + money( r.rounded ),
				r.discountApplied ?
					'Average is below ' + CUTOFF + ', so − ' + money( DISCOUNT ) + ' = ' + money( r.rounded - DISCOUNT ) :
					'Average is ' + CUTOFF + ' or above, so no discount'
			].forEach( function ( t ) {
				var li = document.createElement( 'li' );
				li.textContent = t;
				steps.appendChild( li );
			} );
			out.appendChild( steps );

			if ( r.cappedAtMax ) {
				var cap = document.createElement( 'div' );
				cap.className = 'tc-calc-warn';
				cap.textContent = 'Capped at the ' + money( MAX ) + ' maximum re-sign contract.';
				out.appendChild( cap );
			}

			if ( r.nonPositive ) {
				var warn = document.createElement( 'div' );
				warn.className = 'tc-calc-warn';
				warn.textContent = 'The formula produces ' + money( r.salary ) + ', which is not a usable ' +
					'salary. The rules do not say what happens below this point — ask TAB before posting.';
				out.appendChild( warn );
				return;
			}

			// --- ready-to-paste post ---
			var years = Number( term.value ),
				first = parseInt( season.value, 10 ),
				who = name.value.trim();
			if ( isNaN( first ) ) {
				first = UPCOMING_SEASON;
			}
			var endYear = first + years - 1;

			// "Sonics offer Larry Bird 5 years" — team and player drop out if blank.
			var tm = team.value.trim(),
				term_ = years + ( years === 1 ? ' year' : ' years' ),
				prefix = tm && who ? tm + ' offer ' + who :
					( who || ( tm ? tm + ' offer' : '' ) );

			var lines = [ prefix ? prefix + ' ' + term_ : term_ ];
			lines.push( '(' + scores.join( ', ' ) + ' = ' +
				( Number.isInteger( r.average ) ? r.average : r.average.toFixed( 2 ) ) + ')' );
			lines.push( money( r.salary ) + ' (' + endYear + ')' );
			out.appendChild( postBlock( lines.join( '\n' ) ) );
		}

		[].concat( scoreInputs, [ term, season, team, name ] ).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-resign-calculator' );
		if ( el && !el.dataset.tcBuilt ) {
			el.dataset.tcBuilt = '1';
			build( el );
		}
	} );
}() );