Lumi's Votecount Script V2.0.0 Release
UI has been implemented!
When you add the script you'll get a little pull-tab in the top right hand corner of your screen:
Clicking on the pull-tab will open the vote counter interface. It lets you specify what post number each EoD is.
Clicking the top "Post Votecount" button will have the votecount for the
current day appended to the end of your quick reply box. (No more clipboard stuff)
Clicking a "Post VC" button next to one of the previous EoDs will let you post the votecount for that EoD.
Clicking "Add Day" and "Remove Day" will add and remove the number of days respectively.
This version is hot off the VS Code development environment, so I'm sure there are some bugs, let me know if you run into any!
Code: Select all
// ==UserScript==
// @name Secret Playerlist Vote Counter
// @namespace mailto:luminouslag@gmail.com
// @version 2.0.0
// @description Automatic Vote Counting for Secret Plyerlist Mafia game on The Syndicate
// @author Lumi
// @match https://mafiathesyndicate.com/viewtopic.php?*
// @icon https://www.google.com/s2/favicons?sz=64&domain=mafiathesyndicate.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// ==/UserScript==
console.log('Script Initialized');
let initiallyVisible = GM_getValue('isVisible', false);
///////
/// Clean Up Old Version Code
///////
let version = GM_getValue(`version`);
if (version != '2.0.0') {
for(let i=1; i<=10; i++) {
GM_deleteValue(`EoD${i}`);
}
}
GM_setValue(`version`, '2.0.0');
///////
/// Basic DOM navigation helpers
///////
async function getDocument(thread, start) {
return await window.fetch(`https://mafiathesyndicate.com/viewtopic.php?t=${thread}&start=${start}`).then(r => r.text()).then(html => (new DOMParser()).parseFromString(html, 'text/html'));
}
function appendTextToReplyField(text) {
document.getElementsByTagName('textarea')[0].value += text;
}
function isCorrectThread(targetNum) {
let topicNum = document.getElementsByClassName("topic-title")[0].children[0].href.matchAll(/\?t=([0-9]+)/g).next().value[1];
return topicNum == targetNum;
};
function getNumPosts() {
return parseInt(document.getElementsByClassName('pagination')[0].textContent.matchAll(/([0-9]*) posts/g).next().value[1]);
};
///////
/// Post Parsing and Vote History Construction
///////
function tallyPage(voteHistory, page) {
let posts = page.getElementsByClassName('post');
for (const post of posts) {
let postData = getDataFromPost(post);
if (parseInt(postData.number) == voteHistory.length + 1) {
voteHistory.push(postData);
}
}
return voteHistory;
}
function getDataFromPost(post) {
let postNum = post.getElementsByClassName('post-number')[0].textContent.match(/[0-9]+/g)[0];
let contentChildren = post.getElementsByClassName('content')[0].children;
let author = post.getElementsByClassName('author')[1].getElementsByTagName('a')[1].textContent;
let currentVoteTarget = null;
for (const contentChild of contentChildren) {
if (contentChild.classList.contains('mention')) {
let voteTarget = contentChild.textContent.replaceAll('\n','').matchAll(/\[VOTE: ([^\]]*)\]/g).next().value;
if (voteTarget) currentVoteTarget = voteTarget[1];
}
}
return {
number: postNum,
target: currentVoteTarget,
author: author,
};
}
///////
/// Vote History -> String Calculation
///////
function calcVoteCount(SoD, EoD, dayNum) {
console.log(`Calcuatling vote count for day ${dayNum}`);
let voteHistory = JSON.parse(GM_getValue(`voteHistory`), '[]');
let voteTargets = {};
let postCounts = {};
for (const vote of voteHistory) {
if ((vote.number >= SoD || SoD == 0) && (vote.number <= EoD || EoD == 0)) {
if (!(vote.author in voteTargets)) {
voteTargets[vote.author] = null;
postCounts[vote.author] = 1;
} else {
postCounts[vote.author] += 1;
}
if (vote.target != null) voteTargets[vote.author] = [vote.target, vote.number];
}
}
let voteCount = {};
for (const [key, value] of Object.entries(voteTargets) ) {
if (value != null) {
let target = value[0].toLowerCase().trim();
if (target in voteCount) {
voteCount[target].push([key, value[1], postCounts[key]]);
} else {
voteCount[target] = [[key, value[1], postCounts[key]]];
}
} else {
if ('Not Voting' in voteCount) {
voteCount['Not Voting'].push([key, null, postCounts[key]]);
} else {
voteCount['Not Voting'] = [[key, null, postCounts[key]]];
}
}
}
let output = voteCountToString(voteCount);
output = `[size=150][b]Day ${dayNum} Vote Count[/b][/size]\nSoD: [url=https://mafiathesyndicate.com/viewtopic.php?t=2451&start=${SoD}]#${SoD}[/url] EoD: [url=https://mafiathesyndicate.com/viewtopic.php?t=2451&start=${EoD}]#${EoD}[/url]\n\n` + output;
return output;
}
function voteToString(vote) {
if (vote[1] == null) return `${vote[0]} (${vote[2]})`
return `[url=https://mafiathesyndicate.com/viewtopic.php?t=2451&start=${vote[1]-1}]${vote[0]}[/url] (${vote[2]})`
}
function voteCountToString(voteCount) {
let output = '';
let voteCounts = Object.entries(voteCount).sort((a, b) => b[1].length - a[1].length);
for (const v of voteCounts) {
let key = v[0];
let value = v[1].sort((a,b) => parseInt(a[1]) - parseInt(b[1]));
let valueStrings = [];
for (const vote of value) {
valueStrings.push(voteToString(vote));
}
output += `${key} (${value.length}): ${valueStrings.join(', ')}\n`
}
return output;
}
///////
/// UI
///////
function buildUI() {
let uiBase = document.createElement('div');
document.body.appendChild(uiBase);
uiBase.classList.add('uiBase');
uiBase.style.display = initiallyVisible ? 'block' : 'none'
let uiInteriorBackground = document.createElement('div');
uiBase.appendChild(uiInteriorBackground);
uiInteriorBackground.classList.add('uiInteriorBackground');
let uiInterior = document.createElement('div');
uiInteriorBackground.appendChild(uiInterior);
uiInterior.classList.add('uiInterior');
let toggleButton = document.createElement('button');
document.body.appendChild(toggleButton);
toggleButton.classList.add('toggleButton');
let postVoteCountButton = document.createElement('button');
uiInterior.appendChild(postVoteCountButton);
postVoteCountButton.classList.add('interfaceButton');
postVoteCountButton.textContent = 'Post Votecount';
let eodGrid = document.createElement('div');
uiInterior.appendChild(eodGrid);
eodGrid.classList.add('eodGrid');
let eodGridDivs = [];
let addRemoveDaysContainer = document.createElement('div');
uiInterior.appendChild(addRemoveDaysContainer);
addRemoveDaysContainer.classList.add('addRemoveDaysContainer');
let addDayButon = document.createElement('button');
addRemoveDaysContainer.appendChild(addDayButon);
addDayButon.classList.add('dayButton');
addDayButon.textContent = 'Add Day';
let removeDayButton = document.createElement('button');
addRemoveDaysContainer.appendChild(removeDayButton);
removeDayButton.classList.add('dayButton');
removeDayButton.textContent = 'Remove Day';
let uiWidth = 250;
let buttonGapSize = 10;
let css = `
.uiBase {
position: fixed;
top: 30px;
left: 0px;
width: ${uiWidth}px;
z-index: 99;
border-radius: 0 8px 8px 0;
background: linear-gradient(hsla(210, 100%, 50%, 1),hsla(51, 92.3%, 89.8%, 1));
padding: 4px 4px 4px 0;
}
.uiInteriorBackground {
width: 100%;
background: linear-gradient(hsla(210, 100%, 50%, 1),hsla(210, 100%, 50%, 1),hsla(51, 92.3%, 89.8%, 1));
}
.uiInterior {
background-color: #00000099;
border-radius: 0 4px 4px 0;
width: calc(100% - 30px);
display: flex;
flex-direction: column;
align-items: center;
align-content: flex-start;
padding: ${buttonGapSize}px 15px ${buttonGapSize}px 15px;
gap: ${buttonGapSize}px;
}
.toggleButton {
font: inherit;
outline: inherit;
border: none;
position: fixed;
top: 40px;
left: ${initiallyVisible ? uiWidth+4 : 0}px;
width: 50px;
height: 50px;
z-index: 99;
box-shadow: inset 0 0 5px #000;
border-top: solid hsla(210, 100%, 50%, 1) 4px;
border-bottom: solid hsla(210, 100%, 50%, 1) 4px;
border-right: solid hsla(210, 100%, 50%, 1) 4px;
border-radius: 0 50px 50px 0;
background-image: url("https://cdn.discordapp.com/attachments/1052273214610473060/1052273332294275193/a_1d37c28260e696b2070dff50ec150022.gif");
background-size: 100% 100%;
}
.toggleButton:hover {
box-shadow: inset 0 0 5px #fff;
}
.dayButton {
color: white;
font: inherit;
outline: inherit;
font-size: 1em;
padding: 0.25em 0 0.25em 0;
background: #00000073;
border-radius: 4px;
border: none;
width: 100%;
}
.interfaceButton {
color: white;
font: inherit;
outline: inherit;
font-size: 1.25em;
padding: 0.25em 2em 0.25em 2em;
background: #00000073;
border-radius: 4px;
border: none;
}
.interfaceButton:hover, .dayButton:hover, .eodGridPostButton:hover {
background-color: #20202073;
}
.eodGrid {
display: grid;
padding: 10px;
box-sizing: border-box;
background-color: #00000073;
grid-template-columns: auto 1fr 1fr;
grid-column-gap: 10px;
grid-row-gap: 3px;
width: 100%;
border-radius: 4px;
}
.eodGridBox {
display: flex;
align-items: center;
color: #fff;
width: 100%;
height: 100%;
}
.eodGridInput {
color: white;
font: inherit;
outline: inherit;
font-size: 1em;
background: #00000073;
box-shadow: inset 0 0 5px #000;
padding: 0.25em 0 0.25em 0;
border-radius: 4px;
border: none;
width: 100%;
height: 1.5em;
}
.eodGridPostButton {
color: white;
font: inherit;
outline: inherit;
font-size: 1em;
padding: 0.25em 0 0.25em 0;
background: #00000073;
border-radius: 4px;
border: none;
width: 100%;
}
.addRemoveDaysContainer {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 10px;
width: 100%;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
`;
let style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
let head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(style);
function toggleUI(base, button) {
let isVisible = (base.style.display != 'none');
if (isVisible) {
base.style.display = 'none';
button.style.left = `0px`;
} else {
base.style.display = 'block';
button.style.left = `${uiWidth+4}px`;
}
GM_setValue('isVisible', !isVisible);
}
function createEoDGridRow(eodGrid, eodGridDivs, initialValue, doPushToEoDs) {
let num = eodGridDivs.length + 1;
let eodLabel = document.createElement('div');
eodGrid.appendChild(eodLabel);
eodLabel.classList.add('eodGridBox');
eodLabel.textContent = `EoD ${num}`;
let eodEntry = document.createElement('input');
eodEntry.id = `EoD ${num}`;
eodEntry.type = 'number';
eodGrid.appendChild(eodEntry);
eodEntry.classList.add('eodGridInput');
eodEntry.value = initialValue;
eodEntry.addEventListener('change', (event) => handleEoDInputChange(event));
let eodPost = document.createElement('button');
eodGrid.appendChild(eodPost);
eodPost.classList.add('eodGridPostButton');
eodPost.textContent = 'Post VC';
eodPost.id = `VC ${num}`
eodPost.addEventListener('click', (event) => handlePastDayVC(event));
if (doPushToEoDs) {
let EoDs = JSON.parse(GM_getValue(`EoDs`, '[]'));
EoDs.push(initialValue);
GM_setValue(`EoDs`, JSON.stringify(EoDs));
}
eodGridDivs.push([eodLabel, eodEntry, eodPost]);
}
function removeEoDGridRow(eodGridDivs) {
let row = eodGridDivs.pop();
for (const eodGridBox of row) {
eodGridBox.remove();
}
let EoDs = JSON.parse(GM_getValue(`EoDs`, '[]'));
EoDs.pop();
GM_setValue(`EoDs`, JSON.stringify(EoDs));
}
function handleEoDInputChange(event) {
const value = event.target.value;
const id = event.target.id;
const eodNum = parseInt(id.split(' ')[1]);
let EoDs = JSON.parse(GM_getValue(`EoDs`, '[]'));
EoDs[eodNum - 1] = value;
GM_setValue(`EoDs`, JSON.stringify(EoDs));
}
function buildEoDGrid(eodGrid, eodGridDivs) {
let EoDs = JSON.parse(GM_getValue(`EoDs`, '[]'));
if (EoDs.length == 0) {
createEoDGridRow(eodGrid, eodGridDivs, 0, true);
} else {
for (const EoD of EoDs) {
createEoDGridRow(eodGrid, eodGridDivs, EoD, false);
}
}
}
function onPostVoteCountClick(eodGridDivs) {
console.log('Attempting to Post VC');
let finalEoD = parseInt(eodGridDivs[eodGridDivs.length - 1][1].value);
let dayNum = eodGridDivs.length;
let SoD = 0
let EoD = 0;
if (finalEoD != 0) {
dayNum = eodGridDivs.length + 1;
SoD = parseInt(eodGridDivs[eodGridDivs.length-1][1].value);
} else if (eodGridDivs.length > 1) {
SoD = parseInt(eodGridDivs[eodGridDivs.length-2][1].value);
}
console.log(`SoD: ${SoD} EOD: ${EoD} DayNum: ${dayNum}`)
let votecount = calcVoteCount(SoD, EoD, dayNum);
appendTextToReplyField(votecount);
}
function handlePastDayVC(event) {
console.log(`Attempting to Post VC`);
const id = event.target.id;
const dayNum = parseInt(id.split(' ')[1]);
let SoD = 0;
if (dayNum > 1) {
SoD = parseInt(document.getElementById(`EoD ${dayNum-1}`).value);
}
let EoD = parseInt(document.getElementById(`EoD ${dayNum}`).value);
console.log(`SoD: ${SoD} EoD: ${EoD} DayNum: ${dayNum}`);
let votecount = calcVoteCount(SoD, EoD, dayNum);
appendTextToReplyField(votecount);
}
buildEoDGrid(eodGrid, eodGridDivs);
toggleButton.addEventListener('click', () => { toggleUI(uiBase, toggleButton)});
addDayButon.addEventListener('click', () => { createEoDGridRow(eodGrid, eodGridDivs, 0, true) });
removeDayButton.addEventListener('click', () => { removeEoDGridRow(eodGridDivs) });
postVoteCountButton.addEventListener('click', () => { onPostVoteCountClick(eodGridDivs) });
}
///////
/// Code Injection
///////
if (isCorrectThread(2451)) {
buildUI();
let voteHistory = JSON.parse(GM_getValue(`voteHistory`, '[]'));
let numPosts = getNumPosts();
let curVoteHistoryLength = voteHistory.length;
while (voteHistory.length < numPosts) {
let page = await getDocument(2451, voteHistory.length);
voteHistory = tallyPage(voteHistory, page);
if (voteHistory.length == curVoteHistoryLength) break;
}
GM_setValue(`voteHistory`, JSON.stringify(voteHistory));
};
===============================================
And a reminder for how to add a Tampermonkey Script to your browser:
1)
Get Tampermonkey. The script is currently only tested in Chrome. It probably works to some degree in Firefox, but it might not, I haven't tested it there!
2) Click the Tampermonkey icon in the top right hand corner of your browser.
3) Click the button labeled "Create a new script..."
4) In the script interface that pops up select everything, delete it, and paste in the vote counting script instead. Hit Ctrl + S to save.
5) Reload the thread - you should see the Vote Counter interface.