NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name CG Enhancer
// @namespace https://cgenhancer.azke.fr
// @version 0.4.1
// @description Enhancer script for CodinGame platform
// @match https://www.codingame.com/*
// @copyright 2018+, Azkellas, https://github.com/Azkellas/
// @license GPL-3.0-only
// @homepage https://github.com/Azkellas/cgenhancer
// @require http://code.jquery.com/jquery-latest.js
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function()
{
'use strict';
/* global GM_setValue, GM_getValue, GM_xmlhttpRequest, unsafeWindow */
// options
var useAgentModule = true; // set to false to disable angular debug mode (and agent panel)
var forceExternRequest = false; // set to true to enable fighting against bots of higher leagues
// existing notifications:
// 'clash-invite', 'clash-over', 'invitation-accepted'
// 'contest-scheduled', 'contest-started', 'contest-over', 'contest-soon'
// 'new-league', 'new-league-opened', 'new-blog', 'new-comment', 'new-comment-response', 'new-puzzle', 'new-hint', 'new-level'
// 'contribution-received', 'contribution-accepted', 'contribution-refused'
// 'following', 'friend-registered'
// 'achievement-unlocked'
// 'promoted-league', 'eligible-for-next-league'
// 'puzzle-of-the-week'
// 'career-new-candidate', 'career-update-candidate'
// 'feature', 'custom'
const enableSound = false;
const notifToRemove = ['clash-invite', 'following'];
var GMsetValue;
var GMgetValue;
var GMxmlhttpRequest;
var lastCodeBlocUpdate;
if (typeof GM_getValue !== 'undefined')
{
console.log('[CG Enhancer] Tamper/Violentmoneky detected');
GMsetValue = GM_setValue;
GMgetValue = GM_getValue;
GMxmlhttpRequest = GM_xmlhttpRequest;
}
else
{
console.error('[CG Enhancer] Greasemonkey is not supported');
return;
}
if (!GMsetValue)
{
console.error('[CG Enhancer] Error: Could not detect userscript manager');
return;
}
if (useAgentModule)
{
// required to access codingame local api
// done first before angular has time to load
const ngDebugStr = 'NG_ENABLE_DEBUG_INFO!';
if (unsafeWindow.name.indexOf(ngDebugStr) === -1)
unsafeWindow.name = ngDebugStr + unsafeWindow.name;
}
// jquery
const $ = window.jQuery;
const angular = unsafeWindow.angular;
// agent images
const ideImage = document.createElement('img');
$(ideImage).attr('class', 'selectAgentImage ideImage')
.attr('src', 'https://i.imgur.com/yNpEfYt.png')
.attr('style', 'cursor: pointer;'); // .css doest not work
const arenaImage = document.createElement('img');
$(arenaImage).attr('class', 'selectAgentImage arenaImage')
.attr('src', 'https://i.imgur.com/VkG3qnf.png')
.attr('style', 'cursor: pointer;'); // .css doest not work
const bossImage = document.createElement('img');
$(bossImage).attr('class', 'selectAgentImage bossImage')
.attr('src', 'https://i.imgur.com/bbVA7Qv.png')
.attr('style', 'cursor: pointer;'); // .css doest not work
const binImage = document.createElement('img');
$(binImage).attr('class', 'binImage')
.attr('src', 'https://i.imgur.com/HFPFSnc.png')
.attr('style', 'cursor: pointer; right: 20px; bottom: 7px; position: absolute;'); // .css doesnt not work
// Global variables ------------------------------------------------------------------------------------
var pathName = ''; // url pathname
var agentApi; // local cg api used for global actions, like removing an agent or requesting the leaderboard
var userPseudo;
// last battles panel global variables -------------------------------------------------
var blockTvViewer = false; // boolean stating if the last battle tv is to be displayed or not (to prevent autostart when opening the tab)
// agent managent global variables
// stored for fast agent managing
var bossAgent;
var userAgent;
// leaderboards
var playersData = {}; // stores player agents through the leaderboard. keys: lowercase pseudos, values: agents
var lastLeaderboardUpdate; // timer used to avoid spamming leaderboards request
// templates construction
const baseStyle = `cursor: auto;`;
const rankEloBaseCss = baseStyle + `
text-align: right;
float: right;
display: inline-block;
margin-right: 30px;
`;
const rankPCss = rankEloBaseCss + 'font-size: 30px;';
const eloPCss = rankEloBaseCss;
const attrs = `contentEditable='true' spellcheck='false'`;
const rankDivTemplate = `
<div class='rank-div'>
<p class='p-rank' `+attrs+` contentEditable='true' spellcheck='false' style=' {{defaultStyle}}`+ rankPCss + `'>{{value}}</p>
</div>`;
const eloDivTemplate = `
<div class='elo-div'>
<p class='p-elo' `+attrs+` style='` + eloPCss + `{{defaultStyle}}` + `'>` + `{{value}}` + `</p>
</div>`;
const nameDivTemplate = `
<div class='submission-name'>
<p class='p-name' ` + attrs + `
style='font-size: 18px; margin-top: 3px; float:left; display:inline-block;` + baseStyle + `{{defaultStyle}}` + `'>
{{value}}
</p>
</div>`;
// rank over avatar template
const rankAvatarCss = `
text-font: bold;
text-shadow: #000 1px 1px, #000 -1px 1px, #000 -1px -1px, #000 1px -1px;
margin-left: 5px;
font-weight: 600;
color: rgb(255, 255, 255);
position: absolute;
bottom: 2px;
right: 5px;
`;
// main function: observing all mutations
var observer = new MutationObserver(function(mutations)
{
// check page name
if ($(location).attr('pathname') !== pathName)
{
pathName = $(location).attr('pathname');
console.log('[CG Enhancer] New page detected: ' + pathName);
// reset agentApi since it's related to the current ide
agentApi = null;
// reset leaderboard
lastLeaderboardUpdate = null;
}
// check user pseudonym
const pseudoDiv = $('.navigation-profile_nav-profile-nickname').first();
if (!userPseudo && pseudoDiv)
{
userPseudo = pseudoDiv.attr('title');
if (userPseudo)
console.log('[CG Enhancer] User pseudonym: ' + userPseudo);
}
// if not in IDE
if ($(location).attr('pathname').indexOf('ide/') === -1)
{
// remove community notifications
const contributionNav = $('#navigation-contribute');
if (contributionNav)
{
const bubbleNotif = contributionNav.find('.cg-notification-bubble').first();
if (bubbleNotif)
bubbleNotif.remove();
}
}
// we are in the IDE and main is loaded
if ($(location).attr('pathname').indexOf('ide/') !== -1 && $('.main').length)
{
if (useAgentModule)
{
// get agentApi if needed or disable agentModule
if (!agentApi)
getAgentApi();
handleBlocs();
// add agent buttons
manageAgentPanel();
}
// check if we opened last battles without looking at all mutations
const firstMutation = $(mutations[0].target);
if (firstMutation.attr('class') && firstMutation.attr('class').indexOf('cg-ide-last-battles') !== -1)
{
console.log('[CG Enhancer] Opened last battles');
blockTvViewer = true;
updatePlayersData();
}
// block tv viewer if opened
if (blockTvViewer)
{
// hide battle tv on last battles tab opening
const battleTv = $('.battle-tv').first();
if (battleTv)
{
// hide battleTv
battleTv.attr('class', 'battle-tv-hidden');
// reveal if clicked
battleTv.click(function() {
blockTvViewer = false;
$(this).attr('class', 'battle-tv');
});
}
}
// trigger tv-battle close button
const showButton = $('.battle-tv-hidden .battle-button-label').first();
if (showButton && showButton.text() === 'Close')
showButton.trigger('click');
// add ranks on last battle tab, if open
// this part is not updated with new leaderboard query
if ($('.cg-ide-last-battles').length)
manageLastBattlesTab();
// if the history tab is open
if ($('.cg-ide-results').length)
manageHistoryTab();
}
});
var waitingForDocument = setInterval(function()
{
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true, subtree: true};
// disallow sound for notifications
if (unsafeWindow.session.notificationConfig.soundEnabled !== enableSound)
unsafeWindow.session.notificationConfig.soundEnabled = enableSound;
// remove notifications
for (const notif of notifToRemove)
{
const idx = unsafeWindow.session.enabledNotifications.indexOf(notif);
if (idx !== -1)
unsafeWindow.session.enabledNotifications.splice(idx, 1);
}
console.log('[CG Enhancer] CG Enhancer is now working.');
observer.observe(document, config);
clearInterval(waitingForDocument);
}, 1000);
// helpers
/** create swap button if no cgspunk is detected */
function handleSwapButton()
{
// add swap button if not here (by cgspunk and cgenhancer)
if ($('#cgspkSwapButton').length === 0 && $('#cgeSwapButton').length === 0)
{
console.log('[CG Enhancer] Add swap button');
// code courtesy to cgspunk ( https://github.com/danBhentschel/CGSpunk/ )
const swapButton = document.createElement('BUTTON');
swapButton.setAttribute('id', 'cgeSwapButton');
swapButton.innerHTML = 'SWAP';
swapButton.style.padding = '5px 5px 5px 5px';
const panel = $('.scroll-panel').first();
if (panel)
panel.append(swapButton);
$('#cgeSwapButton').click(rotateAgents);
}
// remove swap button if cgspunk swap button here
if ($('#cgspkSwapButton').length !== 0 && $('#cgeSwapButton').length !== 0)
{
console.log('[CG Enhancer] Remove swap button');
const swapButton = $('#cgeSwapButton');
swapButton.remove();
}
}
/** handle blocs layout */
function handleBlocs()
{
if (lastCodeBlocUpdate && (new Date() - lastCodeBlocUpdate < 100)) // max one each 0.1ms
return;
lastCodeBlocUpdate = new Date();
const bloc_container = $('.blocs-container').first();
const height = bloc_container.height();
const top295 = (height - 295) + 'px';
const top52 = (height - 52) + 'px';
const agentSubmitBloc = $('.testcases-actions-container').first();
const codeBloc = $('.code-bloc').first();
const consoleBloc = $('.console-bloc').first();
const statementBloc = $('.statement-bloc').first();
const cgSyncActive = $('.code-editor-readonly').length;
consoleBloc.find('.frame-number-bloc').each(function(idx, element) {
if ($(element).css('right') !== '0px')
$(element).css('right', '0px');
});
// console on the right side, pair statement-agent, ide-console
if (cgSyncActive)
{
// optimize console layout
const miniLeaderboardBloc = $('.mini-leaderboard').first();
if (miniLeaderboardBloc.css('height') !== '90px')
miniLeaderboardBloc.css('height', '90px');
const maxWidth = consoleBloc.width();
if (miniLeaderboardBloc.css('width') !== (maxWidth-2) + 'px')
miniLeaderboardBloc.css('width', (maxWidth-2) + 'px');
const miniLeaderboard = $('.cg-ide-mini-leaderboard').first();
if (miniLeaderboard.css('display') !== 'inline-flex')
miniLeaderboard.css('display', 'inline-flex');
const consoleContent = $('.console-content').first();
if (consoleContent.css('left') !== '0px')
consoleContent.css('left', '0px');
if (consoleContent.css('top') !== '60px')
consoleContent.css('top', '60px');
miniLeaderboardBloc.find('.leaderboard-item').each(function(idx, value) {
if ($(value).css('width') !== ( (maxWidth-25) / 4) + 'px')
$(value).css('width', ( (maxWidth-25) / 4) + 'px');
if ($(value).css('margin') !== '5px')
$(value).css('margin', '5px');
});
// swap console / agent blocs if cgsync is active
const statementRight = statementBloc.css('right');
const ideLeft = codeBloc.css('left');
if (consoleBloc.css('left') !== ideLeft || agentSubmitBloc.css('right') !== statementRight)
{
// swap console / agent blocs
consoleBloc.css('left', ideLeft);
agentSubmitBloc.css('right', statementRight);
if (consoleBloc.css('right') !== '0px')
consoleBloc.css('right', '0px');
if (consoleBloc.css('margin-top') !== '0px')
consoleBloc.css('margin-top', '0px');
if (agentSubmitBloc.css('left') !== '0px')
agentSubmitBloc.css('left', '0px');
// open console if closed
$('.unminimize-button').click();
// disable expand / minimize
$('.minimize-button').attr('disabled', '');
$('.expand-button').attr('disabled', '');
}
// handle layout
// left side
if (agentSubmitBloc.css('top') !== top295)
agentSubmitBloc.css('top', top295);
if (statementBloc.css('bottom') !== '295px')
statementBloc.css('bottom', '295px');
// right side
if (codeBloc.css('display') !== 'none')
codeBloc.css('display', 'none');
if (consoleBloc.css('top') !== '0px')
consoleBloc.css('top', '0px');
}
else
{
// optimize console layout
const miniLeaderboard = $('.mini-leaderboard').first();
if (miniLeaderboard.css('width') !== '130px')
miniLeaderboard.css('width', '130px');
const consoleContent = $('.console-content').first();
if (consoleContent.css('left') !== '120px')
consoleContent.css('left', '120px');
// swap console / agent blocs if cgsync just got inactive
const statementRight = statementBloc.css('right');
const ideLeft = codeBloc.css('left');
if (consoleBloc.css('right') !== statementRight || agentSubmitBloc.css('left') !== ideLeft)
{
// swap console / agent blocs
consoleBloc.css('right', statementRight);
agentSubmitBloc.css('left', ideLeft);
if (consoleBloc.css('margin-top') === '0px')
consoleBloc.css('margin-top', '');
codeBloc.css('display', 'block');
if (consoleBloc.css('left') !== '0px')
consoleBloc.css('left', '0px');
if (agentSubmitBloc.css('right') !== '0px')
agentSubmitBloc.css('right', '0px');
// disable expand / minimize
$('.minimize-button').removeAttr('disabled');
$('.expand-button').removeAttr('disabled');
// optimize console layout
const miniLeaderboardBloc = $('.mini-leaderboard').first();
const miniLeaderboard = $('.cg-ide-mini-leaderboard').first();
if (miniLeaderboardBloc.css('height') === '90px')
miniLeaderboardBloc.css('height', '');
if (miniLeaderboardBloc.css('width') === miniLeaderboard.css('width'))
miniLeaderboardBloc.css('width', '');
if (miniLeaderboard.css('display') === 'inline-flex')
miniLeaderboard.css('display', '');
const consoleContent = $('.console-content').first();
if (consoleContent.css('left') === '0px')
consoleContent.css('left', '');
if (consoleContent.css('top') === '60px')
consoleContent.css('top', '');
miniLeaderboardBloc.find('.leaderboard-item').each(function(idx, value) {
if ($(value).width() > miniLeaderboard.width())
$(value).css('width', '');
if ($(value).css('margin') === '5px')
$(value).css('margin', '');
});
}
// left side
if (codeBloc.css('bottom') !== '295px')
codeBloc.css('bottom', '295px');
if (agentSubmitBloc.css('top') !== top295)
agentSubmitBloc.css('top', top295);
// right side
if (!consoleBloc.find('.header-button.unminimize-button').length)
{
// the console is open
if (consoleBloc.css('top') !== top295)
consoleBloc.css('top', top295);
if (statementBloc.css('bottom') !== '295px')
statementBloc.css('bottom', '295px');
}
else
{
// the console is minimized
if (consoleBloc.css('top') !== top52)
consoleBloc.css('top', top52);
if (statementBloc.css('bottom') !== '52px')
statementBloc.css('bottom', '52px');
}
}
}
/** create agent fast selection tools */
function manageAgentPanel()
{
// make sure agentApi is operational
if (!agentApi)
return;
// create swap button if no cgspunk is detected
handleSwapButton();
// add buttons for each agent
$('.agent').each(function(agentIdx, agent) {
if ($(agent).find('.fastSelectButtons').length)
return;
$(agent).append(`<div class='fastSelectButtons'></div>`);
const fastDiv = $(agent).find('.fastSelectButtons').first();
$(ideImage).clone().appendTo(fastDiv);
fastDiv.find('.ideImage').first().click(function() {
addAgent(agentIdx, 'ide');
});
$(arenaImage).clone().appendTo(fastDiv);
fastDiv.find('.arenaImage').first().click(function() {
addAgent(agentIdx, 'arena');
});
$(bossImage).clone().appendTo(fastDiv);
fastDiv.find('.bossImage').first().click(function() {
addAgent(agentIdx, 'boss');
});
// add input
$(agent).append(`<div class='fastInput'></div>`);
const inputDiv = $(agent).find('.fastInput').first();
inputDiv.append(`<input class='fastAgentInput' type='text' />`);
const inputBox = inputDiv.find('.fastAgentInput').first();
inputBox.keyup({'index': agentIdx}, addFastPlayer);
inputBox
.css('width', '80px')
.css('height', '20px')
.css('padding-left', '5px')
.css('background-color', 'rgb(112, 112, 112)')
.css('color', 'rgb(255, 255, 255)')
.css('margin-bottom', '0px');
updatePlayersData();
});
}
/** the coloration/rank is only computed once at the opening of the tab / the end of the game */
function manageLastBattlesTab()
{
$('.battle-done').not(':has(.cge-player-rank)').each(function(index, battleDiv) {
// TODO
// might crash for some browsers because of color conversion
// check https://stackoverflow.com/a/11943970 for a safe way to code it
const color = getColor(battleDiv);
$(battleDiv).css('background-color', color);
if (agentApi)
{
const battleAngular = angular.element(battleDiv);
const battleData = battleAngular.scope().battle;
let draw = true;
for (const player of battleData.players)
{
if (player.position)
draw = false;
}
if (draw)
{
$(battleDiv).find('.player-agent')
.first().append('<div class="egal">=</div>');
const egal = $(battleDiv).find('.egal').first();
egal
.css('color', '#c90')
.css('font-weight', '900')
.css('position', 'absolute')
.css('top', '-17px')
.css('right', '-12px')
.css('font-size', '40px')
.css('-webkit-text-stroke', '1px black')
.css('z-index', '200');
}
}
$(battleDiv).find('.player-agent').each(function(avatarIndex, playerAvatar) {
const player = $(playerAvatar).attr('title');
if (player && player !== userPseudo)
{
const playerAgent = playersData[player.toLowerCase()];
const rank = playerAgent ? playerAgent.localRank : '';
const rankDiv =
`<div class='cge-player-rank' style='` + rankAvatarCss + `'>` +
rank +
`</div>`;
$(playerAvatar).append(rankDiv);
}
});
});
}
/**
* add name/rank/elo divs to submit div
* @param {Object} options
*/
function addSubmitDiv(options)
{
const storageHash = options.storageHash + options.type;
const divOptions = {
'storageHash': storageHash,
'default': options.type,
'defaultStyle': options.defaultStyle
};
const newDiv = getDiv(divOptions, options.template);
options.root.append(newDiv)
.find('.p-' + options.type).first()
.click(clickEvent)
.keypress({'type': options.type, 'default': options.type, 'storageHash': storageHash}, keyPressEvent);
}
/** handles submits naming, ranking/elo storage */
function manageHistoryTab()
{
$('.submission-card').not(':has(.date-name-div)').each(function(index, submission) {
// create left side div (date + name)
$(submission)
.children().not('.ide-icon_arrow_black')
.wrapAll( '<div class="date-name-div" />');
// date is required for storageHash
const date = $(submission).find('.date').first();
const storageHash = pathName + date.attr('title');
// add flex style
$(submission).css('display', 'flex');
$(submission).css('flex-direction', 'column');
$(submission).css('flex-wrap', 'wrap');
// modify data display for an exact date
date.text(date.attr('title'));
date.css('font-size', '12px');
// create icon side div (arrow + bin)
$(submission)
.find('.ide-icon_arrow_black')
.wrapAll( '<div class="icons-div" />');
const dateNameDiv = $(submission).find('.date-name-div').first();
dateNameDiv
.css('float', 'left')
.css('width', '150px')
.css('display', 'flex')
.css('flex-direction', 'inherit')
.css('margin-top', '-12px');
// create right side div (rank + elo)
$(submission).append(`<div class='rank-elo-div'></div>`);
const rankEloDiv = $(submission).find('.rank-elo-div').first();
rankEloDiv
.css('width', '100px')
.css('align-self', 'flex-end')
.css('display', 'inline-grid');
const iconsDiv = $(submission).find('.icons-div');
$(binImage).clone().appendTo(iconsDiv);
iconsDiv
.find('.binImage')
.click(function(event) {
$(submission).css('display', 'none');
GMsetValue(storageHash + 'display', 'none'); /* jshint ignore:line */
event.stopPropagation();
});
// add name storage
const options = {};
// commun options
options.storageHash = storageHash;
options.defaultStyle = 'color: rgb(224, 224, 224);';
// add name storage
options.type = 'name';
options.template = nameDivTemplate;
options.root = dateNameDiv;
addSubmitDiv(options);
// add rank storage
options.type = 'rank';
options.template = rankDivTemplate;
options.root = rankEloDiv;
addSubmitDiv(options);
// add elo storage
options.type = 'elo';
options.template = eloDivTemplate;
options.root = rankEloDiv;
addSubmitDiv(options);
const display = GMgetValue(storageHash + 'display'); /* jshint ignore:line */
console.log(storageHash + 'display' + ': ' + display);
if (display === 'none')
{
$(submission).css('display', 'none');
return;
}
});
if ($('.cg-ide-submissions').length && $('.restoreDiv').length === 0)
{
$('.cg-ide-submissions').append('<div class="restoreDiv">restore all</div>');
$('.restoreDiv').first()
.css('position', 'absolute')
.css('bottom', '30px')
.css('right', '40px')
.css('color', '#aaaaaa')
.css('cursor', 'pointer')
.click(function(event) {
$('.submission-card').each(function(index, submission) {
// date is required for storageHash
const date = $(submission).find('.date').first();
const storageHash = pathName + date.attr('title');
if ($(submission).css('display') === 'none')
{
$(submission).css('display', 'flex');
GMsetValue(storageHash + 'display', 'flex'); /* jshint ignore:line */
return;
}
});
manageHistoryTab();
event.stopPropagation();
});
}
}
/** try to get angular api or disable agent panel */
function getAgentApi()
{
const agentForApi = $('.agent').filter(':first');
if (useAgentModule && agentForApi)
{
// if angular is indeed in debug mode
if (angular.element(agentForApi).scope())
agentApi = angular.element(agentForApi).scope().api;
else
{
console.error('[CG Enhancer] Please refresh the tab to use the agent module. ' +
'If it doesn\'t work, ask Azkellas or post on the forum/github');
useAgentModule = false;
}
}
else
{
console.warn('[CG Enhancer] Agent panel is not fully loaded yet');
}
}
/**
* @param {int} index - index of agent scope to apply
*/
function applyAgent(index)
{
angular.element('.agent').eq(index)
.scope().$apply();
}
/**
* @param {int} index - index of agent to remove
*/
function removeAgent(index)
{
agentApi.removeAgent(index);
applyAgent(index);
}
/**
* @param {int} index - index of agent to add
* @param {string} type - type or pseudo of agent to add
*/
function addAgent(index, type)
{
removeAgent(index);
const agent = angular.element('.agent').eq(index);
if (type === 'ide')
agent.scope().api.addAgent({'agentId': -1});
if (type === 'arena')
{
if (userAgent)
agent.scope().api.addAgent(userAgent);
else
type = 'boss'; // the player did not submit any AI yet
}
if (type === 'boss')
{
if (bossAgent)
agent.scope().api.addAgent(bossAgent);
else // could not find the boss (the player is in legned league)
agent.scope().api.addAgent({'agentId': -2}); // -2 is the defaultAI agentId
}
if (type !== 'ide' && type !== 'arena' && type !== 'boss') // type is a real player, not the best way to code it
{
agent.scope().api.addAgent(playersData[type]);
}
applyAgent(index);
}
/**
* rotate agents
*/
function rotateAgents()
{
// code partly courtesy to cgspunk ( https://github.com/danBhentschel/CGSpunk/ )
console.log('[CG Enhancer] Rotating agents');
const agents = [];
// get agents
$('.agent').each(function(index, agent) {
agent = angular.element(agent);
if (agent.scope().$parent.agent !== null) // check if there is indeed an agent or if the agent is empty
agents.push(agent.scope().$parent.agent);
});
// shift agents
agents.push(agents.shift());
// add agents
for (let index = 0; index < agents.length; index++) {
removeAgent(index);
const agent = angular.element('.agent').eq(index);
agent.scope().api.addAgent(agents[index]);
applyAgent(index);
}
}
/**
* use templates to create the div required
* @param {object} data - contains all options required to generate the div
* @param {string} template - html template to use
*/
function getDiv(data, template)
{
// data: {storageHash, default, defaultStyle}
const name = GMgetValue(data.storageHash, data.default); /* jshint ignore:line */
console.log(data.storageHash + ': ' + name);
let style = '';
if (name === data.default)
style = data.defaultStyle;
template = template.replace('{{value}}', name);
template = template.replace('{{defaultStyle}}', style);
return template;
}
/**
* return the color highlight of the battle in the last battle tabs
* difference to determine if the result is unexpected is
* enemyRank > 1.2*userRank + 10 (randomly chosen)
* note: this function does not check for draws, but check wins by looking at which player is displayed first
* note: giving rgb values is mandatory for firefox
* @param {jquery object} battleDiv
*/
function getColor(battleDiv)
{
// if more than 2 players, not coloration
if ($(battleDiv).find('.player-agent').length > 2)
return 'rgb(255, 255, 255)';
// userAgent undefined
if (!userAgent)
return 'rgb(255, 255, 255)';
let userRank;
let enemyRank;
let userWon;
$(battleDiv).find('.player-agent').each(function(playerIdx, playerAvatar) {
const player = $(playerAvatar).attr('title');
if (player && playersData[player.toLowerCase()] && player !== userPseudo)
enemyRank = playersData[player.toLowerCase()].localRank;
if (player && player === userPseudo)
{
userRank = userAgent.localRank;
userWon = (playerIdx === 0);
}
});
// at least one undefined rank
if (!userRank || !enemyRank)
return 'rgb(240, 240, 240)';
// unexpected loss
if (enemyRank > 1.2*userRank + 10 && !userWon)
return 'rgb(255, 240, 240)';
// unexepcted win
if (userRank > 1.2*enemyRank + 10 && userWon)
return 'rgb(240, 255, 240)';
// expected result
return 'rgb(255, 255, 255)';
}
/**
* stop propagation
* @param {DOM event} event
*/
function clickEvent(event)
{
/* jshint validthis: true */
if (!this)
{
console.error('[CG Enhancer] Error: clickEvent must be called inside a click method.');
return;
}
// prevent codingame action
event.stopPropagation();
}
/**
* poorly named function
* it is called when the user tries to select an agent by its pseudo
* @param {DOM event} event
*/
function addFastPlayer(event)
{
/* jshint validthis: true */
if (!this)
{
console.error('[CG Enhancer] Error: addFastPlayer must be called inside a keypress method.');
return;
}
const key = event.which;
if (key === 13) // enter key
{
const pseudo = $(this).val();
// add existing player
if (pseudo && playersData[pseudo.toLowerCase()])
{
console.log('[CG Enhancer] Player ' + pseudo + ' found');
addAgent(event.data.index, pseudo.toLowerCase());
$(this).text(''); // reset pseudo
$(this).css('color', 'rgb(255, 255, 255'); // reset color
$(this).blur(); // focus out
}
// player not found
else
{
console.log('[CG Enhancer] Player ' + pseudo + ' could not be found');
$(this).css('color', 'rgb(255, 160, 160)'); // red coloration if player not found
}
// prevent codingame action
event.stopPropagation();
}
else
{
// reset color to white
$(this).css('color', 'rgb(255, 255, 255)');
}
}
/**
* called in the history tab
* @param {DOM event} event - contains data
*/
function keyPressEvent(event)
{
/* jshint validthis: true */
// event.data :
// {
// type
// defaultValue
// storageHash
// }
if (!this)
{
console.error('[CG Enhancer] Error: keyPressEvent must be called inside a keyUp method.');
return;
}
const key = event.which;
if (key === 13) // enter key
{
// lose focus
$(this).blur(); // lose focus
// make sure no empty value is stored
if ($(this).text() === '')
$(this).text(event.data.default);
// save value (even if default to erase previous value)
GMsetValue(event.data.storageHash, $(this).text()); /* jshint ignore:line */
// apply coloration
if ($(this).text() !== event.data.default)
$(this).css('color', '');
else
$(this).css('color', 'rgb(224, 224, 224)');
// prevent codingame action
event.stopPropagation();
}
}
/**
* update playersData to store agentId and ranks
*/
function updatePlayersData()
{
// angular is not activated
if (useAgentModule === false)
forceExternRequest = true;
// make sure we do not update every 5 sec
// at most once every minute
if (lastLeaderboardUpdate && (new Date() - lastLeaderboardUpdate < 60*1000))
return;
// reset stored leaderboard and user/boss agents
lastLeaderboardUpdate = new Date();
const newPlayersData = {};
userAgent = null;
bossAgent = null;
// we get the leaderboard through the API
if (!forceExternRequest && agentApi)
{
console.log('[CG Enhancer] Requesting the leaderboard through agentApi');
agentApi.getLeaderboard()
.then(function(result) {
// direct access to user agent
userAgent = result.codingamerUserRank;
for (const user of result.users)
{
if (user.pseudo)
{
newPlayersData[user.pseudo.toLowerCase()] = user;
if (user.arenaboss && (!userAgent || userAgent.league.divisionIndex === user.league.divisionIndex))
bossAgent = user;
}
}
// only update playersData at the end to prevent issue #1
playersData = newPlayersData;
})
.catch(function(error) {
console.error('[CG Enhancer] api request failed with error: ' + error);
});
}
// we make an extern api request since we don't have the agentAPI
else
{
console.log('[CG Enhancer] Requesting the leaderboard through an extern request');
const gameSplit = pathName.split('/');
const multi = gameSplit.slice(-1)[0];
let api = '';
if (gameSplit.slice(-2)[0] === 'puzzle')
api = 'getFilteredPuzzleLeaderboard';
else
api = 'getFilteredChallengeLeaderboard';
GMxmlhttpRequest({ /* jshint ignore:line */
url: 'https://www.codingame.com/services/LeaderboardsRemoteService/' + api,
method: 'POST',
responseType: 'json',
data: '[' + multi + ', undefined, global, { active: false, column: undefined, filter: undefined}]',
onload: function(response) {
const rawLeaderboard = response.response.success;
const users = rawLeaderboard.users;
for (const user of users)
{
if (user.pseudo)
{
newPlayersData[user.pseudo.toLowerCase()] = user;
newPlayersData[user.pseudo.toLowerCase()].rank = user.localRank; // to avoid wrong rank in agent panel when selected
if (user.pseudo === userPseudo)
userAgent = user;
}
}
// only update playersData at the end to prevent issue #1
playersData = newPlayersData;
}
});
}
}
})();