let deps =  {
    "axios": "^0.22.0",
    "codegrid-js": "git+https://github.com/Tzhf/codegrid-js.git",
    "dotenv": "^8.2.0",
    "electron-store": "^8.0.1",
    "electron-updater": "^4.3.9",
    "jquery": "^3.5.1",
    "tmi.js": "^1.8.5"
  }
const Game = require("./Classes/Game");
const GameHelper = require("./utils/GameHelper");
const tmi = require("./Classes/tmi");
const game = new Game();
class GameHandler {
	constructor(win, settingsWindow) {
		this.win = win;
		this.settingsWindow = settingsWindow;
		this.initTmi();
		this.init();
	}
	init = () => {
		game.init(this.win, settings);
		// Browser Listening
		this.win.webContents.on("did-navigate-in-page", (e, url) => {
			if (isGameURL(url)) {
				game.start(url, settings.isMultiGuess).then(() => {
					this.win.webContents.send("game-started", game.isMultiGuess);
					TMI.action(`${game.round == 1 ? "🌎 A new seed of " + game.mapName : "🌎 Round " + game.round} has started`);
					openGuesses();
				});
			} else {
				game.outGame();
				this.win.webContents.send("game-quitted");
			}
		});
		this.win.webContents.on("did-frame-finish-load", () => {
			if (!game.isInGame) return;
			this.win.webContents.send("refreshed-in-game", settings.noCompass);
			// Checks and update seed when the game has refreshed
			// update the current location if it was skipped
			// if the streamer has guessed returns scores
			game.refreshSeed().then((scores) => {
				if (scores) showResults(scores.location, scores.scores);
			});
			this.win.webContents.executeJavaScript(`
				window.nextRoundBtn = document.querySelector('[data-qa="close-round-result"]');
				if(window.nextRoundBtn) {
					nextRoundBtn.addEventListener("click", () => {
						nextRoundBtn.setAttribute('disabled', 'disabled');
						ipcRenderer.send('next-round-click');
					});
				}
			`);
		});
		const showResults = (location, scores) => {
			const round = game.seed.state === "finished" ? game.round : game.round - 1;
			this.win.webContents.send("show-round-results", round, location, scores);
			TMI.action(`🌎 Round ${round} has finished. Congrats ${GameHelper.toEmojiFlag(scores[0].flag)} ${scores[0].username} !`);
		};
		ipcMain.on("next-round-click", () => nextRound());
		const nextRound = () => {
			game.nextRound();
			if (game.seed.state === "finished") {
				processTotalScores();
			} else {
				this.win.webContents.send("next-round", game.isMultiGuess);
				TMI.action(`🌎 Round ${game.round} has started`);
				openGuesses();
			}
		};
		TMI.client.on("guess", async (from, userstate, message, self) => {
			const msg = message.split("!g")[1].trim();
			if (!GameHelper.isCoordinates(msg)) return;
			const location = { lat: parseFloat(msg.split(",")[0]), lng: parseFloat(msg.split(",")[1]) };
			game.handleUserGuess(userstate, location).then((res) => {
				if (res === "alreadyGuessed") return TMI.say(`${userstate["display-name"]} you already guessed`);
				const { user, guess } = res;
                this.win.webContents.send("render-guess", guess, game.nbGuesses);
                if (settings.showHasGuessed) return TMI.say(`${GameHelper.toEmojiFlag(user.flag)} ${userstate["display-name"]} guessed`);
				
			});
		});
}
module.exports = GameHandler;
const GameHandler = require("./GameHandler");
const Scoreboard = require("./Classes/Scoreboard");
const Store = require("./utils/Store");
window.addEventListener("DOMContentLoaded", () => {
	window.ipcRenderer = require("electron").ipcRenderer;
	window.$ = window.jQuery = require("jquery");
	window.MAP = null;
	hijackMap();
	const head = document.getElementsByTagName("head")[0];
	const styles = document.createElement("link");
	styles.rel = "stylesheet";
	styles.type = "text/css";
	styles.href = `${path.join(__dirname, "./public/styles.css")}`;
	head.appendChild(styles);
	const scoreboardContainer = document.createElement("div");
	scoreboardContainer.setAttribute("id", "scoreboardContainer");
	scoreboardContainer.innerHTML = `
		
			
			
				
					
						| # | 
						Player | 
						Streak | 
						Distance | 
						Score | 
					
				
				
			
		 `;
	document.body.appendChild(scoreboardContainer);
	const flagIcon = document.createElement("link");
	flagIcon.rel = "stylesheet";
	flagIcon.type = "text/css";
	flagIcon.href = `${path.join(__dirname, "./public/flag-icon.min.css")}`;
	head.appendChild(flagIcon);
	const jqueryUI = document.createElement("script");
	jqueryUI.type = "text/javascript";
	jqueryUI.src = `${path.join(__dirname, "./public/jquery-ui.min.js")}`;
	jqueryUI.addEventListener("load", () => loadDatatables());
	document.body.appendChild(jqueryUI);
	const loadDatatables = () => {
		const datatables = document.createElement("script");
		datatables.type = "text/javascript";
		datatables.src = `${path.join(__dirname, "./public/datatables.bundle.min.js")}`;
		datatables.addEventListener("load", () => init());
		document.body.appendChild(datatables);
	};
	const init = () => {
		const markerRemover = document.createElement("style");
		markerRemover.innerHTML = ".map-pin{display:none}";
		const settingsIcon = document.createElement("div");
		settingsIcon.setAttribute("title", "Settings (ctrl+p)");
		settingsIcon.id = "settingsIcon";
		settingsIcon.innerHTML = "⚙️";
		settingsIcon.addEventListener("click", () => {
			ipcRenderer.send("openSettings");
		});
		document.body.appendChild(settingsIcon);
		const scoreboard = new Scoreboard();
		const showScoreboard = document.createElement("div");
		showScoreboard.setAttribute("title", "Show scoreboard");
		showScoreboard.id = "showScoreboard";
		showScoreboard.innerHTML = "👁️🗨️";
		showScoreboard.addEventListener("click", () => {
			scoreboard.setVisibility();
		});
		ipcRenderer.on("game-started", (e, isMultiGuess) => {
			document.body.appendChild(showScoreboard);
			scoreboard.checkVisibility();
			scoreboard.reset(isMultiGuess);
		});
		ipcRenderer.on("refreshed-in-game", (e, noCompass) => {
			document.body.appendChild(showScoreboard);
			scoreboard.checkVisibility();
			drParseNoCompass(noCompass);
		});
		ipcRenderer.on("game-quitted", () => {
			scoreboard.hide();
			if ($("#showScoreboard")) $("#showScoreboard").remove();
			markerRemover.remove();
			clearMarkers();
		});
		ipcRenderer.on("render-guess", (e, guess, nbGuesses) => {
			scoreboard.setTitle(`GUESSES (${nbGuesses})`);
			scoreboard.renderGuess(guess);
		});
		ipcRenderer.on("render-multiguess", (e, guesses, nbGuesses) => {
			scoreboard.setTitle(`GUESSES (${nbGuesses})`);
			scoreboard.renderMultiGuess(guesses);
		});
		ipcRenderer.on("pre-round-results", () => document.body.appendChild(markerRemover));
		ipcRenderer.on("show-round-results", (e, round, location, scores) => {
			scoreboard.show();
			scoreboard.setTitle(`ROUND ${round} RESULTS`);
			scoreboard.displayScores(scores);
			scoreboard.showSwitch(false);
			populateMap(location, scores);
		});
		ipcRenderer.on("show-final-results", (e, totalScores) => {
			document.body.appendChild(markerRemover);
			scoreboard.show();
			scoreboard.setTitle("HIGHSCORES");
			scoreboard.showSwitch(false);
			scoreboard.displayScores(totalScores, true);
			clearMarkers();
		});
		ipcRenderer.on("next-round", (e, isMultiGuess) => {
			scoreboard.checkVisibility();
			scoreboard.reset(isMultiGuess);
			scoreboard.showSwitch(true);
			setTimeout(() => {
				markerRemover.remove();
				clearMarkers();
			}, 1000);
		});
		ipcRenderer.on("switch-on", () => scoreboard.switchOn(true));
		ipcRenderer.on("switch-off", () => scoreboard.switchOn(false));
		ipcRenderer.on("game-settings-change", (e, noCompass) => drParseNoCompass(noCompass));
	};
});
let markers = [];
let polylines = [];
function populateMap(location, scores) {
	const infowindow = new google.maps.InfoWindow();
	const icon = {
		path: `M13.04,41.77c-0.11-1.29-0.35-3.2-0.99-5.42c-0.91-3.17-4.74-9.54-5.49-10.79c-3.64-6.1-5.46-9.21-5.45-12.07
			c0.03-4.57,2.77-7.72,3.21-8.22c0.52-0.58,4.12-4.47,9.8-4.17c4.73,0.24,7.67,3.23,8.45,4.07c0.47,0.51,3.22,3.61,3.31,8.11
			c0.06,3.01-1.89,6.26-5.78,12.77c-0.18,0.3-4.15,6.95-5.1,10.26c-0.64,2.24-0.89,4.17-1,5.48C13.68,41.78,13.36,41.78,13.04,41.77z
			`,
		fillColor: "#de3e3e",
		fillOpacity: 0.7,
		scale: 1.2,
		strokeColor: "#000000",
		strokeWeight: 1,
		anchor: new google.maps.Point(14, 43),
		labelOrigin: new google.maps.Point(13.5, 15),
	};
	const locationMarker = new google.maps.Marker({
		position: location,
		url: `http://maps.google.com/maps?q=&layer=c&cbll=${location.lat},${location.lng}`,
		icon: icon,
		map: MAP,
	});
	google.maps.event.addListener(locationMarker, "click", () => {
		window.open(locationMarker.url, "_blank");
	});
	markers.push(locationMarker);
	icon.scale = 1;
	scores.forEach((score, index) => {
		const color = index == 0 ? "#E3BB39" : index == 1 ? "#C9C9C9" : index == 2 ? "#A3682E" : score.color;
		icon.fillColor = color;
		const guessMarker = new google.maps.Marker({
			position: score.position,
			icon: icon,
			map: MAP,
			label: { color: "#000", fontWeight: "bold", fontSize: "16px", text: `${index + 1}` },
		});
		google.maps.event.addListener(guessMarker, "mouseover", () => {
			infowindow.setContent(`
				
					${score.flag ? `` : ""}${score.username}
					${score.distance >= 1 ? parseFloat(score.distance.toFixed(1)) + "km" : parseInt(score.distance * 1000) + "m"}
					${score.score}
				
			`);
			infowindow.open(MAP, guessMarker);
		});
		google.maps.event.addListener(guessMarker, "mouseout", () => {
			infowindow.close();
		});
		markers.push(guessMarker);
		polylines.push(
			new google.maps.Polyline({
				strokeColor: color,
				strokeWeight: 4,
				strokeOpacity: 0.6,
				geodesic: true,
				map: MAP,
				path: [score.position, location],
			})
		);
	});
}
function clearMarkers() {
	while (markers[0]) {
		markers.pop().setMap(null);
	}
	while (polylines[0]) {
		polylines.pop().setMap(null);
	}
}
function hijackMap() {
	const MAPS_API_URL = "https://maps.googleapis.com/maps/api/js?";
	const GOOGLE_MAPS_PROMISE = new Promise((resolve, reject) => {
		let scriptObserver = new MutationObserver((mutations) => {
			for (const mutation of mutations) {
				for (const node of mutation.addedNodes) {
					if (node.tagName === "SCRIPT" && node.src.startsWith(MAPS_API_URL)) {
						node.onload = () => {
							scriptObserver.disconnect();
							scriptObserver = undefined;
							resolve();
						};
					}
				}
			}
		});
		let bodyDone = false;
		let headDone = false;
		new MutationObserver((_, observer) => {
			if (!bodyDone && document.body) {
				if (scriptObserver) {
					scriptObserver.observe(document.body, {
						childList: true,
					});
					bodyDone = true;
				}
			}
			if (!headDone && document.head) {
				if (scriptObserver) {
					scriptObserver.observe(document.head, {
						childList: true,
					});
					headDone = true;
				}
			}
			if (headDone && bodyDone) {
				observer.disconnect();
			}
		}).observe(document.documentElement, {
			childList: true,
			subtree: true,
		});
	});
	function runAsClient(f) {
		const script = document.createElement("script");
		script.type = "text/javascript";
		script.text = `(${f.toString()})()`;
		document.body.appendChild(script);
	}
	GOOGLE_MAPS_PROMISE.then(() => {
		runAsClient(() => {
			const google = window.google;
			const isGamePage = () => location.pathname.startsWith("/results/") || location.pathname.startsWith("/game/");
			const onMapUpdate = (map) => {
				try {
					if (!isGamePage()) return;
					MAP = map;
				} catch (error) {
					console.error("GeoguessrHijackMap Error:", error);
				}
			};
			const oldMap = google.maps.Map;
			google.maps.Map = Object.assign(
				function (...args) {
					const res = oldMap.apply(this, args);
					this.addListener("idle", () => {
						if (MAP != null) return;
						onMapUpdate(this);
					});
					return res;
				},
				{
					prototype: Object.create(oldMap.prototype),
				}
			);
		});
	});
}
function drParseNoCompass(noCompass) {
	const style = document.getElementById("noCompass");
	if (noCompass) {
		if (!style) {
			const style = document.createElement("style");
			style.id = "noCompass";
			style.innerHTML = ".compass { display: none }.game-layout__compass{display: none}";
			document.head.appendChild(style);
		}
	} else {
		if (style) style.remove();
	}
}
function drParseNoCar() {
	if (!noCar) return;
	const OPTIONS = { colorR: 0.5, colorG: 0.5, colorB: 0.5 };
	const vertexOld =
		"const float f=3.1415926;varying vec3 a;uniform vec4 b;attribute vec3 c;attribute vec2 d;uniform mat4 e;void main(){vec4 g=vec4(c,1);gl_Position=e*g;a=vec3(d.xy*b.xy+b.zw,1);a*=length(c);}";
	const fragOld =
		"precision highp float;const float h=3.1415926;varying vec3 a;uniform vec4 b;uniform float f;uniform sampler2D g;void main(){vec4 i=vec4(texture2DProj(g,a).rgb,f);gl_FragColor=i;}";
	const vertexNew = `
		const float f=3.1415926;
		varying vec3 a;
		varying vec3 potato;
		uniform vec4 b;
		attribute vec3 c;
		attribute vec2 d;
		uniform mat4 e;
		void main(){
			vec4 g=vec4(c,1);
			gl_Position=e*g;
			a = vec3(d.xy * b.xy + b.zw,1);
			a *= length(c);
			potato = vec3(d.xy, 1.0) * length(c);
		}
	`;
	const fragNew = `
		precision highp float;
		const float h=3.1415926;
		varying vec3 a;
		varying vec3 potato;
		uniform vec4 b;
		uniform float f;
		uniform sampler2D g;
		void main(){
		vec2 aD = potato.xy / a.z;
		float thetaD = aD.y;
		float thresholdD1 = 0.6;
		float thresholdD2 = 0.7;
		float x = aD.x;
		float y = abs(4.0*x - 2.0);
		float phiD = smoothstep(0.0, 1.0, y > 1.0 ? 2.0 - y : y);
		vec4 i = vec4(thetaD > mix(thresholdD1, thresholdD2, phiD)
		? vec3(float(${OPTIONS.colorR}), float(${OPTIONS.colorG}), float(${OPTIONS.colorB})) // texture2DProj(g,a).rgb * 0.25
		: texture2DProj(g,a).rgb,f);
		gl_FragColor=i;
		}
	`;
	function installShaderSource(ctx) {
		const g = ctx.shaderSource;
		function shaderSource() {
			if (typeof arguments[1] === "string") {
				let glsl = arguments[1];
				if (glsl === vertexOld) glsl = vertexNew;
				else if (glsl === fragOld) glsl = fragNew;
				return g.call(this, arguments[0], glsl);
			}
			return g.apply(this, arguments);
		}
		shaderSource.bestcity = "bintulu";
		ctx.shaderSource = shaderSource;
	}
	function installGetContext(el) {
		const g = el.getContext;
		el.getContext = function () {
			if (arguments[0] === "webgl" || arguments[0] === "webgl2") {
				const ctx = g.apply(this, arguments);
				if (ctx && ctx.shaderSource && ctx.shaderSource.bestcity !== "bintulu") {
					installShaderSource(ctx);
				}
				return ctx;
			}
			return g.apply(this, arguments);
		};
	}
	const f = document.createElement;
	document.createElement = function () {
		if (arguments[0] === "canvas" || arguments[0] === "CANVAS") {
			const el = f.apply(this, arguments);
			installGetContext(el);
			return el;
		}
		return f.apply(this, arguments);
	};
}
const path = require("path");
require("dotenv").config({ path: path.join(__dirname, "../../.env") });
const axios = require("axios");
const CG = require("codegrid-js").CodeGrid();
const countryCodes = require("./countryCodes");
const countryCodesNames = require("./countryCodesNames");
class GameHelper {
	/**
	 * Checks if '/game/' is in the URL
	 * @param {string} url Game URL
	 * @return {boolean}
	 */
	static isGameURL = (url) => url.includes("/game/");
	/**
	 * Gets the Game ID from a game URL
	 * Checks if ID is 16 characters in length
	 * @param {string} url Game URL
	 * @return {string|boolean} id or false
	 */
	static getGameId = (url) => {
		const id = url.substring(url.lastIndexOf("/") + 1);
		if (id.length == 16) {
			return id;
		} else {
			return false;
		}
	};
	/**
	 * Fetch a game seed
	 * @param {string} url
	 * @return {Promise} Seed Promise
	 */
	static fetchSeed = async (url) => {
		return axios
			.get(`https://www.geoguessr.com/api/v3/games/${url.substring(url.lastIndexOf("/") + 1)}`)
			.then((res) => res.data)
			.catch((error) => console.log(error));
	};
	/**
	 * Returns a country code
	 * @param {Object} location {lat, lng}
	 * @return {Promise} Country code Promise
	 */
	static getCountryCode = async (location) => {
		return axios
			.get(`https://api.bigdatacloud.net/data/reverse-geocode?latitude=${location.lat}&longitude=${location.lng}&key=${process.env.BDC_KEY}`)
			.then((res) => countryCodes[res.data.countryCode])
			.catch((error) => {
				// if BDC returns an error use CodeGrid
				return new Promise((resolve, reject) => {
					CG.getCode(location.lat, location.lng, (error, code) => {
						resolve(code);
						reject(new Error(error));
					});
				}).then((code) => countryCodes[code.toUpperCase()]);
			});
	};
	/**
	 * Returns a country code
	 * It uses CodeGrid first and then BDC if needed
	 * @param {Object} location {lat, lng}
	 * @return {Promise} Country code Promise
	 */
	static getCountryCodeLocally = async (location) => {
		return new Promise((resolve, reject) => {
			let coordinates = this.getSurroundings(location);
			let promises = [];
			coordinates.forEach((coord) => {
				promises.push(this.getCountryCG(coord));
			});
			Promise.all(promises).then((values) => {
				let unique = new Set(values);
				if (unique.size === 1) {
					console.log(unique.values().next().value);
				} else {
					this.getCountryBDC(location).then((data) => resolve(data));
				}
			});
		});
	};
	/**
	 * Returns a country code (Only using BDC)
	 * Do not use externally - Used by getCountryCodeLocally
	 * Ultimately we will call our own API here and remove/
	 * replace getCountryCode
	 * @param {Object} location {lat, lng}
	 * @return {Promise} Country code Promise
	 */
	static getCountryBDC = async (location) => {
		return axios
			.get(`https://api.bigdatacloud.net/data/reverse-geocode?latitude=${location.lat}&longitude=${location.lng}&key=${process.env.BDC_KEY}`)
			.then((res) => countryCodes[res.data.countryCode])
			.catch((error) => error);
	};
	/**
	 * Returns a country code (Only using CodeGrid)
	 * Do not use externally - Used by getCountryCodeLocally
	 * @param {Object} location {lat, lng}
	 * @return {Promise} Country code Promise
	 */
	static getCountryCG = (location) => {
		return new Promise((resolve, reject) => {
			CG.getCode(location.lat, location.lng, (error, code) => {
				if (error) {
					reject(new Error(error));
				} else {
					resolve(countryCodes[code.toUpperCase()]);
				}
			});
		});
	};
	/**
	 * Returns an array of 9 coodinates as objects.
	 * Each coordinate is 100 meters aways from the given
	 * coordinate y angles from 0 to 315
	 * The first coordinate is the original passed
	 * @param {Object} location {lat, lng}
	 * @return {Array} Coordinates [{lat, lng}, {lat, lng}] x 8
	 */
	static getSurroundings = (location) => {
		const meters = 100;
		const R_EARTH = 6378.137;
		const M = 1 / (((2 * Math.PI) / 360) * R_EARTH) / 1000;
		function moveFrom(coords, angle, distance) {
			let radianAngle = (angle * Math.PI) / 180;
			let x = 0 + distance * Math.cos(radianAngle);
			let y = 0 + distance * Math.sin(radianAngle);
			let newLat = coords.lat + y * M;
			let newLng = coords.lng + (x * M) / Math.cos(coords.lat * (Math.PI / 180));
			return { lat: newLat, lng: newLng };
		}
		let coordinates = [location];
		for (let angle = 0; angle < 360; angle += 45) {
			coordinates.push(moveFrom({ lat: location.lat, lng: location.lng }, angle, meters));
		}
		return coordinates;
	};
	/**
	 * Check if the param is coordinates
	 * @param {string} coordinates
	 * @return {boolean}
	 */
	static isCoordinates = (coordinates) => {
		const regex = /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/g;
		return regex.test(coordinates);
	};
	/**
	 * Returns map scale
	 * @param {Object} bounds map bounds
	 * @return {number} map scale
	 */
	static calculateScale = (bounds) =>
		GameHelper.haversineDistance({ lat: bounds.min.lat, lng: bounds.min.lng }, { lat: bounds.max.lat, lng: bounds.max.lng }) / 7.458421;
	/**
	 * Returns distance in km between two coordinates
	 * @param {Object} mk1 {lat, lng}
	 * @param {Object} mk2 {lat, lng}
	 * @return {number} km
	 */
	static haversineDistance = (mk1, mk2) => {
		const R = 6371.071;
		const rlat1 = mk1.lat * (Math.PI / 180);
		const rlat2 = mk2.lat * (Math.PI / 180);
		const difflat = rlat2 - rlat1;
		const difflon = (mk2.lng - mk1.lng) * (Math.PI / 180);
		const km =
			2 *
			R *
			Math.asin(Math.sqrt(Math.sin(difflat / 2) * Math.sin(difflat / 2) + Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon / 2) * Math.sin(difflon / 2)));
		return km;
	};
	/**
	 * Returns score based on distance and scale
	 * @param {number} distance
	 * @param {number} scale
	 * @return {number} score
	 */
	static calculateScore = (distance, scale) => Math.round(5000 * Math.pow(0.99866017, (distance * 1000) / scale));
	/**
	 * Returns guesses sorted by distance ASC
	 * @param {array} guesses
	 * @return {array} guesses
	 */
	static sortByDistance = (guesses) => guesses.sort((a, b) => a.distance - b.distance);
	/**
	 * Returns guesses sorted by score DESC
	 * @param {array} guesses
	 * @return {array} guesses
	 */
	static sortByScore = (guesses) => guesses.sort((a, b) => b.score - a.score);
	/** Converts a country code into an emoji flag
	 * @param {String} value
	 */
	static toEmojiFlag = (value) => {
		if (value.length == 2) {
			return value.toUpperCase().replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397));
		} else {
			const flag = value
				.toUpperCase()
				.substring(0, 2)
				.replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397));
			const region = value
				.toUpperCase()
				.substring(2)
				.replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397) + " ");
			return `${flag} ${region}`.trim();
		}
	};
	/** Replace special chars
	 * @param {String} val
	 */
	static normalize = (val) => val.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
	/** Matches words above 3 letters
	 * @param {String} input
	 * @param {String} key
	 */
	static isMatch = (input, key) => input.length >= 3 && key.includes(input) && input.length <= key.length;
	/** Find country by code or name
	 * @param {String} input
	 * @return {Object} countryCodesNames
	 */
	static findCountry = (input) => {
		const normalized = GameHelper.normalize(input);
		return countryCodesNames.find((country) => country.code === normalized || GameHelper.isMatch(normalized, country.names.toLowerCase()));
	};
	/** Return a random country code
	 * @return {String}
	 */
	static getRandomFlag = () => countryCodesNames[Math.floor(Math.random() * countryCodesNames.length)].code;
	/** Make game summary link
	 * @param  {string} streamer
	 * @param  {string} mapName
	 * @param  {Object} mode
	 * @param  {Object[]} locations
	 * @param  {Object[]} scores
	 * @return {Promise} link
	 */
	static makeLink = (streamer, mapName, mode, locations, totalScores) => {
		const players = totalScores.map((guess) => {
			return { username: guess.username, flag: guess.flag, score: guess.score, rounds: guess.rounds };
		});
		return axios
			.post(`${process.env.API_URL}/game`, {
				streamer: streamer,
				map: mapName,
				mode: mode,
				locations: locations,
				players: players,
			})
			.then((res) => {
				return `${process.env.BASE_URL}/game/${res.data.code}`;
			})
			.catch((err) => {
				console.log(err);
			});
	};
}
module.exports = GameHelper;
const GameHelper = require("./GameHelper");
function mainWindow() {
	let win = new BrowserWindow({
		show: false,
		webPreferences: {
			preload: path.join(__dirname, "../preload.js"),
			enableRemoteModule: true,
			contextIsolation: false,
			webSecurity: false,
			devTools: false,
		},
	});
	win.setMenuBarVisibility(false);
	win.loadURL("https://www.geoguessr.com/classic");
	win.webContents.on("new-window", (e, link) => {
		e.preventDefault();
		shell.openExternal(link);
	});
	win.on("closed", () => {
		win = null;
	});
	return win;
}
module.exports = mainWindow();
const path = require("path");
const { BrowserWindow } = require("electron");
function updateWindow() {
	const win = new BrowserWindow({
		width: 400,
		minWidth: 400,
		height: 240,
		minHeight: 240,
		frame: false,
		transparent: true,
		webPreferences: {
			nodeIntegration: true,
			contextIsolation: false,
			devTools: false,
		},
	});
	win.setMenuBarVisibility(false);
	win.loadURL(path.join(__dirname, "./update.html"));
	return win;
}
module.exports = updateWindow();
const ipcRenderer = require("electron").ipcRenderer;
const message = document.getElementById("message");
const restartButton = document.getElementById("restart-button");
ipcRenderer.on("download_progress", () => {
	ipcRenderer.removeAllListeners("download_progress");
	message.innerHTML = `Download in progress...`;
});
ipcRenderer.on("update_downloaded", () => {
	ipcRenderer.removeAllListeners("update_downloaded");
	message.innerHTML = `
		Update downloaded successfully. It will be installed on restart.
		Restart now ?
	`;
	restartButton.classList.remove("hidden");
});
ipcRenderer.on("update_error", (err) => {
	ipcRenderer.removeAllListeners("update_error");
	message.innerHTML = "An error occured.";
});
function closeWindow() {
	ipcRenderer.send("close_update_window");
}
function restartApp() {
	ipcRenderer.send("restart_app");
}
const path = require("path");
const { BrowserWindow, shell } = require("electron");
function settingsWindow() {
	const win = new BrowserWindow({
		width: 600,
		minWidth: 600,
		height: 500,
		minHeight: 500,
		show: false,
		frame: false,
		transparent: true,
		webPreferences: {
			nodeIntegration: true,
			contextIsolation: false,
			devTools: false,
		},
	});
	win.setMenuBarVisibility(false);
	win.loadURL(path.join(__dirname, "./settings.html"));
	win.webContents.on("new-window", (e, link) => {
		e.preventDefault();
		shell.openExternal(link);
	});
	return win;
}
const openTab = (e, tab) => {
	const tabcontent = document.getElementsByClassName("tabcontent");
	for (let i = 0; i < tabcontent.length; i++) {
		tabcontent[i].style.display = "none";
	}
	const tablinks = document.getElementsByClassName("tablinks");
	for (let i = 0; i < tablinks.length; i++) {
		tablinks[i].className = tablinks[i].className.replace(" active", "");
	}
	document.getElementById(tab).style.display = "block";
	e.currentTarget.className += " active";
};
document.getElementById("defaultOpen").click();
const GameHelper = require("../utils/GameHelper");
const Store = require("../utils/Store");
const Guess = require("./Guess");
class Game {
	constructor() {
		this.win;
		this.settings;
		this.url;
		this.seed;
		this.mapScale;
		this.location;
		this.country;
		this.isInGame = false;
		this.guessesOpen = false;
		this.isMultiGuess = false;
		this.guesses = [];
		this.total = [];
		this.lastLocation = {};
	}
	init = (win, settings) => {
		this.win = win;
		this.settings = settings;
		this.lastLocation = Store.get("lastLocation", {});
	};
	start = async (url, isMultiGuess) => {
		this.isInGame = true;
		this.isMultiGuess = isMultiGuess;
		if (this.url === url) {
			this.refreshSeed();
		} else {
			this.url = url;
			this.seed = await this.getSeed();
			this.mapScale = GameHelper.calculateScale(this.seed.bounds);
			this.getCountry();
			this.clearGuesses();
		}
	};
	outGame = () => {
		this.isInGame = false;
		this.closeGuesses();
	};
	streamerHasguessed = (newSeed) => newSeed.player.guesses.length != this.seed.player.guesses.length;
	locHasChanged = (newSeed) => JSON.stringify(newSeed.rounds[newSeed.rounds.length - 1]) != JSON.stringify(this.getLocation());
	refreshSeed = async () => {
		const newSeed = await this.getSeed();
		// If a guess has been comitted, process streamer guess then return scores
		if (this.streamerHasguessed(newSeed)) {
			this.win.webContents.send("pre-round-results");
			this.closeGuesses();
			this.seed = newSeed;
			const location = this.location;
			const scores = await this.makeGuess().then(() => this.getRoundScores());
			return { location, scores };
			// Else, if only the loc has changed, the location was skipped, replace current loc
		} else if (this.locHasChanged(newSeed)) {
			this.seed = newSeed;
			this.getCountry();
			return false;
		}
	};
	getSeed = async () => await GameHelper.fetchSeed(this.url);
	getCountry = async () => {
		this.location = this.getLocation();
		this.country = await GameHelper.getCountryCode(this.location);
	};
	makeGuess = async () => {
		this.seed = await this.getSeed();
		if (this.isMultiGuess) await this.processMultiGuesses();
		const streamerGuess = await this.processStreamerGuess();
		this.guesses.push(streamerGuess);
		this.guesses.forEach((guess) => this.pushToTotal(guess));
		this.lastLocation = { lat: this.location.lat, lng: this.location.lng };
		Store.set("lastLocation", this.lastLocation);
		if (this.seed.state != "finished") {
			this.getCountry();
		}
	};
	processMultiGuesses = () => {
		let promises = [];
		this.guesses.forEach(async (guess, index) => {
			promises.push(
				new Promise(async (resolve, reject) => {
					const guessedCountry = await GameHelper.getCountryCode(guess.position);
					guessedCountry === this.country ? guess.streak++ : (guess.streak = 0);
					this.guesses[index].streak = guess.streak;
					Store.setUserStreak(guess.user, guess.streak);
					resolve();
				})
			);
		});
		return Promise.all(promises);
	};
	processStreamerGuess = async () => {
		const index = this.seed.state === "finished" ? 1 : 2;
		const streamerGuess = this.seed.player.guesses[this.seed.round - index];
		const location = { lat: streamerGuess.lat, lng: streamerGuess.lng };
		const streamer = Store.getOrCreateUser(this.settings.channelName, this.settings.channelName);
		const guessedCountry = await GameHelper.getCountryCode(location);
		guessedCountry === this.country ? streamer.addStreak() : streamer.setStreak(0);
		const distance = GameHelper.haversineDistance(location, this.location);
		const score = streamerGuess.timedOut ? 0 : GameHelper.calculateScore(distance, this.mapScale);
		if (score == 5000) streamer.perfects++;
		streamer.calcMeanScore(score);
		streamer.nbGuesses++;
		Store.saveUser(this.settings.channelName, streamer);
		return new Guess(streamer.username, streamer.username, "#FFF", streamer.flag, location, streamer.streak, distance, score);
	};
	handleUserGuess = async (userstate, location) => {
		const index = this.hasGuessedThisRound(userstate.username);
		if (!this.isMultiGuess && index != -1) return "alreadyGuessed";
		const user = Store.getOrCreateUser(userstate.username, userstate["display-name"]);
		if (this.hasPastedPreviousGuess(user.previousGuess, location)) return "pastedPreviousGuess";
		if (JSON.stringify(user.lastLocation) != JSON.stringify(this.lastLocation)) user.setStreak(0);
		if (!this.isMultiGuess) {
			const guessedCountry = await GameHelper.getCountryCode(location);
			guessedCountry === this.country ? user.addStreak() : user.setStreak(0);
		}
		const distance = GameHelper.haversineDistance(location, this.location);
		const score = GameHelper.calculateScore(distance, this.mapScale);
		if (score == 5000) user.perfects++;
		user.calcMeanScore(score);
		const guess = new Guess(userstate.username, userstate["display-name"], userstate.color, user.flag, location, user.streak, distance, score);
		// Modify guess or push it
		if (this.isMultiGuess && index != -1) {
			this.guesses[index] = guess;
			this.guesses[index].modified = true;
		} else {
			user.nbGuesses++;
			this.guesses.push(guess);
		}
		user.setLastLocation({ lat: this.location.lat, lng: this.location.lng });
		user.setPreviousGuess(location);
		Store.saveUser(userstate.username, user);
		return { user: user, guess: guess };
	};
	nextRound = () => {
		this.guesses = [];
		if (this.seed.state != "finished") {
			this.win.webContents.send("next-round", this.isMultiGuess);
		} else {
			this.win.webContents.send("final-results");
		}
	};
	getLocation = () => this.seed.rounds[this.seed.round - 1];
	getLocations = () => {
		return this.seed.rounds.map((round) => {
			return {
				lat: round.lat,
				lng: round.lng,
				heading: Math.round(round.heading),
				pitch: Math.round(round.pitch),
			};
		});
	};
	openGuesses = () => {
		this.guessesOpen = true;
	};
	closeGuesses = () => {
		this.guessesOpen = false;
	};
	clearGuesses = () => {
		this.guesses = [];
		this.total = [];
	};
	/**
	 * @param {string} user
	 * @return {Number} index
	 */
	hasGuessedThisRound = (user) => this.guesses.findIndex((e) => e.user === user);
	/**
	 * @param  {Object} previousGuess
	 * @param  {Object} location {lat, lng}
	 * @return {boolean}
	 */
	hasPastedPreviousGuess = (previousGuess, location) => {
		if (previousGuess === null) return false;
		return previousGuess.lat === location.lat && previousGuess.lng === location.lng;
	};
	/**
	 * @param  {Object} guess
	 */
	pushToTotal = (guess) => {
		const index = this.total.findIndex((e) => e.user === guess.user);
		if (index != -1) {
			this.total[index].scores.push({ round: this.seed.round - 1, score: guess.score });
			this.total[index].score += guess.score;
			this.total[index].distance += guess.distance;
			this.total[index].streak = guess.streak;
			this.total[index].color = guess.color;
			this.total[index].flag = guess.flag;
			this.total[index].rounds++;
		} else {
			this.total.push({ scores: [{ round: this.seed.round, score: guess.score }], ...guess, rounds: 1 });
		}
	};
	/**
	 * @return {Guess[]} sorted guesses by Distance
	 */
	getRoundScores = () => GameHelper.sortByDistance(this.guesses);
	/**
	 * @return {Guess[]} sorted guesses by Score
	 */
	getTotalScores() {
		const scores = GameHelper.sortByScore(this.total);
		// TODO: Remember to check equality
		Store.userAddVictory(scores[0].user);
		return scores;
	}
	get mapName() {
		return this.seed.mapName;
	}
	get mode() {
		return { noMove: this.seed.forbidMoving, noPan: this.seed.forbidRotating, noZoom: this.seed.forbidZooming };
	}
	get round() {
		return this.seed.round;
	}
	get nbGuesses() {
		return this.guesses.length;
	}
}
module.exports = Game;
class Guess {
	/**
	 * @param {String} user
	 * @param {String} username
	 * @param {String} color
	 * @param {String} flag
	 * @param {Object} position {lat, lng}
	 * @param {Number} streak
	 * @param {Number} distance
	 * @param {Number} score
	 * @param {Boolean} modified
	 */
	constructor(user, username, color, flag, position, streak, distance, score, modified = false) {
		this.user = user;
		this.username = username;
		this.color = color === null ? "#FFF" : color;
		this.flag = flag;
		this.position = position;
		this.streak = streak;
		this.distance = distance;
		this.score = score;
		this.modified = modified;
	}
}
module.exports = Guess;
const Store = require("../../utils/Store");
class Scoreboard {
	constructor() {
		this.visibility;
		this.position;
		this.container;
		this.scoreboard;
		this.title;
		this.switchContainer;
		this.switchBtn;
		this.table;
		this.columnState;
		this.isMultiGuess = false;
		this.isResults = false;
		this.isScrolling = false;
		this.speed = 50;
		this.init();
	}
	init() {
		this.visibility = this.getCookie("visibility", true);
		this.position = this.getCookie("scoreboard_position", { top: 20, left: 5, width: 380, height: 180 });
		this.container = $("#scoreboardContainer");
		this.title = $("#scoreboardTitle");
		this.switchContainer = $("#switchContainer");
		this.switchBtn = $("#switchBtn");
		this.scoreboard = $("#scoreboard");
		this.scoreboard.css("top", this.position.top);
		this.scoreboard.css("left", this.position.left);
		this.scoreboard.css("width", this.position.width);
		this.scoreboard.css("height", this.position.height);
		this.scoreboard
			.resizable({
				handles: "n, e, s, w, ne, se, sw, nw",
				containment: "#scoreboardContainer",
			})
			.draggable({
				containment: "#scoreboardContainer",
			})
			.mouseup(() => {
				const currentPosition = this.getPosition();
				if (JSON.stringify(this.position) !== JSON.stringify(currentPosition)) {
					this.setPosition(currentPosition);
					this.setCookie("scoreboard_position", JSON.stringify(currentPosition));
				}
			});
		this.switchBtn.on("change", () => {
			if (this.switchBtn.is(":checked")) {
				ipcRenderer.send("open-guesses");
			} else {
				ipcRenderer.send("close-guesses");
			}
		});
		this.table = $("#datatable").DataTable({
			info: false,
			searching: false,
			paging: false,
			scrollY: 100,
			scrollResize: true,
			scrollCollapse: true,
			language: { zeroRecords: " " },
			dom: "Bfrtip",
			buttons: [
				{
					extend: "colvis",
					text: "⚙️",
					className: "colvis-btn",
					columns: ":not(.noVis)",
				},
			],
			columns: [
				{ data: "Position" },
				{ data: "Player" },
				{ data: "Streak" },
				{
					data: "Distance",
					render: (data, type) => {
						if (type === "display" || type === "filter") {
							return this.toMeter(data);
						}
						return data;
					},
				},
				{ data: "Score" },
			],
			columnDefs: [
				{ targets: 0, width: "35px", className: "noVis" },
				{ targets: 1, width: "auto", className: "noVis" },
				{ targets: 2, width: "55px" },
				{ targets: 3, width: "100px" },
				{ targets: 4, width: "75px", type: "natural" },
			],
		});
		// Column Visisbility
		this.columnState = this.getCookie("CG_ColVis", [
			{ column: 0, state: true },
			{ column: 2, state: true },
			{ column: 3, state: true },
			{ column: 4, state: true },
		]);
		// Handle ColVis change
		this.table.on("column-visibility.dt", (e, settings, column, state) => {
			if (this.isResults || this.isMultiGuess) return;
			const i = this.columnState.findIndex((o) => o.column === column);
			if (this.columnState[i]) {
				this.columnState[i] = { column, state };
			} else {
				this.columnState.push({ column, state });
			}
			this.setCookie("CG_ColVis", JSON.stringify(this.columnState));
		});
		// SCROLLER
		const sliderElem = ``;
		$(".dt-buttons").append(sliderElem);
		const slider = document.getElementById("scrollSpeedSlider");
		slider.oninput = (e) => {
			this.speed = e.currentTarget.value;
			this.scroller(".dataTables_scrollBody");
		};
		const scrollBtn = `
			
			
			
		`;
		$(".dt-buttons").prepend(scrollBtn);
		$("#scrollBtn").on("change", (e) => {
			if (e.currentTarget.checked != true) {
				this.isScrolling = $(e.currentTarget).is(":checked");
				this.stop(".dataTables_scrollBody");
				slider.style.display = "none";
			} else {
				this.isScrolling = $(e.currentTarget).is(":checked");
				this.scroller(".dataTables_scrollBody");
				slider.style.display = "inline";
			}
		});
	}
	/**
	 * @param {boolean} isMultiGuess
	 */
	reset = (isMultiGuess) => {
		this.isMultiGuess = isMultiGuess;
		this.setColVis();
		this.isResults = false;
		this.setTitle("GUESSES (0)");
		this.showSwitch(true);
		this.table.clear().draw();
	};
	setVisibility = () => {
		this.visibility = !this.visibility;
		this.setCookie("visibility", this.visibility);
		this.checkVisibility();
	};
	checkVisibility = () => {
		if (this.visibility) {
			this.show();
		} else {
			this.hide();
		}
	};
	show = () => {
		this.container.show();
	};
	hide = () => {
		this.container.hide();
	};
	renderGuess = (guess) => {
		const row = {
			Position: "",
			Player: `${guess.flag ? `` : ""}${
				guess.username
			}`,
			Streak: guess.streak,
			Distance: guess.distance,
			Score: guess.score,
		};
		const rowNode = this.table.row.add(row).node();
		rowNode.classList.add("expand");
		setTimeout(() => {
			rowNode.classList.remove("expand");
		}, 200);
		this.table.order([3, "asc"]).draw(false);
		this.table
			.column(0)
			.nodes()
			.each((cell, i) => {
				cell.innerHTML = i + 1;
			});
	};
	renderMultiGuess = (guesses) => {
		const rows = guesses.map((guess) => {
			return {
				Position: "",
				Player: `${guess.flag ? `` : ""}${
					guess.username
				}`,
				Streak: "",
				Distance: "",
				Score: "",
			};
		});
		this.table.clear().draw();
		this.table.rows.add(rows).draw();
	};
	displayScores = (scores, isTotal = false) => {
		this.isResults = true;
		if (scores[0]) scores[0].color = "#E3BB39";
		if (scores[1]) scores[1].color = "#C9C9C9";
		if (scores[2]) scores[2].color = "#A3682E";
		const rows = scores.map((score) => {
			return {
				Position: "",
				Player: `${score.flag ? `` : ""}${
					score.username
				}`,
				Streak: score.streak,
				Distance: score.distance,
				Score: `${score.score}${isTotal ? " [" + score.rounds + "]" : ""}`,
			};
		});
		this.table.clear().draw();
		this.table.rows.add(rows);
		this.table.order([4, "desc"]).draw(false);
		let content;
		this.table
			.column(0)
			.nodes()
			.each((cell, i) => {
				content = i + 1;
				if (isTotal) {
					if (i == 0) content = "🏆";
					else if (i == 1) content = "🥈";
					else if (i == 2) content = "🥉";
				}
				cell.innerHTML = content;
			});
		// Restore columns visibility
		this.table.columns().visible(true);
		this.toTop(".dataTables_scrollBody");
	};
	scroller = (elem) => {
		const div = $(elem);
		const loop = () => {
			if (!this.isScrolling) return;
			div.stop().animate({ scrollTop: div[0].scrollHeight }, (div[0].scrollHeight - div.scrollTop() - 84) * this.speed, "linear", () => {
				setTimeout(() => {
					div.stop().animate({ scrollTop: 0 }, 1000, "swing", () => {
						setTimeout(() => {
							loop();
						}, 3000);
					});
				}, 1000);
			});
		};
		loop();
	};
	toTop = (elem) => {
		this.stop(elem);
		setTimeout(() => {
			this.scroller(elem);
		}, 3000);
	};
	stop(elem) {
		$(elem).stop();
	}
	setColVis = () => {
		if (this.isMultiGuess) {
			this.table.columns([0, 2, 3, 4]).visible(false);
		} else {
			this.columnState.forEach((column) => {
				this.table.column(column.column).visible(column.state);
			});
		}
	};
	getPosition = () => ({
		top: this.scoreboard.position().top,
		left: this.scoreboard.position().left,
		width: this.scoreboard.width(),
		height: this.scoreboard.height(),
	});
	setPosition = (position) => (this.position = position);
	setTitle = (title) => this.title.text(title);
	showSwitch = (state) => this.switchContainer.css("display", state ? "block" : "none");
	switchOn = (state) => this.switchBtn.prop("checked", state);
	toMeter = (distance) => (distance >= 1 ? parseFloat(distance.toFixed(1)) + "km" : parseInt(distance * 1000) + "m");
	// ColVis Cookies
	setCookie = (name, value, exdays = 60) => {
		const d = new Date();
		d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
		const expires = "expires=" + d.toUTCString();
		document.cookie = name + "=" + value + ";" + expires + ";path=/";
	};
	getCookie = (name, defaultValue = {}) => {
		const cname = name + "=";
		var decodedCookie = decodeURIComponent(document.cookie);
		var ca = decodedCookie.split(";");
		for (var i = 0; i < ca.length; i++) {
			var c = ca[i];
			while (c.charAt(0) == " ") {
				c = c.substring(1);
			}
			if (c.indexOf(cname) == 0) {
				return JSON.parse(c.substring(cname.length, c.length));
			}
		}
		return defaultValue;
	};
}
module.exports = Scoreboard;
class Settings {
	/**
	 * @param {String} channelName=""
	 * @param {String} botUsername=""
	 * @param {String} token=""
	 * @param {String} cgCmd="!cg"
	 * @param {String} cgMsg="To play along, go to this link, pick a location, and paste the whole command into chat: "
	 * @param {String} userGetStatsCmd="!me"
	 * @param {String} userClearStatsCmd="!clear"
	 * @param {String} setStreakCmd="!setstreak"
	 * @param {Boolean} showHasGuessed=true
	 * @param {Boolean} isMultiGuess=false
	 * @param {Boolean} noCar=false
	 * @param {Boolean} noCompass=false
	 */
	constructor(
		channelName = "",
		botUsername = "",
		token = "",
		cgCmd = "!cg",
		cgMsg = "To play along, go to this link, pick a location, and paste the whole command into chat: ",
		userGetStatsCmd = "!me",
		userClearStatsCmd = "!clear",
		setStreakCmd = "!setstreak",
		showHasGuessed = true,
		isMultiGuess = false,
		noCar = false,
		noCompass = false
	) {
		this.channelName = channelName;
		this.botUsername = botUsername;
		this.token = token;
		this.cgCmd = cgCmd;
		this.cgMsg = cgMsg;
		this.userGetStatsCmd = userGetStatsCmd;
		this.userClearStatsCmd = userClearStatsCmd;
		this.setStreakCmd = setStreakCmd;
		this.showHasGuessed = showHasGuessed;
		this.isMultiGuess = isMultiGuess;
		this.noCar = noCar;
		this.noCompass = noCompass;
	}
	/**
	 * @param {boolean} noCar
	 * @param {boolean} noCompass
	 */
	setGameSettings(isMultiGuess, noCar, noCompass) {
		this.isMultiGuess = isMultiGuess;
		this.noCar = noCar;
		this.noCompass = noCompass;
	}
	/**
	 * @param {Object} commands
	 */
	setTwitchCommands(commands) {
		this.cgCmd = commands.cgCmdd;
		this.cgMsg = commands.cgMsgg;
		this.userGetStatsCmd = commands.userGetStats;
		this.userClearStatsCmd = commands.userClearStats;
		this.setStreakCmd = commands.setStreak;
		this.showHasGuessed = commands.showHasGuessed;
	}
	/**
	 * @param {string} channelName
	 * @param {string} botUsername
	 * @param {string} token
	 */
	setTwitchSettings(channelName, botUsername, token) {
		this.channelName = channelName;
		this.botUsername = botUsername;
		this.token = token;
	}
}
module.exports = Settings;
class User {
	/**
	 * @param {String} user
	 * @param {String} username
	 * @param {String} flag=""
	 * @param {Number} streak=0
	 * @param {Number} bestStreak=0
	 * @param {Number} correctGuesses=0
	 * @param {Number} nbGuesses=0
	 * @param {Number} perfects=0
	 * @param {Number} victories=0
	 * @param {Number} meanScore=null
	 * @param {Object} previousGuess={}
	 * @param {Object} lastLocation=null
	 */
	constructor(
		user,
		username,
		flag = "",
		streak = 0,
		bestStreak = 0,
		correctGuesses = 0,
		nbGuesses = 0,
		perfects = 0,
		victories = 0,
		meanScore = null,
		previousGuess = {},
		lastLocation = null
	) {
		this.user = user;
		this.username = username;
		this.flag = flag;
		this.streak = streak;
		this.bestStreak = bestStreak;
		this.correctGuesses = correctGuesses;
		this.nbGuesses = nbGuesses;
		this.perfects = perfects;
		this.victories = victories;
		this.meanScore = meanScore;
		this.previousGuess = previousGuess;
		this.lastLocation = lastLocation;
	}
	/* Add 1 to streak and correctGuesses. */
	addStreak() {
		this.streak++;
		this.correctGuesses++;
		if (this.streak > this.bestStreak) this.bestStreak = this.streak;
	}
	/** Set user streak
	 * @param {Number} number
	 */
	setStreak(number) {
		this.streak = number;
		if (this.streak > this.bestStreak) this.bestStreak = this.streak;
	}
	/** Set last location
	 * @param {Object} location
	 */
	setLastLocation(location) {
		this.lastLocation = location;
	}
	/** Set previous guess
	 * @param {Object} location
	 */
	setPreviousGuess(location) {
		this.previousGuess = location;
	}
	/** Set a country flag
	 * @param {String} flag country code
	 */
	setFlag(flag) {
		this.flag = flag;
	}
	/** Calculate mean score
	 * @param {Number} score
	 */
	calcMeanScore(score) {
		if (this.meanScore === null) {
			this.meanScore = score;
		} else {
			this.meanScore = (this.meanScore * this.nbGuesses + score) / (this.nbGuesses + 1);
		}
	}
}