├── .gitignore ├── LICENSE ├── amqRoomBrowserFix.user.js ├── amqExpandSearchANNID.user.js ├── amqDiceRoller.user.js ├── deprecated ├── amqTeamRandomizer.user.js └── amqSongList.user.js ├── amqChatTimestamps.user.js ├── amqBuzzer.user.js ├── test ├── amqPerformanceImprovements.user.js └── amqSoloChatBlocker.user.js ├── amqRigTrackerLite.user.js ├── common ├── amqScriptInfo.js └── amqWindows.js ├── localExportDownloader.py ├── amqShortSampleRadio.user.js ├── amqRewardsTracker.user.js ├── amqSpeedrun.user.js ├── amqDiceRollerUI.user.js ├── README.md ├── amqRigTracker.user.js └── amqSoloChatBlock.user.js /.gitignore: -------------------------------------------------------------------------------- 1 | ##IDE / Dev env files## 2 | ###Komodo IDE/Edit### 3 | *.komodoproject 4 | .komodotools 5 | 6 | ##Python Files## 7 | __pycache__/ 8 | *$py.class 9 | *.so 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | ##Export.json variants## 31 | *export*.json 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TheJoseph98 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /amqRoomBrowserFix.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Room Browser Borgar Placement 3 | // @namespace SkayeScripts 4 | // @version 2.2 5 | // @description Moves the "All Settings Menu" icon on room browsers to keep the height consistent. It even looks decent! 6 | // @author RivenSkaye, Zolhungaj 7 | // @match https://animemusicquiz.com/* 8 | // @grant none 9 | // @downloadURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRoomBrowserFix.user.js 10 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqRoomBrowserFix.user.js 11 | // ==/UserScript== 12 | 13 | if (typeof Listener === "undefined") return; 14 | const version = "2.2"; 15 | 16 | let loadInterval = setInterval(() => { 17 | if ($("#loadingScreen").hasClass("hidden")) { 18 | clearInterval(loadInterval); 19 | setup(); 20 | } 21 | }, 500); 22 | 23 | function setup() { 24 | if (ROOM_TILE_TEMPLATE) { 25 | ROOM_TILE_TEMPLATE = ROOM_TILE_TEMPLATE 26 | .replace("", "") 27 | .replace("
", "\n\t\t\t
"); 28 | } 29 | } 30 | 31 | AMQ_addScriptData({ 32 | name: "AMQ Room Browser Borgar Placement", 33 | author: "TheJoseph98", 34 | version: version, 35 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqRoomBrowserFix.user.js", 36 | description: `

Moves the "All Settings Menu" icon on room browsers to keep the height consistent

` 37 | }); 38 | -------------------------------------------------------------------------------- /amqExpandSearchANNID.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Expand Search ANN ID 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.2 5 | // @description Allows the user to search anime by ANNID in Expand Library 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/amqExpandSearchANNID.user.js 11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqExpandSearchANNID.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 | 25 | function setup() { 26 | $("#elQuestionFilterInput").attr("placeholder", "Search anime, song, artist or ANN ID"); 27 | 28 | ExpandQuestionListEntry.prototype.applySearchFilter = function (regexFilter, stricter) { 29 | if (this.annId === parseInt($("#elQuestionFilterInput").val())) { 30 | this.resetSearchFilter(); 31 | return true; 32 | } 33 | 34 | if (stricter && !this.active) { 35 | return false; 36 | } 37 | 38 | if (regexFilter.test(this.name)) { 39 | this.resetSearchFilter(); 40 | } else { 41 | this.songList.forEach(entry => { 42 | entry.applySearchFilter(regexFilter, stricter); 43 | }); 44 | this.updateDisplay(); 45 | } 46 | }; 47 | 48 | AMQ_addScriptData({ 49 | name: "Expand Search by ANN ID", 50 | author: "TheJoseph98", 51 | version: version, 52 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqExpandSearchANNID.user.js", 53 | description: ` 54 |

Allows the user to search expand library by ANN ID in addition to searching by anime name, song name and artist

55 | ` 56 | }); 57 | } -------------------------------------------------------------------------------- /amqDiceRoller.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Dice Roller 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.3 5 | // @description Dice roller for general usage, type "/roll" followed by a number 6 | // @author Anopob & 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/amqDiceRoller.user.js 11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqDiceRoller.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 | 25 | function getRandomIntInclusive(min, max) { 26 | min = Math.ceil(min); 27 | max = Math.floor(max); 28 | return Math.floor(Math.random() * (max - min + 1)) + min; 29 | } 30 | 31 | function sendChatMessage(message) { 32 | gameChat.$chatInputField.val(message); 33 | gameChat.sendMessage(); 34 | } 35 | 36 | function setup() { 37 | new Listener("game chat update", (payload) => { 38 | payload.messages.forEach(message => { 39 | if (message.sender === selfName && message.message.startsWith("/roll")) { 40 | let args = message.message.split(/\s+/); 41 | if (args[1]) { 42 | let maxRoll = parseInt(args[1].trim()); 43 | if (!isNaN(maxRoll) && maxRoll > 0) { 44 | sendChatMessage("rolls " + getRandomIntInclusive(1, maxRoll)); 45 | } 46 | } 47 | } 48 | }); 49 | }).bindListener(); 50 | 51 | AMQ_addScriptData({ 52 | name: "Dice Roller", 53 | author: "Anopob & TheJoseph98", 54 | version: version, 55 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqDiceRoller.user.js", 56 | description: ` 57 |

