1753 lines
		
	
	
		
			47 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			1753 lines
		
	
	
		
			47 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
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 = `
 | 
						|
		<div id='scoreboard'>
 | 
						|
			<div id='scoreboardHeader'>
 | 
						|
				<span></span>
 | 
						|
				<span id='scoreboardTitle'>GUESSES (0)</span>
 | 
						|
				<label id='switchContainer'>
 | 
						|
					<input id='switchBtn' type='checkbox' />
 | 
						|
					<div class='slider'></div>
 | 
						|
				</label>
 | 
						|
			</div>
 | 
						|
			<table id='datatable' width='100%'>
 | 
						|
				<thead>
 | 
						|
					<tr>
 | 
						|
						<th>#</th>
 | 
						|
						<th>Player</th>
 | 
						|
						<th>Streak</th>
 | 
						|
						<th>Distance</th>
 | 
						|
						<th>Score</th>
 | 
						|
					</tr>
 | 
						|
				</thead>
 | 
						|
				<tbody id='guessList'></tbody>
 | 
						|
			</table>
 | 
						|
		</div>`;
 | 
						|
	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 = "<span>⚙️</span>";
 | 
						|
		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 = "<span>👁️🗨️</span>";
 | 
						|
		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(`
 | 
						|
				<p class="gm-iw__content">
 | 
						|
					<span style="font-size:14px;">${score.flag ? `<span class="flag-icon flag-icon-${score.flag}"></span>` : ""}${score.username}</span><br>
 | 
						|
					${score.distance >= 1 ? parseFloat(score.distance.toFixed(1)) + "km" : parseInt(score.distance * 1000) + "m"}<br>
 | 
						|
					${score.score}
 | 
						|
				</p>
 | 
						|
			`);
 | 
						|
			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<span class="one">.</span><span class="two">.</span><span class="three">.</span>`;
 | 
						|
});
 | 
						|
 | 
						|
ipcRenderer.on("update_downloaded", () => {
 | 
						|
	ipcRenderer.removeAllListeners("update_downloaded");
 | 
						|
	message.innerHTML = `
 | 
						|
		Update downloaded successfully. It will be installed on restart.<br>
 | 
						|
		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 = `<input type="range" min="5" max="50" value="20" id="scrollSpeedSlider">`;
 | 
						|
		$(".dt-buttons").append(sliderElem);
 | 
						|
 | 
						|
		const slider = document.getElementById("scrollSpeedSlider");
 | 
						|
 | 
						|
		slider.oninput = (e) => {
 | 
						|
			this.speed = e.currentTarget.value;
 | 
						|
			this.scroller(".dataTables_scrollBody");
 | 
						|
		};
 | 
						|
 | 
						|
		const scrollBtn = `
 | 
						|
			<div class="dt-button scrollBtn">
 | 
						|
			<label>
 | 
						|
				<input type="checkbox" id="scrollBtn"><span>⮃</span>
 | 
						|
			</label>
 | 
						|
			</div>
 | 
						|
		`;
 | 
						|
		$(".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 ? `<span class="flag-icon flag-icon-${guess.flag}"></span>` : ""}<span class='username' style='color:${guess.color}'>${
 | 
						|
				guess.username
 | 
						|
			}</span>`,
 | 
						|
			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 ? `<span class="flag-icon flag-icon-${guess.flag}"></span>` : ""}<span class='username' style='color:${guess.color}'>${
 | 
						|
					guess.username
 | 
						|
				}</span>`,
 | 
						|
				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 ? `<span class="flag-icon flag-icon-${score.flag}"></span>` : ""}<span class='username' style='color:${score.color}'>${
 | 
						|
					score.username
 | 
						|
				}</span>`,
 | 
						|
				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 = "<span class='icon'>🏆</span>";
 | 
						|
					else if (i == 1) content = "<span class='icon'>🥈</span>";
 | 
						|
					else if (i == 2) content = "<span class='icon'>🥉</span>";
 | 
						|
				}
 | 
						|
 | 
						|
				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: <your cg link>"
 | 
						|
	 * @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: <your cg link>",
 | 
						|
		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);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 |