Adds a timestamp to chat messages indicating when the message was sent, this is based on your local system time
`
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/amqBuzzer.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Buzzer
3 | // @namespace https://github.com/TheJoseph98
4 | // @version 1.3
5 | // @description Mutes the song on the buzzer (Enter key on empty answer field) and displays time you buzzed in
6 | // @author TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqBuzzer.user.js
11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqBuzzer.user.js
12 | // ==/UserScript==
13 |
14 | // Wait until the LOADING... screen is hidden and load script
15 | if (typeof Listener === "undefined") return;
16 | let loadInterval = setInterval(() => {
17 | if ($("#loadingScreen").hasClass("hidden")) {
18 | clearInterval(loadInterval);
19 | setup();
20 | }
21 | }, 500);
22 |
23 | const version = "1.3";
24 | let songStartTime = 0;
25 | let buzzerTime = 0;
26 | let isPlayer = false;
27 | let buzzed = false;
28 | let answerHandler;
29 |
30 | function showBuzzMessage(buzzTime) {
31 | gameChat.systemMessage(`Song ${parseInt($("#qpCurrentSongCount").text())}, buzz: ${buzzTime}`);
32 | }
33 |
34 | function formatTime(time) {
35 | let formattedTime = "";
36 | let milliseconds = Math.floor(time % 1000);
37 | let seconds = Math.floor(time / 1000);
38 | let minutes = Math.floor(seconds / 60);
39 | let hours = Math.floor(minutes / 60);
40 | let secondsLeft = seconds - minutes * 60;
41 | let minutesLeft = minutes - hours * 60;
42 | if (hours > 0) {
43 | formattedTime += hours + ":";
44 | }
45 | if (minutes > 0) {
46 | formattedTime += (minutesLeft < 10 && hours > 0) ? "0" + minutesLeft + ":" : minutesLeft + ":";
47 | }
48 | formattedTime += (secondsLeft < 10 && minutes > 0) ? "0" + secondsLeft + "." : secondsLeft + ".";
49 | if (milliseconds < 10) {
50 | formattedTime += "00" + milliseconds;
51 | }
52 | else if (milliseconds < 100) {
53 | formattedTime += "0" + milliseconds;
54 | }
55 | else {
56 | formattedTime += milliseconds;
57 | }
58 | return formattedTime;
59 | }
60 |
61 | function setup() {
62 | let quizReadyListener = new Listener("quiz ready", data => {
63 | // reset the event listener
64 | $("#qpAnswerInput").off("keypress", answerHandler);
65 | $("#qpAnswerInput").on("keypress", answerHandler);
66 | });
67 |
68 | let quizPlayNextSongListener = new Listener("play next song", data => {
69 | // reset the "buzzed" flag and get the start time on song start
70 | buzzed = false;
71 | songStartTime = Date.now();
72 | });
73 |
74 | let quizAnswerResultsListener = new Listener("answer results", result => {
75 | // show the buzz message only if the player is playing the game (ie. is not spectating)
76 | isPlayer = Object.values(quiz.players).some(player => player.isSelf === true);
77 | if (!buzzed && isPlayer) {
78 | showBuzzMessage("N/A");
79 | }
80 | // unmute only if the player muted the sound by buzzing and not by manually muting the song
81 | if (buzzed) {
82 | volumeController.muted = false;
83 | volumeController.adjustVolume();
84 | }
85 | });
86 |
87 | answerHandler = function (event) {
88 | // on enter key
89 | if (event.which === 13) {
90 | // check if the answer field is empty and check if the player has not buzzed before, so to not spam the chat with messages
91 | if ($(this).val() === "" && buzzed === false) {
92 | buzzed = true;
93 | buzzerTime = Date.now();
94 | volumeController.muted = true;
95 | volumeController.adjustVolume();
96 | showBuzzMessage(formatTime(buzzerTime - songStartTime));
97 | }
98 | }
99 | }
100 |
101 | quizReadyListener.bindListener();
102 | quizAnswerResultsListener.bindListener();
103 | quizPlayNextSongListener.bindListener();
104 |
105 | AMQ_addScriptData({
106 | name: "Buzzer",
107 | author: "TheJoseph98 & Anopob",
108 | version: version,
109 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqBuzzer.user.js",
110 | description: `
111 |
Adds a buzzer to AMQ, you activate it by pressing the Enter key in the empty answer field
112 |
When you buzz, your sound will be muted until the answer reveal and in chat you will receive a message stating how fast you buzzed since the start of the song
113 |
The timer starts when the guess phase begins (NOT when you get sound)
28 | `);
29 |
30 | $("#xpBarInner").addClass("notransition");*/
31 |
32 | // disable player answer listeners which updates the avatar pose
33 | quiz._playerAnswerListener.unbindListener();
34 |
35 | // disable the fade in on single roll rewards
36 | let ticketRollContainer = document.getElementById("swTicketRollInnerResultContainer");
37 | let rewardContainer = storeWindow.topBar.tickets.rollSelector.insideRewardContainer
38 | let config = {attributes: true, childList: false};
39 |
40 | ticketRollObserver = new MutationObserver(function (mutationList, observer) {
41 | for (let mutation of mutationList) {
42 | if (mutation.attributeName === "class" && !mutation.target.classList.contains("notActive")) {
43 | rewardContainer.$container.css("pointer-events", "");
44 | rewardContainer.animationDone();
45 | }
46 | }
47 | });
48 |
49 | ticketRollObserver.observe(ticketRollContainer, config);
50 | }
51 |
52 | // load the default pose
53 | QuizAvatarSlot.prototype.updatePose = function () {
54 | if (!this.displayed) {
55 | return;
56 | }
57 | if (this.$avatarImage.attr("src") === undefined) {
58 | let img = this.poseImages.BASE.image;
59 | this.$avatarImage.attr("srcset", img.srcset).attr("src", img.src);
60 | }
61 | }
62 |
63 | // don't load new poses
64 | Object.defineProperty(QuizAvatarSlot.prototype, "pose", {
65 | set: function pose(poseId) {
66 | this._pose = poseId;
67 | }
68 | });
69 |
70 | // load only base pose
71 | QuizAvatarSlot.prototype.loadPoses = function () {
72 | this.poseImages.BASE.load(
73 | function () {
74 | this.updatePose();
75 | }.bind(this)
76 | );
77 | }
78 |
79 | // disable avatar glow when they change groups
80 | Object.defineProperty(QuizPlayer.prototype, "groupNumber", {
81 | set: function groupNumber(newValue) {
82 | let value = parseInt(newValue);
83 | this._groupNumber = value;
84 | }
85 | });
86 |
87 | // disable note counter animations
88 | XpBar.prototype.setCredits = function (credits, noAnimation) {
89 | this.$creditText.text(credits);
90 | this.currentCreditCount = credits;
91 | }
92 |
93 | // disable XP bar animations and glow
94 | XpBar.prototype.xpGain = function (newXpP, newLevel) {
95 | this.setXpPercent(newXpP);
96 | this.setLevel(newLevel);
97 | }
98 |
99 | // disable ticket counter animations
100 | XpBar.prototype.setTickets = function(tickets, noAnimation) {
101 | this.$ticketText.text(tickets);
102 | this.currentTicketCount = tickets;
103 | };
104 |
105 | // disable recursive calling of runAnimation
106 | StoreRollAnimationController.prototype.runAnimation = function () {
107 | this.innerController.drawFrame(0);
108 | this.outerController.drawFrame(0);
109 | }
110 |
111 | // clear the canvas when the animation stops (this is normally handled by runAnimation when it runs every so often, but since it runs only once now it needs to be cleared manually)
112 | StoreRollAnimationController.prototype.stopAnimation = function () {
113 | this.running = false;
114 | this.clear = true;
115 | this.innerController.clearFrame();
116 | this.outerController.clearFrame();
117 | }
118 |
119 | setup();
120 |
121 | AMQ_addStyle(`
122 | /* disable all transitions and animations except sweetalert windows */
123 | :not(.swal2-hide) {
124 | transition: none !important;
125 | -moz-transition: none !important;
126 | -webkit-transition: none !important;
127 | animation: none !important;
128 | -moz-animation: none !important;
129 | -webkit-animation: none !important;
130 | }
131 | /* special case for sweetalert windows, can't be closed if they have no animations, so I just set them to 0s */
132 | .swal2-hide {
133 | -webkit-animation: hideSweetAlert 0s forwards;
134 | animation: hideSweetAlert 0s forwards
135 | }
136 | /* disable scoreboard correct answer glow which makes the scoreboard really laggy in ranked */
137 | .qpsPlayerScore.rightAnswer {
138 | text-shadow: none !important;
139 | }
140 | `)
--------------------------------------------------------------------------------
/amqRigTrackerLite.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Rig Tracker Lite
3 | // @namespace https://github.com/TheJoseph98
4 | // @version 1.2
5 | // @description Rig tracker for AMQ, writes rig to scoreboard next to players' scores
6 | // @author TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRigTrackerLite.user.js
11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRigTrackerLite.user.js
12 | // ==/UserScript==
13 |
14 | // Wait until the LOADING... screen is hidden and load script
15 | if (typeof Listener === "undefined") return;
16 | let loadInterval = setInterval(() => {
17 | if ($("#loadingScreen").hasClass("hidden")) {
18 | clearInterval(loadInterval);
19 | setup();
20 | }
21 | }, 500);
22 |
23 | const version = "1.2";
24 | let scoreboardReady = false;
25 | let playerDataReady = false;
26 | let playerData = {};
27 |
28 | // initialize scoreboard, set rig of all players to 0
29 | function initialiseScoreboard() {
30 | clearScoreboard();
31 | for (let entryId in quiz.scoreboard.playerEntries) {
32 | let tmp = quiz.scoreboard.playerEntries[entryId];
33 | let rig = $(`0`);
34 | tmp.$entry.find(".qpsPlayerName").before(rig);
35 | }
36 | scoreboardReady = true;
37 | }
38 |
39 | // initialize player data, set rig of all players to 0
40 | function initialisePlayerData() {
41 | clearPlayerData();
42 | for (let entryId in quiz.players) {
43 | playerData[entryId] = {
44 | rig: 0
45 | };
46 | }
47 | playerDataReady = true;
48 | }
49 |
50 | // Clears the rig counters from scoreboard
51 | function clearScoreboard() {
52 | $(".qpsPlayerRig").remove();
53 | scoreboardReady = false;
54 | }
55 |
56 | // Clears player data
57 | function clearPlayerData() {
58 | playerData = {};
59 | playerDataReady = false;
60 | }
61 |
62 | // Writes the current rig to scoreboard
63 | function writeRigToScoreboard() {
64 | if (playerDataReady) {
65 | for (let entryId in quiz.scoreboard.playerEntries) {
66 | let entry = quiz.scoreboard.playerEntries[entryId];
67 | let rigCounter = entry.$entry.find(".qpsPlayerRig");
68 | rigCounter.text(playerData[entryId].rig);
69 | }
70 | }
71 | }
72 |
73 | function setup() {
74 | // Initial setup on quiz start
75 | let quizReadyRigTracker = new Listener("quiz ready", (data) => {
76 | initialiseScoreboard();
77 | initialisePlayerData();
78 | });
79 |
80 | // stuff to do on answer reveal
81 | let answerResultsRigTracker = new Listener("answer results", (result) => {
82 | if (!playerDataReady) {
83 | initialisePlayerData();
84 | }
85 | if (!scoreboardReady) {
86 | initialiseScoreboard();
87 | if (playerDataReady) {
88 | writeRigToScoreboard();
89 | }
90 | }
91 | if (playerDataReady) {
92 | for (let player of result.players) {
93 | if (player.listStatus !== null && player.listStatus !== undefined && player.listStatus !== false && player.listStatus !== 0) {
94 | playerData[player.gamePlayerId].rig++;
95 | }
96 | }
97 | if (scoreboardReady) {
98 | writeRigToScoreboard();
99 | }
100 | }
101 | });
102 |
103 | // Reset data when joining a lobby
104 | let joinLobbyListener = new Listener("Join Game", (payload) => {
105 | clearPlayerData();
106 | clearScoreboard();
107 | });
108 |
109 | // Reset data when spectating a lobby
110 | let spectateLobbyListener = new Listener("Spectate Game", (payload) => {
111 | clearPlayerData();
112 | clearScoreboard();
113 | });
114 |
115 | // bind listeners
116 | quizReadyRigTracker.bindListener();
117 | answerResultsRigTracker.bindListener();
118 | joinLobbyListener.bindListener();
119 | spectateLobbyListener.bindListener();
120 |
121 | AMQ_addScriptData({
122 | name: "Rig Tracker Lite",
123 | author: "TheJoseph98",
124 | version: version,
125 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqRigTrackerLite.user.js",
126 | description: `
127 |
Counts how many times a certain player's list has appeared in a quiz and displays it next to each person's score
128 |
Rig is only counted if the player has enabled "Share Entries" in their AMQ list settings (noted by the blue ribbon in their answer field during answer reveal)
129 |
130 |
If you're looking for a version with customisable options including writing to chat for 1v1 games and which can be enabled or disabled at will, check out the original Rig Tracker
131 | `
132 | });
133 |
134 | AMQ_addStyle(`
135 | .qpsPlayerRig {
136 | padding-right: 5px;
137 | opacity: 0.3;
138 | }
139 | `);
140 | }
141 |
--------------------------------------------------------------------------------
/common/amqScriptInfo.js:
--------------------------------------------------------------------------------
1 | // Creates the installed scripts window if it doesn't exist and adds "Installed Userscripts" button to the main page and settings
2 | // This code is fetched automatically
3 | // Do not attempt to add it to tampermonkey
4 |
5 | function AMQ_createInstalledWindow() {
6 | if (!window.setupDocumentDone) return;
7 | if ($("#installedModal").length === 0) {
8 | $("#gameContainer").append($(`
9 |
10 |
11 |
12 |
13 |
16 |
Installed Userscripts
17 |
18 |
19 |
20 | You have the following scripts installed (click on each of them to learn more)
21 | This window can also be opened by going to AMQ settings (the gear icon on bottom right) and clicking "Installed Userscripts"
22 |
23 |
39 | `));
40 |
41 | AMQ_addStyle(`
42 | #installedListContainer h4 {
43 | font-weight: bold;
44 | cursor: pointer;
45 | }
46 | #installedListContainer h4 .name {
47 | margin-left: 8px;
48 | }
49 | #installedListContainer h4 .version {
50 | opacity: .5;
51 | margin-left: 8px;
52 | }
53 | #installedListContainer h4 .link {
54 | margin-left: 8px;
55 | }
56 | #installedListContainer .descriptionContainer {
57 | width: 95%;
58 | margin: auto;
59 | }
60 | #installedListContainer .descriptionContainer img {
61 | width: 80%;
62 | margin: 10px 10%;
63 | }
64 | `);
65 | }
66 | }
67 |
68 |
69 | /*
70 | Adds a new section to the installed scripts window containing the script info, such as name, author, version, link, and description (HTML enabled)
71 | Example metadata object
72 | metadataObj = {
73 | name: "AMQ Song List",
74 | author: "TheJoseph98",
75 | version: "1.0",
76 | link: "https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqScript.js",
77 | description: "Adds a song list to the game which can be accessed mid-quiz by clicking the list icon in the top right corner"
78 | }
79 | */
80 | function AMQ_addScriptData(metadata) {
81 | AMQ_createInstalledWindow();
82 | let $row = $(``)
83 | .append($("")
84 | .append($(``))
85 | .append($(``).text(metadata.name || "Unknown"))
86 | .append($(` by `))
87 | .append($(``).text(metadata.author || "Unknown"))
88 | .append($(``).text(metadata.version || ""))
89 | .append($(``).attr("href", metadata.link || "").text(metadata.link ? "link" : ""))
90 | .click(function () {
91 | let selector = $(this).next();
92 | if (selector.is(":visible")) {
93 | selector.slideUp();
94 | $(this).find(".fa-caret-down").addClass("fa-caret-right").removeClass("fa-caret-down");
95 | }
96 | else {
97 | selector.slideDown();
98 | $(this).find(".fa-caret-right").addClass("fa-caret-down").removeClass("fa-caret-right");
99 | }
100 | })
101 | )
102 | .append($("")
103 | .addClass("descriptionContainer")
104 | .html(metadata.description || "No description provided")
105 | .hide()
106 | );
107 | let $items = $("#installedListContainer .installedScriptItem");
108 | let title = `${metadata.name} by ${metadata.author} ${metadata.version}`;
109 | let index = 0;
110 | for (let item of $items) {
111 | if (title.localeCompare(item.firstChild.innerText) < 1) {
112 | break;
113 | }
114 | index++;
115 | }
116 | if (index === 0) {
117 | $("#installedListContainer").prepend($row);
118 | }
119 | else if (index === $items.length) {
120 | $("#installedListContainer").append($row);
121 | }
122 | else {
123 | $($items[index]).before($row);
124 | }
125 | }
126 |
127 | function AMQ_addStyle(css) {
128 | let style = document.createElement("style");
129 | style.type = "text/css";
130 | style.appendChild(document.createTextNode(css));
131 | document.head.appendChild(style);
132 | }
133 |
--------------------------------------------------------------------------------
/deprecated/amqSongList.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Song List
3 | // @namespace https://github.com/TheJoseph98
4 | // @version 1.3.2
5 | // @description Prints a copyable list to console at the end of each game
6 | // @author TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://raw.githubusercontent.com/TheJoseph98/AMQ-Scripts/master/common/amqScriptInfo.js
10 | // @updateURL https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongList.user.js
11 | // ==/UserScript==
12 |
13 | // Wait until the LOADING... screen is hidden and load script
14 | if (typeof Listener === "undefined") return;
15 | let loadInterval = setInterval(() => {
16 | if ($("#loadingScreen").hasClass("hidden")) {
17 | clearInterval(loadInterval);
18 | setup();
19 | }
20 | }, 500);
21 |
22 | let songs = [];
23 | let videoHosts = ["catbox", "openingsmoe"];
24 | let mp3Hosts = ["catbox"];
25 | let videoResolutions = [720, 480];
26 |
27 | function getVideoURL(URLMap) {
28 | for (let host of videoHosts) {
29 | if (URLMap[host] !== undefined) {
30 | for (let resolution of videoResolutions) {
31 | if (URLMap[host][resolution] !== undefined) {
32 | return URLMap[host][resolution];
33 | }
34 | }
35 | }
36 | }
37 | return null;
38 | }
39 |
40 | function getMP3URL(URLMap) {
41 | for (let host of mp3Hosts) {
42 | if (URLMap[host] !== undefined) {
43 | if (URLMap[host][0] !== undefined) {
44 | return URLMap[host][0];
45 | }
46 | }
47 | }
48 | return null;
49 | }
50 |
51 | function setup() {
52 | let oldWidth = $("#qpOptionContainer").width();
53 | $("#qpOptionContainer").width(oldWidth + 35);
54 | $("#qpOptionContainer > div").append($("")
55 | .attr("id", "qpCopyJSON")
56 | .attr("class", "clickAble qpOption")
57 | .html("")
58 | .click(() => {
59 | $("#copyBox").val(JSON.stringify(songs, null, 4)).select();
60 | document.execCommand("copy");
61 | $("#copyBox").val("").blur();
62 | })
63 | .popover({
64 | content: "Copy JSON to Clipboard",
65 | trigger: "hover",
66 | placement: "bottom"
67 | })
68 | );
69 |
70 | // clear list on quiz ready
71 | let quizReadyListener = new Listener("quiz ready", data => {
72 | songs = [];
73 | });
74 |
75 | let resultListener = new Listener("answer results", result => {
76 | let newSong = {
77 | songNumber: parseInt($("#qpCurrentSongCount").text()),
78 | animeEnglish: result.songInfo.animeNames.english,
79 | animeRomaji: result.songInfo.animeNames.romaji,
80 | annId: result.songInfo.annId,
81 | songName: result.songInfo.songName,
82 | artist: result.songInfo.artist,
83 | type: result.songInfo.type === 3 ? "Insert Song" : (result.songInfo.type === 2 ? "Ending " + result.songInfo.typeNumber : "Opening " + result.songInfo.typeNumber),
84 | correctCount: result.players.filter(player => player.correct === true).length,
85 | activePlayers: Object.values(quiz.players).filter(player => player.avatarSlot._disabled === false).length,
86 | startSample: quizVideoController.moePlayers[quizVideoController.currentMoePlayerId].startPoint,
87 | videoLength: parseFloat(quizVideoController.moePlayers[quizVideoController.currentMoePlayerId].$player.find("video")[0].duration.toFixed(2)),
88 | linkWebm: getVideoURL(result.songInfo.urlMap),
89 | linkMP3: getMP3URL(result.songInfo.urlMap)
90 | };
91 | //console.log(newSong);
92 | songs.push(newSong);
93 | });
94 |
95 | // print list to console on quiz end
96 | let quizEndListener = new Listener("quiz end result", result => {
97 | console.log(songs);
98 | });
99 |
100 | // clear list on quiz over (returning to lobby)
101 | let quizOverListener = new Listener("quiz over", roomSettings => {
102 | songs = [];
103 | });
104 |
105 | // clear list on joining lobby
106 | let quizJoinListener = new Listener("Join Game", payload => {
107 | if (!payload.error) {
108 | songs = [];
109 | }
110 | });
111 |
112 | // clear list on spectating lobby
113 | let quizSpectateListener = new Listener("Spectate Game", payload => {
114 | if (!payload.error) {
115 | songs = [];
116 | }
117 | });
118 |
119 | resultListener.bindListener();
120 | quizEndListener.bindListener();
121 | quizReadyListener.bindListener();
122 | quizOverListener.bindListener();
123 | quizJoinListener.bindListener();
124 | quizSpectateListener.bindListener();
125 |
126 | AMQ_addScriptData({
127 | name: "Song List",
128 | author: "TheJoseph98",
129 | description: `
130 |
Tracks the songs that played during the round and outputs them to your browser's console including song name, artist, anime, number of players, video URLs and more
131 |
Currently stored data can be copied to clipboard by clicking the clipboard icon in the top right while in a quiz
The list resets when the quiz ends (returning to back to lobby), when the quiz starts or when you leave the lobby
135 | `
136 | })
137 |
138 | AMQ_addStyle(`
139 | #qpCopyJSON {
140 | width: 30px;
141 | height: auto;
142 | margin-right: 5px;
143 | }
144 | #qpOptionContainer {
145 | z-index: 10;
146 | }
147 | `);
148 | }
149 |
--------------------------------------------------------------------------------
/localExportDownloader.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import json
4 | import subprocess
5 | from typing import List
6 | from pathlib import Path
7 | # Hacky workaround to fetch an external dependency
8 | try:
9 | import requests
10 | except ModuleNotFoundError:
11 | subprocess.Popen(["python", "-m", "pip", "install", "-U", 'requests']).wait()
12 | import requests
13 | try:
14 | import eyed3
15 | except ModuleNotFoundError:
16 | subprocess.Popen(["python", "-m", "pip", "install", "-U", 'eyed3']).wait()
17 | import eyed3
18 |
19 | """
20 | Function that loads an export.json and returns the songlist.
21 | Also selects the keys to be used, unless given.
22 |
23 | :param filepath: str: A filepath to find the export.json.
24 | It's expected to be given as an argument for the script.
25 | :param lang: str: The title language to extract and work with.
26 | :return: List: A List of dicts with only the info required for
27 | learning songs and becoming a massive booli.
28 | """
29 | def extract_info(filepath: str, lang: str='romaji') -> List:
30 | songs = []
31 | with open(file=filepath, mode='r', encoding='utf_8') as export:
32 | songlist = json.load(export)
33 | for song in songlist:
34 | name = song['name'].replace('/', '_').replace('\\', '_')
35 | artist = song['artist'].replace('/', '_').replace('\\', '_')
36 | anime = song['anime'][lang].replace('/', '_').replace('\\', '_') if song['anime'][lang] else song['anime'][song['anime'].keys()[0]].replace('/', '_').replace('\\', '_')
37 | stype = song['type']
38 | # If there is no mp3, we print some data and skip this song.
39 | if '0' not in song['urls']['catbox']:
40 | print(f"No mp3 available for {artist} - {name} ({anime} {stype}).\nThe available links for this are: {song['urls']}")
41 | continue
42 | mp3 = song['urls']['catbox']['0']
43 | s = {'title': name, 'artist': artist, 'anime': anime, 'type': stype, 'url': mp3}
44 | songs.append(s)
45 | return songs
46 |
47 | """
48 | Downloads a file to wherever it needs to go.
49 |
50 | :param url: str: URL where the file may be found
51 | :param filename: str: Filename (with or without path) to save to.
52 | :param force_replace: bool: Whether or not to replace existing files.
53 | Defaults to FALSE.
54 | :return: bool: True on success, raises something otherwise.
55 | Imagine handling errors.
56 | """
57 | def download(url: str, filename: str, force_replace: bool=False) -> bool:
58 | if(Path(filename).exists() and not force_replace):
59 | # We already have this, no need to download it again.
60 | if verbose:
61 | print(f"The song at '{url}' was already downloaded!")
62 | return
63 | stream = requests.get(url, stream=True)
64 | with open(filename, "wb") as file:
65 | for chunk in stream.iter_content(chunk_size=320):
66 | file.write(chunk)
67 | return True
68 |
69 | illegals = ['<', '>', ':', '"', '|', '?', '*']
70 | # Set some sane default values
71 | replace = False
72 | lang = 'romaji'
73 | path = './'
74 | infile = "./export.json"
75 | verbose = False
76 | # Use some butcher-style argument parsing. sys.argv[0] is this script, ignore that
77 | args = sys.argv[1:]
78 | if 'help' in args:
79 | print("Welcome to the localExportDownloader utility which downloads all files from your export.json, given they weren't downloaded already.")
80 | print("It seems your args included the keyword 'help', which summons this message and halts the program. If you believe this to be a bug, fix the source and move on.")
81 | print("Argument options are requested in the form of 'keyword=value'. The available keywords are:")
82 | print("\tverbose:\t\tWhether or not to print verbose output.\n\tpath:\t\tThe output path for downloading files\n\tlang:\t\tEither 'english' or 'romaji'. Anything else will crash.\n\treplace:\t\tWhether to force replacement of existing files, defaults to False.\n\tinfile: The most important one here! The export.json or whatever it is you named it.\n\t\t\t\tIf this is missing and the current directory doesn't have an export.json, shit WILL hit the fan!")
83 | print("Please do not add illegal chars to the path names, that way I can make sure the filenames are clean. Illegal chars get replaced with '_'")
84 | print("I'll make sure to strip off all single and double quotes for your convenience. I'll also make sure all paths end with a '/' in case you forget.")
85 | print("Please tell me you didn't name a file or folder 'help', or you'd be a big baka!")
86 | exit(0)
87 | for arg in args:
88 | kw,a = arg.split('=')
89 | # Split off any and all quotes, we don't need them here.
90 | kw = kw.replace('"', '').replace("'", "")
91 | a = a.replace('"', '').replace("'", "")
92 | path = a if kw == 'path' else path
93 | lang = a.lower() if kw == 'lang' else lang
94 | replace = True if kw == 'replace' and a.lower() == "true" else replace
95 | infile = a if kw == 'infile' else infile
96 | verbose = True if kw == "verbose" and a.lower() == "true" else False
97 | if not path.endswith('/'):
98 | path += "/"
99 | # Create the path if it's not there yet
100 | Path(path).mkdir(parents=True, exist_ok=True)
101 | for song in extract_info(filepath=infile, lang=lang):
102 | outfile = f"{path}{song['anime']}-{song['type']} - {song['artist']}-{song['title']}.mp3"
103 | for i in illegals:
104 | outfile = outfile.replace(i, "_")
105 | print(outfile)
106 | download(url=song['url'], filename=outfile, force_replace=replace)
107 | id3file = eyed3.load(outfile)
108 | if not id3file.tag:
109 | id3file.initTag()
110 | id3file.tag.clear()
111 | id3file.tag.artist = song['artist']
112 | id3file.tag.title = song['title']
113 | id3file.tag.comment = song['type']
114 | id3file.tag.album = song['anime']
115 | id3file.tag.save()
116 | if verbose:
117 | print(f"\t- Downloaded {song['anime']} {song['type']} and saved to {outfile}")
118 | print("That should be all of the songs unless some weren't uploaded as mp3. Cya!")
119 |
--------------------------------------------------------------------------------
/amqShortSampleRadio.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Short Sample Radio
3 | // @namespace SkayeScripts
4 | // @version 1.5
5 | // @description Loops through your entire list to not answer songs. Pushes difficulty for them down as fast as possible.
6 | // @author Riven Skaye || FokjeM & TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqShortSampleRadio.user.js
11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqShortSampleRadio.user.js
12 | // ==/UserScript==
13 | // Thanks a million for the help and some of the code bud!
14 |
15 | if (typeof Listener === "undefined") return;
16 |
17 | const version = "1.5";
18 | const SCRIPT_INFO = {
19 | name: "AMQ Short Sample Radio",
20 | author: "RivenSkaye",
21 | version: version,
22 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqShortSampleRadio.user.js",
23 | description: `
24 |
Plays all 5 second samples in your list between difficulty settings 10% - 100% until there is nothing left.
25 |
Adds a button in the top bar of lobbies to start the script. Stops when manually returning to lobby or if you've pushed your entire list to below 10% guess rate.
26 |
If your entire list is in 0% - 15% you are a BIG BOOLI and you should feel bad.
27 | `
28 | };
29 | AMQ_addScriptData(SCRIPT_INFO);
30 |
31 | // Create the button to add it later
32 | let ASSRButton = document.createElement("div");
33 | ASSRButton.id = "ASSR";
34 | ASSRButton.innerHTML = "
AMQ pls
"
35 | $(ASSRButton).addClass("clickAble topMenuButton topMenuBigButton");
36 | $(ASSRButton).css("right", "70.5%");
37 | $(ASSRButton).click(() => {
38 | if(!playMore){
39 | playMore = true;
40 | ASSR_START();
41 | } else {
42 | ASSR_STOP();
43 | }});
44 |
45 | /*
46 | * Function to start the game and prevent the AFK timeout
47 | */
48 | function startGame(){
49 | // Start the game
50 | $("#lbStartButton").click();
51 | }
52 |
53 | /*
54 | * Callback function for the MutationObserver on the lobby. Should make sure the script only runs when a lobby is entered.
55 | */
56 | function lobbyOpen(mutations, observer){
57 | mutations.forEach((mutation) => {
58 | mutation.oldValue == "text-center hidden" ? lobby.soloMode ? $("#lnSettingsButton").parent().append(ASSRButton) : null : $(ASSRButton).remove();
59 | });
60 | }
61 | // Create the observer for opening a lobby
62 | let lobbyObserver = new MutationObserver(lobbyOpen);
63 | // create and start the observer
64 | lobbyObserver.observe($("#lobbyPage")[0], {attributes: true, attributeOldValue: true, characterDataOldValue: true, attributeFilter: ["class"]});
65 |
66 | // Variables for listeners so we can unfuck the game
67 | let quizOver;
68 | let oldQuizOver;
69 | let noSongs;
70 | let quizNoSongs;
71 | let playMore = false;
72 |
73 | /*
74 | * Starts the actual script and locks you in 5sNDD settings
75 | */
76 | function ASSR_START(OPs=true, EDs=true, INS=true){
77 | if(!(lobby.inLobby && lobby.soloMode)){
78 | displayMessage("Error", "You must be in a solo lobby.\nIt is recommended that you use a guest account for the impact on your personal stats.", ASSR_STOP);
79 | return;
80 | }
81 |
82 | // Save old listeners
83 | oldQuizOver = quiz._quizOverListner;
84 | noSongs = quiz._noSongsListner;
85 |
86 | //Create and assign the new ones, kill the old ones
87 | quizOver = new Listener("quiz over", payload => {
88 | lobby.setupLobby(payload, quiz.isSpectator);
89 | viewChanger.changeView("lobby", {
90 | supressServerMsg: true,
91 | keepChatOpen: true
92 | });
93 | if (lobby.inLobby && lobby.soloMode) {
94 | playMore ? startGame() : null;
95 | }
96 | else {
97 | displayMessage("Error", "You must be in a solo lobby.\nIt is recommended that you use a guest account for the impact on your personal stats.", ASSR_STOP);
98 | stopCounting();
99 | }
100 | });
101 | quizOver.bindListener();
102 | oldQuizOver.unbindListener();
103 |
104 | quizNoSongs = new Listener("Quiz no songs", () => {
105 | displayMessage("Sasuga", "You must be a true boolli!\nNo remaining songs left in 10% - 100%.", "Whoa snap!", ASSR_STOP());
106 | });
107 | quizNoSongs.bindListener();
108 | noSongs.unbindListener();
109 |
110 | // Set to 20 songs to prevent AFK timeout, 5s per song, advanced difficulties: 10-100, only watched, dups on
111 | hostModal.numberOfSongsSliderCombo.setValue(20);
112 | hostModal.playLengthSliderCombo.setValue(5);
113 | hostModal.songDiffAdvancedSwitch.setOn(true);
114 | hostModal.songDiffRangeSliderCombo.setValue([10,100]);
115 | hostModal.songSelectionAdvancedController.setOn(false);
116 | hostModal.$songPool.slider("setValue", 3);
117 | $("#mhDuplicateShows").prop("checked", true);
118 |
119 | // Turn on Auto Skip for the replay phase. Leave the guess phase because we're not entering anything
120 | options.$AUTO_VOTE_REPLAY.prop("checked", true);
121 | options.updateAutoVoteSkipReplay();
122 |
123 | //Collect the song types and their status
124 | let openings = hostModal.$songTypeOpening;
125 | let endings = hostModal.$songTypeEnding;
126 | let inserts = hostModal.$songTypeInsert;
127 |
128 | //And turn them all on if required
129 | openings.is(":checked")? (OPs ? null : openings.click()) : (OPs ? openings.click() : null);
130 | endings.is(":checked") ? (EDs ? null : endings.click()) : (EDs ? endings.click() : null);
131 | inserts.is(":checked") ? (INS ? null : inserts.click()) : (INS ? inserts.click() : null);
132 |
133 | //Apply game settings
134 | lobby.changeGameSettings();
135 | playMore = true;
136 | startGame();
137 |
138 | // Add event to return to lobby button to stop
139 | $("#qpReturnToLobbyButton").on('click', (() => {ASSR_STOP(); quiz.startReturnLobbyVote();}));
140 | }
141 |
142 | /*
143 | * Function to stop the script, triggered by returning to lobby
144 | */
145 | function ASSR_STOP(){
146 | playMore = false;
147 | quizOver.unbindListener();
148 | oldQuizOver.bindListener();
149 | quizNoSongs.unbindListener();
150 | noSongs.bindListener();
151 | }
152 |
--------------------------------------------------------------------------------
/test/amqSoloChatBlocker.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Solo Chat Block
3 | // @namespace SkayeScripts
4 | // @version 0.9
5 | // @description Puts a nice image over the chat in solo and Ranked rooms, customizable. Improves overall performance.
6 | // @author Riven Skaye // FokjeM & TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/TheJoseph98/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @downloadURL https://github.com/TheJoseph98/AMQ-Scripts/raw/master/test/amqSoloChatBlocker.user.js
11 | // @updateURL https://github.com/TheJoseph98/AMQ-Scripts/raw/master/test/amqSoloChatBlocker.user.js
12 | // ==/UserScript==
13 |
14 | if (!window.setupDocumentDone) return;
15 |
16 | const version = "0.9";
17 | const SCRIPT_INFO = {
18 | name: "AMQ Solo Chat Block",
19 | author: "RivenSkaye & TheJoseph98",
20 | version: version,
21 | link: "https://github.com/TheJoseph98/AMQ-Scripts/raw/master/test/amqSoloChatBlocker.user.js",
22 | description: `
23 |
Hides the chat in Solo rooms, since it's useless anyway. Also allows for killing Ranked chat
24 |
This should hopefully be configurable, someday. For now, you can manually change stuff by setting new values on the SoloChatBlock and BlockRankedChat entries in localStorage.
25 | `
26 | };
27 | AMQ_addScriptData(SCRIPT_INFO);
28 |
29 | // Function to check if localStorage even exists here. If it doesn't, people are using a weird browser and we can't support them.
30 | function storageAvailable() {
31 | let storage;
32 | try {
33 | storage = window.localStorage;
34 | storage.setItem("Riven is amazing", "Hell yeah");
35 | storage.removeItem("Riven is amazing");
36 | return true;
37 | }
38 | catch(e) {
39 | return false;
40 | }
41 | }
42 |
43 | let hostGameListener = new Listener("Host Game", payload => {
44 | if (payload.soloMode) {
45 | changeChat();
46 | }
47 | else {
48 | restoreChat();
49 | }
50 | });
51 |
52 | let joinGameListener = new Listener("Join Game", payload => {
53 | if (payload.settings.gameMode === "Ranked" && localStorage.getItem("BlockRankedChat") === "true") {
54 | changeChat();
55 | }
56 | else {
57 | restoreChat();
58 | }
59 | });
60 |
61 | let spectateGameListener = new Listener("Spectate Game", payload => {
62 | if (payload.settings.gameMode === "Ranked" && localStorage.getItem("BlockRankedChat") === "true") {
63 | changeChat();
64 | }
65 | else {
66 | restoreChat();
67 | }
68 | });
69 |
70 | hostGameListener.bindListener();
71 | joinGameListener.bindListener();
72 | spectateGameListener.bindListener();
73 |
74 | //These are the defaults. Laevateinn should be bliss to everyone. JQuery CSS notation since we leverage Ege's resources.
75 | const gcC_css_default = {
76 | "backgroundImage": "url(https://i.imgur.com/9gdEjUf.jpg)",
77 | "backgroundRepeat": "no-repeat",
78 | "backgroundAttachment": "fixed",
79 | "backgroundPosition": "left top",
80 | "backgroundSize": "cover",
81 | "transform": "scale(1, 1)",
82 | "opacity": 1.0
83 | };
84 | let gcC_css;
85 | let old_gcC_css;
86 | let settings;
87 | let updateBlockLive = false;
88 | storageAvailable ? settings = window.localStorage : displayMessage("Browser Issue", "Your current browser or session does not support localStorage.\nGet a different browser or change applicable settings.", "Aye");
89 |
90 | /*
91 | * unbind chat listeners if chat is blocked.
92 | * Since the host/join/spectate listeners fire before gameChat.openView, it means that chat listeners are unbound (via changeChat function) and then bound again.
93 | * overloading gameChat.openView fixes that
94 | */
95 | let oldOpenView = GameChat.prototype.openView.bind(gameChat);
96 | GameChat.prototype.openView = function () {
97 | oldOpenView();
98 | if (updateBlockLive) {
99 | this._newMessageListner.unbindListener();
100 | this._newSpectatorListner.unbindListener();
101 | this._spectatorLeftListner.unbindListener();
102 | this._playerLeaveListner.unbindListener();
103 | this._spectatorChangeToPlayer.unbindListener();
104 | this._newQueueEntryListener.unbindListener();
105 | this._playerLeftQueueListener.unbindListener();
106 | this._hostPromotionListner.unbindListener();
107 | this._playerNameChangeListner.unbindListener();
108 | this._spectatorNameChangeListner.unbindListener();
109 | this._deletePlayerMessagesListener.unbindListener();
110 | this._deleteChatMessageListener.unbindListener();
111 | }
112 | }
113 |
114 | /*
115 | * Function that actually replaces the chatbox with an image.
116 | * Loads in the last saved settings, or the default if nothing was set.
117 | */
118 | function changeChat(){
119 | if(!settings){
120 | return;
121 | }
122 | updateBlockLive = true;
123 | old_gcC_css = $("#gcContent").css(["backgroundImage", "backgroundRepeat", "backgroundAttachment", "backgroundPosition", "backgroundSize", "transform", "opacity"]);
124 | // If it's not set yet, create the object in localStorage using the defaults. Hail persistence!
125 | !settings.getItem("SoloChatBlock") ? localStorage.setItem("SoloChatBlock", JSON.stringify(gcC_css_default)) : null;
126 | // Load in whatever the last saved ssettings were, or the defaults if we just set them
127 | gcC_css = JSON.parse(settings.getItem("SoloChatBlock"));
128 | // Apply the CSS and hide the chat
129 | $("#gcContent").css(gcC_css);
130 | $("#gcChatContent").css("display", "none");
131 | }
132 |
133 | /*
134 | * Internal function to update settings. Until stuff is finalized, this can be used to change them.
135 | * Saves the given settings to the localStorage object and applies changes to the variable gcC_css.
136 | * Until the script is finalized, call changeChat() to apply them.
137 | */
138 | function updateSettings(bg, repeat, attachment, bgpos, size, transform, opacity){
139 | gcC_css["backgroundImage"] = bg ? `url(${bg})` : gcC_css["background-image"];
140 | gcC_css["backgroundRepeat"] = repeat ? repeat : gcC_css["background-repeat"];
141 | gcC_css["backgroundAttachment"] = attachment ? attachment : gcC_css["background-attachment"];
142 | gcC_css["backgroundPosition"] = bgpos ? bgpos : gcC_css["background-position"];
143 | gcC_css["backgroundSize"] = size ? size : gcC_css["background-attachment"];
144 | gcC_css["transform"] = transform != null ? `scale(${transform ? -1 : 1})` : gcC_css["transform"];
145 | gcC_css["opacity"] = opacity != null ? opacity : gcC_css["opacity"];
146 | // Save the settings to localStorage
147 | localStorage.setItem("SoloChatBlock", JSON.stringify(gcC_css_default));
148 | }
149 |
150 | function restoreChat(){
151 | updateBlockLive = false;
152 | $("#gcContent").css(old_gcC_css);
153 | $("#gcChatContent").css("display", "block");
154 | }
155 |
--------------------------------------------------------------------------------
/amqRewardsTracker.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Rewards Tracker
3 | // @namespace https://github.com/TheJoseph98
4 | // @version 1.2
5 | // @description Tracks rewards gained per hour such as xp, notes and tickets
6 | // @author TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqWindows.js
11 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRewardsTracker.user.js
12 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRewardsTracker.user.js
13 | // ==/UserScript==
14 |
15 | // Wait until the LOADING... screen is hidden and load script
16 | if (typeof Listener === "undefined") return;
17 | let loadInterval = setInterval(() => {
18 | if ($("#loadingScreen").hasClass("hidden")) {
19 | clearInterval(loadInterval);
20 | setup();
21 | }
22 | }, 500);
23 |
24 | const version = "1.2";
25 | let startXP;
26 | let startNotes;
27 | let startTickets;
28 | let startTime;
29 | let gainedXP;
30 | let gainedNotes;
31 | let gainedTickets;
32 | let elapsedTime;
33 | let trackerWindow;
34 | let trackerPaused = false;
35 | let quizXPGainListener;
36 | let updateInterval = setInterval(function () {}, 333);
37 |
38 | function startTracker() {
39 | if (trackerPaused) {
40 | startTime = Date.now() - elapsedTime;
41 | }
42 | else {
43 | resetTracker();
44 | startTime = Date.now();
45 | }
46 | updateInterval = setInterval(updateTracker, 333);
47 | quizXPGainListener.bindListener();
48 | $("#trackerWindowControlsStart").off("click").text("Stop");
49 | $("#trackerWindowControlsStart").click(function () {
50 | stopTracker();
51 | });
52 | }
53 |
54 | function stopTracker() {
55 | trackerPaused = true;
56 | clearInterval(updateInterval);
57 | quizXPGainListener.unbindListener();
58 | $("#trackerWindowControlsStart").off("click").text("Start");
59 | $("#trackerWindowControlsStart").click(function () {
60 | startTracker();
61 | });
62 | }
63 |
64 | function updateTracker() {
65 | elapsedTime = Date.now() - startTime;
66 | let xpHour = Math.round((gainedXP / elapsedTime) * 3600000);
67 | let notesHour = Math.round((gainedNotes / elapsedTime) * 3600000);
68 | let ticketsHour = (gainedTickets / elapsedTime) * 3600000;
69 |
70 | $("#resultsElapsedTime").text(formatTime(elapsedTime));
71 | $("#resultsXPGained").text(gainedXP);
72 | $("#resultsNotesGained").text(gainedNotes);
73 | $("#resultsTicketsGained").text(gainedTickets);
74 | $("#resultsXPPerHour").text(xpHour);
75 | $("#resultsNotesPerHour").text(notesHour);
76 | $("#resultsTicketsPerHour").text(ticketsHour.toFixed(3));
77 | }
78 |
79 | function resetTracker() {
80 | startXP = 0;
81 | startNotes = xpBar.currentCreditCount;
82 | startTickets = xpBar.currentTicketCount;
83 | gainedXP = 0;
84 | gainedNotes = 0;
85 | gainedTickets = 0;
86 |
87 | $("#resultsElapsedTime").text(formatTime(0));
88 | $("#resultsXPGained").text(0);
89 | $("#resultsNotesGained").text(0);
90 | $("#resultsTicketsGained").text(0);
91 | $("#resultsXPPerHour").text(0);
92 | $("#resultsNotesPerHour").text(0);
93 | $("#resultsTicketsPerHour").text("0.000");
94 | stopTracker();
95 |
96 | trackerPaused = false;
97 | }
98 |
99 | // formats the time in milliseconds to hh:mm:ss format
100 | function formatTime(time) {
101 | let seconds = Math.floor(time / 1000);
102 | let hours = Math.floor(seconds / 3600);
103 | let minutes = Math.floor((seconds - hours * 3600) / 60);
104 | seconds = (seconds - minutes * 60 - hours * 3600);
105 |
106 | let formattedHours = hours < 10 ? "0" + hours : hours;
107 | let formattedMinutes = minutes < 10 ? "0" + minutes : minutes;
108 | let formattedSeconds = seconds < 10 ? "0" + seconds : seconds;
109 | return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
110 | }
111 |
112 | function setup() {
113 | trackerWindow = new AMQWindow({
114 | title: "Reward Tracker",
115 | width: 300,
116 | height: 270,
117 | zIndex: 1054,
118 | draggable: true
119 | });
120 |
121 | trackerWindow.addPanel({
122 | width: 1.0,
123 | height: 50,
124 | id: "trackerWindowControls"
125 | });
126 |
127 | trackerWindow.addPanel({
128 | width: 1.0,
129 | height: 135,
130 | position: {
131 | x: 0,
132 | y: 50
133 | },
134 | id: "trackerWindowResults"
135 | });
136 |
137 | trackerWindow.panels[0].panel.append(
138 | $(``)
139 | .append(
140 | $(``).click(function () {
141 | resetTracker();
142 | })
143 | )
144 | .append(
145 | $(``).click(function () {
146 | startTracker();
147 | })
148 | )
149 | );
150 |
151 | trackerWindow.panels[1].panel.append(
152 | $(``)
153 | .append(
154 | $(
155 | `
Adds a new window where you can start or stop a tracker which counts how much XP, notes and tickets you gained since starting and calculates approximate gains per hour.
214 |
The tracker can be opened by clicking the graph icon at the top right corner of the quiz screen.
Adds a new window which can be accessed by clicking the clock icon in the top right while in a quiz which tracks how fast you guessed each song, including total time, average time, fastest time and more
306 |
307 |
Timer start when the guess phase starts (not when you get sound) and ends on your latest Enter key input
308 |
An incorrect answer counts as the full guess time for the song, not submitting an answer with the Enter key (ie. using autosubmit) also counts as full guess time
`
64 | );
65 | }
66 |
67 | this.content.append(this.header);
68 | this.content.append(this.body);
69 | if (this.resizers !== null) {
70 | this.window.append(this.resizers);
71 | let tmp = this;
72 | let startWidth = 0;
73 | let startHeight = 0;
74 | let startX = 0;
75 | let startY = 0;
76 | let startMouseX = 0;
77 | let startMouseY = 0;
78 | this.resizers.find(".windowResizer").each(function (index, resizer) {
79 | $(resizer).mousedown(function (event) {
80 | tmp.window.css("user-select", "none");
81 | startWidth = tmp.window.width();
82 | startHeight = tmp.window.height();
83 | startX = tmp.window.position().left;
84 | startY = tmp.window.position().top;
85 | startMouseX = event.originalEvent.clientX;
86 | startMouseY = event.originalEvent.clientY;
87 | let curResizer = $(this);
88 | $(document.documentElement).mousemove(function (event) {
89 | if (curResizer.hasClass("bottom-right")) {
90 | let newWidth = startWidth + (event.originalEvent.clientX - startMouseX);
91 | let newHeight = startHeight + (event.originalEvent.clientY - startMouseY);
92 | if (newWidth > tmp.minWidth) {
93 | tmp.window.width(newWidth);
94 | }
95 | if (newHeight > tmp.minHeight) {
96 | tmp.body.height(newHeight - 103);
97 | tmp.window.height(newHeight);
98 | }
99 | }
100 | if (curResizer.hasClass("bottom-left")) {
101 | let newWidth = startWidth - (event.originalEvent.clientX - startMouseX);
102 | let newHeight = startHeight + (event.originalEvent.clientY - startMouseY);
103 | let newLeft = startX + (event.originalEvent.clientX - startMouseX);
104 | if (newWidth > tmp.minWidth) {
105 | tmp.window.width(newWidth);
106 | tmp.window.css("left", newLeft + "px");
107 | }
108 | if (newHeight > tmp.minHeight) {
109 | tmp.body.height(newHeight - 103);
110 | tmp.window.height(newHeight);
111 | }
112 | }
113 | if (curResizer.hasClass("top-right")) {
114 | let newWidth = startWidth + (event.originalEvent.clientX - startMouseX);
115 | let newHeight = startHeight - (event.originalEvent.clientY - startMouseY);
116 | let newTop = startY + (event.originalEvent.clientY - startMouseY);
117 | if (newWidth > tmp.minWidth) {
118 | tmp.window.width(newWidth);
119 | }
120 | if (newHeight > tmp.minHeight) {
121 | tmp.window.css("top", newTop + "px");
122 | tmp.body.height(newHeight - 103);
123 | tmp.window.height(newHeight);
124 | }
125 | }
126 | if (curResizer.hasClass("top-left")) {
127 | let newWidth = startWidth - (event.originalEvent.clientX - startMouseX);
128 | let newHeight = startHeight - (event.originalEvent.clientY - startMouseY);
129 | let newLeft = startX + (event.originalEvent.clientX - startMouseX);
130 | let newTop = startY + (event.originalEvent.clientY - startMouseY);
131 | if (newWidth > tmp.minWidth) {
132 | tmp.window.width(newWidth);
133 | tmp.window.css("left", newLeft + "px");
134 | }
135 | if (newHeight > tmp.minHeight) {
136 | tmp.window.css("top", newTop + "px");
137 | tmp.body.height(newHeight - 103);
138 | tmp.window.height(newHeight);
139 | }
140 | }
141 | });
142 | $(document.documentElement).mouseup(function (event) {
143 | $(document.documentElement).off("mousemove");
144 | $(document.documentElement).off("mouseup");
145 | tmp.window.css("user-select", "text");
146 | });
147 | });
148 | });
149 | }
150 | if (this.draggable === true) {
151 | this.window.draggable({
152 | handle: this.header,
153 | containment: "#gameContainer"
154 | });
155 | }
156 |
157 | this.window.append(this.content);
158 | $("#gameContainer").append(this.window);
159 | }
160 |
161 | setId(newId) {
162 | this.id = newId;
163 | this.window.attr("id", this.id);
164 | }
165 |
166 | addClass(newClass) {
167 | this.window.addClass(newClass);
168 | }
169 |
170 | removeClass(removedClass) {
171 | this.window.removeClass(removedClass);
172 | }
173 |
174 | setWidth(newWidth) {
175 | this.width = newWidth;
176 | this.window.width(this.width);
177 | }
178 |
179 | setTitle(newTitle) {
180 | this.title = newTitle;
181 | this.header.find(".modal-title").text(newTitle);
182 | }
183 |
184 | setZIndex(newZIndex) {
185 | this.zIndex = newZIndex;
186 | this.window.css("z-index", this.zIndex.toString());
187 | }
188 |
189 | isVisible() {
190 | return this.window.is(":visible");
191 | }
192 |
193 | clear() {
194 | this.body.children().remove();
195 | }
196 |
197 | open() {
198 | this.window.show();
199 | }
200 |
201 | open(handler) {
202 | this.window.show();
203 | if (handler !== undefined) {
204 | handler();
205 | }
206 | }
207 |
208 | close() {
209 | this.window.hide();
210 | }
211 |
212 | close(handler) {
213 | this.window.hide();
214 | if (handler !== undefined) {
215 | handler();
216 | }
217 | }
218 |
219 | addPanel(data) {
220 | let newPanel = new AMQWindowPanel(data);
221 | this.panels.push(newPanel);
222 | this.body.append(newPanel.panel);
223 | }
224 | }
225 |
226 | class AMQWindowPanel {
227 | constructor(data) {
228 | this.id = data.id === undefined ? "" : data.id;
229 | this.width = data.width === undefined ? 200 : data.width;
230 | this.height = data.height === undefined ? 300 : data.height;
231 | this.position = data.position === undefined ? {x: 0, y: 0} : data.position;
232 | this.scrollable = data.scrollable === undefined ? {x: false, y: false} : data.scrollable;
233 | this.panels = [];
234 |
235 | this.panel = $("")
236 | .addClass("customWindowPanel")
237 | .addClass(data.class === undefined ? "" : data.class)
238 | .attr("id", this.id)
239 | .css("position", "absolute")
240 |
241 | this.updateWidth();
242 | this.updateHeight();
243 | this.updatePosition();
244 | this.updateScrollable();
245 | }
246 |
247 | setId(newId) {
248 | this.id = newId;
249 | this.panel.attr("id", this.id);
250 | }
251 |
252 | addClass(newClass) {
253 | this.panel.addClass(newClass);
254 | }
255 |
256 | removeClass(removedClass) {
257 | this.panel.removeClass(removedClass);
258 | }
259 |
260 | setWidth(newWidth) {
261 | this.width = newWidth;
262 | this.updateWidth();
263 | }
264 |
265 | setHeight(newHeight) {
266 | this.height = newHeight;
267 | this.updateHeight();
268 | }
269 |
270 | updateWidth() {
271 | if (typeof this.width === "string") {
272 | this.panel.css("width", this.width);
273 | }
274 | else if (parseFloat(this.width) >= 0.0 && parseFloat(this.width) <= 1.0) {
275 | this.panel.css("width", (parseFloat(this.width) * 100) + "%");
276 | }
277 | else {
278 | this.panel.width(parseInt(this.width));
279 | }
280 | }
281 |
282 | updateHeight() {
283 | if (typeof this.height === "string") {
284 | this.panel.css("height", this.height);
285 | }
286 | else if (parseFloat(this.height) >= 0.0 && parseFloat(this.height) <= 1.0) {
287 | this.panel.css("height", (parseFloat(this.height) * 100) + "%");
288 | }
289 | else {
290 | this.panel.height(parseInt(this.height));
291 | }
292 | }
293 |
294 | setPositionX(newPositionX) {
295 | this.position.x = newPositionX;
296 | this.updatePosition();
297 | }
298 |
299 | setPositionY(newPositionY) {
300 | this.position.y = newPositionY;
301 | this.updatePosition();
302 | }
303 |
304 | setPosition(newPosition) {
305 | this.position.x = newPosition.x;
306 | this.position.y = newPosition.y;
307 | this.updatePosition();
308 | }
309 |
310 | updatePosition() {
311 | if (typeof this.position.x === "string") {
312 | this.panel.css("left", this.position.x);
313 | }
314 | else if (parseFloat(this.position.x) >= 0.0 && parseFloat(this.position.x) <= 1.0) {
315 | this.panel.css("left", (parseFloat(this.position.x) * 100) + "%");
316 | }
317 | else {
318 | this.panel.css("left", parseInt(this.position.x) + "px");
319 | }
320 |
321 | if (typeof this.position.y === "string") {
322 | this.panel.css("top", this.position.y);
323 | }
324 | else if (parseFloat(this.position.y) >= 0.0 && parseFloat(this.position.y) <= 1.0) {
325 | this.panel.css("top", (parseFloat(this.position.y) * 100) + "%");
326 | }
327 | else {
328 | this.panel.css("top", parseInt(this.position.y) + "px");
329 | }
330 | }
331 |
332 | setScrollableX(newScrollableX) {
333 | this.scrollable.x = newScrollableX;
334 | this.updateScrollable();
335 | }
336 |
337 | setScrollableY(newScrollableY) {
338 | this.scrollable.y = newScrollableY;
339 | this.updateScrollable();
340 | }
341 |
342 | updateScrollable() {
343 | this.panel.css("overflow-x", this.scrollable.x === true ? "auto" : "hidden");
344 | this.panel.css("overflow-y", this.scrollable.y === true ? "auto" : "hidden");
345 | }
346 |
347 | show() {
348 | this.panel.show();
349 | }
350 |
351 | show(handler) {
352 | this.show();
353 | handler();
354 | }
355 |
356 | hide() {
357 | this.panel.hide();
358 | }
359 |
360 | hide(handler) {
361 | this.hide();
362 | handler();
363 | }
364 |
365 | addPanel(data) {
366 | let newPanel = new AMQWindowPanel(data);
367 | this.panels.push(newPanel);
368 | this.panel.append(newPanel.panel);
369 | }
370 |
371 | clear() {
372 | this.panel.children().remove();
373 | }
374 | }
375 |
376 | function windowSetup() {
377 | if ($("#customWindowStyle").length) return;
378 | let style = document.createElement("style");
379 | style.type = "text/css";
380 | style.id = "customWindowStyle";
381 | style.appendChild(document.createTextNode(`
382 | .customWindow {
383 | overflow-y: hidden;
384 | top: 0px;
385 | left: 0px;
386 | margin: 0px;
387 | background-color: #424242;
388 | border: 1px solid rgba(27, 27, 27, 0.2);
389 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
390 | user-select: text;
391 | display: none;
392 | }
393 | .draggableWindow {
394 | cursor: move;
395 | }
396 | .customWindowBody {
397 | width: 100%;
398 | overflow-y: auto;
399 | }
400 | .customWindowContent {
401 | width: 100%;
402 | position: absolute;
403 | top: 0px;
404 | }
405 | .customWindow .close {
406 | font-size: 32px;
407 | }
408 | .windowResizers {
409 | width: 100%;
410 | height: 100%;
411 | }
412 | .windowResizer {
413 | width: 10px;
414 | height: 10px;
415 | position: absolute;
416 | z-index: 100;
417 | }
418 | .windowResizer.top-left {
419 | top: 0px;
420 | left: 0px;
421 | cursor: nwse-resize;
422 | }
423 | .windowResizer.top-right {
424 | top: 0px;
425 | right: 0px;
426 | cursor: nesw-resize;
427 | }
428 | .windowResizer.bottom-left {
429 | bottom: 0px;
430 | left: 0px;
431 | cursor: nesw-resize;
432 | }
433 | .windowResizer.bottom-right {
434 | bottom: 0px;
435 | right: 0px;
436 | cursor: nwse-resize;
437 | }
438 | `));
439 | document.head.appendChild(style);
440 | }
441 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AMQ-Scripts
2 |
3 | UPDATE 2021-02-13: I will no longer create or update AMQ scripts. If there are any bugs or new features you want added, fix/do it yourselves. I will merge open pull requests.
4 |
5 | ### Installation
6 |
7 | Requires Tampermonkey browser extension (Greasemonkey doesn't work).
8 |
9 | - Step 1) Select a script you want to install from the list below
10 | - Step 2) Tampermonkey should automatically prompt you to install the script, if it doesn't, create a new tampermonkey script manually and copy-paste the code
11 |
12 | Scripts:
13 | - [Song List UI](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongListUI.user.js)
14 | - ~[Song List](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongList.user.js)~ Deprecated, use Song List UI instead
15 | - [Rig Tracker](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRigTracker.user.js)
16 | - [Rig Tracker Lite](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRigTrackerLite.user.js)
17 | - ~[Team Randomizer](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqTeamRandomizer.user.js)~ Deprecated, teams are now an official game mode
18 | - [Dice Roller](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqDiceRoller.user.js)
19 | - [Dice Roller UI](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqDiceRollerUI.user.js)
20 | - [Speedrun](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSpeedrun.user.js)
21 | - [Chat Timestamps](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqChatTimestamps.user.js)
22 | - [Buzzer](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqBuzzer.user.js)
23 | - [Song Difficulty Counter](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongDifficultyCounter.user.js)
24 | - [Rewards Tracker](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRewardsTracker.user.js)
25 | - [Expand Library Search by ANN ID](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqExpandSearchANNID.user.js)
26 | - [Short Sample Radio](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqShortSampleRadio.user.js)
27 | - [Solo Chat Block](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSoloChatBlock.user.js)
28 | - [AMQ Room Browser Placement](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRoomBrowserFix.user.js)
29 |
30 |
31 | ### Note
32 |
33 | Scripts in the "test" folder are not complete and might be full of bugs, use at your own risk
34 |
35 | ## Script descriptions and usage information
36 |
37 | ### [Song List UI (amqSongListUI.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongListUI.user.js)
38 |
39 | Adds a window which lists all of the songs that play during a quiz in real time (during answer reveal phase). This window can be opened by pressing the "Song List" icon at the top right of the quiz screen (the same place where you change video resolution, volume and open the room settings) or by pressing the Pause/Break key on the keyboard
40 |
41 | Features:
42 | - Table which displays various information about the song that played such as:
43 | - Which song number it was
44 | - Name of the song
45 | - Artist of the song
46 | - Which anime it was from
47 | - Which song type the song is
48 | - What the ANN ID of the anime is
49 | - What your answer was for that song
50 | - How many people guessed the song
51 | - Which sample of the song played (the start point of the song)
52 | - Green or red background color for if you guessed the song correctly or incorrectly (or standard gray background if you are spectating)
53 | - Customizable settings:
54 | - Show or hide individual columns in the table
55 | - Change anime titles between English and Romaji
56 | - Auto Scroll: automatically scrolls to the end of the list when a new entry gets added during answer reveal phase
57 | - Auto Clear List: automatically clears the table when leaving a lobby, or when a new quiz starts
58 | - Show Correct: enable or disable the green and red background colors for your correct or incorrect guesses
59 | - These settings are saved to your browser's localStorage, if you use any 3rd party programs or tools that delete such data, your settings data might be deleted as well and will have to change settings every time you open AMQ
60 | - Extra song info window, which can be opened by clicking an entry in the table which in addition to information visible in the table itself also shows things such as:
61 | - Which players guessed the song, sorted alphabetically for standard, LMS and BR modes and sorted by fastest guess first in Quickdraw mode (along with their score)
62 | - Which lists the anime is from with watching status and score (Note: they must have "Share Entries" enabled in their AMQ list settings for this to work)
63 | - all video URLs that have been uploaded for that particular song (catbox, animethemes and openings.moe)
64 | - Other options:
65 | - Clear list: shown by the trash icon, manually clears the song list table (must be double clicked)
66 | - Open in New Tab: shown by the + icon, opens the list in a seperate tab in (mostly) full screen
67 | - Export: shown by a blank page icon, creates a downloadable file in JSON format which contains information of all the songs that played, including all information about guesses and which lists individual anime were from including watching status and score, this file can be imported to [AMQ Song List Viewer](https://thejoseph98.github.io/AMQ-Song-List-Viewer/) to view the state of the game at each individual song (Note: might take a few seconds for larger files to load)
68 | - Search input field: Search for specific songs in the list (Note: this searches *everything* even things like the guessed player percentage and sample point and also searches columns you have hidden in the settings)
69 |
70 | - The windows can be dragged and resized (resizing only works at the corners of the windows, you can't resize by clicking and dragging on the edges)
71 |
72 | Known bugs:
73 | - None
74 |
75 | ### ~[Song List (amqSongList.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongList.user.js)~
76 |
77 | ~Adds a button which copies the current song info in JSON format to the user's clipboard, this button can be found at the top right of the quiz~
78 |
79 | ~Features:~
80 | - ~Outputs each individual song info object to the browser's console~
81 | - ~Outputs the entire song list to the browser's console at the end of the game~
82 | - ~Copy JSON to clipboard button: copies the list in JSON format to the user's clipboard, which contains info such as:~
83 | - ~The number of the song~
84 | - ~Name of the song~
85 | - ~Artist of the song~
86 | - ~English and Romaji anime titles~
87 | - ~Song type~
88 | - ~ANN ID of the anime~
89 | - ~Number of players who guessed the song~
90 | - ~Number of total (active) players~
91 | - ~Start sample of the song~
92 | - ~Total length of the song~
93 | - ~URLs for both the webm and mp3 (Host priority: catbox > animethemes > openings.moe, resolution priority for webm: 720p > 480p)~
94 | - ~Example of the JSON output: https://pastebin.com/LmD7k1pW (Note: this data can *not* be used with the [AMQ Song List Viewer](https://thejoseph98.github.io/AMQ-Song-List-Viewer/))~
95 |
96 | Deprecated, use Song List UI instead
97 |
98 | ### [Rig Tracker (amqRigTracker.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRigTracker.user.js)
99 |
100 | Counts how many times each person's list has appeared during a quiz (Note: only counts if the player has "Share Entries" enabled in their AMQ list settings)
101 |
102 | Terminology:
103 | - Rig: the number of times a certain player's list has appeared in game
104 |
105 | Features:
106 | - Display each player's rig on the scoreboard next to each player's score
107 | - Send rig data to chat if there are exactly 2 players in the format `playerName1 x-y playerName2` this is commonly used for 1v1 tournament-style matches where in addition to keeping track player's scores, you might want to keep track of how many times certain lists have appeared
108 | - Customizable options which can be accessed by going to your AMQ settings and selecting "Rig Tracker" tab:
109 | - Rig Tracker: enable or disable the rig tracker (Default: Enabled)
110 | - Write Rig to Chat: Posts rig to chat in the format `playerName1 x-y playerName2` for 1v1 games (Default: Disabled)
111 | - Anime Name: include the name of the anime which played, you can select the English or Romaji title (Default: Enabled, Romaji)
112 | - Player Names: include the names of the players (Default: Enabled)
113 | - Score: include the player's scores in addition to their rig (default: Disabled)
114 | - Final Results: posts the final rig and score data when the quiz ends (default: Enabled)
115 | - Write Rig to Scoreboard: Writes each individual player's rig to the scoreboard to the right of their score (or to the right of their correct guesses for Quickdraw, LMS and BR modes)
116 | - Final Results Options: options for the final results when you have enabled "Final Results" in "Write Rig to Chat" option
117 | - On Quiz End: posts the results when the quiz ends normally
118 | - Player names: include the player's names when posting results
119 | - Score: include the player's scores when posting results
120 | - Rig: include the player's rig when posting results
121 | - On Returning to Lobby: posts the results when the quiz "ends" as a result of returning to lobby vote
122 | - Player names: include the player's names when posting results
123 | - Score: include the player's scores when posting results
124 | - Rig: include the player's rig when posting results
125 |
126 | Known bugs:
127 | - None
128 |
129 | ### [Rig Tracker Lite (amqRigTrackerLite.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRigTrackerLite.user.js)
130 |
131 | A less customizable version of amqRigTracker.user.js with only one feature.
132 |
133 | Terminology:
134 | - Rig: the number of times a certain player's list has appeared in game
135 |
136 | Features:
137 | - Displays each player's individual rig to the right of their score (or correct guesses in Quickdraw, LMS and BR modes) on the scoreboard, resets after each quiz (Note: rig is only counted if the player has "Share Entries" enabled in their AMQ list settings)
138 |
139 | Known bugs:
140 | - None
141 |
142 | ### ~[Team Randomizer (amqTeamRandomizer.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqTeamRandomizer.user.js)~
143 |
144 | ~Randomizes all players into teams of 2 and posts each team in chat. to use, type "/teams" in AMQ chat. Only works while in lobby (before the start of the quiz). Only randomizes the players (not spectators).~
145 |
146 | ~Known bugs:~
147 | - ~None~
148 |
149 | Deprecated, teams are now an official game mode
150 |
151 | ### [Dice Roller (amqDiceRoller.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqDiceRoller.user.js)
152 |
153 | Rolls a random number between 1 and a max value (inclusive). To use, type "/roll" and add a max value, for example "/roll 10". This will roll a random number between 1 and 10. Default max value is 100.
154 |
155 | Known bugs:
156 | - You can add a negative number as the argument (example: "/roll -5"), but it doesn't work
157 |
158 | ### [Dice Roller UI (amqDiceRollerUI.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqDiceRollerUI.user.js)
159 |
160 | Adds a user interface window that allows you to roll and edit custom dice rolls. To open this window, click the box icon (supposed to represent a dice) in the top right corner while in a quiz.
161 |
162 | Features:
163 | - Main dice window where you select a dice and roll a random value associated with that dice or edit that particular dice
164 | - Dice editor window where you can add new dice and manage values for each dice
165 | - The left side of the editor are your dice, you can add, remove or rename your dice
166 | - The right side of the editor are the values for your dice (they're like numbers 1-6 on a regular dice) and you can add or remove values (if you want certain values to have a higher chance of appearing, you can add the same value multiple times, for example, say you had a dice with 4 values: "Season 1", "Season 2", "Season 2", "Season 2", in this example, you will be 3 times more likely to roll "Season 2" as opposed to "Season 1")
167 |
168 | Known bugs:
169 | - None
170 |
171 | ### [Speedrun (amqSpeedrun.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSpeedrun.user.js)
172 |
173 | Adds a user interface window that shows information about fast you answer songs. To open this window, click the clock icon in the top right corner while in a quiz.
174 |
175 | Features:
176 | - Information on the speed of your guesses including:
177 | - Fastest guess
178 | - Slowest (but still correct) guess
179 | - Average time counting all answers
180 | - Average time counting only correct guesses
181 | - Total time
182 | - Correct guess ratio
183 | - Last song guess time
184 | - Individual song guess time breakdown
185 | - Time measured is your latest Enter key input, so if you want fast times, try not to spam enter key too much
186 | - Guessing a song incorrectly counts as full guess time for that song
187 | - Using the auto submit to submit the answer counts as full guess time for that song (but counts as a correct guess for the purposes of the slowest guess time)
188 | - All data is reset on the start of a new quiz
189 |
190 | Known bugs:
191 | - None
192 |
193 | ### [Chat Timestamps (amqChatTimestamps.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqChatTimestamps.user.js)
194 |
195 | Adds timestamps to messages (and system messages) in chat in HH:MM format, based on user's local system time
196 |
197 | Known bugs:
198 | - None
199 |
200 | ### [Buzzer (amqBuzzer.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqBuzzer.user.js)
201 |
202 | Adds a buzzer to AMQ, which mutes the current song and posts the time you buzzed in in the chat. To use, press the Enter key on an empty answer field (doesn't work if you already have something typed in)
203 |
204 | Known bugs:
205 | - None
206 |
207 | ### [Song Difficulty Counter (amqSongDifficultyCounter.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSongDifficultyCounter.user.js)
208 |
209 | A counting tool which counts how many songs there are on any given difficulty. Can be customized to count any difficulty range and any song type. To use, open a solo lobby and click the "Counter" button, next to the "Room settings" button. Usage of guest account strongly recommended so you don't inflate your Songs Played and Guess Rate as the tool simulates games and you need to hear at least 1 song before you can return to lobby.
210 |
211 | Terminology:
212 | - Difficulty: refers to the 1% song difficulty range between it and 1% less than it, for example: Difficulty 52 refers to 51-52% song difficulty, 30% is 29-30%, etc.
213 |
214 | Features:
215 | - Customizable difficulty ranges from 1-100 for all 3 song types
216 | - Option to send song difficulty data to a [public Spreadsheet](https://docs.google.com/spreadsheets/d/1mvwE_7CPN0jV5C76vHVX67ijo4VfhgIkkSxc5LOJLJE/edit?usp=sharing), which will automatically create the data table and graphs.
217 | - Option to update existing sheets, by inputting the same username as on the spreadsheet (NOTE: this is case-sensitive, for example: "thejoseph98" and "THEJOSEPH98" are NOT the same)
218 | - Option to automatically divide the difficulty into years if you find more than 100 songs
219 |
220 | Known bugs:
221 | - Sometimes, it skips if there is only 1 song in a given difficulty range, the cause of the bug is unknown
222 |
223 | ### [Rewards Tracker (amqRewardsTracker.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRewardsTracker.user.js)
224 |
225 | A tool that counts how much XP, notes and tickets you gained and calculates how much you gain of each per hour. To use it, click the line graph icon while in a quiz and click the "Start" button.
226 |
227 | Features:
228 | - Displays how much time has passed since the tracking started
229 | - Displays how much XP, notes and tickets you gained in that time
230 | - Displays how much XP, notes and tickets you gain per hour on average depending on the time passed and your gains in the time passed, updates every 1/3rd of a second
231 | - Option to pause the timer and resume it later
232 |
233 | Known bugs:
234 | - None
235 |
236 | ### [Expand Library Search by ANN ID (amqSearchExpandANNID.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqExpandSearchANNID.user.js)
237 |
238 | A simple script that allows you to search the Expand Library by ANN ID in addition to searching by anime, song or artist
239 |
240 | Known bugs:
241 | - None
242 |
243 | ### [Short Sample Radio (amqShortSampleRadio.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqShortSampleRadio.user.js)
244 |
245 | A simple script to help push your entire list down to 0-10% difficulty. Actually to 9% - 10% exactly as it only plays 10% - 100%. To use, click the nice button marked ASSR it adds. Only works with solo lobbies, errors out in public/multiplayer rooms.
246 |
247 | Features:
248 | - Repeatedly play 5s samples of all of your list matching the 10% - 100% difficulty setting
249 | - Stops only if the game ends unnaturally (disconnect, manual lobby vote, reload page, etc.)
250 | - Plays in batches of 20 songs to prevent AFK timeout
251 | - Warns you if no songs are left on your list
252 |
253 | Known bugs:
254 | - Shitty name, please propose something better
255 |
256 | ### [Solo Chat Block (amqSoloChatBlock.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqSoloChatBlock.user.js)
257 |
258 | A script that replaces the chat in Solo rooms with an image. It's completely useless anyways.
259 |
260 | Features:
261 | - Replace the chat in Solo rooms with an image
262 | - Optionally also block the chat in Ranked!
263 | - Customization options
264 | - Saves settings so changes remain over sessions
265 | - Customizable with a full menu
266 | - Previews of edits without having to save them
267 | - Comes with a pretty Laev pic by default, no configuration needed
268 |
269 | ### [AMQ Room Browser Placement (amqRoomBrowserFix.user.js)](https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqRoomBrowserFix.user.js)
270 |
271 | A script that moves the "View All Settings" button in the Roombrowser to the top of the displayed Room tiles. This fixes the rather annoying visual bug that causes some room tiles
272 | to become taller than others when the settings displayed at the bottom (guess time, song count, list type) become too wide. This most noticeably happens with custom settings that
273 | use variable guess times.
274 |
275 | Features:
276 | - Moves the icon to the top for all current and newly appearing Rooms in the Roombrowser
277 | - Keeps the icon clickable and functioning as it should
278 | - Prevents some rooms from growing taller on all screen sizes officially supported by AMQ
279 |
280 | Note: does not register in Joseph's "Installed Userscripts" to prevent adding that button if this is the only script from the repo.
281 |
--------------------------------------------------------------------------------
/amqRigTracker.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Rig Tracker
3 | // @namespace https://github.com/TheJoseph98
4 | // @version 1.6
5 | // @description Rig tracker for AMQ, supports writing rig to chat for AMQ League games and writing rig to the scoreboard for general use (supports infinitely many players and all modes), many customisable options available
6 | // @author TheJoseph98
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRigTracker.user.js
11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRigTracker.user.js
12 | // ==/UserScript==
13 |
14 | // Wait until the LOADING... screen is hidden and load script
15 | if (typeof Listener === "undefined") return;
16 | let loadInterval = setInterval(() => {
17 | if ($("#loadingScreen").hasClass("hidden")) {
18 | clearInterval(loadInterval);
19 | setup();
20 | }
21 | }, 500);
22 |
23 | const version = "1.6";
24 | let scoreboardReady = false;
25 | let playerDataReady = false;
26 | let returningToLobby = false;
27 | let missedFromOwnList = 0;
28 | let playerData = {};
29 |
30 | // listeners
31 | let quizReadyRigTracker;
32 | let answerResultsRigTracker;
33 | let quizEndRigTracker;
34 | let returnLobbyVoteListener;
35 | let joinLobbyListener;
36 | let spectateLobbyListener;
37 |
38 | // data for the checkboxes
39 | let settingsData = [
40 | {
41 | containerId: "smRigTrackerOptions",
42 | title: "Rig Tracker Options",
43 | data: [
44 | {
45 | label: "Track Rig",
46 | id: "smRigTracker",
47 | popover: "Enables or disabled the rig tracker",
48 | enables: ["smRigTrackerChat", "smRigTrackerScoreboard", "smRigTrackerMissedOwn"],
49 | offset: 0,
50 | default: true
51 | },
52 | {
53 | label: "Write rig to chat",
54 | id: "smRigTrackerChat",
55 | popover: "Writes the rig to chat. Used for AMQ League games, requires 2 players, automatically disabled if the requirement is not met",
56 | enables: ["smRigTrackerAnime", "smRigTrackerPlayerNames", "smRigTrackerScore", "smRigTrackerFinalResult"],
57 | offset: 1,
58 | default: false
59 | },
60 | {
61 | label: "Anime Name",
62 | id: "smRigTrackerAnime",
63 | popover: "Include the anime name when writing rig to chat",
64 | enables: ["smRigTrackerAnimeEnglish", "smRigTrackerAnimeRomaji"],
65 | offset: 2,
66 | default: true
67 | },
68 | {
69 | label: "English",
70 | id: "smRigTrackerAnimeEnglish",
71 | popover: "English anime names",
72 | unchecks: ["smRigTrackerAnimeRomaji"],
73 | offset: 3,
74 | default: false
75 | },
76 | {
77 | label: "Romaji",
78 | id: "smRigTrackerAnimeRomaji",
79 | popover: "Romaji anime names",
80 | unchecks: ["smRigTrackerAnimeEnglish"],
81 | offset: 3,
82 | default: true
83 | },
84 | {
85 | label: "Player Names",
86 | id: "smRigTrackerPlayerNames",
87 | popover: "Include the player names when writing rig to chat",
88 | offset: 2,
89 | default: true
90 | },
91 | {
92 | label: "Score",
93 | id: "smRigTrackerScore",
94 | popover: "Include the players' scores when writing rig to chat",
95 | offset: 2,
96 | default: false
97 | },
98 | {
99 | label: "Final results",
100 | id: "smRigTrackerFinalResult",
101 | popover: "Write the final results of the game",
102 | enables: ["smRigTrackerQuizEnd", "smRigTrackerLobby"],
103 | offset: 2,
104 | default: true
105 | },
106 | {
107 | label: "Write rig to scoreboard",
108 | id: "smRigTrackerScoreboard",
109 | popover: "Writes the rig to the scoreboards next to each person's score",
110 | offset: 1,
111 | default: true
112 | },
113 | {
114 | label: "Display missed from list",
115 | id: "smRigTrackerMissedOwn",
116 | popover: "Display the number of songs you missed from your own list in the chat at the end of the quiz",
117 | enables: ["smRigTrackerMissedAll"],
118 | offset: 1,
119 | default: true
120 | },
121 | {
122 | label: "Display missed from all lists",
123 | id: "smRigTrackerMissedAll",
124 | popover: "Display the number of songs all players missed from their own lists in the chat at the end of the quiz",
125 | offset: 2,
126 | default: false
127 | }
128 | ]
129 | },
130 | {
131 | containerId: "smRigTrackerFinalOptions",
132 | title: "Final Results Options",
133 | data: [
134 | {
135 | label: "On quiz end",
136 | id: "smRigTrackerQuizEnd",
137 | popover: "Write the final results at the end of the quiz",
138 | enables: ["smRigTrackerQuizEndNames", "smRigTrackerQuizEndScore", "smRigTrackerQuizEndRig"],
139 | offset: 0,
140 | default: true
141 | },
142 | {
143 | label: "Player Names",
144 | id: "smRigTrackerQuizEndNames",
145 | popover: "Include player names on final results when the quiz ends",
146 | offset: 1,
147 | default: true
148 | },
149 | {
150 | label: "Score",
151 | id: "smRigTrackerQuizEndScore",
152 | popover: "Include the final score on final result when the quiz ends",
153 | offset: 1,
154 | default: true
155 | },
156 | {
157 | label: "Rig",
158 | id: "smRigTrackerQuizEndRig",
159 | popover: "Include the final rig on final results when the quiz ends",
160 | offset: 1,
161 | default: true
162 | },
163 | {
164 | label: "On returning to lobby",
165 | id: "smRigTrackerLobby",
166 | popover: "Write the final results when returning to lobby",
167 | enables: ["smRigTrackerLobbyNames", "smRigTrackerLobbyScore", "smRigTrackerLobbyRig"],
168 | offset: 0,
169 | default: false
170 | },
171 | {
172 | label: "Player Names",
173 | id: "smRigTrackerLobbyNames",
174 | popover: "Include player names on final results when returning to lobby",
175 | offset: 1,
176 | default: false
177 | },
178 | {
179 | label: "Score",
180 | id: "smRigTrackerLobbyScore",
181 | popover: "Include the final score on final result when returning to lobby",
182 | offset: 1,
183 | default: false
184 | },
185 | {
186 | label: "Rig",
187 | id: "smRigTrackerLobbyRig",
188 | popover: "Include the final rig on final results when returning to lobby",
189 | offset: 1,
190 | default: false
191 | }
192 | ]
193 | }
194 | ];
195 |
196 | // Create the "Rig Tracker" tab in settings
197 | $("#settingModal .tabContainer")
198 | .append($("")
199 | .addClass("tab leftRightButtonTop clickAble")
200 | .attr("onClick", "options.selectTab('settingsCustomContainer', this)")
201 | .append($("")
202 | .text("Rig Tracker")
203 | )
204 | );
205 |
206 | // Create the body base
207 | $("#settingModal .modal-body")
208 | .append($("")
209 | .attr("id", "settingsCustomContainer")
210 | .addClass("settingContentContainer hide")
211 | .append($("")
212 | .addClass("row")
213 | )
214 | );
215 |
216 |
217 | // Create the checkboxes
218 | for (let setting of settingsData) {
219 | $("#settingsCustomContainer > .row")
220 | .append($("")
221 | .addClass("col-xs-6")
222 | .attr("id", setting.containerId)
223 | .append($("")
224 | .attr("style", "text-align: center")
225 | .append($("")
226 | .text(setting.title)
227 | )
228 | )
229 | );
230 | for (let data of setting.data) {
231 | $("#" + setting.containerId)
232 | .append($("")
233 | .addClass("customCheckboxContainer")
234 | .addClass(data.offset !== 0 ? "offset" + data.offset : "")
235 | .addClass(data.offset !== 0 ? "disabled" : "")
236 | .append($("")
237 | .addClass("customCheckbox")
238 | .append($("")
239 | .prop("checked", data.default !== undefined ? data.default : false)
240 | )
241 | .append($(""))
242 | )
243 | .append($("")
244 | .addClass("customCheckboxContainerLabel")
245 | .text(data.label)
246 | )
247 | );
248 | if (data.popover !== undefined) {
249 | $("#" + data.id).parent().parent().find("label:contains(" + data.label + ")")
250 | .attr("data-toggle", "popover")
251 | .attr("data-content", data.popover)
252 | .attr("data-trigger", "hover")
253 | .attr("data-html", "true")
254 | .attr("data-placement", "top")
255 | .attr("data-container", "#settingModal")
256 | }
257 | }
258 | }
259 |
260 | // Update the enabled and checked checkboxes
261 | for (let setting of settingsData) {
262 | for (let data of setting.data) {
263 | updateEnabled(data.id);
264 | $("#" + data.id).click(function () {
265 | updateEnabled(data.id);
266 | if (data.unchecks !== undefined) {
267 | data.unchecks.forEach((settingId) => {
268 | if ($(this).prop("checked")) {
269 | $("#" + settingId).prop("checked", false);
270 | }
271 | else {
272 | $(this).prop("checked", true);
273 | }
274 | })
275 | }
276 | });
277 | }
278 | }
279 |
280 | // Updates the enabled checkboxes, checks each node recursively
281 | function updateEnabled(settingId) {
282 | let current;
283 | settingsData.some((setting) => {
284 | current = setting.data.find((data) => {
285 | return data.id === settingId;
286 | });
287 | return current !== undefined;
288 | });
289 | if (current === undefined) {
290 | return;
291 | }
292 | if (current.enables === undefined) {
293 | return;
294 | }
295 | else {
296 | for (let enableId of current.enables) {
297 | if ($("#" + current.id).prop("checked") && !$("#" + current.id).parent().parent().hasClass("disabled")) {
298 | $("#" + enableId).parent().parent().removeClass("disabled");
299 | }
300 | else {
301 | $("#" + enableId).parent().parent().addClass("disabled");
302 | }
303 | updateEnabled(enableId);
304 | }
305 | }
306 | }
307 |
308 | // Creates the rig counters on the scoreboard and sets them to 0
309 | function initialiseScoreboard() {
310 | clearScoreboard();
311 | for (let entryId in quiz.scoreboard.playerEntries) {
312 | let tmp = quiz.scoreboard.playerEntries[entryId];
313 | let rig = $(`0`);
314 | tmp.$entry.find(".qpsPlayerName").before(rig);
315 | }
316 | scoreboardReady = true;
317 | }
318 |
319 | // Creates the player data for counting rig (and score)
320 | function initialisePlayerData() {
321 | clearPlayerData();
322 | for (let entryId in quiz.players) {
323 | playerData[entryId] = {
324 | rig: 0,
325 | score: 0,
326 | missedList: 0,
327 | name: quiz.players[entryId]._name
328 | };
329 | }
330 | playerDataReady = true;
331 | }
332 |
333 | // Clears the rig counters from scoreboard
334 | function clearScoreboard() {
335 | $(".qpsPlayerRig").remove();
336 | scoreboardReady = false;
337 | }
338 |
339 | // Clears player data
340 | function clearPlayerData() {
341 | playerData = {};
342 | playerDataReady = false;
343 | missedFromOwnList = 0;
344 | }
345 |
346 | // Writes the current rig to scoreboard
347 | function writeRigToScoreboard() {
348 | if (playerDataReady) {
349 | for (let entryId in quiz.scoreboard.playerEntries) {
350 | let entry = quiz.scoreboard.playerEntries[entryId];
351 | let rigCounter = entry.$entry.find(".qpsPlayerRig");
352 | rigCounter.text(playerData[entryId].rig);
353 | }
354 | }
355 | }
356 |
357 | // Writes the rig to chat (for 2 players, automatically disables if there's more or less than 2 players)
358 | function writeRigToChat(animeTitle) {
359 | let tmpData = [];
360 | let message = "";
361 | if (Object.keys(playerData).length !== 2) {
362 | gameChat.systemMessage("Writing rig to chat requires exactly 2 players, writing rig to chat has been disabled");
363 | $("#smRigTrackerChat").prop("checked", false);
364 | updateEnabled("smRigTrackerChat");
365 | }
366 | else {
367 | for (let key of Object.keys(playerData)) {
368 | tmpData.push(playerData[key]);
369 | }
370 | if ($("#smRigTrackerPlayerNames").prop("checked")) {
371 | message += tmpData[0].name + " " + tmpData[0].rig + "-" + tmpData[1].rig + " " + tmpData[1].name;
372 | }
373 | else {
374 | message += tmpData[0].rig + "-" + tmpData[1].rig;
375 | }
376 | if ($("#smRigTrackerScore").prop("checked")) {
377 | message += ", Score: " + tmpData[0].score + "-" + tmpData[1].score;
378 | }
379 | if ($("#smRigTrackerAnime").prop("checked")) {
380 | if ($("#smRigTrackerAnimeRomaji").prop("checked")) {
381 | message += " (" + animeTitle.romaji + ")";
382 | }
383 | else if ($("#smRigTrackerAnimeEnglish").prop("checked")){
384 | message += " (" + animeTitle.english + ")";
385 | }
386 | else {
387 | message += " (" + animeTitle.romaji + ")";
388 | }
389 | }
390 | }
391 | let oldMessage = gameChat.$chatInputField.val();
392 | gameChat.$chatInputField.val(message);
393 | gameChat.sendMessage();
394 | gameChat.$chatInputField.val(oldMessage);
395 | }
396 |
397 | // Write the final result at the end of the game
398 | function writeResultsToChat() {
399 | let tmpData = [];
400 | for (let key of Object.keys(playerData)) {
401 | tmpData.push(playerData[key]);
402 | }
403 | let oldMessage = gameChat.$chatInputField.val();
404 | gameChat.$chatInputField.val("========FINAL RESULT========");
405 | gameChat.sendMessage();
406 | if (!returningToLobby) {
407 | if ($("#smRigTrackerQuizEndScore").prop("checked")) {
408 | if ($("#smRigTrackerQuizEndNames").prop("checked")) {
409 | gameChat.$chatInputField.val("Score: " + tmpData[0].name + " " + tmpData[0].score + "-" + tmpData[1].score + " " + tmpData[1].name);
410 | gameChat.sendMessage();
411 | }
412 | else {
413 | gameChat.$chatInputField.val("Score: " + tmpData[0].score + "-" + tmpData[1].score);
414 | gameChat.sendMessage();
415 | }
416 | }
417 | if ($("#smRigTrackerQuizEndRig").prop("checked")) {
418 | if ($("#smRigTrackerQuizEndNames").prop("checked")) {
419 | gameChat.$chatInputField.val("Rig: " + tmpData[0].name + " " + tmpData[0].rig + "-" + tmpData[1].rig + " " + tmpData[1].name);
420 | gameChat.sendMessage();
421 | }
422 | else {
423 | gameChat.$chatInputField.val("Rig: " + tmpData[0].rig + "-" + tmpData[1].rig);
424 | gameChat.sendMessage();
425 | }
426 | }
427 | }
428 | else {
429 | if ($("#smRigTrackerLobbyScore").prop("checked")) {
430 | if ($("#smRigTrackerLobbyNames").prop("checked")) {
431 | gameChat.$chatInputField.val("Score: " + tmpData[0].name + " " + tmpData[0].score + "-" + tmpData[1].score + " " + tmpData[1].name);
432 | gameChat.sendMessage();
433 | }
434 | else {
435 | gameChat.$chatInputField.val("Score: " + tmpData[0].score + "-" + tmpData[1].score);
436 | gameChat.sendMessage();
437 | }
438 | }
439 | if ($("#smRigTrackerLobbyRig").prop("checked")) {
440 | if ($("#smRigTrackerLobbyNames").prop("checked")) {
441 | gameChat.$chatInputField.val("Rig: " + tmpData[0].name + " " + tmpData[0].rig + "-" + tmpData[1].rig + " " + tmpData[1].name);
442 | gameChat.sendMessage();
443 | }
444 | else {
445 | gameChat.$chatInputField.val("Rig: " + tmpData[0].rig + "-" + tmpData[1].rig);
446 | gameChat.sendMessage();
447 | }
448 | }
449 | }
450 |
451 | gameChat.$chatInputField.val(oldMessage);
452 | }
453 |
454 | function displayMissedList() {
455 | let inQuiz = Object.values(quiz.players).some(player => player.isSelf === true);
456 | if ($("#smRigTrackerMissedOwn").prop("checked") && !$("#smRigTrackerMissedAll").prop("checked") && inQuiz && quiz.gameMode !== "Ranked") {
457 | if (missedFromOwnList === 0){
458 | gameChat.systemMessage(`No misses. GG`);
459 | // Just change anything on the message, it's your game after all.
460 | // If you want the classic "You missed 0 songs" message, either edit the message or remove the if-else statement
461 | }
462 | else{
463 | gameChat.systemMessage(`You missed ${missedFromOwnList === 1 ? missedFromOwnList + " song" : missedFromOwnList + " songs"} from your own list`);
464 | // Quick guide: If you only missed one, customize the " song" and if it's two or more, customize " songs". You can re-arrange the orders though.
465 | }
466 | }
467 | if ($("#smRigTrackerMissedAll").prop("checked") && $("#smRigTrackerMissedOwn").prop("checked") && quiz.gameMode !== "Ranked") {
468 | for (let id in playerData) {
469 | gameChat.systemMessage(`${playerData[id].name} missed ${playerData[id].missedList === 1 ? playerData[id].missedList + " song" : playerData[id].missedList + " songs"} from their own list`);
470 | }
471 | }
472 | }
473 |
474 | function setup() {
475 | // Updates the preset settings tabs and container, this is mostly to allow interaction with the newly added "Custom" tab
476 | options.$SETTING_TABS = $("#settingModal .tab");
477 | options.$SETTING_CONTAINERS = $(".settingContentContainer");
478 |
479 | // Initial setup on quiz start
480 | quizReadyRigTracker = new Listener("quiz ready", (data) => {
481 | returningToLobby = false;
482 | clearPlayerData();
483 | clearScoreboard();
484 | if ($("#smRigTracker").prop("checked") && quiz.gameMode !== "Ranked") {
485 | answerResultsRigTracker.bindListener();
486 | quizEndRigTracker.bindListener();
487 | returnLobbyVoteListener.bindListener();
488 | if ($("#smRigTrackerScoreboard").prop("checked")) {
489 | initialiseScoreboard();
490 | }
491 | initialisePlayerData();
492 | }
493 | else {
494 | answerResultsRigTracker.unbindListener();
495 | quizEndRigTracker.unbindListener();
496 | returnLobbyVoteListener.unbindListener();
497 | }
498 | });
499 |
500 | // stuff to do on answer reveal
501 | answerResultsRigTracker = new Listener("answer results", (result) => {
502 | if (quiz.gameMode === "Ranked") {
503 | return;
504 | }
505 | if (!playerDataReady) {
506 | initialisePlayerData();
507 | }
508 | if (!scoreboardReady && $("#smRigTrackerScoreboard").prop("checked")) {
509 | initialiseScoreboard();
510 | if (playerDataReady) {
511 | writeRigToScoreboard();
512 | }
513 | }
514 | if (playerDataReady) {
515 | for (let player of result.players) {
516 | if (player.listStatus !== null && player.listStatus !== undefined && player.listStatus !== false && player.listStatus !== 0) {
517 | playerData[player.gamePlayerId].rig++;
518 | if (player.correct === false) {
519 | playerData[player.gamePlayerId].missedList++;
520 | }
521 | if (player.correct === false && quiz.players[player.gamePlayerId]._name === selfName) {
522 | missedFromOwnList++;
523 | }
524 | }
525 | if (player.correct === true) {
526 | playerData[player.gamePlayerId].score++;
527 | }
528 | }
529 | if ($("#smRigTrackerChat").prop("checked") && !returningToLobby) {
530 | writeRigToChat(result.songInfo.animeNames);
531 | }
532 | if (scoreboardReady) {
533 | writeRigToScoreboard();
534 | }
535 | }
536 | });
537 |
538 | // stuff to do on quiz end
539 | quizEndRigTracker = new Listener("quiz end result", (result) => {
540 | if ($("#smRigTrackerChat").prop("checked") && $("#smRigTrackerFinalResult").prop("checked") && $("#smRigTrackerQuizEnd").prop("checked")) {
541 | writeResultsToChat();
542 | }
543 | displayMissedList();
544 | });
545 |
546 | // stuff to do on returning to lobby
547 | returnLobbyVoteListener = new Listener("return lobby vote result", (payload) => {
548 | if (payload.passed) {
549 | returningToLobby = true;
550 | if ($("#smRigTrackerChat").prop("checked") && $("#smRigTrackerFinalResult").prop("checked") && $("#smRigTrackerLobby").prop("checked")) {
551 | writeResultsToChat();
552 | }
553 | //displayMissedList();
554 | }
555 | });
556 |
557 | // Reset data when joining a lobby
558 | joinLobbyListener = new Listener("Join Game", (payload) => initialiseGame(payload));
559 |
560 | // Reset data when spectating a lobby
561 | spectateLobbyListener = new Listener("Spectate Game", (payload) => initialiseGame(payload));
562 |
563 | function initialiseGame(payload){
564 | if (payload.error) {
565 | return;
566 | }
567 | clearPlayerData();
568 | clearScoreboard();
569 | if ($("#smRigTracker").prop("checked") && payload.settings.gameMode !== "Ranked") {
570 | answerResultsRigTracker.bindListener();
571 | quizEndRigTracker.bindListener();
572 | returnLobbyVoteListener.bindListener();
573 | initialisePlayers(payload.quizState.players);
574 | window.setTimeout(() => initialiseRig(payload.quizState.songHistory), 0);
575 | //forgive me lord, for I have sinned
576 | //add timeout because this actually ends up happening before the room is ready
577 | }
578 | else {
579 | answerResultsRigTracker.unbindListener();
580 | quizEndRigTracker.unbindListener();
581 | returnLobbyVoteListener.unbindListener();
582 | }
583 | }
584 |
585 | function initialisePlayers(players){
586 | for(let id in players){
587 | let gamePlayerId = players[id].gamePlayerId;
588 | playerData[gamePlayerId] = {
589 | rig: 0,
590 | score: 0,
591 | missedList: 0,
592 | name: players[id].name
593 | }
594 | }
595 | playerDataReady = true;
596 | }
597 | function initialiseRig(songHistory){
598 | initialiseScoreboard();
599 | songHistory.forEach(outer => {
600 | let song = outer.historyInfo
601 | let playersWithSongOnList = song.listStates
602 | .map(state => state.name)
603 | .map(name => getPlayerByName(name))
604 | .filter(player => undefined !== player);
605 |
606 | let correctGuessPlayers = song.correctGuessPlayers
607 | .map(name => getPlayerByName(name))
608 | .filter(player => undefined !== player);
609 |
610 | let playersWhoMissedRig = playersWithSongOnList.filter(player =>
611 | !correctGuessPlayers
612 | .find(player2 => player2.name === player.name));
613 |
614 | playersWithSongOnList.forEach(player => player.rig++);
615 | correctGuessPlayers.forEach(player => player.score++);
616 | playersWhoMissedRig.forEach(player => player.missedList++);
617 | });
618 | writeRigToScoreboard();
619 | scoreboardReady = true;
620 | }
621 |
622 | function getPlayerByName(name){
623 | return Object.values(playerData).find(player => player.name === name)
624 | }
625 |
626 | // Enable or disable rig tracking on checking or unchecking the rig tracker checkbox
627 | $("#smRigTracker").click(function () {
628 | let rigTrackerEnabled = $(this).prop("checked");
629 | if (!rigTrackerEnabled) {
630 | quizReadyRigTracker.unbindListener();
631 | answerResultsRigTracker.unbindListener();
632 | quizEndRigTracker.unbindListener();
633 | returnLobbyVoteListener.unbindListener();
634 | clearScoreboard();
635 | }
636 | else {
637 | quizReadyRigTracker.bindListener();
638 | answerResultsRigTracker.bindListener();
639 | quizEndRigTracker.bindListener();
640 | returnLobbyVoteListener.bindListener();
641 | if ($("#smRigTrackerScoreboard").prop("checked")) {
642 | initialiseScoreboard();
643 | writeRigToScoreboard();
644 | }
645 | }
646 | });
647 |
648 | // Enable or disable rig display on the scoreboard on checking or unchecking the scoreboard checkbox
649 | $("#smRigTrackerScoreboard").click(function () {
650 | let rigTrackerScoreboardEnabled = $(this).prop("checked");
651 | if (rigTrackerScoreboardEnabled) {
652 | initialiseScoreboard();
653 | writeRigToScoreboard();
654 | }
655 | else {
656 | clearScoreboard();
657 | }
658 | });
659 |
660 | // bind listeners
661 | quizReadyRigTracker.bindListener();
662 | answerResultsRigTracker.bindListener();
663 | quizEndRigTracker.bindListener();
664 | returnLobbyVoteListener.bindListener();
665 | joinLobbyListener.bindListener();
666 | spectateLobbyListener.bindListener();
667 |
668 | AMQ_addScriptData({
669 | name: "Rig Tracker",
670 | author: "TheJoseph98",
671 | version: version,
672 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqRigTracker.user.js",
673 | description: `
674 |
Rig tracker for AMQ counts how many times a certain player's list has appeared in a quiz, mainly created for AMQ League games to reduce the need for dedicated players who track the rig
675 |
Rig is only counted if the player has enabled "Share Entries" in their AMQ list settings (noted by the blue ribbon in their answer field during answer reveal)
676 |
Rig tracker has multiple options available which can be accessed by opening AMQ settings and selecting the "Rig Tracker" tab
677 |
678 |
Rig tracker also has an option of writing rig to the scoreboard next to players' scores for non-league and more than 2 players games
679 |
680 |
If you're looking for a smaller version without these options and which can only write rig to scoreboard, check out Rig Tracker Lite
681 | `
682 | });
683 |
684 | // CSS stuff
685 | AMQ_addStyle(`
686 | .qpsPlayerRig {
687 | padding-right: 5px;
688 | opacity: 0.3;
689 | }
690 | .customCheckboxContainer {
691 | display: flex;
692 | }
693 | .customCheckboxContainer > div {
694 | display: inline-block;
695 | margin: 5px 0px;
696 | }
697 | .customCheckboxContainer > .customCheckboxContainerLabel {
698 | margin-left: 5px;
699 | margin-top: 5px;
700 | font-weight: normal;
701 | }
702 | .offset1 {
703 | margin-left: 20px;
704 | }
705 | .offset2 {
706 | margin-left: 40px;
707 | }
708 | .offset3 {
709 | margin-left: 60px;
710 | }
711 | .offset4 {
712 | margin-left: 80px;
713 | }
714 | `);
715 | }
716 |
--------------------------------------------------------------------------------
/amqSoloChatBlock.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AMQ Solo Chat Block
3 | // @namespace SkayeScripts
4 | // @version 1.5
5 | // @description Puts a nice image over the chat in solo and Ranked rooms, customizable. Improves overall performance in Ranked.
6 | // @author Riven Skaye || FokjeM
7 | // @match https://animemusicquiz.com/*
8 | // @grant none
9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js
10 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqSoloChatBlock.user.js
11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqSoloChatBlock.user.js
12 | // ==/UserScript==
13 |
14 | // Make sure not to run on before the page is loaded
15 | if (!window.setupDocumentDone) return;
16 |
17 | // Don't do anything on the sign-in page
18 | if (typeof Listener === "undefined") return;
19 |
20 | /*** Common code to many scripts ***/
21 | //Register to Joseph's list
22 | const version = "1.5";
23 | const SCRIPT_INFO = {
24 | name: "AMQ Solo Chat Block",
25 | author: "RivenSkaye",
26 | version: version,
27 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqSoloChatBlock.user.js",
28 | description: `
29 |
Hides the chat in Solo rooms, since it's useless anyway. Also allows for killing Ranked chat
30 |
This should hopefully be configurable, someday. For now, you can manually change stuff by setting new values on the SoloChatBlock and BlockRankedChat entries in localStorage.
31 | `
32 | };
33 | AMQ_addScriptData(SCRIPT_INFO);
34 |
35 | /*** Setup for this script ***/
36 | // Added bugfix
37 | let lobbyBypass = true;
38 | /*
39 | * Callback function for the MutationObserver on the lobby.
40 | * This observer makes sure the script only runs when a lobby is entered without using eventspace.
41 | * After all, the main target of this script is to reduce the amount of eventhandlers
42 | * in places where they are either useless or causing problems.
43 | */
44 | function lobbyOpen(mutations, observer){
45 | mutations.forEach((mutation) => {
46 | mutation.oldValue == "text-center hidden" ? setTimeout(function(){lobbyBypass = false; changeChat();}, 50) : null;
47 | });
48 | }
49 | function rankedOpen(mutations, observer){
50 | mutations.forEach((mutation) => {
51 | (mutation.oldValue == "text-center hidden" && quiz.gameMode === "Ranked") ? setTimeout(changeChat, 50) : null;
52 | })
53 | }
54 | // Create the observer for opening a lobby and start it. Listen for class changes on the object, since those signal hiding/showing it
55 | let lobbyObserver = new MutationObserver(lobbyOpen);
56 | lobbyObserver.observe($("#lobbyPage")[0], {attributes: true, attributeOldValue: true, characterDataOldValue: true, attributeFilter: ["class"]});
57 | // Fix for room hopping by invite. Requires using an event because there is no detectable difference within any existing objects.
58 | let switchGameListener = new Listener("Spectate Game", () => {
59 | setTimeout(restoreChat, 100);
60 | });
61 |
62 | //These are the default settings. Laevateinn should be bliss to everyone.
63 | const gcC_css_default = {
64 | "backgroundImage": "url(https://i.imgur.com/9gdEjUf.jpg)",
65 | "backgroundRepeat": "no-repeat",
66 | "backgroundPosition": "left top",
67 | "backgroundSize": "cover",
68 | "backgroundAttachment": "fixed",
69 | "transform": "scale(1)",
70 | "opacity": 1.0
71 | };
72 | // Variables for setting and changing data
73 | let gcC_css;
74 | let old_gcC_css;
75 | let settings;
76 | let NCM_restore;
77 | // A small helper to prevent people from expecting preview stuff
78 | let chat_exists = false;
79 | let user_ack = false;
80 |
81 | // The page loaded, so we move on to testing storage. Set settings or error out
82 | storageAvailable ? settings = window.localStorage : displayMessage("Browser Issue", "Your current browser or session does not support localStorage.\nGet a different browser or change applicable settings.");
83 | if(!settings) return; // Exit if we can't do anything
84 | // If we know someone wants Ranked dead, make sure they can spectate too
85 | let rankedObserver = settings.getItem("BlockRankedChat") ? settings.getItem("BlockRankedChat") == "true" ? new MutationObserver(rankedOpen) : null : null;
86 | // Hacky workaround to prevent crashes
87 | rankedObserver ? rankedObserver.observe($("#quizPage")[0], {attributes: true, attributeOldValue: true, characterDataOldValue: true, attributeFilter: ["class"]}) : null;
88 | // Initialize some stuff and create DOM objects
89 | initSettingsWindow();
90 |
91 | /*** Function definitions ***/
92 | /*
93 | * Function that actually replaces the chatbox with an image.
94 | * Loads in the last saved settings, or the default if nothing was set.
95 | */
96 | function changeChat(){
97 | // This should only be false if a lobby has not been opened before
98 | chat_exists = true;
99 | // Then check if this is valid, since we wouldn't want to restore undefined.
100 | if(!lobbyBypass) { // This is a fix for spectating Ranked before having entered a lobby
101 | if(!settings || (!inRanked() && lobby.settings.roomSize > 1)){
102 | restoreChat();
103 | return;
104 | }
105 | }
106 | // Check if it has a value already, to prevent entering a solo room twice in one session from breaking the chat
107 | old_gcC_css = old_gcC_css ? old_gcC_css : getOldStyles(Object.keys(gcC_css));
108 | // unbind all listeners
109 | gameChat._newMessageListner.unbindListener();
110 | gameChat._newSpectatorListner.unbindListener();
111 | gameChat._spectatorLeftListner.unbindListener();
112 | gameChat._playerLeaveListner.unbindListener();
113 | gameChat._spectatorChangeToPlayer.unbindListener();
114 | gameChat._newQueueEntryListener.unbindListener();
115 | gameChat._playerLeftQueueListener.unbindListener();
116 | gameChat._hostPromotionListner.unbindListener();
117 | gameChat._playerNameChangeListner.unbindListener();
118 | gameChat._spectatorNameChangeListner.unbindListener();
119 | gameChat._deletePlayerMessagesListener.unbindListener();
120 | gameChat._deleteChatMessageListener.unbindListener();
121 | // If you join another game, we gotta restore the chat
122 | switchGameListener.bindListener();
123 | // For the complete delete mode
124 | if(settings.getItem("NoChatMode") ? settings.getItem("NoChatMode") == "true" : false){
125 | noChatMode();
126 | }
127 | // In any other case
128 | else {
129 | // Apply the CSS and hide the chat
130 | $("#gcContent").css(gcC_css);
131 | }
132 | // And always remove the input box
133 | $("#gcChatContent").css("display", "none");
134 | }
135 |
136 | /*
137 | * Edgecase function, someone wants the chat block GONE
138 | */
139 | function noChatMode(){
140 | if(!lobbyBypass){
141 | if(lobby.settings.roomSize > 1 && !lobby.settings.gameMode == "Ranked"){
142 | return;
143 | }
144 | } else {
145 | if(!quiz.gameMode == "Ranked"){
146 | return;
147 | }
148 | }
149 | $("#gcContent").css({"backgroundImage": "none", "opacity": 0});
150 | let killkeys = ['background-color', '-webkit-box-shadow', 'box-shadow'];
151 | NCM_restore = $("#gameChatContainer").css(killkeys);
152 | killkeys.forEach((key) => {
153 | let val;
154 | key == 'background-color' ? val = "rgba(0,0,0,0)" : val = "none";
155 | // just delete the properties altogether
156 | $("#gameChatContainer").css(key, val);
157 | });
158 | $("#lobbyCountContainer").css({'right': '-25vw'});
159 | }
160 |
161 | /*
162 | * Undo the changes specific to No Chat Mode
163 | */
164 | function undoNCM(){
165 | $("#gameChatContainer").css(NCM_restore);
166 | $("#lobbyCountContainer").css({'right': '0px'});
167 | $("#gcContent").css(gcC_css);
168 | NCM_restore = null;
169 | }
170 |
171 | /*
172 | * Restores the chat to its original state.
173 | * This should always be called when joining a new, non-targeted room
174 | */
175 | function restoreChat(){
176 | // If we're in No Chat Mode, restore to script defaults first!
177 | NCM_restore ? undoNCM() : null;
178 | switchGameListener.unbindListener();
179 | $("#gcContent").css(old_gcC_css);
180 | $("#gcChatContent").css("display", "");
181 | // DO NOT BIND THE LISTENERS or you'd be listening to two chats. Enjoy running it on ranked and leaving midway then
182 | }
183 |
184 | /*
185 | * Internal function to update settings. Just grab the current CSS values and roll with it.
186 | * WYSIWYG, which is exactly what a user wants to save.
187 | */
188 | function updateSettings(){
189 | // Get all dem CSS. Ordering is important for CSS!
190 | // Background image link, or none if empty
191 | gcC_css.backgroundImage = $("#soloChatBlockImg").val() ? `url(${$("#soloChatBlockImg").val()})` : "none";
192 | // Repeat value
193 | gcC_css.backgroundRepeat = $("#SoloChatBlockRepeat").val();
194 | // Jump through a hoop, get the checked radio button's value and ignore the rest
195 | gcC_css.backgroundPosition = $('input[name="SoloChatBlockPositionSelect"]:checked').val();
196 | // Selected size option
197 | gcC_css.backgroundSize = $("#SoloChatBlockSize").val();
198 | // Selected attachment option
199 | gcC_css.backgroundAttachment = $("#SoloChatBlockAttachment").val();
200 | // Transform value, this decides whether or not to flip the image
201 | gcC_css.transform = $("#SoloChatBlockTransform").val();
202 | // Opacity, we need to transform this to usable values
203 | gcC_css.opacity = Number($("#soloChatBlockOpacity").val())/100;
204 | // Apply for good measure
205 | $("#gcContent").css(gcC_css);
206 | // Save the settings to localStorage
207 | settings.setItem("SoloChatBlock", JSON.stringify(gcC_css));
208 | }
209 |
210 | /*
211 | * Function to apply content changes in preview mode.
212 | * Takes a property name and a value to update in the chat block
213 | */
214 | function settingsChangePreview(property, value){
215 | if(!chat_exists){
216 | user_ack ? null : displayMessage("Can't change options!", "The chat object does not exist until after entering a room or lobby.\nYou may change the settings and save them, but preview mode is unavailable.");
217 | user_ack = true;
218 | return;
219 | }
220 | if(!lobbyBypass) {
221 | if(!inRanked() || lobby.settings.roomSize > 1){
222 | return;
223 | }
224 | } else if(!inRanked()){
225 | return;
226 | }
227 | // If it's a backgroundImage or opacity value, we should set it to CSS values. These can be quite tricky and I just want it to be easy in the HTML part. Pass only property and value.
228 | property === "backgroundImage" ? value ? value = `url(${value})` : "none" : property === "opacity" ? value = Number(value)/100 : null;
229 | // Except for the above cases before changing them, any property-value pair should be usable as-is
230 | let preview_css = Object.assign({}, gcC_css);
231 | preview_css[property] = value;
232 | $("#gcContent").css(property, value);
233 | }
234 |
235 | /*** Helper functions ***/
236 | /*
237 | * Helper function to determine if the user is in Ranked
238 | */
239 | function inRanked(){
240 | if(!lobbyBypass){
241 | return settings.getItem("BlockRankedChat") ? (settings.getItem("BlockRankedChat") == "true" && lobby.settings.gameMode === "Ranked") : function(){settings.setItem("BlockRankedChat", false); return false;}() && lobby.settings.gameMode === "Ranked";
242 | } else {
243 | return settings.getItem("BlockRankedChat") ? (settings.getItem("BlockRankedChat") == "true" && quiz.gameMode === "Ranked") : function(){settings.setItem("BlockRankedChat", false); return false;}() && quiz.gameMode === "Ranked";
244 | }
245 | }
246 | /*
247 | * Function to check if localStorage even exists here. If it doesn't, people are using a weird browser and we can't support them.
248 | * Tests the storage by adding and removing an object from it. If it all succeeds, we can carry on and run this script
249 | */
250 | function storageAvailable() {
251 | let storage;
252 | try {
253 | storage = window.localStorage;
254 | storage.setItem("Riven is amazing", "Hell yeah");
255 | storage.removeItem("Riven is amazing");
256 | return true;
257 | }
258 | catch(e) {
259 | return false;
260 | }
261 | }
262 | /*
263 | * Function that gets all currently set styles on the chat and saves them to old_gcC_css
264 | */
265 | function getOldStyles(keys){
266 | let ret = {};
267 | keys.forEach(key => {
268 | let val = $("#gcContent").css(key);
269 | ret[key] = typeof val !== "undefined" ? val : "";
270 | });
271 | return ret;
272 | }
273 |
274 | /*** Not even really a function anymore. This behemoth creates and inserts the settings modal. BOW TO IT, FOR IT PRESENTS YOU WITH THE OPTION TO VIEW YOUR WAIFU! ***/
275 |
276 | /*
277 | * Function that runs once to create the settings window and add it to the game.
278 | * Adds an entry to the settings slide-out to customize options. Use it while in a lobby to see live changes.
279 | * Click save to save the settings, close the window to undo them.
280 | * Initializes some starting data as well.
281 | */
282 | function initSettingsWindow(){
283 | //create the window. Inspired by TheJoseph98's amqScriptInfo.js, which can be found in the @require link up top.
284 | if (!window.setupDocumentDone) return;
285 |
286 | // If it's not set yet, create the object in localStorage using the defaults. Hail persistence!
287 | !settings.getItem("SoloChatBlock") ? localStorage.setItem("SoloChatBlock", JSON.stringify(gcC_css_default)) : null;
288 | // Load in whatever the last saved ssettings were, or the defaults if we just set them
289 | gcC_css = JSON.parse(settings.getItem("SoloChatBlock"));
290 |
291 | // If it doesn't exist, create the entire modal window without the relevant content. THIS IS HELL
292 | // Adding content is done in a seperate function call to allow for easy fixing when "Cancel" is pressed
293 | if ($("#soloChatBlock").length === 0) {
294 | $("#gameContainer").append($(`
295 |
296 |
297 |
298 |
299 |
302 |
Solo Chat Block Configuration
303 |
Changes are applied as preview, click 'Cancel' to undo them. Exiting without clicking cancel leaves the preview as-is.
304 |
305 |
306 |
307 |
Block Ranked Chat?
308 |
309 |
310 |
311 |
312 |
Changing this applies immediately. Re-enter ranked to trigger.
313 |
314 |
315 |
No Chat Mode?
316 |
317 |
318 |
319 |
320 |
Changing this applies immediately. Deletes chat completely.
321 |
322 |
323 |
324 |
325 |
329 |
330 |
331 |
332 | `));
333 |
334 | // Add the menu option
335 | $("#optionsContainer > ul").prepend($(`
336 |
Solo Chat Block
337 | `));
338 |
339 | // Add the event for turning it on/off for Ranked
340 | $("#mhKillRankedChat").change(function(){
341 | // If the setting doesn't exist, or it's false, set it to true. If it's true, set it to false. Double inline if because these are much faster than if{if...else}...else
342 | settings.getItem("BlockRankedChat") ? settings.getItem("BlockRankedChat") == "true" ? settings.setItem("BlockRankedChat", false) : settings.setItem("BlockRankedChat", true) : settings.setItem("BlockRankedChat", true);
343 | });
344 |
345 | // Rig up for No Chat Mode
346 | $("#mhNoChatMode").change(function(){
347 | settings.getItem("NoChatMode") ? settings.getItem("NoChatMode") == "true" ? settings.setItem("NoChatMode", false) : settings.setItem("NoChatMode", true) : settings.setItem("NoChatMode", false)
348 | // Alternate between on and off
349 | NCM_restore ? undoNCM() : noChatMode();
350 | });
351 |
352 | // Fill up the modal with the configuration options
353 | addSettingsContent();
354 |
355 | // They clicked "Cancel"! Remove everything we added and create it from scratch. Don't forget to restore shit!
356 | // The cancel and save buttons are static, so their events should only be assigned an action once
357 | $("#scbcCancel").click(function(){
358 | // Put the CSS back to the last saved state
359 | $("#gcContent").css(gcC_css);
360 | // Make sure NOTHING remains since we'll be creating everything from scratch
361 | $("#scbcContainer").empty();
362 | // And fill it up again, since it's less work to paste HTML than to traverse it
363 | addSettingsContent();
364 | });
365 |
366 | // This is the worst case scenario. Someone doesn't like Laevateinn and wants to change the image.
367 | $("#scbcSave").click(updateSettings);
368 | }
369 | }
370 |
371 | /*
372 | * Internal helper function, creates and adds all of the new menu internals.
373 | * Split off so we can easily delete them and put them back instead of manually resetting all values.
374 | * When creating these from scratch, make sure to call $(parent).empty() to ensure no data remains!
375 | */
376 | function addSettingsContent(){
377 | // Input element for the image link
378 | let imgSelect = $(`
379 |
Image link
380 |
381 |
`);
382 | // Select element for the repeat option
383 | let repeatSelect = $(`
384 |
Repeat value
385 |
393 |
`);
394 | // Radiobuttons to select the anchoring position. SWEET JESUS NEVER AGAIN
395 | let positionSelect = $(`
396 |
Anchor Position Selector
397 |
398 |
399 |
400 |
401 |
402 |
403 |
Left Top
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
Left Center
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
Left Bottom
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
Center Top
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
Center Center
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
Center Bottom
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
Right Top
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
Right Center
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
Right Bottom
468 |
469 |
`);
470 | // Select element for the usual CSS Size options. Custom Values can be set through the console / inspector stuff
471 | let sizeSelect = $(`
472 |
Size Selector
473 |
Static options only, working with multiple value types is a lot of effort, set those through the console instead.
474 |
479 |
480 |
`);
481 | // Select element for the attachment value. Some of the names aren't logical, so the text presented to the user is added to make it more logical
482 | let attachmentSelect = $(`
483 |
Attachment Selector
484 |
This usually doesn't need editing, but it's included as to provide all possible options.
485 | The only case that would warrant changing it is by selecting flip to "force as-is" as the transform should make the image cover the chat
486 |
491 |
492 |
`);
493 | // Select element for flipping or not flipping the image.
494 | let transformSelect = $(`
495 |
Flip Image Selector
496 |
Whether or not to flip the image. This property uses CSS transform's scale function to make the image fill out the chat space. Use "Force original" to prevent the CSS transform property from being used.
497 |
501 |
502 |
`);
503 | // Change the opacity of the chat so the image becomes see-through
504 | let opacitySelect = $(`
505 |
Element opacity
506 |
Set the entire box's opacity on a scale of 0 (invisible) to 100 (default, fully visible)
507 |
508 |
`);
509 | // Add all of the options to the modal window to create a nice menu. Thanks for laying out the groundwork Ege and Joseph!
510 | $("#scbcContainer").append(imgSelect);
511 | $("#scbcContainer").append(repeatSelect);
512 | $("#scbcContainer").append(positionSelect);
513 | $("#scbcContainer").append(sizeSelect);
514 | $("#scbcContainer").append(attachmentSelect);
515 | $("#scbcContainer").append(transformSelect);
516 | $("#scbcContainer").append(opacitySelect);
517 | // Assign the functions for the change events for live previews
518 | $("#soloChatBlockImg").change(function(){
519 | settingsChangePreview("backgroundImage", this.value);
520 | });
521 | $("#SoloChatBlockRepeat").change(function(){
522 | settingsChangePreview("backgroundRepeat", this.value);
523 | });
524 | // This uses a class instead of an ID because all those radiobuttons change the same thing
525 | $(".SoloChatBlockPositionRadio").change(function(){
526 | settingsChangePreview("backgroundPosition", this.value);
527 | });
528 | $("#SoloChatBlockSize").change(function(){
529 | settingsChangePreview("backgroundSize", this.value);
530 | });
531 | $("#SoloChatBlockAttachment").change(function(){
532 | settingsChangePreview("backgroundAttachment", this.value);
533 | });
534 | $("#SoloChatBlockTransform").change(function(){
535 | settingsChangePreview("transform", this.value);
536 | });
537 | $("#soloChatBlockOpacity").on('input', function(){
538 | settingsChangePreview("opacity", this.value);
539 | });
540 | }
541 |
--------------------------------------------------------------------------------