Adds a dice roller to AMQ. To roll, type "/roll #" in chat (# can be any number). You will receive a random number between 1 and the selected number

58 |

Because it requires typing to chat, it does not work in ranked, due to slow mode

59 | ` 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /deprecated/amqTeamRandomizer.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Team Randomizer 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.0.1 5 | // @description Team randomizer for tag team custom mode, once all players join the lobby, type "/teams" in chat to randomize teams, the teams will be output to chat 6 | // @author TheJoseph98 7 | // @match https://animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/TheJoseph98/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // @updateURL https://github.com/TheJoseph98/AMQ-Scripts/raw/master/amqTeamRandomizer.user.js 11 | // ==/UserScript== 12 | 13 | if (!window.setupDocumentDone) return; 14 | 15 | let players = []; 16 | let playersPerTeam = 2; 17 | 18 | let commandListener = new Listener("Game Chat Message", (payload) => { 19 | if (payload.sender === selfName && payload.message.startsWith("/teams")) { 20 | if (lobby.inLobby) { 21 | let message = ""; 22 | sendChatMessage("Randomizing teams..."); 23 | 24 | for (let playerId in lobby.players) { 25 | players.push(lobby.players[playerId]._name); 26 | } 27 | 28 | shuffle(players); 29 | 30 | for (let teamId = 0; teamId < players.length / playersPerTeam; teamId++) { 31 | message += "Team " + (teamId + 1) + ": "; 32 | for (let playerId = 0; playerId < playersPerTeam; playerId++) { 33 | let playerIdx = teamId * playersPerTeam + playerId; 34 | if (playerId === 0 && playerIdx < players.length) { 35 | message += "@" + players[playerIdx]; 36 | } 37 | if (playerId !== 0 && playerIdx < players.length) { 38 | message += " / @" + players[playerIdx]; 39 | } 40 | } 41 | sendChatMessage(message); 42 | message = ""; 43 | } 44 | 45 | players = []; 46 | } 47 | else { 48 | gameChat.systemMessage("Must be in pre-game lobby"); 49 | } 50 | } 51 | }); 52 | 53 | 54 | function shuffle(array) { 55 | let counter = array.length; 56 | while (counter > 0) { 57 | let index = Math.floor(Math.random() * counter); 58 | counter--; 59 | let temp = array[counter]; 60 | array[counter] = array[index]; 61 | array[index] = temp; 62 | } 63 | return array; 64 | } 65 | 66 | function sendChatMessage(message) { 67 | gameChat.$chatInputField.val(message); 68 | gameChat.sendMessage(); 69 | } 70 | 71 | commandListener.bindListener(); 72 | 73 | AMQ_addScriptData({ 74 | name: "Team Randomizer", 75 | author: "TheJoseph98", 76 | description: ` 77 |

Team randomizer for the Tag Teams custom mode

78 |

Type "/teams" in chat to randomize the teams

79 |

Works only while in the lobby (ie. not currently in a quiz)

80 | ` 81 | }) 82 | -------------------------------------------------------------------------------- /amqChatTimestamps.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Chat Timestamps 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.5 5 | // @description Adds timestamps to chat messages 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/amqChatTimestamps.user.js 11 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqChatTimestamps.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.5"; 24 | 25 | function setup() { 26 | let gameChatNode = document.getElementById("gcMessageContainer"); 27 | 28 | let gameChatObserver = new MutationObserver(mutations => { 29 | mutations.forEach(mutation => { 30 | if (!mutation.addedNodes) return; 31 | 32 | for (let i = 0; i < mutation.addedNodes.length; i++) { 33 | let node = mutation.addedNodes[i]; 34 | if ($(node).hasClass("gcTimestamp")) return; 35 | if ($(node).hasClass("ps__scrollbar-y-rail")) return; 36 | if ($(node).hasClass("ps__scrollbar-x-rail")) return; 37 | let d = new Date(); 38 | let mins = d.getMinutes() < 10 ? "0" + d.getMinutes() : d.getMinutes(); 39 | let hours = d.getHours() < 10 ? "0" + d.getHours() : d.getHours(); 40 | let timeFormat = hours + ":" + mins; 41 | if ($(node).find(".gcTeamMessageIcon").length === 1) { 42 | $(node).find(".gcTeamMessageIcon").after($(`${timeFormat}`)); 43 | } 44 | else { 45 | $(node).prepend($(`${timeFormat}`)); 46 | } 47 | 48 | // scroll to bottom 49 | let chat = gameChat.$chatMessageContainer; 50 | let atBottom = chat.scrollTop() + chat.innerHeight() >= chat[0].scrollHeight - 25; 51 | if (atBottom) { 52 | chat.scrollTop(chat.prop("scrollHeight")); 53 | } 54 | } 55 | }); 56 | }); 57 | 58 | gameChatObserver.observe(gameChatNode, { 59 | childList: true, 60 | attributes: false, 61 | CharacterData: false 62 | }); 63 | 64 | AMQ_addScriptData({ 65 | name: "Chat Timestamps", 66 | author: "TheJoseph98", 67 | version: version, 68 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqChatTimestamps.user.js", 69 | description: `

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)

