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