AutoMudae

Automates the use of Mudae bot in Discord

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         AutoMudae
// @description  Automates the use of Mudae bot in Discord
// @version      0.8.5
// @author       Nxve
// @license      GNU GPLv3
// @namespace    https://github.com/Nxve/AutoMudae
// @supportURL   https://github.com/Nxve/AutoMudae/issues
// @match        https://discord.com/channels/*
// @exclude      https://discord.com/channels/@me
// @run-at       document-start
// @icon         https://icons.duckduckgo.com/ip2/discord.com.ico
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_info
// ==/UserScript==

(function () {
    const window = unsafeWindow;
    const localStorage = window.localStorage;

    //// Logger
    const _logger = {
        _preffix: '%c[AUTO MUDAE]',
        _symbols: { error: '[!]', info: '[i]', log: '[*]', plus: '[+]', debug: '[!]', warn: '[!]' },
        _color: { error: 'red', info: 'cyan', log: 'white', plus: 'lime', debug: 'cyan', warn: 'gold' },
        _history: [],
        _lastMessageHash: null,
        _hash: s => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0),
        _print: function (type, ...etc) {
            const hash = [...arguments].toString();
            // if (hash === this._lastMessageHash) return;

            console.log(`${this._preffix}%c${this._symbols[type]}`, 'background: black; color: magenta;', `background: black; color: ${this._color[type]}`, ...etc);

            if (type !== 'debug') this._history.push([type, [...etc]]);
            this._lastMessageHash = hash;
        },
        _reprompt: function () {
            this._history.forEach(log => this[log[0]](...log[1]));
        }
    };

    const logger = {};

    ['error', 'info', 'log', 'plus', 'debug', 'warn'].forEach(method => {
        logger[method] = function () { this._print(method, ...arguments) };
    });

    /// I use prototype here to prevent exposing private properties in DevTools.
    Object.setPrototypeOf(logger, _logger);

    window.logger = logger;

    //// ENUM
    const E = {};

    E.DISCORD_INFO = {
        CHANNEL_ID: 'channel_id',
        GUILD_ID: 'guild_id',
        SESSION_ID: 'session_id'
    };
    
    E.AUTOMUDAE_STATE = {
        INJECT: 'inject',
        SETUP: 'setup',
        ERROR: 'error',
        IDLE: 'idle',
        RUN: 'run',
    };
    
    E.MUDAE_INFO = {
        ROLLS_MAX: 'rolls_max',
        ROLLS_LEFT: 'rolls_left',
        POWER: 'power',
        CAN_RT: 'can_rt',
        CAN_MARRY: 'can_marry',
        CONSUMPTION: 'kakera_consumption'
    };

    E.TOAST = {
        INFO: 'info',
        WARN: 'warn',
        CRITICAL: 'critical',
        KAKERA: 'kakera',
        CHARCLAIM: 'charclaim',
        SOULMATE: 'soulmate'
    };
    
    E.EMOJI = {
        '💓': '%F0%9F%92%93',
        '💕': '%F0%9F%92%95',
        '💖': '%F0%9F%92%96',
        '💗': '%F0%9F%92%97',
        '💘': '%F0%9F%92%98',
        '❤️': '%E2%9D%A4%EF%B8%8F',
        '❣️': '%E2%9D%A3%EF%B8%8F',
        '💞': '%F0%9F%92%9E',
        '♥️': '%E2%99%A5%EF%B8%8F'
    };

    E.EMOJI_KAKERA = {
        kakeraP: 'kakeraP%3A609264156347990016',
        kakera: 'kakera%3A469791929106956298',
        kakeraT: 'kakeraT%3A609264180851376132',
        kakeraG: 'kakeraG%3A609264166381027329',
        kakeraY: 'kakeraY%3A605112931168026629',
        kakeraO: 'kakeraO%3A605112954391887888',
        kakeraR: 'kakeraR%3A605112980295647242',
        kakeraW: 'kakeraW%3A608192076286263297',
        kakeraL: 'kakeraL%3A815961697918779422',
    };
    
    E.KAKERA = {
        PURPLE: 'kakeraP',
        BLUE: 'kakera',
        CYAN: 'kakeraT',
        GREEN: 'kakeraG',
        YELLOW: 'kakeraY',
        ORANGE: 'kakeraO',
        RED: 'kakeraR',
        RAINBOW: 'kakeraW',
        LIGHT: 'kakeraL',
    };

    E.GMVALUE = {
        PREFERENCES: 'preferences',
        VERSION: 'version',
        TOKENLIST: 'tokenlist'
    };

    E.PREFERENCES = {
        KAKERA: 'kakera',
        MENTIONS: 'mentions',
        ROLL: 'roll',
        SOUND: 'sound',
        EXTRA: 'extra'
    };

    E.INFO_FIELD = {
        KAKERA: 'kakera',
        COLLECTED_CHARACTERS: 'collected-characters',
        ROLLS_LEFT: 'rolls-left',
        ROLLS_MAX: 'rolls-max',
        POWER: 'power',
        POWER_CONSUMPTION: 'consumption',
        CAN_MARRY: 'marry',
        CAN_RT: 'rt'
    };

    E.SLASH_COMMANDS = {
        "wx": { version: "832172261968314389", id: "832172261968314388" },
        "wa": { version: "832172151729422418", id: "832172151729422417" },
        "wg": { version: "832172216665374751", id: "832172216665374750" },
        "hx": { version: "832172373536669707", id: "832172373536669706" },
        "ha": { version: "832172457028747337", id: "832172457028747336" },
        "hg": { version: "832172416192872459", id: "832172416192872458" },
    };

    //// SOUND
    const audioCtx = new AudioContext();

    function beep(gain, hz, ms, times = 1){
        for (let i = 0; i < times; i++) {
            const v = audioCtx.createOscillator();
            const u = audioCtx.createGain();
            v.connect(u);
            v.frequency.value = hz;
            v.type = "square";
            u.connect(audioCtx.destination);
            u.gain.value = gain * 0.01;
            const durationInSeconds = ms * .001;
            v.start(audioCtx.currentTime + i * (durationInSeconds*1.5));
            v.stop(audioCtx.currentTime + durationInSeconds + i * (durationInSeconds*1.5));
        }
    };

    const SOUND = {
        foundCharacter: () => {beep(5, 400, 100, 1)},
        marry: () => {beep(10, 600, 100, 1)},
        critical: () => {beep(15, 70, 80, 6)},
        lastResetNoRolls: () => {beep(10, 60, 250, 2)},
        newSoulmate: () => {beep(10, 600, 100, 2)}
    };

    //// CSS
    const CSS = {};

    CSS.decorators = `
    li[id^=chat-message]:is(.plus, .critical){
        position: relative;
    }

    li[id^=chat-message]:is(.plus, .critical)::after{
        position: absolute;
        bottom: 0;
        width: 22px;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    li[id^=chat-message].plus{
        background-color: hsl(138deg 100% 50% / 10%);
    }

    li[id^=chat-message].plus::after {
        content: '+';
        background-color: hsl(109deg 45% 18%);
        color: lime;
    }

    li[id^=chat-message].critical{
        background-color: hsl(0deg 100% 50% / 10%);
    }

    li[id^=chat-message].critical::after {
        content: '!';
        background-color: hsl(0deg 45% 18%);
        color: red;
    }
    `;

    CSS.general = `
    ::-webkit-scrollbar {
        width: 2px;
    }
    
    ::-webkit-scrollbar-thumb {
            background-color: rgba(0, 0, 0, 0.8);
    }

    .automudae-hide, .automudae-hide *, .automudae-hide::before, .automudae-hide::after {
        display: none !important;
    }
    `;

    CSS.stateText = `
    #automudae-state {
        display: flex;
        gap: 10px;
        margin-right: 10px;
        color: var(--text-normal);
    }
    `;

    CSS.runButton = `
    #automudae-run-button {
        display: flex;
        align-items: center;
        gap: 4px;
        padding: 1px 5px;
        margin-right: 20px;
        background-color: var(--button-outline-brand-background-active);
        cursor: pointer;
        transition: 200ms;
    }
    
    #automudae-run-button:hover {
        background-color: var(--button-outline-brand-background-hover);
        transform: scale(1.1);
    }
    
    #automudae-run-button::before {
        content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='15' fill='%23DCDDDE' viewBox='0 0 16 12'%3E%3Cpath d='m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z'/%3E%3C/svg%3E");
    }
    
    #automudae-run-button.running::before {
        content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23DCDDDE' viewBox='0 0 16 12'%3E%3Cpath d='M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z'/%3E%3C/svg%3E");
    }
    
    #automudae-run-button::after {
        content: "Run";
    }
    
    #automudae-run-button.running::after {
        content: "Pause";
    }
    `;

    CSS.injectionsAndError = `
    #automudae-injections-wrapper,
    #automudae-error {
        position: absolute;
        inset: auto 0;
        top: 8px;
        width: fit-content;
        margin-inline: auto;
        padding: 5px;
        z-index: 9999;
    }

    #automudae-injections-wrapper {
        display: flex;
        gap: 10px;
        padding: 0;
    }

    #automudae-injections-wrapper > div {
        padding: 5px;
        background-color: var(--button-outline-brand-background-active);
        color: var(--text-normal);
        font-weight: 500;
        cursor: pointer;
        transition: 200ms;
    }

    #automudae-injections-wrapper > div:hover {
        background-color: var(--button-outline-brand-background-hover);
        transform: scale(1.1);
    }

    #automudae-error {
        background-color: var(--button-danger-background);
        color: white;
        animation: popIn 150ms forwards;
        transform: scale(0);
    }

    @keyframes popIn {
        to {
            top: 40px;
            transform: scale(1);
        }
    }
    `;

    CSS.sidePanels = `
    [id^=automudae-panel] {
        background-color: var(--background-primary);
        font-weight: 500;
        display: flex;
        flex-direction: column;
        gap: 10px;
        transition: 500ms;
        overflow: hidden;
        height: fit-content;
    }

    [id^=automudae-panel] > * {
        background-color: var(--interactive-muted);
    }

    [id^=automudae-panel] :is(h1, h2) {
        background-color: var(--background-tertiary);
        color: var(--text-normal);
        display: flex;
        align-items: center;
        justify-content: center;
    }

    [id^=automudae-panel] h1 {
        font-size: large;
        height: 1.5rem;
        background-color: var(--button-outline-brand-background-active);
        cursor: pointer;
    }

    [id^=automudae-panel] h1:hover {
        background-color: var(--button-outline-brand-background-hover) !important;
    }

    [id^=automudae-panel] h2 {
        font-size: medium;
        height: 1rem;
    }

    [id^=automudae-panel] textarea {
        font-weight: 900;
        max-height: 100px;
    }

    [id^=automudae-panel] span {
        font-size: small;
        color: var(--text-normal);
    }

    [id^=automudae-panel] ul {
        width: 100%;
        font-size: small;
        color: var(--text-normal);
        max-height: 10rem;
        overflow-x: clip;
        overflow-y: auto;
    }

    [id^=automudae-panel] li:nth-child(odd) {
        background-color: var(--background-primary);
    }

    .automudae-section {
        margin-bottom: 5px;
    }

    .automudae-section-body {
        display: flex;
        padding: 4px;
        flex-wrap: wrap;
    }

    .automudae-section-body > div {
        display: flex;
        padding-inline: 3px;
        border-radius: 5px;
        align-items: center;
    }

    .automudae-section-body > div:hover {
        background-color: var(--button-secondary-background-hover);
    }

    #automudae-panel-info .automudae-section-body {
        flex-direction: column;
        gap: 8px;
    }

    #automudae-section-kakera > div {
        justify-content: space-between;
    }

    #automudae-section-kakera > div > div {
        flex-direction: column;
        padding: 0;
    }

    #automudae-section-status .automudae-section-body {
        padding: 0;
    }

    .automudae-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
    }

    .automudae-row > div {
        display: flex;
        align-items: center;
    }
    
    .automudae-row-expandable {
        padding: 3px;
        display: block !important;
    }

    .automudae-row-expandable > div:not(:first-child) {
        margin-top: 2px;
        background-color: var(--background-primary);
        max-height: 0px;
        overflow: hidden;
        transition: max-height 300ms linear;
    }
    
    .automudae-row-expandable:hover > div:not(:first-child) {
        max-height: 300px;
    }
    
    .automudae-row-expandable > div:not(:first-child) > .automudae-row:hover {
        background-color: var(--background-accent);
    }

    [id^=automudae-panel] > div {
        max-height: 600px;
        transition: max-height 400ms cubic-bezier(0, 1, 1, 1);
    }

    [id^=automudae-panel].collapsed {
        gap: 0px;
    }
    
    [id^=automudae-panel].collapsed > div {
        max-height: 0px;
    }

    [data-requirerestart] {
        position: relative;
    }
    
    [data-requirerestart]::before {
        content: '*';
        color: yellow;
        position: absolute;
        left: 0px;
    }
    
    [data-requirerestart]:hover::before {
        content: '* Require restart to apply changes!';
        position: absolute;
        bottom: 20px;
        background-color: var(--background-tertiary);
        font-size: x-small;
        padding: 2px 10px;
        color: yellow;
        border-radius: 5px;
        pointer-events: none;
    }
    `;

    CSS.toasts = `
    #automudae-toasts-wrapper {
        position: absolute;
        right: 15px;
        width: 37%;
        height: 97%;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        align-items: flex-end;
        gap: 8px;
        z-index: 9;
    }
    
    .automudae-toast {
        background-color: white;
        padding: 5px;
        font-weight: 500;
        animation: slide-in-blurred-left 0.6s cubic-bezier(0.230, 1.000, 0.320, 1.000) both;
    }
    
    .automudae-toast.info {
        --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%235865f2' viewBox='0 0 16 16'%3E%3Cpath d='M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm8.93 4.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E");
        background-color: var(--button-outline-brand-border);
    }
    
    .automudae-toast.kakera {
        --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%23dee0fc' viewBox='0 0 16 16'%3E%3Cpath d='M3.1.7a.5.5 0 0 1 .4-.2h9a.5.5 0 0 1 .4.2l2.976 3.974c.149.185.156.45.01.644L8.4 15.3a.5.5 0 0 1-.8 0L.1 5.3a.5.5 0 0 1 0-.6l3-4zm11.386 3.785-1.806-2.41-.776 2.413 2.582-.003zm-3.633.004.961-2.989H4.186l.963 2.995 5.704-.006zM5.47 5.495 8 13.366l2.532-7.876-5.062.005zm-1.371-.999-.78-2.422-1.818 2.425 2.598-.003zM1.499 5.5l5.113 6.817-2.192-6.82L1.5 5.5zm7.889 6.817 5.123-6.83-2.928.002-2.195 6.828z'/%3E%3C/svg%3E");
        background-color: var(--brand-experiment-200);
    }
    
    .automudae-toast.charclaim {
        --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%2346c46e' viewBox='0 0 16 16'%3E%3Cpath d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/%3E%3C/svg%3E");
        background-color: var(--text-positive);
    }
    
    .automudae-toast.soulmate {
        --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='violet' viewBox='0 0 16 16'%3E%3Cpath d='M7.657 6.247c.11-.33.576-.33.686 0l.645 1.937a2.89 2.89 0 0 0 1.829 1.828l1.936.645c.33.11.33.576 0 .686l-1.937.645a2.89 2.89 0 0 0-1.828 1.829l-.645 1.936a.361.361 0 0 1-.686 0l-.645-1.937a2.89 2.89 0 0 0-1.828-1.828l-1.937-.645a.361.361 0 0 1 0-.686l1.937-.645a2.89 2.89 0 0 0 1.828-1.828l.645-1.937zM3.794 1.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387A1.734 1.734 0 0 0 4.593 5.69l-.387 1.162a.217.217 0 0 1-.412 0L3.407 5.69A1.734 1.734 0 0 0 2.31 4.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387A1.734 1.734 0 0 0 3.407 2.31l.387-1.162zM10.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732L9.1 2.137a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L10.863.1z'/%3E%3C/svg%3E");
        background-color: violet;
    }
    
    .automudae-toast.warn{
        --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%23faa81a' class='bi bi-exclamation-diamond-fill' viewBox='0 0 16 16'%3E%3Cpath d='M9.05.435c-.58-.58-1.52-.58-2.1 0L.436 6.95c-.58.58-.58 1.519 0 2.098l6.516 6.516c.58.58 1.519.58 2.098 0l6.516-6.516c.58-.58.58-1.519 0-2.098L9.05.435zM8 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%3E%3C/svg%3E");
        background-color: var(--text-warning);
    }
    
    .automudae-toast.critical{
        --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%23ed4245' class='bi bi-exclamation-diamond-fill' viewBox='0 0 16 16'%3E%3Cpath d='M9.05.435c-.58-.58-1.52-.58-2.1 0L.436 6.95c-.58.58-.58 1.519 0 2.098l6.516 6.516c.58.58 1.519.58 2.098 0l6.516-6.516c.58-.58.58-1.519 0-2.098L9.05.435zM8 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%3E%3C/svg%3E");
        background-color: var(--status-danger);
    }

    .automudae-toast.link {
        cursor: alias;
    }
    
    .automudae-toast.missing {
        animation: wobble-hor-bottom 0.8s both;
    }

    .automudae-toast:hover::before {
        --width: 18px;
        --height: 100;
        content: var(--svg);
        position: absolute;
        left: calc(calc(-1 * var(--width)) - 5px);
        top: calc(.5% * calc(100 - var(--height)));
        height: calc(1% * var(--height));
        width: var(--width);
        display: flex;
        align-items: center;
        justify-content: center;
        animation: flipHorz 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
        pointer-events: none;
    }
    
    .automudae-toast:nth-last-child(12) {
        opacity: 0.6;
    }
    .automudae-toast:nth-last-child(13) {
        opacity: 0.5;
    }
    .automudae-toast:nth-last-child(14) {
        opacity: 0.4;
    }
    .automudae-toast:nth-last-child(15) {
        opacity: 0.3;
    }
    .automudae-toast:nth-last-child(16) {
        opacity: 0.2;
    }
    .automudae-toast:nth-last-child(17) {
        opacity: 0.1;
    }
    .automudae-toast:nth-last-child(n+18) {
        opacity: 0;
    }

    @keyframes flipHorz {
        0% {
            transform: rotateY(0);
        }
        100% {
            transform: rotateY(-360deg);
        }
    }

    @keyframes slide-in-blurred-left {
        0% {
            transform: translateX(-1000px) scaleX(2.5) scaleY(0.2);
            transform-origin: 100% 50%;
            filter: blur(40px);
        }
        100% {
          transform: translateX(0) scaleY(1) scaleX(1);
          transform-origin: 50% 50%;
          filter: blur(0);
        }
    }

    @keyframes wobble-hor-bottom {
        0%,
        100% {
            transform: translateX(0%);
            transform-origin: 50% 50%;
        }
        15% {
            transform: translateX(-30px) rotate(-6deg);
        }
        30% {
            transform: translateX(15px) rotate(6deg);
        }
        45% {
            transform: translateX(-15px) rotate(-3.6deg);
        }
        60% {
            transform: translateX(9px) rotate(2.4deg);
        }
        75% {
            transform: translateX(-6px) rotate(-1.2deg);
        }
      }
    `;

    CSS.tokenList = `
    #automudae-tokenlist-wrapper {
        position: absolute;
        width: 100%;
        height: 100%;
        z-index: 9999;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    #automudae-tokenlist {
        background-color: var(--background-tertiary);
        width: 400px;
        height: 80vh;
        display: flex;
        flex-direction: column;
        font-weight: 400;
        color: white;
        justify-content: space-between;
        align-items: center;
        padding: 5px;
    }

    #automudae-tokenlist-accept {
        width: 100%;
        text-align: center;
        padding-block: 5px;
        background-color: var(--status-positive-background);
        transition: 200ms;
        cursor: pointer;
    }

    #automudae-tokenlist h3 {
        font-size: x-large;
        position: relative;
    }

    #automudae-tokenlist h3::after {
        content: '';
        position: absolute;
        left: 0px;
        bottom: -4px;
        height: 2px;
        width: 100%;
        background-color: var(--background-modifier-accent);
    }

    #automudae-tokenlist ul {
        width: 400px;
        height: 60vh;
        background-color: var(--background-floating);
        display: flex;
        flex-direction: column;
        gap: 5px;
        overflow-y: overlay;
    }

    #automudae-tokenlist-accept:hover {
        background-color: var(--status-positive);
    }

    #automudae-tokenlist-controls {
        display: flex;
        flex-direction: row;
        justify-content: flex-end;
        gap: 1rem;
        padding-right: 10px;
    }

    #automudae-tokenlist-controls > div {
        padding: 3px;
        font-size: small;
        cursor: pointer;
        transition: 200ms;
    }

    #automudae-tokenlist-controls > div:hover {
        background-color: white;
        color: black;
    }

    #automudae-tokenlist input {
        width: 99%;
        border: none;
        background: none;
        color: var(--text-normal);
    }

    #automudae-tokenlist li:nth-child(odd) {
        background-color: var(--background-accent);
    }

    #automudae-tokenlist li:nth-child(even) {
        background-color: var(--background-modifier-selected);
    }

    #automudae-tokenlist li {
        position: relative;
    }
    
    #automudae-tokenlist li:hover:not(:focus-within) input {
        opacity: .1;
    }
    
    #automudae-tokenlist li:hover:not(:focus-within)::before {
        content: attr(data-username);
        position: absolute;
        height: 100%;
        display: flex;
        align-items: center;
        margin-left: 5px;
    }
    `;

    GM_addStyle(Object.values(CSS).join(' '));

    //// Utils
    const pickRandom = (arr) => arr[arr.length * Math.random() | 0];
    const getLast = (arr) => arr[arr.length - 1];

    //// DOM Elements
    const DOM = {
        el_ChannelList: null,
        el_MemberList: null,
        el_Chat: null,
        el_ChatWrapper: null,
        el_InjectionsWrapper: null,
        el_RunButton: null,
        el_StateSpan: null,
        el_ToastsWrapper: null,
        el_ErrorPopup: null
    };

    //// CONSTS
    const INTERVAL_SEND_MESSAGE = 1500;
    const INTERVAL_ROLL = 2000;
    const INTERVAL_THINK = 200;
    const MUDAE_USER_ID = '432610292342587392';

    //// Discord Data & Utils
    const Discord = {
        info: new Map(), /// Map<E.DISCORD_INFO, >()
        lastMessageTime: 0,
        cdSendMessage: 0,
        nonce: Math.floor(Math.random() * 1000000),

        Message: {
            getDate: (el_Message) => {
                const messageDate = new Date(this.el_Message.querySelector("time[id^='message-timestamp']")?.dateTime);

                if (messageDate.toString() === "Invalid Date") {
                    logger.error("Couldn't retrieve timestamp for this Discord message:", el_Message);
                    return;
                }

                return messageDate;
            },

            getAuthorId: (el_Message) => {
                let el_TargetMessage = el_Message;
                let el_Avatar;

                while (!el_Avatar) {
                    el_Avatar = el_TargetMessage.querySelector(`img[class^='avatar']`);
                    if (el_Avatar) break;

                    el_TargetMessage = el_TargetMessage.previousElementSibling;

                    while (el_TargetMessage && el_TargetMessage.tagName !== "LI") {
                        el_TargetMessage = el_TargetMessage.previousElementSibling;
                    }

                    if (!el_Avatar && !el_TargetMessage) return logger.error("Couldn't get avatar for this Discord message:", el_Message);
                }

                const match = /avatars\/(\d+)\//.exec(el_Avatar.src);

                if (match) return match[1];
            },

            getId: (el_Message) => getLast(el_Message.id.split("-")),

            isFromMudae: function (el_Message) {
                return this.getAuthorId(el_Message) === MUDAE_USER_ID
            },

            isFromMe: function (el_Message) {
                return AutoMudae.users.find(user => user.id === this.getAuthorId(el_Message), this);
            }
        }

    };

    /// AutoMudae
    class MudaeUser {
        id
        username
        avatar
        token
        nick
        info
        sendTUTimer

        constructor(token, id, username, avatar) {
            this.token = token;
            this.info = new Map();

            return new Promise(async (resolve) => {
                if (id){
                    this.id = id;
                    this.username = username;
                    this.avatar = avatar;
    
                    await this.fetchNick();
                    return resolve(this);
                }

                fetch("https://discord.com/api/v9/users/@me", { "headers": { "authorization": token } })
                .then(response => response.json())
                .then(async (data) => {
                    this.id = data.id;
                    this.username = data.username;
                    this.avatar = data.avatar;

                    await this.fetchNick();
                })
                .catch(err => logger.error(`Couldn't retrieve info for some user.`, err))
                .finally(() => resolve(this));
            });
        }

        async fetchNick(){
            return new Promise(resolve => {
                const guildId = window.location.pathname.split("/")[2];

                fetch(`https://discord.com/api/v9/users/${this.id}/profile?guild_id=${guildId}`, {
                    "headers": {
                        "authorization": this.token
                    }
                })
                .then(response => response.json())
                .then(data => {
                    const { guild_member: { nick } } = data;
                    this.nick = nick;
                })
                .catch(err => logger.error(`Couldn't retrieve the nick for user [${this.username}]`, err))
                .finally(() => resolve());
            });
        }

        hasNeededInfo() {
            return [E.MUDAE_INFO.ROLLS_MAX, E.MUDAE_INFO.ROLLS_LEFT, E.MUDAE_INFO.POWER, E.MUDAE_INFO.CAN_RT, E.MUDAE_INFO.CAN_MARRY, E.MUDAE_INFO.CONSUMPTION].every(info => this.info.has(info), this);
        }

        send(content) {
            const now = performance.now();

            if (now - Discord.cdSendMessage < INTERVAL_SEND_MESSAGE) return;

            fetch(`https://discord.com/api/v9/channels/${Discord.info.get(E.DISCORD_INFO.CHANNEL_ID)}/messages`, {
                "method": "POST",
                "headers": {
                    "authorization": this.token,
                    "content-type": "application/json"
                },
                "body": `{"content":"${content || '?'}","nonce":"${++Discord.nonce}","tts":false}`
            });

            Discord.cdSendMessage = now;
        }

        react(el_Message, E_EMOJI = E.EMOJI["💓"]) {
            fetch(`https://discord.com/api/v9/channels/${Discord.info.get(E.DISCORD_INFO.CHANNEL_ID)}/messages/${Discord.Message.getId(el_Message)}/reactions/${E_EMOJI}/%40me`, {
                "method": "PUT",
                "headers": {
                    "authorization": this.token,
                }
            });
        }

        setTUTimer(ms) {
            if (this.sendTUTimer) clearTimeout(this.sendTUTimer);

            this.sendTUTimer = setTimeout((user) => { user.send("$tu") }, ms, this);
        }

        roll() {
            const rollPreferences = AutoMudae.preferences.get(E.PREFERENCES.ROLL);

            const command = E.SLASH_COMMANDS[rollPreferences.type];

            fetch("https://discord.com/api/v9/interactions", {
                "method": "POST",
                "headers": {
                    "authorization": this.token,
                    "content-type": "multipart/form-data; boundary=----BDR",
                },
                "body": `------BDR\r\nContent-Disposition: form-data; name="payload_json"\r\n\r\n{"type":2,"application_id":"${MUDAE_USER_ID}","guild_id":"${Discord.info.get(E.DISCORD_INFO.GUILD_ID)}","channel_id":"${Discord.info.get(E.DISCORD_INFO.CHANNEL_ID)}","session_id":"${Discord.info.get(E.DISCORD_INFO.SESSION_ID)}","data":{"version":"${command.version}","id":"${command.id}","name":"${rollPreferences.type}","type":1},"nonce":"${++Discord.nonce}"}\r\n------BDR--\r\n`
            });
        }
    }

    const AutoMudae = {
        users: [], /// MudaeUser[]
        preferences: null, /// Map<string, any>
        state: E.AUTOMUDAE_STATE.INJECT,
        chatObserver: new MutationObserver(ms => ms.forEach(m => { if (m.addedNodes.length) { handleNewChatAppend(m.addedNodes) } })),
        cdGatherInfo: 0,
        cdRoll: 0,
        lastResetHash: '',

        timers: {
            _t: new Map(),
            set(identifier, callback, ms, isInterval = false) {
                if (this._t.has(identifier)) clearTimeout(identifier);
                const timer = isInterval ? setInterval(callback, ms) : setTimeout(callback, ms);
                this._t.set(identifier, timer);
            },
            clear() { [...this._t.values()].forEach(t => { clearTimeout(t); clearInterval(t) }); this._t.clear(); }
        },

        toasts: {
            add(E_TOAST, formattableText, el_SubjectMessage = null) {
                if (!DOM.el_ToastsWrapper) return;

                const text = formattableText.replace(/\[(.+?)\]/g, "<strong>$1</strong>");

                const el_Toast = document.createElement("div");
                el_Toast.classList.add("automudae-toast", E_TOAST);
                el_Toast.innerHTML = `<span>${text}</span>`;

                if (el_SubjectMessage){
                    el_Toast.classList.add("link");

                    el_Toast.onclick = function(){
                        if (this.classList.contains("missing")){
                            this.classList.remove("missing");
                            void this.offsetWidth;
                            this.classList.add("missing");  
                            return;
                        }

                        if (!el_SubjectMessage) return this.classList.add("missing");

                        const loadedMessages = [...el_SubjectMessage.parentElement.children];
                        const messageIndex = loadedMessages.indexOf(el_SubjectMessage);
                        const distanceFromBottom = loadedMessages.length - messageIndex;
                        const quantityMargin = 19;

                        if (messageIndex >= quantityMargin && distanceFromBottom <= quantityMargin){
                            el_SubjectMessage.scrollIntoView();
                            return;
                        }

                        this.classList.add("missing");
                    };
                }

                DOM.el_ToastsWrapper.appendChild(el_Toast);
            },
            clear() {
                if (DOM.el_ToastsWrapper) DOM.el_ToastsWrapper.innerHTML = "";
            }
        },

        /// Info
        hasNeededInfo() {
            return this.users.every(user => user.hasNeededInfo());
        },

        isLastReset() {
            const now = new Date(), h = now.getHours(), m = now.getMinutes();
            return (h % 3 == 2 && m >= 36) || (h % 3 == 0 && m < 36)
        },

        /// Utils
        mudaeTimeToMs(timeString) {
            if (!timeString.includes("h")) return Number(timeString) * 60 * 1000;

            const match = /(\d+h)?\s?(\d+)?/.exec(timeString);

            if (!match) return;

            const h = match[1];
            const m = match[2];
            let totalMs = 0;

            if (h) totalMs += Number(h.replace(/\D/g, '')) * 60 * 60 * 1000;
            if (m) totalMs += Number(m) * 60 * 1000;

            return totalMs;
        },

        getMarriageableUser(preferableNicknames) {
            if (!preferableNicknames || preferableNicknames.length === 0){
                return this.users.find(user => user.info.get(E.MUDAE_INFO.CAN_MARRY));
            }

            let marriageableUser;

            for (let i = 0; i < this.users.length; i++) {
                const user = this.users[i];
                
                if (user.info.get(E.MUDAE_INFO.CAN_MARRY)){
                    marriageableUser = user;

                    if (preferableNicknames.includes(user.nick)) break;                    
                }
            }

            return marriageableUser;
        },

        clearError(){
            if (DOM.el_ErrorPopup) DOM.el_ErrorPopup = DOM.el_ErrorPopup.remove();
        },

        error(msg) {
            this.clearError();
            if (!msg) return;

            const el_ErrorPopup = document.createElement("div");
            el_ErrorPopup.id = "automudae-error";
            el_ErrorPopup.innerHTML = `<span>${msg}</span>`;
            document.body.appendChild(el_ErrorPopup);

            DOM.el_ErrorPopup = el_ErrorPopup;
        },

        /// Workflow
        renderTokenList(){
            const isTokenValid = token => token && token.length >= 70 && token.length < 80 && /\w+\.\w+\.[-\w]+$/.test(token);

            function handleTokenInput(){
                if (!isTokenValid(this.value)) this.parentElement.remove();                
            }

            const el_TokenListWrapper = document.createElement("div");
            el_TokenListWrapper.id = "automudae-tokenlist-wrapper";
            el_TokenListWrapper.innerHTML = `<div id="automudae-tokenlist"><h3>Token List</h3><div><ul></ul><div id="automudae-tokenlist-controls"><div id="automudae-tokenlist-add">Add</div><div id="automudae-tokenlist-clear">Clear</div></div></div><div id="automudae-tokenlist-accept">Accept</div></div>`;
            
            document.body.appendChild(el_TokenListWrapper);

            const el_TokenList = document.querySelector("#automudae-tokenlist ul");

            const addInputField = (defaultValue) => {
                if (el_TokenList.childElementCount < 20){
                    const el_TokenInput = document.createElement("input");

                    el_TokenInput.onblur = handleTokenInput;

                    if (defaultValue) el_TokenInput.value = defaultValue;

                    el_TokenList.appendChild(document.createElement("li").appendChild(el_TokenInput).parentElement);
                }
            };

            document.getElementById("automudae-tokenlist-clear").onclick = () => el_TokenList.innerHTML = "";

            document.getElementById("automudae-tokenlist-add").onclick = () => addInputField();

            document.getElementById("automudae-tokenlist-accept").onclick = () => {
                const tokenSet = new Set();

                document.querySelectorAll("#automudae-tokenlist input").forEach(el_Input => {
                    const token = el_Input.value;
                    if (isTokenValid(token)) tokenSet.add(token);
                });

                if (tokenSet.size === 0){
                    AutoMudae.error("Please provide a valid token.");
                    return;
                }

                const tokenList = [...tokenSet];

                GM_setValue(E.GMVALUE.TOKENLIST, tokenList.join(";"));

                el_TokenListWrapper.remove();
                AutoMudae.inject(tokenList);
            };

            GM_getValue(E.GMVALUE.TOKENLIST)?.split(";").forEach(token => addInputField(token));
        },

        toggleInjectionButtons(){
            if (DOM.el_InjectionsWrapper){
                DOM.el_InjectionsWrapper.classList.toggle("automudae-hide");
                return;
            }

            const el_LoggedUsersButton = document.createElement("div");
            el_LoggedUsersButton.id = "automudae-use-logged-button";
            el_LoggedUsersButton.innerHTML = "<span>Use Logged Users</span>";

            const el_TokenListButton = document.createElement("div");
            el_TokenListButton.id = "automudae-use-tokenlist-button";
            el_TokenListButton.innerHTML = "<span>Use Token List</span>";

            el_LoggedUsersButton.onclick = (_e) => AutoMudae.inject(false);
            el_TokenListButton.onclick = (_e) => AutoMudae.renderTokenList();

            const el_InjectionsWrapper = document.createElement("div");
            el_InjectionsWrapper.id = "automudae-injections-wrapper";

            el_InjectionsWrapper.appendChild(el_LoggedUsersButton);
            el_InjectionsWrapper.appendChild(el_TokenListButton);

            DOM.el_InjectionsWrapper = el_InjectionsWrapper;

            document.body.appendChild(el_InjectionsWrapper);
        },

        preRender() {
            const el_DiscordToolBar = document.querySelector("[class^='toolbar']");
            el_DiscordToolBar.innerHTML = "";

            /// Run Button
            const el_RunButton = document.createElement("div");
            el_RunButton.id = "automudae-run-button";
            el_RunButton.classList.add("automudae-hide");

            el_DiscordToolBar.appendChild(el_RunButton);

            DOM.el_RunButton = el_RunButton;

            /// State Text
            const el_StateWrapper = document.createElement("div");
            el_StateWrapper.id = "automudae-state";
            el_StateWrapper.innerHTML = "<b>AutoMudae:</b>";

            const el_StateSpan = document.createElement("span");
            el_StateSpan.appendChild(document.createTextNode("Idle"));

            el_StateWrapper.appendChild(el_StateSpan);
            el_DiscordToolBar.appendChild(el_StateWrapper);

            DOM.el_StateSpan = el_StateSpan;

            /// Injection Buttons
            this.toggleInjectionButtons();
        },

        inject(tokenList) {
            this.toggleInjectionButtons();

            logger.info("Injecting...");

            this.setState(E.AUTOMUDAE_STATE.SETUP);

            AutoMudae.setup(tokenList)
            .then(() => {
                this.clearError();
    
                const requirements = "Required:\n- All your accounts should have custom avatars\n- Arrange your $TU to expose all needed information: $ta claim rolls daily keys kakerareact kakerapower kakerainfo kakerastock rt dk rollsreset\n- Set your claim feedback to default: $rc none\n- Set your rolls left message to default: $rollsleft 0\nCan only roll with slash commands.\nDon't search for messages in Discord.\n- Don't scroll up the channel.";
                const recommendations = "Recommended:\n- Use slash rolls.\n- Don't use non-slash rolls while the channel is in peak usage by other members.\n- Set your user order priorizing roll and kakera claiming.";
    
                const exposeLogger = this.preferences.get(E.PREFERENCES.EXTRA).logger;
    
                if (exposeLogger) {
                    const doNothing = () => { };
    
                    for (const method in logger) {
                        if (!Object.hasOwn(logger, method)) continue;
    
                        window.console[method] = doNothing;
                    }
    
                    console.clear();
                    window.logger = logger;
                    logger.debug("Turned off native console. Use logger instead. I recommend disabling network log, since Discord usualy prompt a lot of these.");
                    logger.debug(requirements);
                    logger.debug(recommendations);
                    logger._reprompt();
                }
    
                this.render();
                this.tryEnable();
    
                if (!exposeLogger) {
                    logger.info(requirements);
                    logger.info(recommendations);
                }
            })
            .catch(err => {
                logger.error(err);
                this.error(err);
                this.setState(E.AUTOMUDAE_STATE.INJECT);
                this.toggleInjectionButtons();
            });
        },

        async setup(tokenList){
            return new Promise(async (resolve, reject) =>  {
                const windowPathname = window.location?.pathname;
    
                if (!windowPathname) {
                    reject("Couldn't retrieve current window URL.");
                }
    
                const [_, pathDiscriminator, guildId, channelId] = windowPathname.split("/");
    
                if (pathDiscriminator !== "channels") {
                    reject("You must be viewing the desired channel.");
                }
    
                if (!guildId || !channelId) {
                    reject("Couldn't retrieve active guild or channel.");
                }
    
                DOM.el_ChannelList = document.querySelector("#channels > ul");
                DOM.el_MemberList = document.querySelector("div[class^='members'] > div");
                DOM.el_Chat = document.querySelector("ol[class^='scrollerInner']");
                DOM.el_ChatWrapper = document.querySelector("main[class^='chatContent']");
    
                if (!DOM.el_Chat || !DOM.el_MemberList || !DOM.el_ChannelList || !DOM.el_ChatWrapper) {
                    reject("Make sure you're viewing the desired channel and the page is fully loaded.");
                }
    
                if (!localStorage || !localStorage.MultiAccountStore || !localStorage.tokens) {
                    reject("Couldn't retrieve information from Discord.");
                }
    
                const users = [];
    
                if (tokenList){    
                    for (let i = 0; i < tokenList.length; i++) {
                        users.push(await new MudaeUser(tokenList[i]));
                    }
                } else {
                    const storeUsers = JSON.parse(localStorage.MultiAccountStore)?._state.users;
                    const tokens = JSON.parse(localStorage.tokens);
        
                    if (!storeUsers || !tokens) {
                        return "Couldn't retrieve information about your accounts.";
                    }
    
                    for (let i = 0; i < storeUsers.length; i++) {
                        const { id, username, avatar } = storeUsers[i];
        
                        const token = tokens[id];
        
                        if (!token) {
                            return `Couldn't retrieve information about user [${username}]`;
                        }
        
                        users.push(await new MudaeUser(token, id, username, avatar));
                    }
                }
    
                this.users = users;
                Discord.info.set(E.DISCORD_INFO.CHANNEL_ID, channelId);
                Discord.info.set(E.DISCORD_INFO.GUILD_ID, guildId);
    
                const defaultPreferences = `[
                    ["${E.PREFERENCES.KAKERA}", {"kakeraP": false, "kakera": false, "kakeraT": false, "kakeraG": false, "kakeraY": false, "kakeraO": false, "kakeraR": false, "kakeraW": false, "kakeraL": false}],
                    ["${E.PREFERENCES.MENTIONS}", ""],
                    ["${E.PREFERENCES.ROLL}", {"enabled":true,"type":"wx"}],
                    ["${E.PREFERENCES.SOUND}", {"foundcharacter":true,"marry":true,"cantmarry":true, "lastresetnorolls":true,"soulmate":true,"wishsteal":true}],
                    ["${E.PREFERENCES.EXTRA}", {"logger":true}]
                ]`;
    
                const savedVersion = GM_getValue(E.GMVALUE.VERSION, null);
    
                const isPreferencesOutdated = !savedVersion || savedVersion !== GM_info.script.version;
    
                const stringifiedPreferences = isPreferencesOutdated ? defaultPreferences : GM_getValue(E.GMVALUE.PREFERENCES, defaultPreferences);
    
                this.preferences = new Map(JSON.parse(stringifiedPreferences));
    
                GM_setValue(E.GMVALUE.VERSION, GM_info.script.version);

                resolve();
            });
        },

        render() {
            logger.info("Rendering...");
            const el_InfoPanel = document.createElement("div");
            el_InfoPanel.id = "automudae-panel-info";
            el_InfoPanel.innerHTML = `
            <h1>Auto-Mudae Info</h1>
            <div>
                <div class="automudae-section">
                    <h2>Collected</h2>
                    <div class="automudae-section-body">
                        <div class="automudae-row">
                            <span>Kakera:</span>
                            <div><img class="emoji" src="https://cdn.discordapp.com/emojis/469835869059153940.webp?quality=lossless"><span id="automudae-field-${E.INFO_FIELD.KAKERA}">0</span></div>
                        </div>
                        <div class="automudae-row">
                            <span>Characters:</span>
                        </div>
                        <ul id="automudae-field-${E.INFO_FIELD.COLLECTED_CHARACTERS}"></ul>
                    </div>
                </div>
                <div class="automudae-section" id="automudae-section-status">
                    <h2>Status</h2>
                    <div class="automudae-section-body">
                        <div class="automudae-row-expandable">
                            <div class="automudae-row">
                                <span>Rolls:</span>
                                <div><span>(</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_LEFT}">?</span><span>/</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_MAX}">?</span><span>)</span></div>
                            </div>
                            <div>
                                ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><span>(</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_LEFT}-${user.id}">?</span><span>/</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_MAX}-${user.id}">?</span><span>)</span></div></div>`).join("")}
                            </div>
                        </div>
                        <div class="automudae-row-expandable">
                            <div class="automudae-row">
                                <span>Power:</span>
                                <div><span id="automudae-field-${E.INFO_FIELD.POWER}">?</span><span>%</span></div>
                            </div>
                            <div>
                                ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><div><span id="automudae-field-${E.INFO_FIELD.POWER}-${user.id}">?</span><span>%</span></div></div></div>`).join("")}
                            </div>
                        </div>
                        <div class="automudae-row-expandable">
                            <div class="automudae-row">
                                <span>Kakera Power Consumption:</span>
                                <div><span id="automudae-field-${E.INFO_FIELD.POWER_CONSUMPTION}">?</span><span>%</span></div>
                            </div>
                            <div>
                                ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><div><span id="automudae-field-${E.INFO_FIELD.POWER_CONSUMPTION}-${user.id}">?</span><span>%</span></div></div></div>`).join("")}
                            </div>
                        </div>
                        <div class="automudae-row-expandable">
                            <div class="automudae-row">
                                <span>Can Marry?</span>
                                <span id="automudae-field-${E.INFO_FIELD.CAN_MARRY}">?</span>
                            </div>
                            <div>
                                ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><span id="automudae-field-${E.INFO_FIELD.CAN_MARRY}-${user.id}">?</span></div></div>`).join("")}
                            </div>
                        </div>
                        <div class="automudae-row-expandable">
                            <div class="automudae-row">
                                <span>Can RT?</span>
                                <span id="automudae-field-${E.INFO_FIELD.CAN_RT}">?</span>
                            </div>
                            <div>
                                ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><span id="automudae-field-${E.INFO_FIELD.CAN_RT}-${user.id}">?</span></div></div>`).join("")}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            `;

            const el_ConfigPanel = document.createElement("div");
            el_ConfigPanel.id = "automudae-panel-config";
            el_ConfigPanel.innerHTML = `
            <h1>Auto-Mudae Config</h1>
            <div>
                <div class="automudae-section" id="automudae-section-kakera">
                    <h2>Kakera to Collect</h2>
                    <div class="automudae-section-body">
                        <div><input type="checkbox" id="opt-kakera-kakeraP"><label for="opt-kakera-kakeraP"><img class="emoji" src="https://cdn.discordapp.com/emojis/609264156347990016.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakera"><label for="opt-kakera-kakera"><img class="emoji" src="https://cdn.discordapp.com/emojis/469835869059153940.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraT"><label for="opt-kakera-kakeraT"><img class="emoji" src="https://cdn.discordapp.com/emojis/609264180851376132.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraG"><label for="opt-kakera-kakeraG"><img class="emoji" src="https://cdn.discordapp.com/emojis/609264166381027329.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraY"><label for="opt-kakera-kakeraY"><img class="emoji" src="https://cdn.discordapp.com/emojis/605112931168026629.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraO"><label for="opt-kakera-kakeraO"><img class="emoji" src="https://cdn.discordapp.com/emojis/605112954391887888.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraR"><label for="opt-kakera-kakeraR"><img class="emoji" src="https://cdn.discordapp.com/emojis/605112980295647242.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraW"><label for="opt-kakera-kakeraW"><img class="emoji" src="https://cdn.discordapp.com/emojis/608192076286263297.webp?quality=lossless"></label></div>
                        <div><input type="checkbox" id="opt-kakera-kakeraL"><label for="opt-kakera-kakeraL"><img class="emoji" src="https://cdn.discordapp.com/emojis/815961697918779422.webp?quality=lossless"></label></div>
                    </div>
                </div>
                <div class="automudae-section">
                    <h2>Interesting Mentions</h2>
                    <div class="automudae-section-body">
                        <textarea spellcheck="false" id="opt-mentions"></textarea>
                    </div>
                </div>
                <div class="automudae-section">
                    <h2>Roll</h2>
                    <div class="automudae-section-body">
                        <div>
                            <input type="checkbox" id="opt-roll-enabled"><label for="opt-roll-enabled"><span>Enabled</span></label>
                        </div>
                        <div>
                            <select id="opt-roll-type">
                                <option value="wx">wx</option>
                                <option value="wa">wa</option>
                                <option value="wg">wg</option>
                                <option value="hx">hx</option>
                                <option value="ha">ha</option>
                                <option value="hg">hg</option>
                            </select>
                        </div>
                    </div>
                </div>
                <div class="automudae-section">
                    <h2>Sound</h2>
                    <div class="automudae-section-body">
                        <div>
                            <input type="checkbox" id="opt-sound-foundcharacter"><label for="opt-sound-foundcharacter"><span>Found character</span></label>
                        </div>
                        <div>
                            <input type="checkbox" id="opt-sound-marry"><label for="opt-sound-marry"><span>Marry</span></label>
                        </div>
                        <div>
                            <input type="checkbox" id="opt-sound-cantmarry"><label for="opt-sound-cantmarry"><span>Can't marry</span></label>
                        </div>
                        <div>
                            <input type="checkbox" id="opt-sound-lastresetnorolls"><label for="opt-sound-lastresetnorolls"><span>Can't roll in the last reset</span></label>
                        </div>
                        <div>
                            <input type="checkbox" id="opt-sound-soulmate"><label for="opt-sound-soulmate"><span>New soulmates</span></label>
                        </div>
                        <div>
                            <input type="checkbox" id="opt-sound-wishsteal"><label for="opt-sound-wishsteal"><span>Wish steals</span></label>
                        </div>
                    </div>
                </div>
                <div class="automudae-section">
                    <h2>Extra</h2>
                    <div class="automudae-section-body">
                        <div data-requirerestart>
                            <input type="checkbox" id="opt-extra-logger"><label for="opt-extra-logger"><span>Replace Console with Logger</span></label>
                        </div>
                    </div>
                </div>
            </div>
            `;

            const el_ToastsWrapper = document.createElement("div");
            el_ToastsWrapper.id = "automudae-toasts-wrapper";

            DOM.el_ChannelList.prepend(el_InfoPanel);
            DOM.el_MemberList.prepend(el_ConfigPanel);
            DOM.el_ChatWrapper.prepend(el_ToastsWrapper);

            DOM.el_ToastsWrapper = el_ToastsWrapper;

            document.querySelector("[class^='channelTextArea']").style.width = "60%";

            /// Make side panels collapsable
            function collapse() { this.parentElement.classList.toggle("collapsed") };

            document.querySelectorAll("[id^='automudae-panel'] > h1").forEach(el_Header => el_Header.onclick = collapse);

            /// Config Update & Functionality
            function handleCheckboxPreference() {
                const [_, category, key] = this.id.split("-");
                const categoryPreferences = AutoMudae.preferences.get(category);

                categoryPreferences[key] = this.checked;

                AutoMudae.preferences.set(category, categoryPreferences);
                AutoMudae.savePreferences();
            };

            document.querySelectorAll("input[type='checkbox'][id^='opt-']").forEach(el_OptCheckbox => {
                const [_, category, key] = el_OptCheckbox.id.split("-");

                el_OptCheckbox.checked = AutoMudae.preferences.get(category)[key];
                el_OptCheckbox.onchange = handleCheckboxPreference;
            });

            const el_OptMentions = document.getElementById("opt-mentions");

            el_OptMentions.value = this.preferences.get(E.PREFERENCES.MENTIONS);

            el_OptMentions.onblur = function () {
                AutoMudae.preferences.set(E.PREFERENCES.MENTIONS, this.value);
                AutoMudae.savePreferences();
            };

            const el_OptRollType = document.getElementById("opt-roll-type");

            el_OptRollType.value = this.preferences.get(E.PREFERENCES.ROLL).type;

            el_OptRollType.onchange = function () {
                const rollPreferences = AutoMudae.preferences.get(E.PREFERENCES.ROLL);

                rollPreferences.type = this.value;

                AutoMudae.preferences.set(E.PREFERENCES.ROLL, rollPreferences);
                AutoMudae.savePreferences();
            };
        },

        tryEnable() {
            if (this.state !== E.AUTOMUDAE_STATE.SETUP) return;
            if (!Object.values(E.DISCORD_INFO).every(info => Discord.info.has(info))) return;

            this.setState(E.AUTOMUDAE_STATE.IDLE);
            DOM.el_RunButton.onclick = (_e) => AutoMudae.toggle();
            DOM.el_RunButton.classList.remove("automudae-hide");
            logger.plus("Ready to go!");
        },

        toggle() {
            if (this.state !== E.AUTOMUDAE_STATE.IDLE && this.state !== E.AUTOMUDAE_STATE.RUN) return;

            if (this.state === E.AUTOMUDAE_STATE.IDLE) {
                this.clearError();

                let msToStartResetHandler = 1;
                const now = new Date();

                if (now.getMinutes() !== 37) {
                    const nextReset = new Date(now);
                    nextReset.setHours(now.getMinutes() > 37 ? now.getHours() + 1 : now.getHours(), 37);
                    msToStartResetHandler = nextReset - now;
                }

                this.timers.set("think", this.think, INTERVAL_THINK, true);
                this.timers.set("initHourlyResetHandler", () => { AutoMudae.handleHourlyReset(); AutoMudae.timers.set("HandleHourlyReset", AutoMudae.handleHourlyReset, 1 * 60 * 60 * 1000, true) }, msToStartResetHandler);
                this.chatObserver.observe(DOM.el_Chat, { childList: true });
                this.setState(E.AUTOMUDAE_STATE.RUN);
                logger.log("Running..");
                return;
            }

            this.chatObserver.disconnect();
            this.timers.clear();
            this.users.forEach(user => {
                if (user.sendTUTimer) clearTimeout(user.sendTUTimer);
                user.info.clear();
            });
            this.setState(E.AUTOMUDAE_STATE.IDLE);
            logger.log("Turned off.");
        },

        setState(E_STATE) {
            this.state = E_STATE;

            const stateTexts = {};
            stateTexts[E.AUTOMUDAE_STATE.INJECT] = "Idle";
            stateTexts[E.AUTOMUDAE_STATE.SETUP] = "Setting up...";
            stateTexts[E.AUTOMUDAE_STATE.ERROR] = "Error!";
            stateTexts[E.AUTOMUDAE_STATE.IDLE] = "Idle";
            stateTexts[E.AUTOMUDAE_STATE.RUN] = "Running...";

            DOM.el_StateSpan.innerText = stateTexts[E_STATE];

            if ((E_STATE === E.AUTOMUDAE_STATE.RUN || E_STATE === E.AUTOMUDAE_STATE.IDLE) && DOM.el_RunButton){
                const isRun = E_STATE === E.AUTOMUDAE_STATE.RUN;
                DOM.el_RunButton.classList[isRun ? "add" : "remove"]("running");
            }
        },

        think() {
            const now = performance.now();
            const dateNow = new Date(), h = dateNow.getHours(), m = dateNow.getMinutes();

            if (!AutoMudae.hasNeededInfo()) {
                if (now - AutoMudae.cdGatherInfo < 1000) return;

                for (let i = 0; i < AutoMudae.users.length; i++) {
                    const user = AutoMudae.users[i];

                    if (!user.hasNeededInfo()) {
                        logger.log(`Gathering needed info for user [${user.username}]..`);

                        user.send("$tu");

                        break;
                    }

                }

                AutoMudae.cdGatherInfo = now;
                return;
            }

            const userWithRolls = AutoMudae.users.find(user => user.info.get(E.MUDAE_INFO.ROLLS_LEFT) > 0);

            if (AutoMudae.preferences.get(E.PREFERENCES.ROLL).enabled) {
                if (userWithRolls && now - Discord.lastMessageTime > INTERVAL_ROLL && now - AutoMudae.cdRoll > (INTERVAL_ROLL * .5)) {
                    userWithRolls.roll();
                    AutoMudae.cdRoll = now;
                }
            }

            if (!userWithRolls && m > 38 && AutoMudae.isLastReset() && AutoMudae.getMarriageableUser()) {
                const currentResetHash = `${dateNow.toDateString()} ${h}`;

                if (AutoMudae.lastResetHash !== currentResetHash) {
                    AutoMudae.lastResetHash = currentResetHash;

                    //# Add option to auto-use $us or $rolls

                    const warnMessage = "You have no more rolls, can still marry and it's the last reset. You could use $us or $rolls, then $tu.";

                    logger.warn(warnMessage);
                    AutoMudae.toasts.add(E.TOAST.WARN, warnMessage);
                    if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).lastresetnorolls) SOUND.lastResetNoRolls();
                }
                
            }

        },

        savePreferences() {
            GM_setValue(E.GMVALUE.PREFERENCES, JSON.stringify(this.preferences));
        },

        updateInfoPanel(E_INFO_FIELD, content, user) {
            const el_OverallField = document.getElementById(`automudae-field-${E_INFO_FIELD}`);

            if (E_INFO_FIELD === E.INFO_FIELD.KAKERA) {
                const newKakera = Number(el_OverallField.innerText) + Number(content);

                el_OverallField.innerText = newKakera;
                return;
            }

            if (E_INFO_FIELD === E.INFO_FIELD.COLLECTED_CHARACTERS) {
                const el_CharacterItem = document.createElement("li");
                el_CharacterItem.appendChild(document.createTextNode((user ? `[${user.username}] ` : '') + content));
                el_OverallField.appendChild(el_CharacterItem);

                return;
            }

            const el_UserField = document.getElementById(`automudae-field-${E_INFO_FIELD}-${user.id}`);

            el_UserField.innerText = content;

            if (E_INFO_FIELD === E.INFO_FIELD.ROLLS_LEFT || E_INFO_FIELD === E.INFO_FIELD.ROLLS_MAX) {
                const numeralFields = [...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].map(el_UserField => el_UserField.innerText).filter(text => /\d+/.test(text));

                if (numeralFields.length > 0) el_OverallField.innerText = numeralFields.reduce((total, current) => Number(total) + Number(current));

                return;
            }

            if (E_INFO_FIELD === E.INFO_FIELD.POWER) {
                const highestPower = getLast([...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].map(el_UserField => el_UserField.innerText).filter(text => /\d+/.test(text)).sort((a, b) => Number(a) - Number(b)));

                if (highestPower) el_OverallField.innerText = `↓ ${highestPower}`;

                return;
            }

            if (E_INFO_FIELD === E.INFO_FIELD.POWER_CONSUMPTION) {
                const lowestConsumption = [...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].map(el_UserField => el_UserField.innerText).filter(text => /\d+/.test(text)).sort((a, b) => Number(a) - Number(b))[0];

                if (lowestConsumption) el_OverallField.innerText = `↑ ${lowestConsumption}`;

                return;
            }

            if (E_INFO_FIELD === E.INFO_FIELD.CAN_MARRY || E_INFO_FIELD === E.INFO_FIELD.CAN_RT) {
                const hasAny = [...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].some(el_UserField => el_UserField.innerText === "Yes");

                el_OverallField.innerText = hasAny ? "Yes" : "No";

                return;
            }
        },

        handleHourlyReset() {
            if (!AutoMudae.hasNeededInfo()) return;

            logger.log("Hourly reset. Gathering updated status..");

            AutoMudae.users.forEach(user => user.info.delete(E.MUDAE_INFO.ROLLS_LEFT));
        }
    };

    //# Remove this exposure
    window.Discord = Discord;
    window.AutoMudae = AutoMudae;

    function observeToReact(el_Message, userToReact) {
        let runs = 0;

        const observer = setInterval(() => {
            if (!el_Message || runs++ >= 30) return clearInterval(observer);

            const el_ReactionImg = el_Message.querySelector(`div[class^='reactionInner']${userToReact ? "" : "[aria-label^='kakera']"}[aria-label*='1 rea'] img`);

            if (!el_ReactionImg) return;

            clearInterval(observer);

            if (userToReact) {
                const emoji = E.EMOJI[el_ReactionImg.alt];

                if (!emoji) {
                    const errMessage = `Couldn't find emoji code for [${el_ReactionImg.alt}]. Address this to AutoMudae's creator, please.`;
                    logger.error(errMessage);
                    AutoMudae.toasts.add(E.TOAST.CRITICAL, errMessage, el_Message);
                    return;
                }

                userToReact.react(el_Message, emoji);
                return;
            }

            const kakeraCode = el_ReactionImg.alt;

            if (!AutoMudae.preferences.get(E.PREFERENCES.KAKERA)[kakeraCode]) return;

            const userWithEnoughPower = kakeraCode === E.KAKERA.PURPLE
                ? AutoMudae.users[0]
                : AutoMudae.users.find(user => user.info.get(E.MUDAE_INFO.POWER) >= user.info.get(E.MUDAE_INFO.CONSUMPTION));

            if (userWithEnoughPower) userWithEnoughPower.react(el_Message, E.EMOJI_KAKERA[kakeraCode]);
        }, 100);
    };

    function handleNewChatAppend(el_Children) {
        document.querySelector("div[class^='scrollerSpacer']")?.scrollIntoView();

        el_Children.forEach(el_Child => {
            if (el_Child.tagName !== "LI") return;
            Discord.lastMessageTime = performance.now();

            const el_Message = el_Child;

            if (!Discord.Message.isFromMudae(el_Message)) return;

            const el_PreviousElement = el_Message.previousElementSibling
                ? (el_Message.previousElementSibling.id === "---new-messages-bar" ? el_Message.previousElementSibling.previousElementSibling : el_Message.previousElementSibling)
                : null;

            /// Handle player commands
            if (el_PreviousElement) {
                const el_PreviousMessage = el_PreviousElement;

                const user = Discord.Message.isFromMe(el_PreviousMessage);

                if (user) {
                    const command = el_PreviousMessage.querySelector("div[id^='message-content']")?.innerText;
                    const mudaeResponse = el_Message.querySelector("div[id^='message-content']")?.innerText;

                    if (command && mudaeResponse && mudaeResponse.startsWith(`${user.username}, `)) {
                        if (command === "$tu") {
                            const matchRolls = /tem (\d+) rolls/.exec(mudaeResponse);
                            if (matchRolls) {
                                const rolls = Number(matchRolls[1]);

                                const hasRollsMax = user.info.has(E.MUDAE_INFO.ROLLS_MAX);

                                if (!hasRollsMax || user.info.get(E.MUDAE_INFO.ROLLS_MAX) < rolls) {
                                    user.info.set(E.MUDAE_INFO.ROLLS_MAX, rolls);

                                    AutoMudae.updateInfoPanel(E.INFO_FIELD.ROLLS_MAX, rolls, user);
                                }

                                user.info.set(E.MUDAE_INFO.ROLLS_LEFT, rolls);
                                AutoMudae.updateInfoPanel(E.INFO_FIELD.ROLLS_LEFT, rolls, user);
                            }

                            const matchPower = /Power: (\d+)%/.exec(mudaeResponse);
                            if (matchPower) {
                                const power = Number(matchPower[1]);

                                user.info.set(E.MUDAE_INFO.POWER, power);
                                AutoMudae.updateInfoPanel(E.INFO_FIELD.POWER, power, user);
                            }

                            if (/\$rt/.test(mudaeResponse)) {
                                const cooldownRTMatch = /: (.+) min. \(\$rtu\)/.exec(mudaeResponse);

                                user.info.set(E.MUDAE_INFO.CAN_RT, !cooldownRTMatch);

                                if (cooldownRTMatch) {
                                    logger.log(`Scheduled a RT check for user [${user.username}]. [${cooldownRTMatch[1]}]`);

                                    user.setTUTimer(AutoMudae.mudaeTimeToMs(cooldownRTMatch[1]) + 500);
                                }

                                const canRT = user.info.get(E.MUDAE_INFO.CAN_RT);
                                AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_RT, canRT ? "Yes" : "No", user);
                            } else {
                                user.info.set(E.MUDAE_INFO.CAN_RT, false);
                                AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_RT, "No", user);
                            }

                            if (/casar/.test(mudaeResponse)) {
                                const cantMarry = /se casar novamente (.+) min/.exec(mudaeResponse);

                                user.info.set(E.MUDAE_INFO.CAN_MARRY, !cantMarry);

                                AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_MARRY, cantMarry ? "No" : "Yes", user);
                            }

                            const matchKakeraConsumption = /kakera consume (\d+)%/.exec(mudaeResponse);
                            if (matchKakeraConsumption) {
                                const consumption = Number(matchKakeraConsumption[1]);

                                user.info.set(E.MUDAE_INFO.CONSUMPTION, consumption);

                                AutoMudae.updateInfoPanel(E.INFO_FIELD.POWER_CONSUMPTION, consumption, user);
                            }

                            if (!user.hasNeededInfo()) {
                                AutoMudae.toggle();

                                const errMsg = `Couldn't retrieve needed info for user [${user.username}]. Make sure your $tu configuration exposes every information.`;
                                logger.error(errMsg);
                                AutoMudae.error(errMsg);
                                return;
                            }

                            logger.info(`Got all needed info for user [${user.username}].`);
                            return;
                        };
                    }
                }
            }

            if (!AutoMudae.hasNeededInfo()) return;

            const el_MessageContent = el_Message.querySelector("div[id^='message-content']");

            if (el_MessageContent) {
                const messageContent = el_MessageContent.innerText;

                /// Handle character claims & steals
                const characterClaimMatch = /(.+) e (.+) agora são casados!/.exec(messageContent.trim());

                if (characterClaimMatch || messageContent.includes("(Silver IV Bônus)")) {
                    let usernameThatClaimed, characterName;

                    if (characterClaimMatch) {
                        [_, usernameThatClaimed, characterName] = characterClaimMatch;
                    }

                    let user;

                    if (usernameThatClaimed) {
                        user = AutoMudae.users.find(user => user.username === usernameThatClaimed);
                    }

                    /// Claim
                    if (user) {
                        user.info.set(E.MUDAE_INFO.CAN_MARRY, false);

                        AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_MARRY, "No", user);
                        AutoMudae.updateInfoPanel(E.INFO_FIELD.COLLECTED_CHARACTERS, characterName, user);

                        if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).marry) SOUND.marry();

                        const logMessage = `User [${usernameThatClaimed}] claimed character [${characterName}]!`;
                        logger.plus(logMessage);
                        AutoMudae.toasts.add(E.TOAST.CHARCLAIM, logMessage, el_Message);

                        el_Message.classList.add("plus");

                        document.querySelectorAll("[class^='embedAuthorName']").forEach(el_AuthorName => {
                            if (el_AuthorName.innerText === characterName) {
                                const el_ParentMessage = el_AuthorName.closest("li");
                                el_ParentMessage.classList.add("plus");
                            }
                        });
                    } else {
                        const el_Mentions = el_Message.querySelectorAll("span.mention");

                        let isIncludingMe = false;

                        for (let i = 0; i < el_Mentions.length; i++) {
                            const mentionedNick = el_Mentions[i].innerText.substr(1);

                            if (AutoMudae.users.some(user => user.nick === mentionedNick)) {
                                isIncludingMe = true;
                                break;
                            }
                        }

                        /// Steal
                        if (isIncludingMe) {
                            if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).wishsteal) SOUND.critical();

                            el_Message.classList.add("critical");

                            if (characterName) {
                                document.querySelectorAll("[class^='embedAuthorName']").forEach(el_AuthorName => {
                                    if (el_AuthorName.innerText === characterName) {
                                        const el_ParentMessage = el_AuthorName.closest("li");
                                        el_ParentMessage.classList.add("critical");
                                    }
                                });
                            }

                            const stealWarn = characterClaimMatch
                                ? `User [${usernameThatClaimed}] claimed character [${characterName}] wished by you.`
                                : "A character wished by you was claimed by another user.";

                            logger.warn(stealWarn);
                            AutoMudae.toasts.add(E.TOAST.CRITICAL, stealWarn, el_Message);
                        }
                    }

                    return;
                }

                /// Handle "no more rolls" messages
                const noMoreRollsMatch = /(.+), os rolls são limitado/.exec(messageContent);

                if (noMoreRollsMatch) {
                    const user = AutoMudae.users.find(user => user.username === noMoreRollsMatch[1]);

                    return user && setTimeout(() => user.send("$tu"), 250);
                }

                const el_KakeraClaimStrong = el_Message.querySelector("div[id^='message-content'] span[class^='emojiContainer'] + strong");

                /// Handle kakera claiming
                if (el_KakeraClaimStrong) {
                    const kakeraClaimMatch = /^(.+)\s\+(\d+)$/.exec(el_KakeraClaimStrong.innerText);

                    if (kakeraClaimMatch) {
                        const [_, messageUsername, kakeraQuantity] = kakeraClaimMatch;

                        const user = AutoMudae.users.find(user => user.username === messageUsername);

                        if (user) {
                            const kakeraType = el_KakeraClaimStrong.previousElementSibling?.firstElementChild?.alt.replace(/:/g, '');

                            const powerCost = kakeraType === E.KAKERA.PURPLE ? 0 : user.info.get(E.MUDAE_INFO.CONSUMPTION);

                            if (powerCost > 0) {
                                const newPower = user.info.get(E.MUDAE_INFO.POWER) - powerCost;

                                user.info.set(E.MUDAE_INFO.POWER, newPower);
                                AutoMudae.updateInfoPanel(E.INFO_FIELD.POWER, newPower, user);
                            }

                            el_Message.classList.add("plus");
                            AutoMudae.updateInfoPanel(E.INFO_FIELD.KAKERA, kakeraQuantity);
                            logger.plus(`+${kakeraQuantity} kakera! [Remaining Power for user [${user.username}]: ${user.info.get(E.MUDAE_INFO.POWER)}%]`);
                            AutoMudae.toasts.add(E.TOAST.KAKERA, `+[${kakeraQuantity}] Kakera`, el_Message);
                        }

                        return;
                    }
                }
            }

            const el_ImageWrapper = el_Message.querySelector("div[class^='embedDescription'] + div[class^='imageContent'] div[class^='imageWrapper']");

            /// Handle character messages
            if (el_ImageWrapper) {
                const el_Footer = el_Message.querySelector("span[class^='embedFooterText']");

                const isCharacterLookupMessage = (el_Footer && (/^\d+ \/ \d+$/.test(el_Footer.innerText) || /^Pertence a .+ ~~ \d+ \/ \d+$/.test(el_Footer.innerText)));

                if (isCharacterLookupMessage) return;

                const characterName = el_Message.querySelector("span[class^='embedAuthorName']").innerText;
                const el_ReplyAvatar = el_Message.querySelector("img[class^='executedCommandAvatar']");
                let replyUserId;

                if (el_ReplyAvatar) {
                    replyUserId = /avatars\/(\d+)\//.exec(el_ReplyAvatar.src);

                    if (!replyUserId) return logger.error("Couldn't get reply user ID for", el_Message);

                    const user = AutoMudae.users.find(user => user.id === replyUserId[1]);

                    if (user) {
                        const rollsLeft = user.info.get(E.MUDAE_INFO.ROLLS_LEFT) - 1;
                        user.info.set(E.MUDAE_INFO.ROLLS_LEFT, rollsLeft);
                        AutoMudae.updateInfoPanel(E.INFO_FIELD.ROLLS_LEFT, rollsLeft, user);

                        if (el_Message.querySelector("div[class^='embedDescription']").innerText.includes("Sua nova ALMA")) {
                            if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).soulmate) SOUND.newSoulmate();

                            const logMessage = `New soulmate: [${characterName}]!`;
                            logger.plus(logMessage);
                            AutoMudae.toasts.add(E.TOAST.SOULMATE, logMessage, el_Message);
                        }
                    }
                }

                if (!el_Footer || el_Footer.innerText.includes("2 ROLLS RESTANTES") && !el_Footer.innerText.includes("Pertence")) {
                    let el_InterestingCharacter, isWished;

                    const mentionedNicknames = [...el_Message.querySelectorAll("span.mention")].map(el_Mention => el_Mention.innerText.substr(1));

                    for (let i = 0; i < mentionedNicknames.length; i++) {
                        const mentionedNick = mentionedNicknames[i];

                        if (AutoMudae.users.some(user => user.nick === mentionedNick) || AutoMudae.preferences.get(E.PREFERENCES.MENTIONS).split(",").map(nick => nick.trim()).includes(mentionedNick)) {
                            el_InterestingCharacter = el_Message;
                            isWished = true;
                            break;
                        }
                    }

                    const marriageableUser = AutoMudae.getMarriageableUser(mentionedNicknames);

                    if (marriageableUser && !el_InterestingCharacter && AutoMudae.isLastReset()) {
                        //# Search in a database
                        if (characterName === "hmm") {
                            el_InterestingCharacter = el_Message;
                        };
                    }

                    if (el_InterestingCharacter) {
                        const logMessage = `Found character [${characterName}]`;
                        logger.info(logMessage);
                        AutoMudae.toasts.add(E.TOAST.INFO, logMessage, el_Message);

                        if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).foundcharacter) SOUND.foundCharacter();

                        if (marriageableUser) {
                            //# Make it verify if marriageableUser can still marry after all delay calculations (In case of multiple marriageable characters at the same time)
                            if (!isWished) {
                                setTimeout(() => marriageableUser.react(el_Message, pickRandom(Object.values(E.EMOJI))), 8500);
                                return;
                            }

                            const isProtected = !!el_Message.querySelector("img[alt=':wishprotect:']");

                            if (!isProtected || isProtected && marriageableUser.id === replyUserId){
                                observeToReact(el_Message, marriageableUser);
                                return;
                            }

                            setTimeout(() => observeToReact(el_Message, marriageableUser), 2905);
                            return;
                        }

                        if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).cantmarry) SOUND.critical();

                        const warnMessage = `Can't marry right now. You may lose character [${characterName}]`;
                        logger.warn(warnMessage);
                        AutoMudae.toasts.add(E.TOAST.WARN, warnMessage, el_Message);
                    }

                    return;
                }

                /// Owned characters
                if (el_Footer.innerText.includes("Pertence")) {
                    /// Observe kakera reactions append
                    observeToReact(el_Message);
                }

                return;
            }
        });
    }

    //// SessionId Hook
    window.console.info = function () {
        for (const arg of arguments) {
            const match = /\[READY\] (?:.+) as (.+)/.exec(arg) || /resuming session (.+),/.exec(arg);

            if (match) {
                window.console.info = console.info;
                Discord.info.set(E.DISCORD_INFO.SESSION_ID, match[1]);
                AutoMudae.tryEnable();
            }
        }
        console.info(...arguments);
    };

    //// Main
    window.addEventListener("load", main, false);

    function main() {
        const findToolbarTimer = setInterval(() => {
            if (document.querySelector("[class^='toolbar']")){
                clearInterval(findToolbarTimer);
                AutoMudae.preRender();
            }
        }, 200);
    };
})();