DuelingNexus Chat Improvements Plugin

Revamps the chat and visual features of dueling.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         DuelingNexus Chat Improvements Plugin
// @namespace    https://duelingnexus.com/
// @version      0.6.2
// @description  Revamps the chat and visual features of dueling.
// @author       Sock#3222
// @grant        none
// @match        https://duelingnexus.com/game/*
// ==/UserScript==

// TODO: add github link

let makeReadOnly = function (obj, prop) {
    let val = obj[prop];
    delete obj[prop];
    Object.defineProperty(obj, prop, {
        value: val,
        writable: false,
        enumerable: true,
        configurable: true,
    });
    return val;
}

let ChatImprovements = {
    log: [],
    storage: {
        _cache: null,
        get: null,
        set: null,
    },
};

const LOCAL_STORAGE_KEY = "ChatImprovementsCache";

let updateLocalStorage = function () {
    let json = JSON.stringify(ChatImprovements.storage._cache);
    localStorage.setItem("ChatImprovementsCache", json);
};

let checkCache = function () {
    if(ChatImprovements.storage._cache) {
        return;
    }
    
    let localCopy;
    try {
        localCopy = localStorage.getItem(LOCAL_STORAGE_KEY);
        localCopy = JSON.parse(localCopy)
    }
    catch(e) {
        console.error("Error parsing local copy:", localCopy);
    }
    
    ChatImprovements.storage._cache = localCopy || {};
    
    updateLocalStorage();
};

ChatImprovements.storage.set = function (item, value) {
    checkCache();
    
    ChatImprovements.storage._cache[item] = value;
    
    updateLocalStorage();
    
    return value;
};

ChatImprovements.storage.get = function (item) {
    checkCache();
    
    return ChatImprovements.storage._cache[item];
};

ChatImprovements.storage.clear = function (item) {
    checkCache();
    
    ChatImprovements.storage._cache = {};
    
    updateLocalStorage();
};

// standard getter/setter properties

let defaultProperties = {
    playSounds: true,
    temporaryChat: true,
    showOptions: false,
    
    showNormalEvents: true,
    showChainEvents: true,
};

for(let [prop, defaultValue] of Object.entries(defaultProperties)) {
    // define if unset (inital run)
    if(typeof ChatImprovements.storage.get(prop) === "undefined") {
        ChatImprovements.storage.set(prop, defaultValue);
    }
    
    // getter & setter
    Object.defineProperty(ChatImprovements, prop, {
        get: function () {
            return ChatImprovements.storage.get(prop);
        },
        set: function (value) {
            return ChatImprovements.storage.set(prop, value);
        },
    });
}

makeReadOnly(ChatImprovements, "log");
window.ChatImprovements = ChatImprovements;

let waitFrame = function () {
    return new Promise(resolve => {
        requestAnimationFrame(resolve); //faster than set time out
    });
};

const waitForElementJQuery = async function (selector, source = $("body")) {
    let query;
    while (source.find(selector).length === 0) {
        await waitFrame();
    }
    return query;
};