114 | ` 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /test/amqPerformanceImprovements.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Performance Improvements 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.1 5 | // @description Disables a bunch of animations, transition effects and CPU heavy tasks 6 | // @author TheJoseph98 7 | // @match https://animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/TheJoseph98/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // ==/UserScript== 11 | 12 | // don't load the script on login page 13 | if (!window.setupDocumentDone) return; 14 | 15 | function setup() { 16 | /* 17 | $("#quizScoreboardEntryTemplate").html(` 18 |
19 |
20 | {1} 21 |
22 |
23 |

{2} {0}

26 |
27 |
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 |

28 | `)); 29 | 30 | $("#mainMenu").prepend($(` 31 | 34 | `)) 35 | .css("margin-top", "20vh"); 36 | 37 | $("#optionsContainer > ul").prepend($(` 38 |
  • Installed Userscripts
  • 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

    132 | 133 |

    An example output can be found here

    134 |

    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 | `
    156 |

    Elapsed Time

    157 |

    XP gained

    158 |

    Notes gained

    159 |

    Tickets gained

    160 |

    XP/hour

    161 |

    Notes/hour

    162 |

    Tickets/hour

    163 |
    ` 164 | ) 165 | ) 166 | .append( 167 | $( 168 | `
    169 |

    00:00:00

    170 |

    0

    171 |

    0

    172 |

    0

    173 |

    0

    174 |

    0

    175 |

    0

    176 |
    ` 177 | ) 178 | ) 179 | ) 180 | 181 | let oldWidth = $("#qpOptionContainer").width(); 182 | $("#qpOptionContainer").width(oldWidth + 35); 183 | $("#qpOptionContainer > div").append($(`
    `) 184 | .click(() => { 185 | if (trackerWindow.isVisible()) { 186 | trackerWindow.close(); 187 | } 188 | else { 189 | trackerWindow.open(); 190 | } 191 | }) 192 | .popover({ 193 | content: "Results Tracker", 194 | trigger: "hover", 195 | placement: "bottom" 196 | }) 197 | ); 198 | 199 | quizXPGainListener = new Listener("quiz xp credit gain", data => { 200 | gainedNotes = data.credit - startNotes; 201 | gainedXP += data.xpInfo.lastGain; 202 | gainedTickets = data.tickets - startTickets; 203 | }); 204 | 205 | resetTracker(); 206 | 207 | AMQ_addScriptData({ 208 | name: "Rewards Tracker", 209 | author: "TheJoseph98", 210 | version: version, 211 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqRewardsTracker.user.js", 212 | description: ` 213 |

    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.

    215 | ` 216 | }); 217 | 218 | AMQ_addStyle(` 219 | #qpResultsTracker { 220 | width: 30px; 221 | margin-right: 5px; 222 | } 223 | #trackerWindowResultsLeft { 224 | width: 50%; 225 | float: left; 226 | text-align: left; 227 | padding-left: 5px; 228 | } 229 | #trackerWindowResultsRight { 230 | width: 50%; 231 | float: right; 232 | text-align: right; 233 | padding-right: 5px; 234 | } 235 | #trackerWindowResultsLeft > p { 236 | margin-bottom: 0; 237 | } 238 | #trackerWindowResultsRight > p { 239 | margin-bottom: 0; 240 | } 241 | #trackerWindowControls { 242 | border-bottom: 1px solid #6d6d6d; 243 | text-align: center; 244 | } 245 | #trackerWindowControlsContainer > button { 246 | width: 70px; 247 | margin: 7px; 248 | } 249 | `); 250 | } 251 | -------------------------------------------------------------------------------- /amqSpeedrun.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Speedrun 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.3 5 | // @description Tracks guess times for each song, including total and average times 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/amqSpeedrun.user.js 12 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqSpeedrun.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.3"; 25 | let fastestGuess = 999999; 26 | let slowestGuess = 0; 27 | let totalTime = 0; 28 | let averageCorrect = 0; 29 | let correctCount = 0; 30 | let averageTotal = 0; 31 | let guessRate = 0; 32 | let previousGuess = 0; 33 | let times = {}; 34 | let songStartTime = 0; 35 | let answerSubmitTime = 0; 36 | let autoSubmitFlag = true; 37 | let speedrunWindow; 38 | 39 | let oldWidth = $("#qpOptionContainer").width(); 40 | $("#qpOptionContainer").width(oldWidth + 35); 41 | $("#qpOptionContainer > div").append($(`
    `) 42 | .click(() => { 43 | if (speedrunWindow.isVisible()) { 44 | speedrunWindow.close(); 45 | } 46 | else { 47 | speedrunWindow.open(); 48 | } 49 | }) 50 | .popover({ 51 | content: "Speedrun", 52 | trigger: "hover", 53 | placement: "bottom" 54 | }) 55 | ); 56 | 57 | function createSpeedrunWindow() { 58 | speedrunWindow = new AMQWindow({ 59 | title: "Speedrun", 60 | width: 250, 61 | height: 400, 62 | minWidth: 250, 63 | minHeight: 330, 64 | zIndex: 1055, 65 | draggable: true, 66 | resizable: true, 67 | }); 68 | 69 | speedrunWindow.addPanel({ 70 | width: 1.0, 71 | height: 145, 72 | id: "speedrunWindowUpper" 73 | }); 74 | 75 | speedrunWindow.panels[0].addPanel({ 76 | width: "calc(65% - 5px)", 77 | height: 1.0, 78 | position: { 79 | x: 5, 80 | y: 0 81 | }, 82 | id: "speedrunInfoLeft" 83 | }); 84 | 85 | speedrunWindow.panels[0].addPanel({ 86 | width: "calc(35% - 5px)", 87 | height: 1.0, 88 | position: { 89 | x: 0.65, 90 | y: 0 91 | }, 92 | id: "speedrunInfoRight" 93 | }); 94 | 95 | speedrunWindow.addPanel({ 96 | width: 1.0, 97 | height: "calc(100% - 145px)", 98 | position: { 99 | x: 0, 100 | y: 145 101 | }, 102 | scrollable: { 103 | x: false, 104 | y: true 105 | } 106 | }); 107 | 108 | speedrunWindow.panels[1].addPanel({ 109 | width: "calc(50% - 5px)", 110 | height: "auto", 111 | position: { 112 | x: 5, 113 | y: 0 114 | }, 115 | id: "speedrunTimesLeft" 116 | }); 117 | 118 | speedrunWindow.panels[1].addPanel({ 119 | width: "calc(50% - 5px)", 120 | height: "auto", 121 | position: { 122 | x: 0.5, 123 | y: 0 124 | }, 125 | id: "speedrunTimesRight" 126 | }); 127 | 128 | speedrunWindow.panels[0].panels[0].panel 129 | .html(` 130 |

    Fastest correct guess:

    131 |

    Slowest correct guess:

    132 |

    Guess rate:

    133 |

    Average time (correct):

    134 |

    Average time (total):

    135 |

    Total time:

    136 |

    Previous guess time:

    137 | `); 138 | 139 | speedrunWindow.panels[0].panels[1].panel 140 | .html(` 141 |

    0.000

    142 |

    0.000

    143 |

    0%

    144 |

    0.000

    145 |

    0.000

    146 |

    0.000

    147 |

    0.000

    148 | `); 149 | } 150 | 151 | function updateInfo(songNumber, newTime) { 152 | $("#srFastestTime").text(formatTime(fastestGuess)); 153 | $("#srSlowestTime").text(formatTime(slowestGuess)); 154 | $("#srGuessRate").text((guessRate * 100).toFixed(2) + "%"); 155 | $("#srAverageCorrect").text(formatTime(averageCorrect)); 156 | $("#srAverageTotal").text(formatTime(averageTotal)); 157 | $("#srTotalTime").text(formatTime(totalTime)); 158 | $("#srPreviousTime").text(formatTime(previousGuess)); 159 | speedrunWindow.panels[1].panels[0].panel.append($("

    ").text("Song " + songNumber)); 160 | speedrunWindow.panels[1].panels[1].panel.append($("

    ").text(formatTime(newTime.time))); 161 | speedrunWindow.panels[1].panel.scrollTop(speedrunWindow.panels[1].panel.get(0).scrollHeight); 162 | } 163 | 164 | function formatTime(time) { 165 | let formattedTime = ""; 166 | let milliseconds = Math.floor(time % 1000); 167 | let seconds = Math.floor(time / 1000); 168 | let minutes = Math.floor(seconds / 60); 169 | let hours = Math.floor(minutes / 60); 170 | let secondsLeft = seconds - minutes * 60; 171 | let minutesLeft = minutes - hours * 60; 172 | if (hours > 0) { 173 | formattedTime += hours + ":"; 174 | } 175 | if (minutes > 0) { 176 | formattedTime += (minutesLeft < 10 && hours > 0) ? "0" + minutesLeft + ":" : minutesLeft + ":"; 177 | } 178 | formattedTime += (secondsLeft < 10 && minutes > 0) ? "0" + secondsLeft + "." : secondsLeft + "."; 179 | if (milliseconds < 10) { 180 | formattedTime += "00" + milliseconds; 181 | } 182 | else if (milliseconds < 100) { 183 | formattedTime += "0" + milliseconds; 184 | } 185 | else { 186 | formattedTime += milliseconds; 187 | } 188 | return formattedTime; 189 | } 190 | 191 | function resetTimes() { 192 | $("#speedrunTimesLeft").html(""); 193 | $("#speedrunTimesRight").html(""); 194 | $("#srFastestTime").text("0.000"); 195 | $("#srSlowestTime").text("0.000"); 196 | $("#srGuessRate").text("0%"); 197 | $("#srAverageCorrect").text("0.000"); 198 | $("#srAverageTotal").text("0.000"); 199 | $("#srTotalTime").text("0.000"); 200 | $("#srPreviousTime").text("0.000"); 201 | fastestGuess = 999999; 202 | slowestGuess = 0; 203 | totalTime = 0; 204 | averageCorrect = 0; 205 | correctCount = 0; 206 | averageTotal = 0; 207 | guessRate = 0; 208 | previousGuess = 0; 209 | times = {}; 210 | } 211 | 212 | function setup() { 213 | createSpeedrunWindow(); 214 | 215 | // clear times on quiz ready 216 | let quizReadyListener = new Listener("quiz ready", data => { 217 | resetTimes(); 218 | $("#qpAnswerInput").off("keypress", answerHandler); 219 | $("#qpAnswerInput").on("keypress", answerHandler); 220 | }); 221 | 222 | // start timer on song start 223 | let quizPlayNextSongListener = new Listener("play next song", data => { 224 | songStartTime = Date.now(); 225 | autoSubmitFlag = true; 226 | }); 227 | 228 | 229 | // set time on answer reveal 230 | let quizAnswerResultsListener = new Listener("answer results", result => { 231 | // check if the player is playing the game 232 | let findPlayer = Object.values(quiz.players).find((tmpPlayer) => { 233 | return tmpPlayer._name === selfName && tmpPlayer.avatarSlot._disabled === false 234 | }); 235 | if (findPlayer !== undefined) { 236 | let playerIdx = Object.values(result.players).findIndex(tmpPlayer => { 237 | return findPlayer.gamePlayerId === tmpPlayer.gamePlayerId 238 | }); 239 | let tmpGuessTime = answerSubmitTime - songStartTime; 240 | let tmpCorrect = result.players[playerIdx].correct; 241 | let songNumber = parseInt($("#qpCurrentSongCount").text()); 242 | if (tmpCorrect === false) { 243 | tmpGuessTime = (quizVideoController.getCurrentPlayer().bufferLength - 13) * 1000; 244 | guessRate = (guessRate*(songNumber-1) + 0) / songNumber; 245 | if (tmpGuessTime < fastestGuess) { 246 | fastestGuess = tmpGuessTime; 247 | } 248 | } 249 | else { 250 | correctCount++; 251 | if (autoSubmitFlag === true) { 252 | tmpGuessTime = (quizVideoController.getCurrentPlayer().bufferLength - 13) * 1000; 253 | } 254 | if (tmpGuessTime < fastestGuess) { 255 | fastestGuess = tmpGuessTime; 256 | } 257 | if (tmpGuessTime > slowestGuess) { 258 | slowestGuess = tmpGuessTime; 259 | } 260 | averageCorrect = (averageCorrect*(correctCount-1) + tmpGuessTime) / correctCount; 261 | guessRate = (guessRate*(songNumber-1) + 1) / songNumber; 262 | } 263 | previousGuess = tmpGuessTime; 264 | averageTotal = (averageTotal*(songNumber-1) + tmpGuessTime) / songNumber; 265 | totalTime += tmpGuessTime; 266 | times[songNumber] = { 267 | correct: tmpCorrect, 268 | time: tmpGuessTime 269 | } 270 | updateInfo(songNumber, times[songNumber]); 271 | } 272 | }); 273 | 274 | let answerHandler = function (event) { 275 | if (event.which === 13) { 276 | answerSubmitTime = Date.now(); 277 | autoSubmitFlag = false; 278 | } 279 | } 280 | 281 | quizReadyListener.bindListener(); 282 | quizAnswerResultsListener.bindListener(); 283 | quizPlayNextSongListener.bindListener(); 284 | 285 | $(".modal").on("show.bs.modal", () => { 286 | speedrunWindow.setZIndex(1025); 287 | }); 288 | 289 | $(".modal").on("hidden.bs.modal", () => { 290 | speedrunWindow.setZIndex(1055); 291 | }); 292 | 293 | $(".slCheckbox label").hover(() => { 294 | speedrunWindow.setZIndex(1025); 295 | }, () => { 296 | speedrunWindow.setZIndex(1055); 297 | }); 298 | 299 | AMQ_addScriptData({ 300 | name: "Speedrun", 301 | author: "TheJoseph98", 302 | version: version, 303 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqSpeedrun.user.js", 304 | description: ` 305 |

    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

    309 | 310 | ` 311 | }); 312 | 313 | AMQ_addStyle(` 314 | #qpSpeedrun { 315 | width: 30px; 316 | margin-right: 5px; 317 | } 318 | #speedrunWindowUpper { 319 | border-bottom: 1px solid #6d6d6d; 320 | } 321 | #speedrunInfoLeft > p { 322 | font-size: 14px; 323 | margin-bottom: 0px; 324 | } 325 | #speedrunInfoRight > p { 326 | font-size: 14px; 327 | margin-bottom: 0px; 328 | text-align: right; 329 | } 330 | #speedrunTimesLeft > p { 331 | font-size: 18px; 332 | margin-bottom: 0px; 333 | } 334 | #speedrunTimesRight > p { 335 | font-size: 18px; 336 | margin-bottom: 0px; 337 | text-align: right; 338 | } 339 | `); 340 | } 341 | -------------------------------------------------------------------------------- /amqDiceRollerUI.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Dice Roller UI 3 | // @namespace https://github.com/TheJoseph98 4 | // @version 1.3 5 | // @description Adds a window where you can roll dice 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/amqDiceRollerUI.user.js 12 | // @updateURL https://github.com/joske2865/AMQ-Scripts/raw/master/amqDiceRollerUI.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.3"; 25 | let diceRolls = {}; 26 | let diceWindow; 27 | let diceManagerWindow; 28 | 29 | function createDiceWindow() { 30 | diceWindow = new AMQWindow({ 31 | id: "diceWindow", 32 | title: "Dice", 33 | width: 200, 34 | height: 225, 35 | zIndex: 1055, 36 | draggable: true 37 | }); 38 | 39 | diceWindow.addPanel({ 40 | width: 1.0, 41 | height: 50, 42 | id: "diceSelectContainer" 43 | }); 44 | 45 | diceWindow.addPanel({ 46 | width: 1.0, 47 | height: 50, 48 | position: { 49 | x: 0, 50 | y: 50 51 | }, 52 | id: "diceRollButtonContainer" 53 | }); 54 | 55 | diceWindow.addPanel({ 56 | width: 1.0, 57 | height: 50, 58 | position: { 59 | x: 0, 60 | y: 100 61 | }, 62 | id: "diceChoiceContainer" 63 | }); 64 | 65 | diceWindow.panels[0].panel.append($(``)); 66 | for (let key in diceRolls) { 67 | $("#diceSelect").append($(``).attr("value", key)); 68 | } 69 | 70 | diceWindow.panels[1].panel 71 | .append($(``) 72 | .click(function () { 73 | rollDice($("#diceSelect").val()); 74 | }) 75 | ) 76 | .append($(``) 77 | .click(function () { 78 | if (diceManagerWindow.isVisible()) { 79 | diceManagerWindow.close(); 80 | } 81 | else { 82 | diceManagerWindow.open(); 83 | } 84 | }) 85 | ) 86 | 87 | diceWindow.panels[2].panel.append($(``)); 88 | 89 | let oldWidth = $("#qpOptionContainer").width(); 90 | $("#qpOptionContainer").width(oldWidth + 35); 91 | $("#qpOptionContainer > div").append($(`
    `) 92 | .click(function () { 93 | if(diceWindow.isVisible()) { 94 | diceWindow.close(); 95 | } 96 | else { 97 | diceWindow.open(); 98 | } 99 | }) 100 | .popover({ 101 | placement: "bottom", 102 | content: "Dice Roller", 103 | trigger: "hover" 104 | }) 105 | ); 106 | } 107 | 108 | function createDiceManagerWindow() { 109 | diceManagerWindow = new AMQWindow({ 110 | id: "diceManagerWindow", 111 | title: "Dice Editor", 112 | width: 500, 113 | height: 350, 114 | minWidth: 500, 115 | minHeight: 350, 116 | draggable: true, 117 | resizable: true, 118 | zIndex: 1052 119 | }); 120 | 121 | diceManagerWindow.addPanel({ 122 | width: 200, 123 | height: 1.0, 124 | id: "diceKeysManager" 125 | }); 126 | 127 | diceManagerWindow.addPanel({ 128 | width: "calc(100% - 200px)", 129 | height: 50, 130 | position: { 131 | x: 200, 132 | y: 0 133 | }, 134 | id: "diceValuesManager" 135 | }); 136 | 137 | diceManagerWindow.addPanel({ 138 | width: "calc(100% - 200px)", 139 | height: "calc(100% - 50px)", 140 | position: { 141 | x: 200, 142 | y: 50 143 | }, 144 | scrollable: { 145 | x: false, 146 | y: true 147 | } 148 | }); 149 | 150 | diceManagerWindow.panels[0].panel 151 | .append($(`
    `) 152 | .append($(``) 153 | .change(function () { 154 | displayValues($(this).val()); 155 | }) 156 | ) 157 | .append($(``) 158 | .click(function () { 159 | let selectedKey = $("#diceManagerSelect").val(); 160 | delete diceRolls[selectedKey]; 161 | $(`#diceManagerSelect > option`).filter((index, elem) => $(elem).attr("value") === selectedKey).remove(); 162 | $(`#diceSelect > option`).filter((index, elem) => $(elem).attr("value") === selectedKey).remove(); 163 | displayValues($("#diceManagerSelect").val()); 164 | saveDice(); 165 | }) 166 | ) 167 | ) 168 | .append($(`
    `) 169 | .append($(``)) 170 | .append($(``) 171 | .click(function () { 172 | let newKey = $("#diceManagerKeyInput").val(); 173 | diceRolls[newKey] = []; 174 | $("#diceSelect").append($(``).attr("value", newKey)); 175 | $("#diceManagerSelect").append($(``).attr("value", newKey)); 176 | $("#diceManagerSelect").val(newKey); 177 | displayValues(newKey); 178 | saveDice(); 179 | }) 180 | ) 181 | .append($(``) 182 | .click(function () { 183 | let oldKey = $("#diceManagerSelect").val(); 184 | let newKey = $("#diceManagerKeyInput").val(); 185 | if (oldKey === newKey) { 186 | return; 187 | } 188 | diceRolls[newKey] = diceRolls[oldKey]; 189 | delete diceRolls[oldKey]; 190 | $(`#diceManagerSelect > option`).filter((index, elem) => $(elem).attr("value") === oldKey).attr("value", newKey).text(newKey); 191 | $(`#diceSelect > option`).filter((index, elem) => $(elem).attr("value") === oldKey).attr("value", newKey).text(newKey); 192 | displayValues(newKey); 193 | saveDice(); 194 | }) 195 | ) 196 | ) 197 | 198 | for (let key in diceRolls) { 199 | $("#diceManagerSelect").append($(``).attr("value", key)); 200 | } 201 | 202 | diceManagerWindow.panels[1].panel 203 | .append($(`
    `) 204 | .append($(``)) 205 | .append($(``) 206 | .click(function () { 207 | let newValue = $("#diceManagerValueInput").val(); 208 | let selectedKey = $("#diceManagerSelect").val(); 209 | if (selectedKey !== null) { 210 | diceRolls[selectedKey].push(newValue); 211 | displayValue(newValue); 212 | saveDice(); 213 | } 214 | }) 215 | ) 216 | ) 217 | 218 | displayValues($("#diceManagerSelect").val()); 219 | } 220 | 221 | function rollDice(key) { 222 | if (key === null) { 223 | $("#diceChoice").text(""); 224 | return; 225 | } 226 | if (diceRolls[key].length === 0) { 227 | $("#diceChoice").text(""); 228 | return; 229 | } 230 | let randomIdx = Math.floor(Math.random() * diceRolls[key].length); 231 | let randomChoice = diceRolls[key][randomIdx]; 232 | $("#diceChoice").text(randomChoice); 233 | } 234 | 235 | function clearValues() { 236 | diceManagerWindow.panels[2].panel.children().remove(); 237 | } 238 | 239 | function displayValues(key) { 240 | clearValues(); 241 | if (key === null) { 242 | return; 243 | } 244 | for (let value of diceRolls[key]) { 245 | displayValue(value); 246 | } 247 | } 248 | 249 | function displayValue(newValue) { 250 | diceManagerWindow.panels[2].panel 251 | .append($(`
    `) 252 | .append($(``) 253 | .text(newValue) 254 | ) 255 | .append($(``) 256 | .click(function () { 257 | let selectedKey = $("#diceManagerSelect").val(); 258 | let index = diceRolls[selectedKey].indexOf(newValue); 259 | if (index !== -1) diceRolls[selectedKey].splice(index, 1); 260 | $(this).parent().remove(); 261 | saveDice(); 262 | }) 263 | ) 264 | ) 265 | } 266 | 267 | function saveDice() { 268 | localStorage.setItem("amqDice", JSON.stringify(diceRolls)); 269 | } 270 | 271 | function loadDice() { 272 | let savedDice = localStorage.getItem("amqDice"); 273 | if (savedDice !== null) { 274 | diceRolls = JSON.parse(savedDice); 275 | } 276 | } 277 | 278 | function setup() { 279 | loadDice(); 280 | createDiceManagerWindow(); 281 | createDiceWindow(); 282 | 283 | $(".modal").on("show.bs.modal", () => { 284 | diceWindow.setZIndex(1025); 285 | diceManagerWindow.setZIndex(1022); 286 | }); 287 | 288 | $(".modal").on("hidden.bs.modal", () => { 289 | diceWindow.setZIndex(1055); 290 | diceManagerWindow.setZIndex(1052); 291 | }); 292 | 293 | $(".slCheckbox label").hover(() => { 294 | diceWindow.setZIndex(1025); 295 | diceManagerWindow.setZIndex(1022); 296 | }, () => { 297 | diceWindow.setZIndex(1055); 298 | diceManagerWindow.setZIndex(1052); 299 | }); 300 | 301 | AMQ_addScriptData({ 302 | name: "Dice Roller UI", 303 | author: "TheJoseph98", 304 | version: version, 305 | link: "https://github.com/joske2865/AMQ-Scripts/raw/master/amqDiceRollerUI.user.js", 306 | description: ` 307 |

    Adds a window where you can select a dice and roll a random value associated with that dice

    308 |

    This window can be opened by clicking the cube icon at the top right while in quiz (there is no dice icon available, blame Egerod)

    309 | 310 | 311 | ` 312 | }); 313 | 314 | AMQ_addStyle(` 315 | #qpDiceRoll { 316 | width: 30px; 317 | margin-right: 5px; 318 | } 319 | #diceWindow .customWindowPanel { 320 | border-bottom: 1px solid #6d6d6d; 321 | } 322 | #diceSelectContainer { 323 | line-height: 50px; 324 | text-align: center; 325 | } 326 | #diceSelect { 327 | color: black; 328 | } 329 | #diceRollButtonContainer { 330 | line-height: 50px; 331 | text-align: center; 332 | } 333 | #diceRollButtonContainer > button { 334 | margin: 0px 5px; 335 | } 336 | #diceChoiceContainer { 337 | display: table; 338 | text-align: center; 339 | } 340 | #diceChoice { 341 | display: table-cell; 342 | vertical-align: middle; 343 | font-size: 16px; 344 | } 345 | #diceKeysManager { 346 | border-right: 1px solid #6d6d6d; 347 | } 348 | #diceValuesManager { 349 | border-bottom: 1px solid #6d6d6d; 350 | } 351 | .diceManagerInputContainer { 352 | margin: 10px 0px 20px 0px; 353 | text-align: center; 354 | } 355 | .diceManagerInputContainer > button { 356 | width: 66px; 357 | padding: 6px; 358 | margin: 5px 5px; 359 | } 360 | #diceManagerSelect { 361 | color: black; 362 | width: 170px; 363 | margin: 5px 0px; 364 | } 365 | #diceManagerKeyInput { 366 | color: black; 367 | width: 170px; 368 | margin: 5px; 369 | } 370 | #diceValueInputContainer { 371 | text-align: center; 372 | padding: 8px; 373 | } 374 | #diceManagerValueInput { 375 | color: black; 376 | } 377 | #diceValueInputContainer > * { 378 | margin: 0px 6px; 379 | } 380 | .diceManagerValueContainer { 381 | height: 31px; 382 | margin: 10px 10px 0px 10px; 383 | } 384 | .diceManagerValueContainer > span { 385 | font-size: 22px; 386 | } 387 | .diceManagerValueContainer > button { 388 | float: right; 389 | padding: 6px; 390 | } 391 | `); 392 | } 393 | -------------------------------------------------------------------------------- /common/amqWindows.js: -------------------------------------------------------------------------------- 1 | // AMQ Window Script 2 | // This code is fetched automatically 3 | // Do not attempt to add it to tampermonkey 4 | 5 | if (typeof Listener === "undefined") return; 6 | windowSetup(); 7 | 8 | class AMQWindow { 9 | constructor(data) { 10 | this.id = data.id === undefined ? "" : data.id; 11 | this.title = data.title === undefined ? "Window" : data.title; 12 | this.resizable = data.resizable === undefined ? false : data.resizable; 13 | this.draggable = data.draggable === undefined ? false : data.draggable; 14 | this.width = data.width === undefined ? 200 : data.width; 15 | this.height = data.height === undefined ? 300 : data.height; 16 | this.minWidth = data.minWidth === undefined ? 200 : data.minWidth; 17 | this.minHeight = data.minHeight === undefined ? 300: data.minHeight; 18 | this.position = data.position === undefined ? {x: 0, y: 0} : data.position; 19 | this.closeHandler = data.closeHandler === undefined ? function () {} : data.closeHandler; 20 | this.zIndex = data.zIndex === undefined ? 1060 : data.zIndex; 21 | this.resizers = null; 22 | this.panels = []; 23 | 24 | this.window = $("
    ") 25 | .addClass("customWindow") 26 | .addClass(data.class === undefined ? "" : data.class) 27 | .attr("id", this.id) 28 | .css("position", "absolute") 29 | .css("z-index", this.zIndex.toString()) 30 | .offset({ 31 | top: this.position.y !== undefined ? this.position.y : 0, 32 | left: this.position.x !== undefined ? this.position.x : 0 33 | }) 34 | .height(this.height) 35 | .width(this.width) 36 | 37 | this.content = $(`
    `); 38 | 39 | this.header = $("
    ") 40 | .addClass("modal-header customWindowHeader") 41 | .addClass(this.draggable === true ? "draggableWindow" : "") 42 | .append($(`
    `) 43 | .click(() => { 44 | this.close(this.closeHandler); 45 | }) 46 | ) 47 | .append($("

    ") 48 | .addClass("modal-title") 49 | .text(this.title) 50 | ) 51 | 52 | this.body = $(``) 53 | .addClass(this.resizable === true ? "resizableWindow" : "") 54 | .height(this.height - 75); 55 | 56 | if (this.resizable === true) { 57 | this.resizers = $( 58 | `
    59 |
    60 |
    61 |
    62 |
    63 |
    ` 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 | 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 | --------------------------------------------------------------------------------