let onload = function () {
    // css 
    $("head").append($(`<style>
    #ci-ext-misc {
        float: left;
        width: 25%;
        overflow-x: hidden;
        overflow-y: auto;
        box-sizing: border-box;
    }
    #card-column {
        float: none;
        width: auto;
    }
    
    #ci-ext-misc-sections > div {
        overflow-x: hidden;
        overflow-y: auto;
        box-sizing: border-box;
    }
    #ci-ext-misc-sections > div:hover {
        z-index: 10;
    }
    
    #ci-ext-log > p, #ci-ext-event-log > p {
        padding: 0;
        margin: 3px;
    }
    
    #ci-ext-event-log .interact-name {
        cursor: pointer;
    }
    
    #ci-ext-log, #ci-ext-event-log, #ci-ext-options {
        background: rgb(0, 0, 0);
        background: rgba(0, 0, 0, 0.7);
    }
    @keyframes fullScale {
        from {
            transform: scale(1);
        }
        to {
            transform: scale(2);
        }
    }
    .material-preview {
        margin: 3px;
    }
    
    #ci-ext-misc-buttons {
        display: flex;
        justify-content: space-between;
    }
    
    #ci-ext-misc-buttons > button {
        flex-grow: 1;
        flex-basis: 0;
    }
    </style>`));
    
    // TODO: move chaining options on bottom right
    // TODO: add hide options for sleeves/avatars
    
    // boilerplate
    const globalOptions = f;
    const playSound = Q;
    const sendEvent = K;
    const gameChatContent = $("#game-chat-content");
    const gameChatTextbox = $("#game-chat-textbox");
    const gameChatArea = $("#game-chat-area");
    const showCardInColumn = Cc;
    const SECONDS = 1000;
    
    const monsterTypeMap = {};
    for(let key in Wf) {
        let value = Wf[key];
        monsterTypeMap[value] = parseInt(key, 10);
    }
    window.monsterTypeMap = monsterTypeMap;
    const isTrapCard            = (card) => card.type & monsterTypeMap["Trap"];
    const isSpellCard           = (card) => card.type & monsterTypeMap["Spell"];
    const isMonster             = (card) => !isTrapCard(card) && !isSpellCard(card);
    const isToken               = (card) => card.type & monsterTypeMap["Token"];
    const isXyzMonster          = (card) => card.type & monsterTypeMap["Xyz"];
    const isPendulumMonster     = (card) => card.type & monsterTypeMap["Pendulum"];
    const isLinkMonster         = (card) => card.type & monsterTypeMap["Link"];
    const isFusionMonster       = (card) => card.type & monsterTypeMap["Fusion"];
    const isRitualMonster       = (card) => isMonster(card) && (card.type & monsterTypeMap["Ritual"]);
    const isSynchroMonster      = (card) => card.type & monsterTypeMap["Synchro"];
    const isNormalMonster       = (card) => card.type & monsterTypeMap["Normal"];

    gameChatContent.css("overflow-y", "auto")
                   // .css("height", "230px")
                   // .css("background-color", "transparent");
    // gameChatArea.css("background-color", "rgba(0, 0, 0)")
                // .css("background-color", "rgba(0, 0, 0, 0.7)");
    let chatLog = $("<div id=ci-ext-log>");
    let chatEventLog = $("<div id=ci-ext-event-log>");
    
    /**/
    Gb.GameSet = window.Td = function Td(a) {
        Q("set");
        // console.log("Td", a);
    };

    Gb.GameSummoning = window.Ud = function Ud(a) {
        xc(a.cardCode, function() {
            Q("summon");
            // console.log("Ud", a);
            kf(a.cardCode);
        });
        return true;
    }

    Gb.GameSpSummoning = window.Vd = function Vd(a) {
        xc(a.cardCode, function() {
            Q("summon-special");
            // console.log("Vd", a);
            kf(a.cardCode);
        });
        return true;
    }

    Gb.GameFlipSummoning = window.Wd = function Wd(a) {
        xc(a.cardCode, function() {
            Q("summon-flip");
            // console.log("Wd", a);
            kf(a.cardCode);
        });
        return true;
    }
    
    const CHAIN_SYMBOL = "\uD83D\uDD17";
    Gb.GameChaining = window.Xd = function Xd(a) {
        xc(a.cardCode, function() {
            Q("activate");
            let cardName = a.cardCode ? "#@" + a.cardCode : "A card";
            // console.log("!!!!", cardName);
            // notifyEvent(" of " + cardName + " was activated (from " + GameLocations[a.location]);
            let message = `Chain Link ${a.chainCount}: ${cardName}`
            if(a.chainCount > 1) {
                message = CHAIN_SYMBOL + " " + message;
            }
            notifyEvent(message, Events.CHAIN);
            kf(a.cardCode);
        });
        return true;
    }
    
    /*
        GameDamage: Yd,
        GameRecover: Zd,
        GamePayLpCost: $d,
        GameLpUpdate: ae,
        GameAttack: be,
        GameBattle: ce,
        GameReloadField: de,
        GameTagSwap: ee,
        GameFieldDisabled: fe,
        GameWaiting: ge,
        GameEquip: he,
        GameBecomeTarget: ie,
        GameWin: je,
        GameTossCoin: ke,
        GameTossDice: le,
        GameAddCounter: me,
        GameRemoveCounter: ne,
        GameConfirmCards: oe,
        GameConfirmDeckTop: pe,
        GameDeckTop: qe,
        GameRetry: re,
        GameSelectIdleCommand: se,
        GameSelectBattleCommand: te,
        GameSelectCard: ue,
        GameSelectUnselect: ve,
        GameSortCards: we,
        GameSelectTribute: xe,
        GameSelectYesNo: ye,
        GameSelectEffectYesNo: ze,
        GameSelectChain: Ae,
        GameSelectPosition: Be,
        GameSelectOption: Ce,
        GameSelectSum: De,
        GameSelectPlace: Ee,
        GameSelectCounter: Fe,
        GameAnnounceAttrib: Ge,
        GameAnnounceRace: He,
        GameAnnounceNumber: Ie,
        GameAnnounceCard: Je
    */
    
    let eventTypeList = [
        
    ];
    
    let timestamp = function () {
        let now = new Date();
        return [
            now.getHours(),
            now.getMinutes(),
            now.getSeconds()
        ].map(time => time.toString().padStart(2, "0")).join(":");
    }
    
    const scrollToBottom = function (el) {
        el = $(el);
        el.animate({
            scrollTop: el.prop("scrollHeight")
        }, 150);
    }
    
    const MESSAGE_PARSE_REGEX = /#@(\d+)|.+?/g;
    let colorOfCard = function (card) {
        if(isTrapCard(card)) {
            return "#BC5A84";
        }
        else if(isSpellCard(card)) {
            return "#1D9E74";
        }
        else {
            return "#B83D00";
        }
    }
    let displayMessage = function (content, color, ...kinds) {
        //showCardInColumn
        let matches;
        let message = $("<p>");
        while(matches = MESSAGE_PARSE_REGEX.exec(content)) {
            let id = parseInt(matches[1]);
            let card = X[id];
            if(id && card) {
                let interactive = $("<span>")
                    .css("color", "white")
                    .css("background-color", colorOfCard(card))
                    .data("id", id)
                    .addClass("interact-name");
                interactive.hover(function () {
                    showCardInColumn(id);
                });
                interactive.text('"' + card.name + '"');
                message.append(interactive);
            }
            else {
                message.append(matches[0]);
            }
        }
        for(let kind of kinds) {
            message.addClass(kind);
        }
        if(color) {
            message.css("color", color);
        }
        gameChatContent.append(message);
        let copy = message.clone(true);
        if(kinds.indexOf("notified-event") !== -1) {
            $(".interact-name", copy).unbind().click(function () {
                showCardInColumn($(this).data("id"));
                showCardColumn.click();
            });
            chatEventLog.append(copy);
        }
        else {
            chatLog.append(copy);
        }
        // scroll to message
        scrollToBottom(chatLog);
        scrollToBottom(gameChatContent);
        // handle UI
        if(ChatImprovements.temporaryChat) {
            if(gameChatContent.children().length > 10) {
                gameChatArea.find("p:first").remove();
            }
            setTimeout(function() {
                message.remove();
            }, 10 * SECONDS);
        }
        return message;
    }
    ChatImprovements.displayMessage = displayMessage;
    
    // overwrite send message
    window.td = displayMessage;
    
    let unifyMessage = function (message) {
        return timestamp() + " " + message;
    }
    
    let displayOpponentsMessage = function (a) {
        if(ChatImprovements.playSounds) {
            playSound("chat-message");
        }
        let playerId = a.playerId;
        let message = a.message;
        let color;
        
        if(0 <= playerId && 3 >= playerId) {
            let name = B[playerId].name;
            message = "[" + name + "]: " + message;
        }
        else {
            color = "yellow";
        }
        
        message = unifyMessage(message);
        displayMessage(message, color);
    }
    
    gameChatTextbox.unbind();
    gameChatTextbox.keyup(function(ev) {
        if(ev.keyCode == 13) {
            let message = gameChatTextbox.val();
            gameChatTextbox.val("");
            sendEvent({
                type: "SendChatMessage",
                message: message
            });
            message = unifyMessage("[" + Ib + "]: " + message);
            // TODO: assign each person a different color?
            if(4 > A) {
                displayMessage(message);
            }
            else {
                displayMessage(message, "yellow");
            }
        }
    });
    
    // create sections
    let miscContainer = $("<div id=ci-ext-misc>");
    let miscSections = $("<div id=ci-ext-misc-sections>");
    let optionsColumn = $("<div id=ci-ext-options>");
    let cardColumn = $("#card-column");
    let gameContainer = $("#game-container");
    gameContainer.prepend(miscContainer);
    
    class GameOption {
        constructor(tag, id, option, type, info = {}) {
            this.tag = tag;
            this.id = id;
            this.option = option;
            this.type = type;
            
            this.isBaseOption = !!info.isBaseOption;
            this.showValue = !!info.showValue;
            this.decoration = info.decoration || (() => "");
            
            this.resolveOnLoad = info.resolveOnLoad;
            this.resolve = info.resolve;
            if(this.resolve) {
                this.resolve = this.resolve.bind(this);
            }
            
            if(this.isRange) {
                this.min = info.min;
                this.max = info.max;
            }
        }
        
        get isRange() {
            return this.type === "range";
        }
        
        get isCheckbox() {
            return this.type === "checkbox";
        }
        
        toElement(formatTable = true) {
            let base = $("<input>");
            
            base.attr("id", id)
                .attr("type", this.type);
            
            if(this.isRange) {
                base.attr("min", this.min)
                    .attr("max", this.max);
            }
            
            let currentValue;
            if(this.isBaseOption) {
                currentValue = globalOptions.options[this.option];
            }
            else {
                currentValue = ChatImprovements[this.option];
            }
            
            if(this.isCheckbox) {
                base.prop("checked", currentValue);
            }
            else {
                base.val(currentValue);
            }
            
            base.data("option", this.option);
            
            let onValueChange = () => {
                let option = this.option;
                let value = this.isCheckbox ? base.prop("checked") : base.val();
                
                if(this.isBaseOption) {
                    globalOptions.options[option] = value;
                    globalOptions.save();
                    // NOTE: can break
                    jd && jd(option, value);
                }
                else {
                    ChatImprovements[option] = value;
                }
                
                if(updateValueTd) {
                    updateValueTd.text(this.decoration(value));
                }
                if(this.resolve) {
                    this.resolve(value);
                }
            };
            
            let updateValueTd;
            if(this.showValue) {
                updateValueTd = $("<td>");
                // DRY broken here a bit
                updateValueTd.text(this.decoration(currentValue))
                             .css("cursor", "pointer");
                
                updateValueTd.click(() => {
                    let newValue = prompt("Enter the new value for \"" + this.tag + "\":");
                    
                    if(newValue !== null) {
                        base.val(newValue);
                        onValueChange();
                    }
                });
            }
            
            base.change(onValueChange);
            
            if(!formatTable) {
                return base;
            }
            
            let tr = $("<tr>");
            
            tr.append($("<td>").text(this.tag));
            tr.append($("<td>").append(base));
            
            if(this.showValue) {
                tr.append(updateValueTd);
            }
            
            return tr;
        }
    }
    /*
        var a = $(this).data("option"),
        b = $(this).val();
        $("#options-" + a + "-value").text(b + "%");
        f.options[a] = b;
        f.save();
        jd && jd(a, b)
    */
    // initialize options column
    let optionsColumnInfo = [
        [
            "Game Options",
            new GameOption(
                "Sounds volume",
                "ci-ext-option-sounds-volume",
                "sounds",
                "range",
                {
                    min: 0,
                    max: 100,
                    isBaseOption: true,
                    showValue: true,
                    decoration: (x) => `${x}%`,
                }
            ),
            new GameOption(
                "Music volume",
                "ci-ext-option-music-volume",
                "music",
                "range",
                {
                    min: 0,
                    max: 100,
                    isBaseOption: true,
                    showValue: true,
                    decoration: (x) => `${x}%`,
                }
            ),
            new GameOption(
                "Animations speed",
                "ci-ext-option-animation-speed",
                "speed",
                "range",
                {
                    min: 0,
                    max: 500,
                    isBaseOption: true,
                    showValue: true,
                    decoration: (x) => `${x}%`,
                }
            ),
        ],
        [
            new GameOption(
                "Place monsters automatically",
                "ci-ext-option-auto-place-monsters",
                "auto-place-monsters",
                "checkbox",
                {
                    isBaseOption: true,
                }
            ),
            new GameOption(
                "Place spells automatically",
                "ci-ext-option-auto-place-spells",
                "auto-place-spells",
                "checkbox",
                {
                    isBaseOption: true,
                }
            ),
            new GameOption(
                "Temporary chat",
                "ci-ext-option-temporary-chat",
                "temporaryChat",
                "checkbox",
                {
                    resolve: function () {
                        // TODO: hide existing chat
                    }
                }
            ),
            new GameOption(
                "Play chat sounds",
                "ci-ext-option-chat-sounds",
                "playSounds",
                "checkbox",
            ),
            new GameOption(
                "Show options",
                "ci-ext-option-show-options",
                "showOptions",
                "checkbox",
                {
                    resolve: function (value) {
                        console.log("Resolving showOptions value", value);
                        $("#options-show-button").toggle(value);
                        return this;
                    },
                    resolveOnLoad: true,
                }
            ),
        ],
        [
            "Event Filters",
            new GameOption(
                "Show normal events",
                "ci-ext-option-hide-all-events",
                "showNormalEvents",
                "checkbox",
            ),
            new GameOption(
                "Show chaining events",
                "ci-ext-option-hide-all-events",
                "showChainEvents",
                "checkbox",
            ),
        ]
    ];
    
    for(let stratum of optionsColumnInfo) {
        let table = $("<table>");
        while(stratum.length && typeof stratum[0] === "string") {
            let title = stratum.shift();
            optionsColumn.append($("<h2>").text(title));
        }
        for(let option of stratum) {
            let tr = option.toElement(true);
            if(option.resolveOnLoad) {
                console.log(tr);
                option.resolve();
            }
            table.append(tr);
        }
        optionsColumn.append(table);
    }
    
    /*.click(function() {
                var a = $(this).data("option"),
                b = $(this).val();
                $("#options-" + a + "-value").text(b + "%");
                f.options[a] = b;
                f.save();
                if(jd) {
                    jd(a, b);
                }
            })*/
    
    let miscSectionButtons = $("<div id=ci-ext-misc-buttons>");
    
    // moved earlier for hideMiscBut
    let minimizeToggle = $("<button class=engine-button title=minimize>&minus;</button>")
        .data("toggled", false)
        .click(function () {
            let toggled = $(this).data("toggled");
            gameChatContent.toggle(toggled);
            gameChatTextbox.toggle(toggled);
            toggled = !toggled;
            $(this).data("toggled", toggled);
            scrollToBottom(gameChatContent);
        });
    
    const hideMiscBut = function (but) {
        return function (ev) {
            for(let child of miscSections.children()) {
                let isVisible = child.id === but;
                $(child).toggle(isVisible);
                if(isVisible) {
                    scrollToBottom(child);
                }
            }
            gameChatContent.toggle(but !== "ci-ext-log" && but !== "ci-ext-event-log" && !minimizeToggle.data("toggled"));
        };
    };
    
    // button toggles for sections
    let showCardColumn = $("<button id=ci-ext-show-card-column class=engine-button>Card Info</button>");
    let showChatLog = $("<button id=ci-ext-show-chat-log class=engine-button>Chat Log</button>");
    let showEventLog = $("<button id=ci-ext-show-event-log class=engine-button>Event Log</button>");
    let showOptions = $("<button id=ci-ext-show-options class=engine-button>Options</button>");
    
    showCardColumn.click(hideMiscBut("card-column"));
    showChatLog.click(hideMiscBut("ci-ext-log"));
    showEventLog.click(hideMiscBut("ci-ext-event-log"));
    showOptions.click(hideMiscBut("ci-ext-options"));
    
    miscSectionButtons.append(showCardColumn, showChatLog, showEventLog, showOptions);
    miscContainer.append(miscSectionButtons);
    
    cardColumn.detach();
    miscSections.append(cardColumn);
    miscSections.append(chatLog);
    miscSections.append(chatEventLog);
    miscSections.append(optionsColumn);
    
    showCardColumn.click();
    
    miscContainer.append(miscSections);
    
    
    // update ui
    // TODO: toggle even newly inserted messages
    
    // let updateMuteToggleText;
    // let muteToggle = $("<button class=engine-button></button>")
        // .click(function () {
            // ChatImprovements.playSounds = !ChatImprovements.playSounds;
            // updateMuteToggleText();
        // })
        // .css("float", "right");
        
    // updateMuteToggleText = function () {
        // muteToggle.text(ChatImprovements.playSounds ? "Mute" : "Unmute");
    // };
    // updateMuteToggleText();
    
    // let updateNotificationToggleText;
    // let notificationToggle = $("<button class=engine-button></button>")
        // .click(function () {
            // ChatImprovements.showEvents = !ChatImprovements.showEvents;
            // updateNotificationToggleText();
            // $("#game-chat-area .notified-event").toggle(ChatImprovements.showEvents);
        // })
        // .css("float", "right");
    
    // updateNotificationToggleText = function () {
        // notificationToggle.text(ChatImprovements.showEvents ? "Hide events" : "Show events");
    // };
    // updateNotificationToggleText();
    
    // let 
    gameChatArea.prepend(
        minimizeToggle, /*muteToggle,*/ /*notificationToggle*/
    );
    
    // listeners[type] = [...];
    let listeners = {};
    ChatImprovements.addEventListener = function (ev, cb) {
        // TODO: verify
        listeners[ev] = listeners[ev] || [];
        listeners[ev].push(cb);
    }
    // reference
    const log = ChatImprovements.log;
    rb.shift = function (...args) {
        let res = Array.prototype.shift.apply(rb, args);
        log.push(res);
        if(typeof res.length !== "undefined") {
            alert("whoa! unexpected arguments passed to sb.shift!");
        }
        let eventType = res.type;
        if(listeners[eventType]) {
            for(let cb of listeners[eventType]) {
                cb(res);
            }
        }
        return res;
    }
    
    const Events = {
        CHAIN: "chain",
        NORMAL: "normal",
    };
    const notificationColors = {
        [Events.NORMAL]: "#00FF00",
        [Events.CHAIN]: "#AAFFAA",
    };
    const notificationPrefixes = {
        [Events.NORMAL]: "Event: ",
        [Events.CHAIN]: "",
    };
    const eventEnabledKeys = {
        [Events.NORMAL]: "showNormalEvents",
        [Events.CHAIN]: "showChainEvents",
    };
    const notifyEvent = function (event, kind = Events.NORMAL) {
        let prefix = notificationPrefixes[kind];
        let color = notificationColors[kind];
        let message = displayMessage(prefix + event, color, "notified-event", "event-" + kind);
        
        let enabledKey = eventEnabledKeys[kind];
        message.toggle(ChatImprovements[enabledKey]);
    };
    
    const GameLocations = {
        TOKEN_PILE: 0,
        DECK: 1,
        HAND: 2,
        FIELD_MONSTER: 4,
        FIELD_SPELLTRAP: 8,
        FIELD: 4 | 8,
        GY: 16,
        BANISHED: 32,
        EXTRA_DECK: 64,
        XYZ_MATERIAL: 128,
    };
    const LocationNames = {
        [GameLocations.TOKEN_PILE]: "token pile",
        [GameLocations.DECK]: "the Deck",
        [GameLocations.HAND]: "the hand",
        [GameLocations.FIELD_MONSTER]: "a Monster Zone",
        [GameLocations.FIELD_SPELLTRAP]: "a Spell & Trap Zone",
        [GameLocations.GY]: "the GY",
        [GameLocations.BANISHED]: "being banished",
        [GameLocations.EXTRA_DECK]: "the Extra Deck",
        192: " an Xyz Monster [bugged response, please report!]",
    };
    
    let cardCodeToSkip = null;
    
    let movedFromTo = function (move, start, end) {
        return (move.previousLocation & start) !== 0 &&
               (move.currentLocation & end) !== 0;
    }
    ChatImprovements.addEventListener("GameMove", function (move) {
        let cardName = move.cardCode ? "#@" + move.cardCode : "A card";
        if(move.cardCode) {
            cardCodeToSkip = move.cardCode;
        }
        // sent to GY
        if(movedFromTo(move, GameLocations.XYZ_MATERIAL, GameLocations.GY) ||
           movedFromTo(move, GameLocations.XYZ_MATERIAL, GameLocations.BANISHED)) {
            status = "was detached as Xyz Material"
        }
        else if(move.currentLocation === GameLocations.GY) {
            status = "was sent to the GY from " + LocationNames[move.previousLocation];
        }
        // returned to the deck
        else if(move.currentLocation === GameLocations.DECK) {
            status = "was shuffled/placed into the Deck from " + LocationNames[move.previousLocation];
        }
        // returned to the extra deck
        else if(move.currentLocation === GameLocations.EXTRA_DECK) {
            status = "was returned to the Extra Deck from " + LocationNames[move.previousLocation];
        }
        // attached as xyz material?
        else if(move.currentLocation & GameLocations.XYZ_MATERIAL) {
            status = "was attached as Xyz Material from " + LocationNames[move.previousLocation];
        }
        // banished
        else if(move.currentLocation === GameLocations.BANISHED) {
            status = "was banished from " + LocationNames[move.previousLocation];
        }
        // sent to hand
        else if(movedFromTo(move, GameLocations.GY, GameLocations.HAND)) {
            // console.log("why??????");
            status = "was returned from the GY to the hand";
        }
        else if(movedFromTo(move, GameLocations.DECK, GameLocations.HAND)) {
            status = "was added from the Deck to the hand";
        }
        else if(movedFromTo(move, GameLocations.BANISHED, GameLocations.HAND)) {
            status = "was added to the hand from being banished";
        }
        else if(movedFromTo(move, GameLocations.FIELD, GameLocations.HAND)) {
            status = "was returned from the field to the hand";
        }
        else if(movedFromTo(move, GameLocations.EXTRA_DECK, GameLocations.HAND)) {
            status = "was added to the hand from the face-up Extra Deck";
        }
        // monster summons
        else if(move.currentLocation === GameLocations.FIELD_MONSTER) {
            status = "was Summoned from " + LocationNames[move.previousLocation];
        }
        // spell card activations
        else if(move.currentLocation === GameLocations.FIELD_SPELLTRAP) {
            // TODO: set vs. activate
            console.log(move);
            status = "was activated/set from " + LocationNames[move.previousLocation];
        }
        else if(movedFromTo(move, GameLocations.FIELD, GameLocations.TOKEN_PILE)) {
            status = "was removed from the field";
        }
        else {
            // 0 from 4
            status = "- UNSURE!! " + move.currentLocation + " from " + move.previousLocation;
        }
        notifyEvent(cardName + " " + status);
        // TODO: more
    });
    
    ChatImprovements.addEventListener("cfReveal", function (code) {
        if(code && code !== cardCodeToSkip) {
            notifyEvent("Revealed #@" + code);
        }
        cardCodeToSkip = null;
    });
    
    ChatImprovements.addEventListener("targetCardAnimation", function (code) {
        if(code) {
            notifyEvent("Targeted #@" + code);
        }
    });
    
    
    // redefine window resizing
    window.Wb = function Wb() {
        var a = $("#ci-ext-misc-sections").position().top;
        
        // originally: - 24
        const offset = 37;
        $("#ci-ext-misc-sections div")
            .css("max-height", $(window).height() - a - offset);
        $("#game-siding-column")
            .css("max-height", $(window).height() - a - offset);
        
        a = 4 === Ab ? 7 : 6;
        var b = $(window).width() - $("#ci-ext-misc").width() - 50,
            // c = $(window).height();// - $("#game-chat-area").height() - 8 - 48;
            c = $(window).height() - $("#game-chat-textbox").outerHeight() - 8 - 48;
        9 * c / a < b ? ($("#game-field").css("height", c + "px"), b = c / a, $("#game-field").css("width", 9 * b + "px")) : ($("#game-field").css("width", b + "px"), b /= 9, $("#game-field").css("height", b * a + "px"));
        $(".game-field-zone").css("width",
            b + "px").css("height", b + "px");
        $(".game-field-hand").css("width", 5 * b + "px").css("height", b + "px");
        Cb = b;
        Db = Math.floor(.95 * b);
        E = 177 * Db / 254;
        Zc(m[0]);
        Zc(m[1]);
        $("#game-position-atk-up").css("width", E);
        $("#game-position-atk-up").css("height", Db);
        $("#game-position-atk-up").css("margin-right", Db - E + 3);
        $("#game-position-atk-down").css("width", E);
        $("#game-position-atk-down").css("height", Db);
        $("#game-position-atk-down").css("margin-right", Db - E + 3);
        $("#game-position-def-up").css("width", E);
        $("#game-position-def-up").css("height",
            Db);
        $("#game-position-def-up").css("margin-right", Db - E + 3);
        $("#game-position-def-down").css("width", E);
        $("#game-position-def-down").css("height", Db);
        $(".game-selection-card-image").css("width", E);
        zb && $c();
    }
    
    window.qf = function qf(a, b) {
        if(listeners["targetCardAnimation"]) {
            for(let cb of listeners["targetCardAnimation"]) {
                cb(a.code);
            }
        }
        let originalZ = a.a.css("z-index");
        a.a.css("z-index", 10000)
           .css("animation", "fullScale " + (600 * C) + "ms");
        a.a.animate({
            opacity: .5
        }, {
            duration: 100 * C
        }).animate({
            opacity: 1
        }, {
            duration: 100 * C
        }).animate({
            opacity: .5
        }, {
            duration: 100 * C
        }).animate({
            opacity: 1
        }, {
            duration: 100 * C
        }).animate({
            opacity: .5
        }, {
            duration: 100 * C
        }).animate({
            opacity: 1
        }, {
            duration: 100 * C,
            complete: function () {
                a.a.css("animation", "")
                   .css("z-index", originalZ);
                if(b) {
                    b();
                }
            }
        });
    };
    
    window.df = function df(a, b, c, d, e) {
        if(listeners["cfReveal"]) {
            for(let cb of listeners["cfReveal"]) {
                cb(b);
            }
        }
        var g = a.a.offset(),
            k = a.location & O.j || c & 5 ? b : 0,
            w = hg(a.controller, a.location, c) - a.va,
            F = false;
        if(a.Kb !== k) {
            F = true;
        }
        if(null !== a.b) {
            a.b.hide();
            a.K.hide();
        }
        a.code = b;
        a.position = c;
        $("<div />").animate({
            height: 1
        }, {
            duration: d,
            step: function(b, c) {
                b = c.pos;
                c = "translate(";
                c += (a.ta.left - g.left) * (1 - b);
                c += "px, ";
                c += (a.ta.top - g.top) * (1 - b);
                c += "px)";
                c += " rotate(" + (a.va + w * b) + "deg)";
                if(F) {
                    if(.5 < b) {
                        ig(a, k);
                    }
                    c += " scalex(" + Math.abs(1 - 2 * b) + ")";
                }
                a.a.css("transform", c)
            },
            complete: function() {
                null !== a.b && (a.b.show(), a.K.show());
                a.a.css("position",
                    "");
                nf(a);
                e()
            }
        })
    }
    
    // re-add listener
    // remove current resize listener
    // $(window).off("resize", Vb);
    // $(window).resize(Vb);
    Fb.ChatMessageReceived = window.qd = displayOpponentsMessage;
    console.info("ChatImprovements plugin loaded!");
    
    //
    let overlayExtension;
    $(".game-field-zone").on("mouseover", function (ev) {
        let player = $(this).data("player");
        let location = $(this).data("location");
        let index = $(this).data("index");
        let card = T(player, location, index);
        if(!card) return;
        let overlays = card.l;
        if(!overlayExtension) {
            overlayExtension = $("<p id=game-tooltip-overlay-extension></p>");
            $("#game-tooltip .card-if-monster").append(overlayExtension);
        }
        if(overlays && overlays.length) {
            $(overlayExtension).empty();
            let { width, height } = this.querySelector("img");
            console.log("width, height:", width, height);
            let plural = overlays.length === 1 ? "" : "s";
            let msg = "[" + overlays.length.toString() + " material" + plural + "]\n";
            $(overlayExtension).append($("<p>" + msg + "</p>"));
            for(let overlay of overlays) {
                let imgSrc = ra(overlay.code);
                let img = $("<img class=material-preview src='" + imgSrc + "' width=" + width + " height=" + height + ">");
                // img.width = width;
                // img.height = height;
                $(overlayExtension).append(img);
            }
            $(overlayExtension).show();
        }
        else {
            $(overlayExtension).hide();
        }
    });
    $(".game-field-zone").on("mouseout", function (ev) {
        $(overlayExtension).hide();
    });
};

waitForElementJQuery("#game-room-container:visible, #game-field:visible").then(() => {
    onload();
});
// on-load stuff