jstris+

3rd party matchmaking, custom skins/sfx/gfx, and many more improvements to jstris!

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         jstris+
// @namespace    http://tampermonkey.net/
// @version      2.5.8
// @description  3rd party matchmaking, custom skins/sfx/gfx, and many more improvements to jstris!
// @author       orz and frey
// @run-at       document-idle
// @match        https://*.jstris.jezevec10.com/*
// @grant        none
// ==/UserScript==


/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ 451:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {

var __webpack_unused_export__;

__webpack_unused_export__ = ({ value: true });
exports.g7 = exports.xv = __webpack_unused_export__ = exports.gN = void 0;
var decoder_1 = __webpack_require__(154);
var encoder_1 = __webpack_require__(662);
var field_1 = __webpack_require__(389);
Object.defineProperty(exports, "gN", ({ enumerable: true, get: function () { return field_1.Field; } }));
__webpack_unused_export__ = ({ enumerable: true, get: function () { return field_1.Mino; } });
exports.xv = {
    decode: function (data) {
        return (0, decoder_1.decode)(data);
    },
};
exports.g7 = {
    encode: function (data) {
        return "v115@".concat((0, encoder_1.encode)(data));
    },
};


/***/ }),

/***/ 90:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {


var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createActionEncoder = exports.createActionDecoder = void 0;
var defines_1 = __webpack_require__(54);
function decodeBool(n) {
    return n !== 0;
}
var createActionDecoder = function (width, fieldTop, garbageLine) {
    var fieldMaxHeight = fieldTop + garbageLine;
    var numFieldBlocks = fieldMaxHeight * width;
    function decodePiece(n) {
        switch (n) {
            case 0:
                return defines_1.Piece.Empty;
            case 1:
                return defines_1.Piece.I;
            case 2:
                return defines_1.Piece.L;
            case 3:
                return defines_1.Piece.O;
            case 4:
                return defines_1.Piece.Z;
            case 5:
                return defines_1.Piece.T;
            case 6:
                return defines_1.Piece.J;
            case 7:
                return defines_1.Piece.S;
            case 8:
                return defines_1.Piece.Gray;
        }
        throw new Error('Unexpected piece');
    }
    function decodeRotation(n) {
        switch (n) {
            case 0:
                return defines_1.Rotation.Reverse;
            case 1:
                return defines_1.Rotation.Right;
            case 2:
                return defines_1.Rotation.Spawn;
            case 3:
                return defines_1.Rotation.Left;
        }
        throw new Error('Unexpected rotation');
    }
    function decodeCoordinate(n, piece, rotation) {
        var x = n % width;
        var originY = Math.floor(n / 10);
        var y = fieldTop - originY - 1;
        if (piece === defines_1.Piece.O && rotation === defines_1.Rotation.Left) {
            x += 1;
            y -= 1;
        }
        else if (piece === defines_1.Piece.O && rotation === defines_1.Rotation.Reverse) {
            x += 1;
        }
        else if (piece === defines_1.Piece.O && rotation === defines_1.Rotation.Spawn) {
            y -= 1;
        }
        else if (piece === defines_1.Piece.I && rotation === defines_1.Rotation.Reverse) {
            x += 1;
        }
        else if (piece === defines_1.Piece.I && rotation === defines_1.Rotation.Left) {
            y -= 1;
        }
        else if (piece === defines_1.Piece.S && rotation === defines_1.Rotation.Spawn) {
            y -= 1;
        }
        else if (piece === defines_1.Piece.S && rotation === defines_1.Rotation.Right) {
            x -= 1;
        }
        else if (piece === defines_1.Piece.Z && rotation === defines_1.Rotation.Spawn) {
            y -= 1;
        }
        else if (piece === defines_1.Piece.Z && rotation === defines_1.Rotation.Left) {
            x += 1;
        }
        return { x: x, y: y };
    }
    return {
        decode: function (v) {
            var value = v;
            var type = decodePiece(value % 8);
            value = Math.floor(value / 8);
            var rotation = decodeRotation(value % 4);
            value = Math.floor(value / 4);
            var coordinate = decodeCoordinate(value % numFieldBlocks, type, rotation);
            value = Math.floor(value / numFieldBlocks);
            var isBlockUp = decodeBool(value % 2);
            value = Math.floor(value / 2);
            var isMirror = decodeBool(value % 2);
            value = Math.floor(value / 2);
            var isColor = decodeBool(value % 2);
            value = Math.floor(value / 2);
            var isComment = decodeBool(value % 2);
            value = Math.floor(value / 2);
            var isLock = !decodeBool(value % 2);
            return {
                rise: isBlockUp,
                mirror: isMirror,
                colorize: isColor,
                comment: isComment,
                lock: isLock,
                piece: __assign(__assign({}, coordinate), { type: type, rotation: rotation }),
            };
        },
    };
};
exports.createActionDecoder = createActionDecoder;
function encodeBool(flag) {
    return flag ? 1 : 0;
}
var createActionEncoder = function (width, fieldTop, garbageLine) {
    var fieldMaxHeight = fieldTop + garbageLine;
    var numFieldBlocks = fieldMaxHeight * width;
    function encodePosition(operation) {
        var type = operation.type, rotation = operation.rotation;
        var x = operation.x;
        var y = operation.y;
        if (!(0, defines_1.isMinoPiece)(type)) {
            x = 0;
            y = 22;
        }
        else if (type === defines_1.Piece.O && rotation === defines_1.Rotation.Left) {
            x -= 1;
            y += 1;
        }
        else if (type === defines_1.Piece.O && rotation === defines_1.Rotation.Reverse) {
            x -= 1;
        }
        else if (type === defines_1.Piece.O && rotation === defines_1.Rotation.Spawn) {
            y += 1;
        }
        else if (type === defines_1.Piece.I && rotation === defines_1.Rotation.Reverse) {
            x -= 1;
        }
        else if (type === defines_1.Piece.I && rotation === defines_1.Rotation.Left) {
            y += 1;
        }
        else if (type === defines_1.Piece.S && rotation === defines_1.Rotation.Spawn) {
            y += 1;
        }
        else if (type === defines_1.Piece.S && rotation === defines_1.Rotation.Right) {
            x += 1;
        }
        else if (type === defines_1.Piece.Z && rotation === defines_1.Rotation.Spawn) {
            y += 1;
        }
        else if (type === defines_1.Piece.Z && rotation === defines_1.Rotation.Left) {
            x -= 1;
        }
        return (fieldTop - y - 1) * width + x;
    }
    function encodeRotation(_a) {
        var type = _a.type, rotation = _a.rotation;
        if (!(0, defines_1.isMinoPiece)(type)) {
            return 0;
        }
        switch (rotation) {
            case defines_1.Rotation.Reverse:
                return 0;
            case defines_1.Rotation.Right:
                return 1;
            case defines_1.Rotation.Spawn:
                return 2;
            case defines_1.Rotation.Left:
                return 3;
        }
        throw new Error('No reachable');
    }
    return {
        encode: function (action) {
            var lock = action.lock, comment = action.comment, colorize = action.colorize, mirror = action.mirror, rise = action.rise, piece = action.piece;
            var value = encodeBool(!lock);
            value *= 2;
            value += encodeBool(comment);
            value *= 2;
            value += (encodeBool(colorize));
            value *= 2;
            value += encodeBool(mirror);
            value *= 2;
            value += encodeBool(rise);
            value *= numFieldBlocks;
            value += encodePosition(piece);
            value *= 4;
            value += encodeRotation(piece);
            value *= 8;
            value += piece.type;
            return value;
        },
    };
};
exports.createActionEncoder = createActionEncoder;


/***/ }),

/***/ 448:
/***/ ((__unused_webpack_module, exports) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Buffer = void 0;
var ENCODE_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var Buffer = /** @class */ (function () {
    function Buffer(data) {
        if (data === void 0) { data = ''; }
        this.values = data.split('').map(decodeToValue);
    }
    Buffer.prototype.poll = function (max) {
        var value = 0;
        for (var count = 0; count < max; count += 1) {
            var v = this.values.shift();
            if (v === undefined) {
                throw new Error('Unexpected fumen');
            }
            value += v * Math.pow(Buffer.tableLength, count);
        }
        return value;
    };
    Buffer.prototype.push = function (value, splitCount) {
        if (splitCount === void 0) { splitCount = 1; }
        var current = value;
        for (var count = 0; count < splitCount; count += 1) {
            this.values.push(current % Buffer.tableLength);
            current = Math.floor(current / Buffer.tableLength);
        }
    };
    Buffer.prototype.merge = function (postBuffer) {
        for (var _i = 0, _a = postBuffer.values; _i < _a.length; _i++) {
            var value = _a[_i];
            this.values.push(value);
        }
    };
    Buffer.prototype.isEmpty = function () {
        return this.values.length === 0;
    };
    Object.defineProperty(Buffer.prototype, "length", {
        get: function () {
            return this.values.length;
        },
        enumerable: false,
        configurable: true
    });
    Buffer.prototype.get = function (index) {
        return this.values[index];
    };
    Buffer.prototype.set = function (index, value) {
        this.values[index] = value;
    };
    Buffer.prototype.toString = function () {
        return this.values.map(encodeFromValue).join('');
    };
    Buffer.tableLength = ENCODE_TABLE.length;
    return Buffer;
}());
exports.Buffer = Buffer;
function decodeToValue(v) {
    return ENCODE_TABLE.indexOf(v);
}
function encodeFromValue(index) {
    return ENCODE_TABLE[index];
}


/***/ }),

/***/ 941:
/***/ ((__unused_webpack_module, exports) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createCommentParser = void 0;
var COMMENT_TABLE = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
var MAX_COMMENT_CHAR_VALUE = COMMENT_TABLE.length + 1;
var createCommentParser = function () {
    return {
        decode: function (v) {
            var str = '';
            var value = v;
            for (var count = 0; count < 4; count += 1) {
                var index = value % MAX_COMMENT_CHAR_VALUE;
                str += COMMENT_TABLE[index];
                value = Math.floor(value / MAX_COMMENT_CHAR_VALUE);
            }
            return str;
        },
        encode: function (ch, count) {
            return COMMENT_TABLE.indexOf(ch) * Math.pow(MAX_COMMENT_CHAR_VALUE, count);
        },
    };
};
exports.createCommentParser = createCommentParser;


/***/ }),

/***/ 154:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.decode = exports.extract = exports.Page = void 0;
var inner_field_1 = __webpack_require__(778);
var buffer_1 = __webpack_require__(448);
var defines_1 = __webpack_require__(54);
var action_1 = __webpack_require__(90);
var comments_1 = __webpack_require__(941);
var quiz_1 = __webpack_require__(946);
var field_1 = __webpack_require__(389);
var Page = /** @class */ (function () {
    function Page(index, field, operation, comment, flags, refs) {
        this.index = index;
        this.operation = operation;
        this.comment = comment;
        this.flags = flags;
        this.refs = refs;
        this._field = field.copy();
    }
    Object.defineProperty(Page.prototype, "field", {
        get: function () {
            return new field_1.Field(this._field.copy());
        },
        set: function (field) {
            this._field = (0, inner_field_1.createInnerField)(field);
        },
        enumerable: false,
        configurable: true
    });
    Page.prototype.mino = function () {
        return field_1.Mino.from(this.operation);
    };
    return Page;
}());
exports.Page = Page;
var FieldConstants = {
    GarbageLine: 1,
    Width: 10,
};
function extract(str) {
    var format = function (version, data) {
        var trim = data.trim().replace(/[?\s]+/g, '');
        return { version: version, data: trim };
    };
    var data = str;
    // url parameters
    var paramIndex = data.indexOf('&');
    if (0 <= paramIndex) {
        data = data.substring(0, paramIndex);
    }
    // v115@~
    {
        var match = str.match(/[vmd]115@/);
        if (match !== undefined && match !== null && match.index !== undefined) {
            var sub = data.substr(match.index + 5);
            return format('115', sub);
        }
    }
    // v110@~
    {
        var match = str.match(/[vmd]110@/);
        if (match !== undefined && match !== null && match.index !== undefined) {
            var sub = data.substr(match.index + 5);
            return format('110', sub);
        }
    }
    throw new Error('Unsupported fumen version');
}
exports.extract = extract;
function decode(fumen) {
    var _a = extract(fumen), version = _a.version, data = _a.data;
    switch (version) {
        case '115':
            return innerDecode(data, 23);
        case '110':
            return innerDecode(data, 21);
    }
    throw new Error('Unsupported fumen version');
}
exports.decode = decode;
function innerDecode(data, fieldTop) {
    var fieldMaxHeight = fieldTop + FieldConstants.GarbageLine;
    var numFieldBlocks = fieldMaxHeight * FieldConstants.Width;
    var buffer = new buffer_1.Buffer(data);
    var updateField = function (prev) {
        var result = {
            changed: true,
            field: prev,
        };
        var index = 0;
        while (index < numFieldBlocks) {
            var diffBlock = buffer.poll(2);
            var diff = Math.floor(diffBlock / numFieldBlocks);
            var numOfBlocks = diffBlock % numFieldBlocks;
            if (diff === 8 && numOfBlocks === numFieldBlocks - 1) {
                result.changed = false;
            }
            for (var block = 0; block < numOfBlocks + 1; block += 1) {
                var x = index % FieldConstants.Width;
                var y = fieldTop - Math.floor(index / FieldConstants.Width) - 1;
                result.field.addNumber(x, y, diff - 8);
                index += 1;
            }
        }
        return result;
    };
    var pageIndex = 0;
    var prevField = (0, inner_field_1.createNewInnerField)();
    var store = {
        repeatCount: -1,
        refIndex: {
            comment: 0,
            field: 0,
        },
        quiz: undefined,
        lastCommentText: '',
    };
    var pages = [];
    var actionDecoder = (0, action_1.createActionDecoder)(FieldConstants.Width, fieldTop, FieldConstants.GarbageLine);
    var commentDecoder = (0, comments_1.createCommentParser)();
    while (!buffer.isEmpty()) {
        // Parse field
        var currentFieldObj = void 0;
        if (0 < store.repeatCount) {
            currentFieldObj = {
                field: prevField,
                changed: false,
            };
            store.repeatCount -= 1;
        }
        else {
            currentFieldObj = updateField(prevField.copy());
            if (!currentFieldObj.changed) {
                store.repeatCount = buffer.poll(1);
            }
        }
        // Parse action
        var actionValue = buffer.poll(3);
        var action = actionDecoder.decode(actionValue);
        // Parse comment
        var comment = void 0;
        if (action.comment) {
            // コメントに更新があるとき
            var commentValues = [];
            var commentLength = buffer.poll(2);
            for (var commentCounter = 0; commentCounter < Math.floor((commentLength + 3) / 4); commentCounter += 1) {
                var commentValue = buffer.poll(5);
                commentValues.push(commentValue);
            }
            var flatten = '';
            for (var _i = 0, commentValues_1 = commentValues; _i < commentValues_1.length; _i++) {
                var value = commentValues_1[_i];
                flatten += commentDecoder.decode(value);
            }
            var commentText = unescape(flatten.slice(0, commentLength));
            store.lastCommentText = commentText;
            comment = { text: commentText };
            store.refIndex.comment = pageIndex;
            var text = comment.text;
            if (quiz_1.Quiz.isQuizComment(text)) {
                try {
                    store.quiz = new quiz_1.Quiz(text);
                }
                catch (e) {
                    store.quiz = undefined;
                }
            }
            else {
                store.quiz = undefined;
            }
        }
        else if (pageIndex === 0) {
            // コメントに更新がないが、先頭のページのとき
            comment = { text: '' };
        }
        else {
            // コメントに更新がないとき
            comment = {
                text: store.quiz !== undefined ? store.quiz.format().toString() : undefined,
                ref: store.refIndex.comment,
            };
        }
        // Quiz用の操作を取得し、次ページ開始時点のQuizに1手進める
        var quiz = false;
        if (store.quiz !== undefined) {
            quiz = true;
            if (store.quiz.canOperate() && action.lock) {
                if ((0, defines_1.isMinoPiece)(action.piece.type)) {
                    try {
                        var nextQuiz = store.quiz.nextIfEnd();
                        var operation = nextQuiz.getOperation(action.piece.type);
                        store.quiz = nextQuiz.operate(operation);
                    }
                    catch (e) {
                        // console.error(e.message);
                        // Not operate
                        store.quiz = store.quiz.format();
                    }
                }
                else {
                    store.quiz = store.quiz.format();
                }
            }
        }
        // データ処理用に加工する
        var currentPiece = void 0;
        if (action.piece.type !== defines_1.Piece.Empty) {
            currentPiece = action.piece;
        }
        // pageの作成
        var field = void 0;
        if (currentFieldObj.changed || pageIndex === 0) {
            // フィールドに変化があったとき
            // フィールドに変化がなかったが、先頭のページだったとき
            field = {};
            store.refIndex.field = pageIndex;
        }
        else {
            // フィールドに変化がないとき
            field = { ref: store.refIndex.field };
        }
        pages.push(new Page(pageIndex, currentFieldObj.field, currentPiece !== undefined ? field_1.Mino.from({
            type: (0, defines_1.parsePieceName)(currentPiece.type),
            rotation: (0, defines_1.parseRotationName)(currentPiece.rotation),
            x: currentPiece.x,
            y: currentPiece.y,
        }) : undefined, comment.text !== undefined ? comment.text : store.lastCommentText, {
            quiz: quiz,
            lock: action.lock,
            mirror: action.mirror,
            colorize: action.colorize,
            rise: action.rise,
        }, {
            field: field.ref,
            comment: comment.ref,
        }));
        // callback(
        //     currentFieldObj.field.copy()
        //     , currentPiece
        //     , store.quiz !== undefined ? store.quiz.format().toString() : store.lastCommentText,
        // );
        pageIndex += 1;
        if (action.lock) {
            if ((0, defines_1.isMinoPiece)(action.piece.type)) {
                currentFieldObj.field.fill(action.piece);
            }
            currentFieldObj.field.clearLine();
            if (action.rise) {
                currentFieldObj.field.riseGarbage();
            }
            if (action.mirror) {
                currentFieldObj.field.mirror();
            }
        }
        prevField = currentFieldObj.field;
    }
    return pages;
}


/***/ }),

/***/ 54:
/***/ ((__unused_webpack_module, exports) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.parseRotation = exports.parseRotationName = exports.Rotation = exports.parsePiece = exports.parsePieceName = exports.isMinoPiece = exports.Piece = void 0;
var Piece;
(function (Piece) {
    Piece[Piece["Empty"] = 0] = "Empty";
    Piece[Piece["I"] = 1] = "I";
    Piece[Piece["L"] = 2] = "L";
    Piece[Piece["O"] = 3] = "O";
    Piece[Piece["Z"] = 4] = "Z";
    Piece[Piece["T"] = 5] = "T";
    Piece[Piece["J"] = 6] = "J";
    Piece[Piece["S"] = 7] = "S";
    Piece[Piece["Gray"] = 8] = "Gray";
})(Piece = exports.Piece || (exports.Piece = {}));
function isMinoPiece(piece) {
    return piece !== Piece.Empty && piece !== Piece.Gray;
}
exports.isMinoPiece = isMinoPiece;
function parsePieceName(piece) {
    switch (piece) {
        case Piece.I:
            return 'I';
        case Piece.L:
            return 'L';
        case Piece.O:
            return 'O';
        case Piece.Z:
            return 'Z';
        case Piece.T:
            return 'T';
        case Piece.J:
            return 'J';
        case Piece.S:
            return 'S';
        case Piece.Gray:
            return 'X';
        case Piece.Empty:
            return '_';
    }
    throw new Error("Unknown piece: ".concat(piece));
}
exports.parsePieceName = parsePieceName;
function parsePiece(piece) {
    switch (piece.toUpperCase()) {
        case 'I':
            return Piece.I;
        case 'L':
            return Piece.L;
        case 'O':
            return Piece.O;
        case 'Z':
            return Piece.Z;
        case 'T':
            return Piece.T;
        case 'J':
            return Piece.J;
        case 'S':
            return Piece.S;
        case 'X':
        case 'GRAY':
            return Piece.Gray;
        case ' ':
        case '_':
        case 'EMPTY':
            return Piece.Empty;
    }
    throw new Error("Unknown piece: ".concat(piece));
}
exports.parsePiece = parsePiece;
var Rotation;
(function (Rotation) {
    Rotation[Rotation["Spawn"] = 0] = "Spawn";
    Rotation[Rotation["Right"] = 1] = "Right";
    Rotation[Rotation["Reverse"] = 2] = "Reverse";
    Rotation[Rotation["Left"] = 3] = "Left";
})(Rotation = exports.Rotation || (exports.Rotation = {}));
function parseRotationName(rotation) {
    switch (rotation) {
        case Rotation.Spawn:
            return 'spawn';
        case Rotation.Left:
            return 'left';
        case Rotation.Right:
            return 'right';
        case Rotation.Reverse:
            return 'reverse';
    }
    throw new Error("Unknown rotation: ".concat(rotation));
}
exports.parseRotationName = parseRotationName;
function parseRotation(rotation) {
    switch (rotation.toLowerCase()) {
        case 'spawn':
            return Rotation.Spawn;
        case 'left':
            return Rotation.Left;
        case 'right':
            return Rotation.Right;
        case 'reverse':
            return Rotation.Reverse;
    }
    throw new Error("Unknown rotation: ".concat(rotation));
}
exports.parseRotation = parseRotation;


/***/ }),

/***/ 662:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {


var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.encode = void 0;
var inner_field_1 = __webpack_require__(778);
var buffer_1 = __webpack_require__(448);
var defines_1 = __webpack_require__(54);
var action_1 = __webpack_require__(90);
var comments_1 = __webpack_require__(941);
var quiz_1 = __webpack_require__(946);
var FieldConstants = {
    GarbageLine: 1,
    Width: 10,
};
function encode(pages) {
    var updateField = function (prev, current) {
        var _a = encodeField(prev, current), changed = _a.changed, values = _a.values;
        if (changed) {
            // フィールドを記録して、リピートを終了する
            buffer.merge(values);
            lastRepeatIndex = -1;
        }
        else if (lastRepeatIndex < 0 || buffer.get(lastRepeatIndex) === buffer_1.Buffer.tableLength - 1) {
            // フィールドを記録して、リピートを開始する
            buffer.merge(values);
            buffer.push(0);
            lastRepeatIndex = buffer.length - 1;
        }
        else if (buffer.get(lastRepeatIndex) < (buffer_1.Buffer.tableLength - 1)) {
            // フィールドは記録せず、リピートを進める
            var currentRepeatValue = buffer.get(lastRepeatIndex);
            buffer.set(lastRepeatIndex, currentRepeatValue + 1);
        }
    };
    var lastRepeatIndex = -1;
    var buffer = new buffer_1.Buffer();
    var prevField = (0, inner_field_1.createNewInnerField)();
    var actionEncoder = (0, action_1.createActionEncoder)(FieldConstants.Width, 23, FieldConstants.GarbageLine);
    var commentParser = (0, comments_1.createCommentParser)();
    var prevComment = '';
    var prevQuiz = undefined;
    var innerEncode = function (index) {
        var currentPage = pages[index];
        currentPage.flags = currentPage.flags ? currentPage.flags : {};
        var field = currentPage.field;
        var currentField = field !== undefined ? (0, inner_field_1.createInnerField)(field) : prevField.copy();
        // フィールドの更新
        updateField(prevField, currentField);
        // アクションの更新
        var currentComment = currentPage.comment !== undefined
            ? ((index !== 0 || currentPage.comment !== '') ? currentPage.comment : undefined)
            : undefined;
        var piece = currentPage.operation !== undefined ? {
            type: (0, defines_1.parsePiece)(currentPage.operation.type),
            rotation: (0, defines_1.parseRotation)(currentPage.operation.rotation),
            x: currentPage.operation.x,
            y: currentPage.operation.y,
        } : {
            type: defines_1.Piece.Empty,
            rotation: defines_1.Rotation.Reverse,
            x: 0,
            y: 22,
        };
        var nextComment;
        if (currentComment !== undefined) {
            if (currentComment.startsWith('#Q=')) {
                // Quiz on
                if (prevQuiz !== undefined && prevQuiz.format().toString() === currentComment) {
                    nextComment = undefined;
                }
                else {
                    nextComment = currentComment;
                    prevComment = nextComment;
                    prevQuiz = new quiz_1.Quiz(currentComment);
                }
            }
            else {
                // Quiz off
                if (prevQuiz !== undefined && prevQuiz.format().toString() === currentComment) {
                    nextComment = undefined;
                    prevComment = currentComment;
                    prevQuiz = undefined;
                }
                else {
                    nextComment = prevComment !== currentComment ? currentComment : undefined;
                    prevComment = prevComment !== currentComment ? nextComment : prevComment;
                    prevQuiz = undefined;
                }
            }
        }
        else {
            nextComment = undefined;
            prevQuiz = undefined;
        }
        if (prevQuiz !== undefined && prevQuiz.canOperate() && currentPage.flags.lock) {
            if ((0, defines_1.isMinoPiece)(piece.type)) {
                try {
                    var nextQuiz = prevQuiz.nextIfEnd();
                    var operation = nextQuiz.getOperation(piece.type);
                    prevQuiz = nextQuiz.operate(operation);
                }
                catch (e) {
                    // console.error(e.message);
                    // Not operate
                    prevQuiz = prevQuiz.format();
                }
            }
            else {
                prevQuiz = prevQuiz.format();
            }
        }
        var currentFlags = __assign({ lock: true, colorize: index === 0 }, currentPage.flags);
        var action = {
            piece: piece,
            rise: !!currentFlags.rise,
            mirror: !!currentFlags.mirror,
            colorize: !!currentFlags.colorize,
            lock: !!currentFlags.lock,
            comment: nextComment !== undefined,
        };
        var actionNumber = actionEncoder.encode(action);
        buffer.push(actionNumber, 3);
        // コメントの更新
        if (nextComment !== undefined) {
            var comment = escape(currentPage.comment);
            var commentLength = Math.min(comment.length, 4095);
            buffer.push(commentLength, 2);
            // コメントを符号化
            for (var index_1 = 0; index_1 < commentLength; index_1 += 4) {
                var value = 0;
                for (var count = 0; count < 4; count += 1) {
                    var newIndex = index_1 + count;
                    if (commentLength <= newIndex) {
                        break;
                    }
                    var ch = comment.charAt(newIndex);
                    value += commentParser.encode(ch, count);
                }
                buffer.push(value, 5);
            }
        }
        else if (currentPage.comment === undefined) {
            prevComment = undefined;
        }
        // 地形の更新
        if (action.lock) {
            if ((0, defines_1.isMinoPiece)(action.piece.type)) {
                currentField.fill(action.piece);
            }
            currentField.clearLine();
            if (action.rise) {
                currentField.riseGarbage();
            }
            if (action.mirror) {
                currentField.mirror();
            }
        }
        prevField = currentField;
    };
    for (var index = 0; index < pages.length; index += 1) {
        innerEncode(index);
    }
    // テト譜が短いときはそのまま出力する
    // 47文字ごとに?が挿入されるが、実際は先頭にv115@が入るため、最初の?は42文字後になる
    var data = buffer.toString();
    if (data.length < 41) {
        return data;
    }
    // ?を挿入する
    var head = [data.substr(0, 42)];
    var tails = data.substring(42);
    var split = tails.match(/[\S]{1,47}/g) || [];
    return head.concat(split).join('?');
}
exports.encode = encode;
// フィールドをエンコードする
// 前のフィールドがないときは空のフィールドを指定する
// 入力フィールドの高さは23, 幅は10
function encodeField(prev, current) {
    var FIELD_TOP = 23;
    var FIELD_MAX_HEIGHT = FIELD_TOP + 1;
    var FIELD_BLOCKS = FIELD_MAX_HEIGHT * FieldConstants.Width;
    var buffer = new buffer_1.Buffer();
    // 前のフィールドとの差を計算: 0〜16
    var getDiff = function (xIndex, yIndex) {
        var y = FIELD_TOP - yIndex - 1;
        return current.getNumberAt(xIndex, y) - prev.getNumberAt(xIndex, y) + 8;
    };
    // データの記録
    var recordBlockCounts = function (diff, counter) {
        var value = diff * FIELD_BLOCKS + counter;
        buffer.push(value, 2);
    };
    // フィールド値から連続したブロック数に変換
    var changed = true;
    var prev_diff = getDiff(0, 0);
    var counter = -1;
    for (var yIndex = 0; yIndex < FIELD_MAX_HEIGHT; yIndex += 1) {
        for (var xIndex = 0; xIndex < FieldConstants.Width; xIndex += 1) {
            var diff = getDiff(xIndex, yIndex);
            if (diff !== prev_diff) {
                recordBlockCounts(prev_diff, counter);
                counter = 0;
                prev_diff = diff;
            }
            else {
                counter += 1;
            }
        }
    }
    // 最後の連続ブロックを処理
    recordBlockCounts(prev_diff, counter);
    if (prev_diff === 8 && counter === FIELD_BLOCKS - 1) {
        changed = false;
    }
    return {
        changed: changed,
        values: buffer,
    };
}


/***/ }),

/***/ 389:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {


var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Mino = exports.Field = void 0;
var inner_field_1 = __webpack_require__(778);
var defines_1 = __webpack_require__(54);
function toMino(operationOrMino) {
    return operationOrMino instanceof Mino ? operationOrMino.copy() : Mino.from(operationOrMino);
}
var Field = /** @class */ (function () {
    function Field(field) {
        this.field = field;
    }
    Field.create = function (field, garbage) {
        return new Field(new inner_field_1.InnerField({
            field: field !== undefined ? inner_field_1.PlayField.load(field) : undefined,
            garbage: garbage !== undefined ? inner_field_1.PlayField.loadMinify(garbage) : undefined,
        }));
    };
    Field.prototype.canFill = function (operation) {
        if (operation === undefined) {
            return true;
        }
        var mino = toMino(operation);
        return this.field.canFillAll(mino.positions());
    };
    Field.prototype.canLock = function (operation) {
        if (operation === undefined) {
            return true;
        }
        if (!this.canFill(operation)) {
            return false;
        }
        // Check on the ground
        return !this.canFill(__assign(__assign({}, operation), { y: operation.y - 1 }));
    };
    Field.prototype.fill = function (operation, force) {
        if (force === void 0) { force = false; }
        if (operation === undefined) {
            return undefined;
        }
        var mino = toMino(operation);
        if (!force && !this.canFill(mino)) {
            throw Error('Cannot fill piece on field');
        }
        this.field.fillAll(mino.positions(), (0, defines_1.parsePiece)(mino.type));
        return mino;
    };
    Field.prototype.put = function (operation) {
        if (operation === undefined) {
            return undefined;
        }
        var mino = toMino(operation);
        for (; 0 <= mino.y; mino.y -= 1) {
            if (!this.canLock(mino)) {
                continue;
            }
            this.fill(mino);
            return mino;
        }
        throw Error('Cannot put piece on field');
    };
    Field.prototype.clearLine = function () {
        this.field.clearLine();
    };
    Field.prototype.at = function (x, y) {
        return (0, defines_1.parsePieceName)(this.field.getNumberAt(x, y));
    };
    Field.prototype.set = function (x, y, type) {
        this.field.setNumberAt(x, y, (0, defines_1.parsePiece)(type));
    };
    Field.prototype.copy = function () {
        return new Field(this.field.copy());
    };
    Field.prototype.str = function (option) {
        if (option === void 0) { option = {}; }
        var skip = option.reduced !== undefined ? option.reduced : true;
        var separator = option.separator !== undefined ? option.separator : '\n';
        var minY = option.garbage === undefined || option.garbage ? -1 : 0;
        var output = '';
        for (var y = 22; minY <= y; y -= 1) {
            var line = '';
            for (var x = 0; x < 10; x += 1) {
                line += this.at(x, y);
            }
            if (skip && line === '__________') {
                continue;
            }
            skip = false;
            output += line;
            if (y !== minY) {
                output += separator;
            }
        }
        return output;
    };
    return Field;
}());
exports.Field = Field;
var Mino = /** @class */ (function () {
    function Mino(type, rotation, x, y) {
        this.type = type;
        this.rotation = rotation;
        this.x = x;
        this.y = y;
    }
    Mino.from = function (operation) {
        return new Mino(operation.type, operation.rotation, operation.x, operation.y);
    };
    Mino.prototype.positions = function () {
        return (0, inner_field_1.getBlockXYs)((0, defines_1.parsePiece)(this.type), (0, defines_1.parseRotation)(this.rotation), this.x, this.y).sort(function (a, b) {
            if (a.y === b.y) {
                return a.x - b.x;
            }
            return a.y - b.y;
        });
    };
    Mino.prototype.operation = function () {
        return {
            type: this.type,
            rotation: this.rotation,
            x: this.x,
            y: this.y,
        };
    };
    Mino.prototype.isValid = function () {
        try {
            (0, defines_1.parsePiece)(this.type);
            (0, defines_1.parseRotation)(this.rotation);
        }
        catch (e) {
            return false;
        }
        return this.positions().every(function (_a) {
            var x = _a.x, y = _a.y;
            return 0 <= x && x < 10 && 0 <= y && y < 23;
        });
    };
    Mino.prototype.copy = function () {
        return new Mino(this.type, this.rotation, this.x, this.y);
    };
    return Mino;
}());
exports.Mino = Mino;


/***/ }),

/***/ 778:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getPieces = exports.getBlocks = exports.getBlockXYs = exports.getBlockPositions = exports.PlayField = exports.InnerField = exports.createInnerField = exports.createNewInnerField = void 0;
var defines_1 = __webpack_require__(54);
var FieldConstants = {
    Width: 10,
    Height: 23,
    PlayBlocks: 23 * 10, // Height * Width
};
function createNewInnerField() {
    return new InnerField({});
}
exports.createNewInnerField = createNewInnerField;
function createInnerField(field) {
    var innerField = new InnerField({});
    for (var y = -1; y < FieldConstants.Height; y += 1) {
        for (var x = 0; x < FieldConstants.Width; x += 1) {
            var at = field.at(x, y);
            innerField.setNumberAt(x, y, (0, defines_1.parsePiece)(at));
        }
    }
    return innerField;
}
exports.createInnerField = createInnerField;
var InnerField = /** @class */ (function () {
    function InnerField(_a) {
        var _b = _a.field, field = _b === void 0 ? InnerField.create(FieldConstants.PlayBlocks) : _b, _c = _a.garbage, garbage = _c === void 0 ? InnerField.create(FieldConstants.Width) : _c;
        this.field = field;
        this.garbage = garbage;
    }
    InnerField.create = function (length) {
        return new PlayField({ length: length });
    };
    InnerField.prototype.fill = function (operation) {
        this.field.fill(operation);
    };
    InnerField.prototype.fillAll = function (positions, type) {
        this.field.fillAll(positions, type);
    };
    InnerField.prototype.canFill = function (piece, rotation, x, y) {
        var _this = this;
        var positions = getBlockPositions(piece, rotation, x, y);
        return positions.every(function (_a) {
            var px = _a[0], py = _a[1];
            return 0 <= px && px < 10
                && 0 <= py && py < FieldConstants.Height
                && _this.getNumberAt(px, py) === defines_1.Piece.Empty;
        });
    };
    InnerField.prototype.canFillAll = function (positions) {
        var _this = this;
        return positions.every(function (_a) {
            var x = _a.x, y = _a.y;
            return 0 <= x && x < 10
                && 0 <= y && y < FieldConstants.Height
                && _this.getNumberAt(x, y) === defines_1.Piece.Empty;
        });
    };
    InnerField.prototype.isOnGround = function (piece, rotation, x, y) {
        return !this.canFill(piece, rotation, x, y - 1);
    };
    InnerField.prototype.clearLine = function () {
        this.field.clearLine();
    };
    InnerField.prototype.riseGarbage = function () {
        this.field.up(this.garbage);
        this.garbage.clearAll();
    };
    InnerField.prototype.mirror = function () {
        this.field.mirror();
    };
    InnerField.prototype.shiftToLeft = function () {
        this.field.shiftToLeft();
    };
    InnerField.prototype.shiftToRight = function () {
        this.field.shiftToRight();
    };
    InnerField.prototype.shiftToUp = function () {
        this.field.shiftToUp();
    };
    InnerField.prototype.shiftToBottom = function () {
        this.field.shiftToBottom();
    };
    InnerField.prototype.copy = function () {
        return new InnerField({ field: this.field.copy(), garbage: this.garbage.copy() });
    };
    InnerField.prototype.equals = function (other) {
        return this.field.equals(other.field) && this.garbage.equals(other.garbage);
    };
    InnerField.prototype.addNumber = function (x, y, value) {
        if (0 <= y) {
            this.field.addOffset(x, y, value);
        }
        else {
            this.garbage.addOffset(x, -(y + 1), value);
        }
    };
    InnerField.prototype.setNumberFieldAt = function (index, value) {
        this.field.setAt(index, value);
    };
    InnerField.prototype.setNumberGarbageAt = function (index, value) {
        this.garbage.setAt(index, value);
    };
    InnerField.prototype.setNumberAt = function (x, y, value) {
        return 0 <= y ? this.field.set(x, y, value) : this.garbage.set(x, -(y + 1), value);
    };
    InnerField.prototype.getNumberAt = function (x, y) {
        return 0 <= y ? this.field.get(x, y) : this.garbage.get(x, -(y + 1));
    };
    InnerField.prototype.getNumberAtIndex = function (index, isField) {
        if (isField) {
            return this.getNumberAt(index % 10, Math.floor(index / 10));
        }
        return this.getNumberAt(index % 10, -(Math.floor(index / 10) + 1));
    };
    InnerField.prototype.toFieldNumberArray = function () {
        return this.field.toArray();
    };
    InnerField.prototype.toGarbageNumberArray = function () {
        return this.garbage.toArray();
    };
    return InnerField;
}());
exports.InnerField = InnerField;
var PlayField = /** @class */ (function () {
    function PlayField(_a) {
        var pieces = _a.pieces, _b = _a.length, length = _b === void 0 ? FieldConstants.PlayBlocks : _b;
        if (pieces !== undefined) {
            this.pieces = pieces;
        }
        else {
            this.pieces = Array.from({ length: length }).map(function () { return defines_1.Piece.Empty; });
        }
        this.length = length;
    }
    PlayField.load = function () {
        var lines = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            lines[_i] = arguments[_i];
        }
        var blocks = lines.join('').trim();
        return PlayField.loadInner(blocks);
    };
    PlayField.loadMinify = function () {
        var lines = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            lines[_i] = arguments[_i];
        }
        var blocks = lines.join('').trim();
        return PlayField.loadInner(blocks, blocks.length);
    };
    PlayField.loadInner = function (blocks, length) {
        var len = length !== undefined ? length : blocks.length;
        if (len % 10 !== 0) {
            throw new Error('Num of blocks in field should be mod 10');
        }
        var field = length !== undefined ? new PlayField({ length: length }) : new PlayField({});
        for (var index = 0; index < len; index += 1) {
            var block = blocks[index];
            field.set(index % 10, Math.floor((len - index - 1) / 10), (0, defines_1.parsePiece)(block));
        }
        return field;
    };
    PlayField.prototype.get = function (x, y) {
        return this.pieces[x + y * FieldConstants.Width];
    };
    PlayField.prototype.addOffset = function (x, y, value) {
        this.pieces[x + y * FieldConstants.Width] += value;
    };
    PlayField.prototype.set = function (x, y, piece) {
        this.setAt(x + y * FieldConstants.Width, piece);
    };
    PlayField.prototype.setAt = function (index, piece) {
        this.pieces[index] = piece;
    };
    PlayField.prototype.fill = function (_a) {
        var type = _a.type, rotation = _a.rotation, x = _a.x, y = _a.y;
        var blocks = getBlocks(type, rotation);
        for (var _i = 0, blocks_1 = blocks; _i < blocks_1.length; _i++) {
            var block = blocks_1[_i];
            var _b = [x + block[0], y + block[1]], nx = _b[0], ny = _b[1];
            this.set(nx, ny, type);
        }
    };
    PlayField.prototype.fillAll = function (positions, type) {
        for (var _i = 0, positions_1 = positions; _i < positions_1.length; _i++) {
            var _a = positions_1[_i], x = _a.x, y = _a.y;
            this.set(x, y, type);
        }
    };
    PlayField.prototype.clearLine = function () {
        var newField = this.pieces.concat();
        var top = this.pieces.length / FieldConstants.Width - 1;
        for (var y = top; 0 <= y; y -= 1) {
            var line = this.pieces.slice(y * FieldConstants.Width, (y + 1) * FieldConstants.Width);
            var isFilled = line.every(function (value) { return value !== defines_1.Piece.Empty; });
            if (isFilled) {
                var bottom = newField.slice(0, y * FieldConstants.Width);
                var over = newField.slice((y + 1) * FieldConstants.Width);
                newField = bottom.concat(over, Array.from({ length: FieldConstants.Width }).map(function () { return defines_1.Piece.Empty; }));
            }
        }
        this.pieces = newField;
    };
    PlayField.prototype.up = function (blockUp) {
        this.pieces = blockUp.pieces.concat(this.pieces).slice(0, this.length);
    };
    PlayField.prototype.mirror = function () {
        var newField = [];
        for (var y = 0; y < this.pieces.length; y += 1) {
            var line = this.pieces.slice(y * FieldConstants.Width, (y + 1) * FieldConstants.Width);
            line.reverse();
            for (var _i = 0, line_1 = line; _i < line_1.length; _i++) {
                var obj = line_1[_i];
                newField.push(obj);
            }
        }
        this.pieces = newField;
    };
    PlayField.prototype.shiftToLeft = function () {
        var height = this.pieces.length / 10;
        for (var y = 0; y < height; y += 1) {
            for (var x = 0; x < FieldConstants.Width - 1; x += 1) {
                this.pieces[x + y * FieldConstants.Width] = this.pieces[x + 1 + y * FieldConstants.Width];
            }
            this.pieces[9 + y * FieldConstants.Width] = defines_1.Piece.Empty;
        }
    };
    PlayField.prototype.shiftToRight = function () {
        var height = this.pieces.length / 10;
        for (var y = 0; y < height; y += 1) {
            for (var x = FieldConstants.Width - 1; 1 <= x; x -= 1) {
                this.pieces[x + y * FieldConstants.Width] = this.pieces[x - 1 + y * FieldConstants.Width];
            }
            this.pieces[y * FieldConstants.Width] = defines_1.Piece.Empty;
        }
    };
    PlayField.prototype.shiftToUp = function () {
        var blanks = Array.from({ length: 10 }).map(function () { return defines_1.Piece.Empty; });
        this.pieces = blanks.concat(this.pieces).slice(0, this.length);
    };
    PlayField.prototype.shiftToBottom = function () {
        var blanks = Array.from({ length: 10 }).map(function () { return defines_1.Piece.Empty; });
        this.pieces = this.pieces.slice(10, this.length).concat(blanks);
    };
    PlayField.prototype.toArray = function () {
        return this.pieces.concat();
    };
    Object.defineProperty(PlayField.prototype, "numOfBlocks", {
        get: function () {
            return this.pieces.length;
        },
        enumerable: false,
        configurable: true
    });
    PlayField.prototype.copy = function () {
        return new PlayField({ pieces: this.pieces.concat(), length: this.length });
    };
    PlayField.prototype.toShallowArray = function () {
        return this.pieces;
    };
    PlayField.prototype.clearAll = function () {
        this.pieces = this.pieces.map(function () { return defines_1.Piece.Empty; });
    };
    PlayField.prototype.equals = function (other) {
        if (this.pieces.length !== other.pieces.length) {
            return false;
        }
        for (var index = 0; index < this.pieces.length; index += 1) {
            if (this.pieces[index] !== other.pieces[index]) {
                return false;
            }
        }
        return true;
    };
    return PlayField;
}());
exports.PlayField = PlayField;
function getBlockPositions(piece, rotation, x, y) {
    return getBlocks(piece, rotation).map(function (position) {
        position[0] += x;
        position[1] += y;
        return position;
    });
}
exports.getBlockPositions = getBlockPositions;
function getBlockXYs(piece, rotation, x, y) {
    return getBlocks(piece, rotation).map(function (position) {
        return { x: position[0] + x, y: position[1] + y };
    });
}
exports.getBlockXYs = getBlockXYs;
function getBlocks(piece, rotation) {
    var blocks = getPieces(piece);
    switch (rotation) {
        case defines_1.Rotation.Spawn:
            return blocks;
        case defines_1.Rotation.Left:
            return rotateLeft(blocks);
        case defines_1.Rotation.Reverse:
            return rotateReverse(blocks);
        case defines_1.Rotation.Right:
            return rotateRight(blocks);
    }
    throw new Error('Unsupported block');
}
exports.getBlocks = getBlocks;
function getPieces(piece) {
    switch (piece) {
        case defines_1.Piece.I:
            return [[0, 0], [-1, 0], [1, 0], [2, 0]];
        case defines_1.Piece.T:
            return [[0, 0], [-1, 0], [1, 0], [0, 1]];
        case defines_1.Piece.O:
            return [[0, 0], [1, 0], [0, 1], [1, 1]];
        case defines_1.Piece.L:
            return [[0, 0], [-1, 0], [1, 0], [1, 1]];
        case defines_1.Piece.J:
            return [[0, 0], [-1, 0], [1, 0], [-1, 1]];
        case defines_1.Piece.S:
            return [[0, 0], [-1, 0], [0, 1], [1, 1]];
        case defines_1.Piece.Z:
            return [[0, 0], [1, 0], [0, 1], [-1, 1]];
    }
    throw new Error('Unsupported rotation');
}
exports.getPieces = getPieces;
function rotateRight(positions) {
    return positions.map(function (current) { return [current[1], -current[0]]; });
}
function rotateLeft(positions) {
    return positions.map(function (current) { return [-current[1], current[0]]; });
}
function rotateReverse(positions) {
    return positions.map(function (current) { return [-current[0], -current[1]]; });
}


/***/ }),

/***/ 946:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Quiz = void 0;
var defines_1 = __webpack_require__(54);
var Operation;
(function (Operation) {
    Operation["Direct"] = "direct";
    Operation["Swap"] = "swap";
    Operation["Stock"] = "stock";
})(Operation || (Operation = {}));
var Quiz = /** @class */ (function () {
    function Quiz(quiz) {
        this.quiz = Quiz.verify(quiz);
    }
    Object.defineProperty(Quiz.prototype, "next", {
        get: function () {
            var index = this.quiz.indexOf(')') + 1;
            var name = this.quiz[index];
            if (name === undefined || name === ';') {
                return '';
            }
            return name;
        },
        enumerable: false,
        configurable: true
    });
    Quiz.isQuizComment = function (comment) {
        return comment.startsWith('#Q=');
    };
    Quiz.create = function (first, second) {
        var create = function (hold, other) {
            var parse = function (s) { return s ? s : ''; };
            return new Quiz("#Q=[".concat(parse(hold), "](").concat(parse(other[0]), ")").concat(parse(other.substring(1))));
        };
        return second !== undefined ? create(first, second) : create(undefined, first);
    };
    Quiz.trim = function (quiz) {
        return quiz.trim().replace(/\s+/g, '');
    };
    Object.defineProperty(Quiz.prototype, "least", {
        get: function () {
            var index = this.quiz.indexOf(')');
            return this.quiz.substr(index + 1);
        },
        enumerable: false,
        configurable: true
    });
    Object.defineProperty(Quiz.prototype, "current", {
        get: function () {
            var index = this.quiz.indexOf('(') + 1;
            var name = this.quiz[index];
            if (name === ')') {
                return '';
            }
            return name;
        },
        enumerable: false,
        configurable: true
    });
    Object.defineProperty(Quiz.prototype, "hold", {
        get: function () {
            var index = this.quiz.indexOf('[') + 1;
            var name = this.quiz[index];
            if (name === ']') {
                return '';
            }
            return name;
        },
        enumerable: false,
        configurable: true
    });
    Object.defineProperty(Quiz.prototype, "leastAfterNext2", {
        get: function () {
            var index = this.quiz.indexOf(')');
            if (this.quiz[index + 1] === ';') {
                return this.quiz.substr(index + 1);
            }
            return this.quiz.substr(index + 2);
        },
        enumerable: false,
        configurable: true
    });
    Quiz.prototype.getOperation = function (used) {
        var usedName = (0, defines_1.parsePieceName)(used);
        var current = this.current;
        if (usedName === current) {
            return Operation.Direct;
        }
        var hold = this.hold;
        if (usedName === hold) {
            return Operation.Swap;
        }
        // 次のミノを利用できる
        if (hold === '') {
            if (usedName === this.next) {
                return Operation.Stock;
            }
        }
        else {
            if (current === '' && usedName === this.next) {
                return Operation.Direct;
            }
        }
        throw new Error("Unexpected hold piece in quiz: ".concat(this.quiz));
    };
    Object.defineProperty(Quiz.prototype, "leastInActiveBag", {
        get: function () {
            var separateIndex = this.quiz.indexOf(';');
            var quiz = 0 <= separateIndex ? this.quiz.substring(0, separateIndex) : this.quiz;
            var index = quiz.indexOf(')');
            if (quiz[index + 1] === ';') {
                return quiz.substr(index + 1);
            }
            return quiz.substr(index + 2);
        },
        enumerable: false,
        configurable: true
    });
    Quiz.verify = function (quiz) {
        var replaced = this.trim(quiz);
        if (replaced.length === 0 || quiz === '#Q=[]()' || !quiz.startsWith('#Q=')) {
            return quiz;
        }
        if (!replaced.match(/^#Q=\[[TIOSZJL]?]\([TIOSZJL]?\)[TIOSZJL]*;?.*$/i)) {
            throw new Error("Current piece doesn't exist, however next pieces exist: ".concat(quiz));
        }
        return replaced;
    };
    Quiz.prototype.direct = function () {
        if (this.current === '') {
            var least = this.leastAfterNext2;
            return new Quiz("#Q=[".concat(this.hold, "](").concat(least[0], ")").concat(least.substr(1)));
        }
        return new Quiz("#Q=[".concat(this.hold, "](").concat(this.next, ")").concat(this.leastAfterNext2));
    };
    Quiz.prototype.swap = function () {
        if (this.hold === '') {
            throw new Error("Cannot find hold piece: ".concat(this.quiz));
        }
        var next = this.next;
        return new Quiz("#Q=[".concat(this.current, "](").concat(next, ")").concat(this.leastAfterNext2));
    };
    Quiz.prototype.stock = function () {
        if (this.hold !== '' || this.next === '') {
            throw new Error("Cannot stock: ".concat(this.quiz));
        }
        var least = this.leastAfterNext2;
        var head = least[0] !== undefined ? least[0] : '';
        if (1 < least.length) {
            return new Quiz("#Q=[".concat(this.current, "](").concat(head, ")").concat(least.substr(1)));
        }
        return new Quiz("#Q=[".concat(this.current, "](").concat(head, ")"));
    };
    Quiz.prototype.operate = function (operation) {
        switch (operation) {
            case Operation.Direct:
                return this.direct();
            case Operation.Swap:
                return this.swap();
            case Operation.Stock:
                return this.stock();
        }
        throw new Error('Unexpected operation');
    };
    Quiz.prototype.format = function () {
        var quiz = this.nextIfEnd();
        if (quiz.quiz === '#Q=[]()') {
            return new Quiz('');
        }
        var current = quiz.current;
        var hold = quiz.hold;
        if (current === '' && hold !== '') {
            return new Quiz("#Q=[](".concat(hold, ")").concat(quiz.least));
        }
        if (current === '') {
            var least = quiz.least;
            var head = least[0];
            if (head === undefined) {
                return new Quiz('');
            }
            if (head === ';') {
                return new Quiz(least.substr(1));
            }
            return new Quiz("#Q=[](".concat(head, ")").concat(least.substr(1)));
        }
        return quiz;
    };
    Quiz.prototype.getHoldPiece = function () {
        if (!this.canOperate()) {
            return defines_1.Piece.Empty;
        }
        var name = this.hold;
        if (name === undefined || name === '' || name === ';') {
            return defines_1.Piece.Empty;
        }
        return (0, defines_1.parsePiece)(name);
    };
    Quiz.prototype.getNextPieces = function (max) {
        if (!this.canOperate()) {
            return max !== undefined ? Array.from({ length: max }).map(function () { return defines_1.Piece.Empty; }) : [];
        }
        var names = (this.current + this.next + this.leastInActiveBag).substr(0, max);
        if (max !== undefined && names.length < max) {
            names += ' '.repeat(max - names.length);
        }
        return names.split('').map(function (name) {
            if (name === undefined || name === ' ' || name === ';') {
                return defines_1.Piece.Empty;
            }
            return (0, defines_1.parsePiece)(name);
        });
    };
    Quiz.prototype.toString = function () {
        return this.quiz;
    };
    Quiz.prototype.canOperate = function () {
        var quiz = this.quiz;
        if (quiz.startsWith('#Q=[]();')) {
            quiz = this.quiz.substr(8);
        }
        return quiz.startsWith('#Q=') && quiz !== '#Q=[]()';
    };
    Quiz.prototype.nextIfEnd = function () {
        if (this.quiz.startsWith('#Q=[]();')) {
            return new Quiz(this.quiz.substr(8));
        }
        return this;
    };
    return Quiz;
}());
exports.Quiz = Quiz;


/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {

;// CONCATENATED MODULE: ./src/config.js
// these are default values
var config = {
  FIRST_OPEN: true,

  ENABLE_LINECLEAR_ANIMATION: true,
  ENABLE_LINECLEAR_SHAKE: true,
  ENABLE_PLACE_BLOCK_ANIMATION: true,
  ENABLE_ACTION_TEXT: true,

  PIECE_FLASH_OPACITY: 0.5,
  PIECE_FLASH_LENGTH: 0.5,
  LINE_CLEAR_LENGTH: 0.5,
  LINE_CLEAR_SHAKE_STRENGTH: 1,
  LINE_CLEAR_SHAKE_LENGTH: 1,

  BACKGROUND_IMAGE_URL: "",
  CUSTOM_SKIN_URL: "",
  CUSTOM_GHOST_SKIN_URL: "",
  ENABLE_REPLAY_SKIN: true,
  ENABLE_KEYBOARD_DISPLAY: false,

  ENABLE_OPPONENT_SFX: true,
  OPPONENT_SFX_VOLUME_MULTPLIER: 0.5,
  ENABLE_CUSTOM_VFX: false,
  ENABLE_CUSTOM_SFX: false,
  CUSTOM_SFX_JSON: "",
  CUSTOM_PLUS_SFX_JSON: "",

  ENABLE_STAT_APP: false,
  ENABLE_STAT_PPD: false,
  ENABLE_STAT_CHEESE_BLOCK_PACE: false,
  ENABLE_STAT_CHEESE_TIME_PACE: false,
  ENABLE_STAT_PPB: false,
  ENABLE_STAT_SCORE_PACE: false,
  ENABLE_STAT_PC_NUMBER: false,

  ENABLE_AUTOMATIC_REPLAY_CODES: false,
  ENABLE_CHAT_TIMESTAMPS: true,
  SHOW_QUEUE_INFO: true,
  SHOW_MM_BUTTON: true,
  TOGGLE_CHAT_KEYCODE: null,
  CLOSE_CHAT_KEYCODE: null,
  SCREENSHOT_KEYCODE: null,

  UNDO_KEYCODE: null,
};

const defaultConfig = { ...config };

var listeners = [];

const initConfig = () => {
  for (var i in config) {
    var val = JSON.parse(localStorage.getItem(i));
    if (val != undefined && val != null) {
      config[i] = val;
    }
  }
}

const set = function (name, val) {
  config[name] = val;
  localStorage.setItem(name, JSON.stringify(val));
  for (var { event, listener } of listeners) {
    if (event == name)
      listener(val);
  }
}

const config_reset = function (name) {
  set(name, defaultConfig[name]);
}

const onChange = (event, listener) => {
  listeners.push({ event, listener });
}

const Config = () => ({ ...config, set, onChange, reset: config_reset });
;// CONCATENATED MODULE: ./src/util.js
const shouldRenderEffectsOnView = (view) => {
  return view.holdCanvas && view.holdCanvas.width >= 70;
}


const lerp = (start, end, amt) => {
  return (1 - amt) * start + amt * end;
}

// https://jsfiddle.net/12aueufy/1/
var shakingElements = [];

const shake = function (element, magnitude = 16, numberOfShakes = 15, angular = false) {
  if (!element) return;

  //First set the initial tilt angle to the right (+1)
  var tiltAngle = 1;

  //A counter to count the number of shakes
  var counter = 1;

  //The total number of shakes (there will be 1 shake per frame)

  //Capture the element's position and angle so you can
  //restore them after the shaking has finished
  var startX = 0,
    startY = 0,
    startAngle = 0;

  // Divide the magnitude into 10 units so that you can
  // reduce the amount of shake by 10 percent each frame
  var magnitudeUnit = magnitude / numberOfShakes;

  //The `randomInt` helper function
  var randomInt = (min, max) => {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  };

  //Add the element to the `shakingElements` array if it
  //isn't already there


  if (shakingElements.indexOf(element) === -1) {
    //console.log("added")
    shakingElements.push(element);

    //Add an `updateShake` method to the element.
    //The `updateShake` method will be called each frame
    //in the game loop. The shake effect type can be either
    //up and down (x/y shaking) or angular (rotational shaking).
    if (angular) {
      angularShake();
    } else {
      upAndDownShake();
    }
  }

  //The `upAndDownShake` function
  function upAndDownShake() {

    //Shake the element while the `counter` is less than
    //the `numberOfShakes`
    if (counter < numberOfShakes) {

      //Reset the element's position at the start of each shake
      element.style.transform = 'translate(' + startX + 'px, ' + startY + 'px)';

      //Reduce the magnitude
      magnitude -= magnitudeUnit;

      //Randomly change the element's position
      var randomX = randomInt(-magnitude, magnitude);
      var randomY = randomInt(-magnitude, magnitude);

      element.style.transform = 'translate(' + randomX + 'px, ' + randomY + 'px)';

      //Add 1 to the counter
      counter += 1;

      requestAnimationFrame(upAndDownShake);
    }

    //When the shaking is finished, restore the element to its original
    //position and remove it from the `shakingElements` array
    if (counter >= numberOfShakes) {
      element.style.transform = 'translate(' + startX + ', ' + startY + ')';
      shakingElements.splice(shakingElements.indexOf(element), 1);
    }
  }

  //The `angularShake` function
  function angularShake() {
    if (counter < numberOfShakes) {

      //Reset the element's rotation
      element.style.transform = 'rotate(' + startAngle + 'deg)';

      //Reduce the magnitude
      magnitude -= magnitudeUnit;

      //Rotate the element left or right, depending on the direction,
      //by an amount in radians that matches the magnitude
      var angle = Number(magnitude * tiltAngle).toFixed(2);

      element.style.transform = 'rotate(' + angle + 'deg)';
      counter += 1;

      //Reverse the tilt angle so that the element is tilted
      //in the opposite direction for the next shake
      tiltAngle *= -1;

      requestAnimationFrame(angularShake);
    }

    //When the shaking is finished, reset the element's angle and
    //remove it from the `shakingElements` array
    if (counter >= numberOfShakes) {
      element.style.transform = 'rotate(' + startAngle + 'deg)';
      shakingElements.splice(shakingElements.indexOf(element), 1);
    }
  }

};


// @params callback: (name: string , loggedIn: boolean) => {}
const getPlayerName = (callback) => {
  fetch("https://jstris.jezevec10.com/profile").then(res => {
    if (res.url.includes("/u/")) {
      let username = res.url.substring(res.url.indexOf("/u/") + 3);
      callback(username, true);
    } else {
      callback("", false)
    }
  }).catch(e => {
    console.log(e);
    callback("", false)
  })
}

let notificationsSupported = false

const authNotification = () => {
  if (!window.Notification) {
    notificationsSupported = false
  } else if (Notification.permission != 'granted') {
    Notification.requestPermission().then((p) => {
      if (p === 'granted') {
        notificationsSupported = true
      } else {
        console.log('User has blocked notifications.')
      }
    }).catch((err) => {
      console.error(err)
    })
  } else {
    notificationsSupported = true
  }
}

const notify = (title, body) => {
  if (notificationsSupported) {
    new Notification(title, {
      body: body,
      icon: 'https://jstrisplus.github.io/jstris-plus-assets/logo.png'
    })
  }
}

let plusSfx = { //fallback
  READY: "https://jstrisplus.github.io/jstris-plus-assets/sfx/ready.wav",
  PB: "https://jstrisplus.github.io/jstris-plus-assets/sfx/personalBest.wav"
}
const setPlusSfx = (sfx) => {
  let d = document.getElementById('custom_plus_sfx_json_err')
  try {
    sfx = JSON.parse(sfx)
  } catch (e) {

    if (d) {
      d.textContent = "SFX json is invalid"
    }
    return
  }
  d.textContent = `Loaded ${sfx.name} Jstris+ SFX`
  plusSfx = sfx
}
const playSound = (id) => {
  if (!plusSfx[id]) {
    return console.error(`unknown sfx ${id}`)
  }
  const audio = new Audio(plusSfx[id]);
  audio.play();
}

;// CONCATENATED MODULE: ./src/actiontext.js



const DELAY = 1500; // ms
const FADEOUT = 0.15; // s
const SPIKE_TIMER = 1000;
const MAX_HEIGHT = 250;

class Displayer {

    constructor(index) {
        this.index = index;
        this.id = 0;
        this.displayedActions = [];
        this.spike = 0;
        this.lastAttack = 0;
        this.lastSpikeAttack = 0;
    }

    displayNewAction(value, atk) {

        if (!Config().ENABLE_ACTION_TEXT)
            return;

        let ctime = (new Date()).getTime();
        let spike_tracker = document.getElementById(`atk_spike_${this.index + 1}`);
        if (ctime - this.lastAttack < SPIKE_TIMER) {
            this.spike += value;
        } else {
            this.spike = value
        }
        if (this.spike >= 10) {
            spike_tracker.classList.remove("fade");
            spike_tracker.classList.add("fade", "in");
            spike_tracker.innerHTML = `${this.spike} SPIKE`;
            this.lastSpikeAttack = ctime;
            setTimeout((time) => {
                if (this.lastSpikeAttack == time) {
                    spike_tracker.classList.remove("in");
                    setTimeout((remove_from) => {
                        remove_from.innerHTML = "";
                    }, FADEOUT * 1000, spike_tracker);
                    this.spike = 0;
                }
            }, SPIKE_TIMER, ctime);
        }
        this.lastAttack = ctime;
        let action = document.createElement("p");
        action.innerHTML = `+${value}<br> ${atk}`;
        action.setAttribute("id", `atk_text_${this.index + 1}_${this.id++}`);
        action.setAttribute("class", "action-text fade in");
        action.style.textAlign = "center";
        if (value >= 5) {
            action.style.fontSize = "large";
            action.style.fontWeight = "bold";
        }
        if (value >= 10) {
            action.style.color = "red";
        }
        document.getElementById(`atk_div_${this.index + 1}`).prepend(action);
        this.displayedActions.splice(0, 0, action.id);
        setTimeout((ind, id) => {
            try {
                let target = document.getElementById(`atk_text_${ind + 1}_${id - 1}`);
                target.classList.remove("in");
                setTimeout((target) => {
                    try {
                        this.displayedActions = this.displayedActions.filter((i) => i != target.id);
                        target.parentNode.removeChild(target);
                    } catch (e) { } // idc
                }, FADEOUT * 1000, target);
            } catch (e) { } // idc
        }, DELAY, this.index, this.id);
    }

    reset() {
        for (let action of this.displayedActions) {
            try {
                action.parentNode.removeChild(action);
            } catch (e) { }
        }
        this.displayedActions = [];
        this.id = 0;
    }

}

class DisplayerManager {
    constructor() {
        this.displayers = [];
    }

    createDisplayer() {
        let a = new Displayer();
        a.index = this.addDisplayer(a);
        return a;
    }

    addDisplayer(displayer) {
        for (let i = 0; i < this.displayers.length; i++) {
            if (this.displayers[i] == null) {
                this.displayers[i] = displayer;
                return i;
            }
        }
        this.displayers.push(displayer);
        return this.displayers.length - 1;
    }

    destroyDisplayer(displayer) {
        for (let i = 0; i < this.displayers.length; i++) {
            if (this.displayers[i] == displayer) {
                this.displayers[i] = null;
                return i;
            }
        }
    }
}

const initActionText = () => {
    'use strict';
    window.displayerManager = new DisplayerManager();
    let lstages = document.getElementsByClassName("lstage");
    if (lstages.length == 0) {
        let canvases = document.querySelectorAll("div#main > canvas"); // who tf uses the same ID for multiple thing smh
        for (let canvas of canvases) {
            let div = document.createElement("div");
            div.setAttribute("class", "lstage");
            canvas.parentNode.insertBefore(div, canvas);
            div.appendChild(canvas);
        }
    }
    lstages = document.getElementsByClassName("lstage");
    for (let i = 1; i <= lstages.length; i++) {
        let lstage = lstages[i - 1];
        let num = window.displayerManager.createDisplayer();
        let spike_tracker = document.createElement("p");
        spike_tracker.setAttribute("id", `atk_spike_${num.index + 1}`);
        spike_tracker.setAttribute("style", `max-width: 96px; color: yellow; font-weight: bold;`);
        spike_tracker.setAttribute("class", "spike-tracker fade in");
        lstage.appendChild(spike_tracker);
        let atkdiv = document.createElement("div");
        atkdiv.setAttribute("style", `max-width: 96px; max-height: ${MAX_HEIGHT}px; overflow: hidden; padding: 5px;`);
        atkdiv.setAttribute("id", `atk_div_${num.index + 1}`);
        lstage.appendChild(atkdiv);
    }
    if (typeof trim != "function") { var trim = a => { a = a.slice(0, -1); a = a.substr(a.indexOf("{") + 1); return a } }
    if (typeof getArgs != "function") {
        var getArgs = a => {
            let args = a.toString().match(/function\s*(?:[_a-zA-Z]\w*\s*)?\(((?:(?:[_a-zA-Z]\w*)\s*,\s*?)*(?:[_a-zA-Z]\w*)?)\)/);
            if (args.length > 1) return args[1].split(/\s*,\s*/g);
            return [];
        }
    }
    let displayActionText = function () {
        try {
            let parseCanvasName = function (name) {
                let number = name.match(/(\d+)$/);
                if (number === null) return 1; // no number, assume is first player
                return parseInt(number[0]);
            }


            let IS_BOT = false;
            let playerNum;
            switch (this.v.constructor.name) {
                case "Ctx2DView":
                case "View":
                    playerNum = parseCanvasName(this.v.ctx.canvas.id) - 1;
                    break;
                case "WebGLView":
                    playerNum = parseCanvasName(this.v.ctxs[0].elem.id) - 1;
                    break;
                case "SlotView":
                    IS_BOT = !!(this.p && this.p.bot && this.p.bot.IS_BOT);
                    playerNum = (this.v.displayer) ? this.v.displayer.index : -1;
                    break;
                default:
                    console.log("Uhoh looks like something unknown happened >.<");
                    break;
            }

            if (IS_BOT || (this.clock !== 0 && playerNum !== -1)) {
                if (!this.displayer) {
                    this.displayer = window.displayerManager.displayers[playerNum];
                }

                // generate clear text string
                let clearText;
                if (type !== this.Scoring.A.PERFECT_CLEAR) {
                    let lcNames = ["", "Single", "Double", "Triple", "Quad", "Multi"];
                    clearText = lcNames[Math.min(linesCleared, 5)];

                    let blockName = this.blockSets[this.activeBlock.set].blocks[this.activeBlock.id].name;
                    if (this.spinPossible) clearText = blockName + "&#x2011;Spin " + clearText; // &#x2011; is non-breaking hyphen, &nbsp; is non-brekaing space
                    else if (this.spinMiniPossible) clearText = blockName + "&#x2011;Spin " + clearText + " Mini";
                }
                else {
                    clearText = "Perfect Clear!";
                }

                if (b2b && this.isBack2Back) clearText = "B2B " + clearText;
                if (cmb > 0) clearText += ` combo${cmb}`;

                this.displayer.displayNewAction(atk, clearText);
            }
        } catch (e) { console.log(e); }
    }
    try {

        let functionStr = trim(GameCore.prototype.checkLineClears.toString());

        const linesClearedPattern = /switch\((_0x[a-f0-9]+)\)/
        const matchLineClearCheck = functionStr.match(linesClearedPattern);
        if (!matchLineClearCheck) {
            console.log("action text injection failed.");
        }

        // find switch(linesCleared) to get linesCleared variable + get a variable down for linesSent before it's incremented
        functionStr = functionStr.replace(linesClearedPattern, (_, p1) => `
            let linesCleared=${p1}; let atkBefore = this.gamedata.linesSent; switch(${p1})
        `);

        // insert displayActionText after the following code:
        // let atkMeta={type:_,b2b:this._,cmb:this._};
        let replacePattern = /let (_0x[a-f0-9]+)=\{'type':_0x[a-f0-9]+,'b2b':this\[.*\],'cmb':this\[.*\]};/;

        const matchCheck = functionStr.match(replacePattern);
        if (!matchCheck) {
            console.log("action text injection failed.");
        }

        let replacer = function (match, atkMeta) {
            console.log('replacing yay')
            return match + `
            let atk = this.gamedata.linesSent - atkBefore;
            let type = ${atkMeta}.type;
            let b2b = ${atkMeta}.b2b;
            let cmb = ${atkMeta}.cmb;
            `
                + trim(displayActionText.toString());
        }
        functionStr = functionStr.replace(replacePattern, replacer);

        GameCore.prototype.checkLineClears = new Function(...getArgs(GameCore.prototype.checkLineClears), functionStr);
        console.log(functionStr);
    } catch (e) {
        console.log(e); 7
        console.log("Could not inject into line clears!");
    }
    try {
        Replayer.prototype.checkLineClears = function (a) {
            GameCore.prototype.checkLineClears.call(this, a);
        }
        const oldInitReplay = Replayer.prototype.initReplay
        Replayer.prototype.initReplay = function () {
            try {
                if (this.v.displayer && this.v.displayer.reset)
                    this.v.displayer.reset()
            } catch (e) {
                console.log(e);
            }
            return oldInitReplay.apply(this, arguments);
        }
    } catch (e) {
        console.log(e);
        console.log("Could not inject into line clears!");
    }
    try {
        SlotView.prototype.onResized = function () {
            this.block_size = this.slot.gs.liveBlockSize;
            this.holdQueueBlockSize = this.slot.gs.holdQueueBlockSize;
            this.drawBgGrid();
            this.clearMainCanvas();
            if (this.slot.gs.isExtended) {
                this.QueueHoldEnabled = true;
                this.holdCanvas.style.display = 'block';
                this.queueCanvas.style.display = 'block';
                if (shouldRenderEffectsOnView(this)) {
                    if (this.displayer === undefined) {
                        this.displayer = window.displayerManager.createDisplayer();
                    }
                    try {
                        let top = this.holdCanvas.height + parseInt(this.holdCanvas.style.top);
                        let left = parseInt(this.holdCanvas.style.left);
                        if (!document.getElementById(`atk_spike_${this.displayer.index + 1}`)) {
                            let spike_tracker = document.createElement("p");
                            spike_tracker.setAttribute("class", "layer fade in");
                            spike_tracker.setAttribute("style", `top: ${top}px; left: ${left}px; width: ${this.holdCanvas.width}px; height: 20px; color: yellow; font-weight: bold;`);
                            spike_tracker.setAttribute("id", `atk_spike_${this.displayer.index + 1}`);
                            this.holdCanvas.parentNode.appendChild(spike_tracker);

                        }
                        if (!document.getElementById(`atk_div_${this.displayer.index + 1}`)) {
                            let atkdiv = document.createElement("div");
                            atkdiv.setAttribute("class", "layer");
                            atkdiv.setAttribute("style", `top: ${top + 40}px; left: ${left}px; width: ${this.holdCanvas.width}px; max-height: ${MAX_HEIGHT}px; overflow: hidden;`);
                            atkdiv.setAttribute("id", `atk_div_${this.displayer.index + 1}`);
                            this.holdCanvas.parentNode.appendChild(atkdiv);
                        }
                    } catch (e) { console.log(e); }
                }
            } else {
                this.QueueHoldEnabled = false;
                this.holdCanvas.style.display = 'none';
                this.queueCanvas.style.display = 'none';
            }
        };
    } catch (e) {
        console.log(e);
        console.log("Could not inject into SlotView!");
    }
};

;// CONCATENATED MODULE: ./src/replayManager.js
let isReplayerReversing = false

const initReplayManager = () => {
    let skipping = false


    let repControls = document.getElementById("repControls")
    let skipButton = document.createElement("button")
    skipButton.textContent = "skip"
    skipButton.onclick = function () {
        if (skipping) {
            skipButton.textContent = "skip"
        } else {
            skipButton.textContent = "step"
        }
        skipping = !skipping
    }
    if (repControls) repControls.appendChild(skipButton)
    let nextFrame = ReplayController.prototype.nextFrame
    ReplayController.prototype.nextFrame = function () {
        if (!skipping) {
            return nextFrame.apply(this, arguments)
        }

        // find the next upcoming hard drop
        let nextHdTime = -1;
        this.g.forEach((r, _) => {
            for (let i = r.ptr; i < r.actions.length; i++) {
                let action = r.actions[i].a;
                let time = r.actions[i].t;

                if (action == Action.HARD_DROP) {
                    if (nextHdTime == -1 || time < nextHdTime)
                        nextHdTime = time;
                    break;
                }
            }
        });

        // play all replayers until that time
        if (nextHdTime < 0) return;
        this.g.forEach((r, _) => r.playUntilTime(nextHdTime));
    }
    let prevFrame = ReplayController.prototype.prevFrame
    ReplayController.prototype.prevFrame = function () {
        isReplayerReversing = true
        if (!skipping) {
            let v = prevFrame.apply(this, arguments)
            isReplayerReversing = false
            return v
        }
        let skipBack = 0
        let passed = false
        this.g.forEach((r, _) => {
            for (let i = r.ptr - 1; i >= 0; i--) {
                let action = r.actions[i].a;
                skipBack += 1

                if (action == Action.HARD_DROP) {
                    if (passed) {
                        skipBack -= 1
                        break
                    }
                    passed = true
                }
            }
        });
        for (let i = 0; i < skipBack; i++) {
            isReplayerReversing = true
            prevFrame.apply(this, arguments)
            isReplayerReversing = false
        }
        isReplayerReversing = false
    }
    let lR = ReplayController.prototype.loadReplay
    ReplayController.prototype.loadReplay = function () {
        let v = lR.apply(this, arguments)
        document.getElementById("next").onclick = this.nextFrame.bind(this)
        document.getElementById("prev").onclick = this.prevFrame.bind(this)
        return v
    }
}
;// CONCATENATED MODULE: ./src/jstris-fx.js



// helper function
const initGFXCanvas = (obj, refCanvas) => {
  obj.GFXCanvas = refCanvas.cloneNode(true);
  /*
  obj.GFXCanvas = document.createElement("canvas");
  obj.GFXCanvas.className = "layer mainLayer gfxLayer";
  obj.GFXCanvas.height = refCanvas.height;
  obj.GFXCanvas.width = refCanvas.width;
  obj.GFXCanvas.style = refCanvas.style;
  */
  obj.GFXCanvas.id = "";
  obj.GFXCanvas.className = "layer mainLayer gfxLayer";
  obj.GFXctx = obj.GFXCanvas.getContext("2d")
  obj.GFXctx.clearRect(0, 0, obj.GFXCanvas.width, obj.GFXCanvas.height);
  refCanvas.parentNode.appendChild(obj.GFXCanvas);
}

const initFX = () => {
  'use strict';
  // where you actually inject things into the settings

  // -- injection below --
  if (window.Game) {
    const oldReadyGo = Game.prototype.readyGo
    Game.prototype.readyGo = function () {
      let val = oldReadyGo.apply(this, arguments)

      if (!this.GFXCanvas || !this.GFXCanvas.parentNode) {
        initGFXCanvas(this, this.canvas);
      }

      this.GFXQueue = [];

      this.GFXLoop = () => {
        if (!this.GFXQueue) this.GFXQueue = [];

        this.GFXctx.clearRect(0, 0, this.GFXCanvas.width, this.GFXCanvas.height);

        this.GFXQueue = this.GFXQueue.filter(e => e.process.call(e, this.GFXctx));

        if (this.GFXQueue.length)
          requestAnimationFrame(this.GFXLoop);
      }
      //  window.game = this;

      return val;
    }
  }

  if (window.SlotView) {
    const oldOnResized = SlotView.prototype.onResized;
    SlotView.prototype.onResized = function () {

      oldOnResized.apply(this, arguments);

      if (this.g && this.g.GFXCanvas && Replayer.prototype.isPrototypeOf(this.g)) {
        this.g.GFXCanvas.width = this.canvas.width;
        this.g.GFXCanvas.height = this.canvas.height;
        this.g.GFXCanvas.style.top = this.canvas.style.top;
        this.g.GFXCanvas.style.left = this.canvas.style.left;
        this.g.block_size = this.g.v.block_size;
      }


    }
  }

  // -- injection below --
  const oldInitReplay = Replayer.prototype.initReplay
  Replayer.prototype.initReplay = function () {
    let val = oldInitReplay.apply(this, arguments)

    // SlotViews have replayers attached to them, don't want to double up on the canvases
    //if (SlotView.prototype.isPrototypeOf(this.v))
    //    return;
    window.replayer = this;


    // always clear and re-init for slotviews
    if (window.SlotView && SlotView.prototype.isPrototypeOf(this.v)) {

      // do not do gfx if the board is too small
      let life = this.v.slot.gs.p.Live
      if (!shouldRenderEffectsOnView(this.v) && life?.roomConfig?.mode !== 2) {
        return val;
      }
      let foundGFXCanvases = this.v.slot.slotDiv.getElementsByClassName("gfxLayer");

      for (var e of foundGFXCanvases) {
        if (e.parentNode) {
          e.parentNode.removeChild(e);
        }
      }
      this.GFXCanvas = null;
    }

    if (!this.GFXCanvas || !this.GFXCanvas.parentNode || !this.GFXCanvas.parentNode == this.v.canvas.parentNode) {
      initGFXCanvas(this, this.v.canvas);
      console.log("replayer initializing gfx canvas");
    }

    this.GFXQueue = [];

    this.block_size = this.v.block_size;

    this.GFXLoop = () => {
      if (!this.GFXQueue) this.GFXQueue = [];

      this.GFXctx.clearRect(0, 0, this.GFXCanvas.width, this.GFXCanvas.height);

      this.GFXQueue = this.GFXQueue.filter(e => e.process.call(e, this.GFXctx));

      if (this.GFXQueue.length)
        requestAnimationFrame(this.GFXLoop);
    }

    this.v.canvas.parentNode.appendChild(this.GFXCanvas);

    return val;
  }

  const oldLineClears = GameCore.prototype.checkLineClears;
  GameCore.prototype.checkLineClears = function () {

    if (!this.GFXCanvas || isReplayerReversing)
      return oldLineClears.apply(this, arguments);

    let oldAttack = this.gamedata.attack;

    let cleared = 0;
    for (var row = 0; row < 20; row++) {
      let blocks = 0;
      for (var col = 0; col < 10; col++) {
        let block = this.matrix[row][col];
        if (9 === block) { // solid garbage
          break;
        };
        if (0 !== block) {
          blocks++
        }
      };
      if (10 === blocks) { // if line is full
        cleared++; // add to cleared

        // send a line clear animation on this line
        if (Config().ENABLE_LINECLEAR_ANIMATION && Config().LINE_CLEAR_LENGTH > 0) {
          this.GFXQueue.push({
            opacity: 1,
            delta: 1 / (Config().LINE_CLEAR_LENGTH * 1000 / 60),
            row,
            blockSize: this.block_size,
            amountParted: 0,
            process: function (ctx) {
              if (this.opacity <= 0)
                return false;

              var x1 = 1;
              var x2 = this.blockSize * 5 + this.amountParted;
              var y = 1 + this.row * this.blockSize;

              // Create gradient
              var leftGradient = ctx.createLinearGradient(0, 0, this.blockSize * 5 - this.amountParted, 0);
              leftGradient.addColorStop(0, `rgba(255,255,255,${this.opacity})`);
              leftGradient.addColorStop(1, `rgba(255,170,0,0)`);
              // Fill with gradient
              ctx.fillStyle = leftGradient;

              ctx.fillRect(x1, y, this.blockSize * 5 - this.amountParted, this.blockSize);

              // Create gradient
              var rightGradient = ctx.createLinearGradient(0, 0, this.blockSize * 5 - this.amountParted, 0);
              rightGradient.addColorStop(0, `rgba(255,170,0,0)`);
              rightGradient.addColorStop(1, `rgba(255,255,255,${this.opacity})`);
              // Fill with gradient
              ctx.fillStyle = rightGradient;
              ctx.fillRect(x2, y, this.blockSize * 5 - this.amountParted, this.blockSize);

              this.amountParted = lerp(this.amountParted, this.blockSize * 5, 0.1);
              this.opacity -= this.delta;
              return true;
            }

          })
        }
      }
    }
    if (cleared > 0) { // if any line was cleared, send a shake
      let attack = this.gamedata.attack - oldAttack;
      if (Config().ENABLE_LINECLEAR_SHAKE)
        shake(
          this.GFXCanvas.parentNode.parentNode,
          Math.min(1 + attack * 5, 50) * Config().LINE_CLEAR_SHAKE_STRENGTH,
          Config().LINE_CLEAR_SHAKE_LENGTH * (1000 / 60)
        );
      if (this.GFXQueue.length)
        requestAnimationFrame(this.GFXLoop);
    }
    return oldLineClears.apply(this, arguments);

  }
  // have to do this so we can properly override ReplayerCore
  Replayer.prototype.checkLineClears = GameCore.prototype.checkLineClears;

  // placement animation
  const oldPlaceBlock = GameCore.prototype.placeBlock
  GameCore.prototype.placeBlock = function (col, row, time) {

    if (!this.GFXCanvas || !Config().ENABLE_PLACE_BLOCK_ANIMATION || isReplayerReversing)
      return oldPlaceBlock.apply(this, arguments);

    const block = this.blockSets[this.activeBlock.set]
      .blocks[this.activeBlock.id]
      .blocks[this.activeBlock.rot];

    let val = oldPlaceBlock.apply(this, arguments);

    // flashes the piece once you place it
    if (Config().PIECE_FLASH_LENGTH > 0) {
      this.GFXQueue.push({
        opacity: Config().PIECE_FLASH_OPACITY,
        delta: Config().PIECE_FLASH_OPACITY / (Config().PIECE_FLASH_LENGTH * 1000 / 60),
        col,
        row,
        blockSize: this.block_size,
        block,
        process: function (ctx) {
          if (this.opacity <= 0)
            return false;


          ctx.fillStyle = `rgba(255,255,255,${this.opacity})`;
          this.opacity -= this.delta;

          for (var i = 0; i < this.block.length; i++) {
            for (var j = 0; j < this.block[i].length; j++) {

              if (!this.block[i][j])
                continue;

              var x = 1 + (this.col + j) * this.blockSize
              var y = 1 + (this.row + i) * this.blockSize

              ctx.fillRect(x, y, this.blockSize, this.blockSize);
            }
          }
          return true;
        }
      })
    }

    var trailLeftBorder = 10;
    var trailRightBorder = 0;
    var trailBottom = 0;
    for (var i = 0; i < block.length; i++) {
      for (var j = 0; j < block[i].length; j++) {
        if (!block[i][j])
          continue;
        trailLeftBorder = Math.max(Math.min(trailLeftBorder, j), 0);
        trailRightBorder = Math.min(Math.max(trailRightBorder, j), 10);
        trailBottom = Math.max(trailBottom, i);
      }
    }

    // flashes the piece once you place it
    this.GFXQueue.push({
      opacity: 0.3,
      col,
      row,
      blockSize: this.block_size,
      trailTop: 1,
      block,
      trailLeftBorder,
      trailRightBorder,
      trailBottom,
      process: function (ctx) {
        if (this.opacity <= 0)
          return false;

        var {
          trailLeftBorder,
          trailRightBorder,
          trailBottom
        } = this;

        var row = this.row + trailBottom

        var gradient = ctx.createLinearGradient(0, 0, 0, row * this.blockSize - this.trailTop);
        gradient.addColorStop(0, `rgba(255,255,255,0)`);
        gradient.addColorStop(1, `rgba(255,255,255,${this.opacity})`);

        // Fill with gradient
        ctx.fillStyle = gradient;
        ctx.fillRect((this.col + trailLeftBorder) * this.blockSize, this.trailTop, (trailRightBorder - trailLeftBorder + 1) * this.blockSize, row * this.blockSize - this.trailTop);

        const middle = (trailLeftBorder + trailRightBorder) / 2

        this.trailLeftBorder = lerp(trailLeftBorder, middle, 0.1);
        this.trailRightBorder = lerp(trailRightBorder, middle, 0.1);

        this.opacity -= 0.0125;

        return true;
      }
    })



    requestAnimationFrame(this.GFXLoop);

  }
  // have to do this so we can properly override ReplayerCore
  Replayer.prototype.placeBlock = GameCore.prototype.placeBlock;
};

;// CONCATENATED MODULE: ./src/matchmaking.js




let ROOMBA = "JstrisPlus"


function createElementFromHTML(htmlString) {
  var div = document.createElement('div');
  div.innerHTML = htmlString.trim();

  // Change this to div.childNodes to support multiple top-level nodes.
  return div.firstChild;
}
function addMatchesBtn(name) {
  for (let dropDown of document.getElementsByClassName("dropdown-menu")) {
    let found = false
    let children = dropDown.children
    for (let i = 0; i < children.length; i++) {
      let child = children[i]
      if (!child.children.length > 0) continue
      if (child.children[0].href == "https://jstris.jezevec10.com/profile") {
        found = true
        let li = document.createElement("li")
        let a = document.createElement("a")
        a.style.color = "#ffb700"
        a.href = "https://jstris.jezevec10.com/matches/" + name + "?plus=true"
        a.textContent = "Matchmaking History"
        li.appendChild(a)
        dropDown.insertBefore(li, child.nextSibling)
        break
      }
    }
    if (found) {
      break
    }
  }
}
function createStatBlock(stats) {

  let statInfo = document.createElement("div");
  statInfo.className = "t-main";
  statInfo.style.flexWrap = "wrap"
  // statInfo.style.whiteSpace = "pre-wrap"
  for (const [key, value] of Object.entries(stats)) {
    let stat = document.createElement("div")
    stat.className = "t-itm"
    let desc = document.createElement("div")
    desc.className = "t-desc"
    desc.textContent = key
    let val = document.createElement("div")
    val.className = "t-val"
    val.textContent = value
    stat.appendChild(val)
    stat.appendChild(desc)
    statInfo.append(stat)
  }
  return statInfo
}
const insertChatButtons = (sendMessage) => {
  let chatBox = document.getElementById("chatContent");
  let chatButtons = document.createElement("div");
  chatButtons.className = "mm-chat-buttons-container"
  let readyButton = document.createElement("button")
  readyButton.className = "mm-ready-button"
  readyButton.textContent = "Ready"
  chatButtons.prepend(readyButton)
  chatBox.appendChild(chatButtons);
  readyButton.addEventListener("click", () => {
    readyButton.disabled = true
    setTimeout(() => {
      readyButton.disabled = false
    }, 1000)
    sendMessage("!ready");
  })
  return (function (boundButtonsDiv) {
    return () => {
      try {
        document.getElementById("chatContent").removeChild(boundButtonsDiv);
      } catch (e) {
        //console.log(e);
        console.log("Ready button was already removed.");
      }
    }
  })(chatButtons) // do this to make sure that the returned kill callback is removing the correct div
}

const initMM = () => {
  let HOST = "wss://jeague.tali.software/";
  let APIHOST = "https://jeague.tali.software/api/v1/"
  let readyKiller = null
  // development server
  if (false) {}

  // local server
  if (false) {}


  let p = document.createElement("button");
  p.className = "mmButton";
  p.id = "queueButton";
  p.textContent = "Enter Matchmaking";
  const JEAGUE_VERSION = "UT99";
  let urlParts = window.location.href.split("/");
  if (typeof Live == "function") {
    let chatListener = Live.prototype.showInChat
    let suppressChat = false
    let nameListener = Live.prototype.getName
    Live.prototype.getName = function () {
      if (arguments[0] && this.clients[arguments[0]] && this.clients[arguments[0]].name == ROOMBA) {
        return "[Matchmaking]"
      }
      let v = nameListener.apply(this, arguments)
      return v
    }
    Live.prototype.showInChat = function () {
      if (suppressChat) return
      let val = chatListener.apply(this, arguments)
      return val
    }
    let responseListener = Live.prototype.handleResponse
    Live.prototype.handleResponse = function () {
      let res = arguments[0]
      suppressChat = false
      if (res.t == 6) {
        if (res.m == "<em>Room wins counter set to zero.</em>") {
          for (let client of Object.values(this.clients)) {
            if (client.name == ROOMBA) {
              suppressChat = true
            }
          }
        }
      } else if (res.t == 2) {
        if (res.n == ROOMBA) {
          suppressChat = true
          cc.style.display = "none"
        }
      } else if (res.t == 3) {
        if (this.clients[res.cid] && this.clients[res.cid].name == ROOMBA) {
          suppressChat = true
          cc.style.display = "flex"
          if (readyKiller) {
            readyKiller()
          }
        }
      } else if (res.t == 4) {
        let found = false
        if (res.players) {

          for (let key in res.players) {
            if (res.players[key].n == ROOMBA) {
              found = true
              break
            }
          }
        }
        if (res.spec) {
          for (let key in res.spec) {
            if (res.spec[key].n == ROOMBA) {
              found = true
              break
            }
          }
        }
        if (found) {
          cc.style.display = "none"
        } else {
          cc.style.display = "flex"
        }
      }
      let val = responseListener.apply(this, arguments)
      suppressChat = false
      if (res.t == 12) {
        for (let gs of this.p.GS.slots) {
          gs.v.isKO = false
          gs.v.KOplace = null
        }
      }
      return val
    }


    if (Config().SHOW_QUEUE_INFO) {
      document.body.classList.add("show-queue-info");
    }
    Config().onChange("SHOW_QUEUE_INFO", val => {
      if (val) {
        document.body.classList.add("show-queue-info");
      } else {
        document.body.classList.remove("show-queue-info");
      }
    })
    if (Config().SHOW_MM_BUTTON) {
      document.body.classList.add("show-mm-button");
    }
    Config().onChange("SHOW_MM_BUTTON", val => {
      if (val) {
        document.body.classList.add("show-mm-button");
      } else {
        document.body.classList.remove("show-mm-button");
      }
    })
    let queueinfo = document.createElement("div");
    queueinfo.className = "mmInfoContainer";
    queueinfo.textContent = "not connected to matchmaking";
    let cc = document.createElement("div")
    cc.className = 'mmContainer'
    document.body.appendChild(cc)
    cc.prepend(queueinfo);
    let mmLoaded = false
    let liveObj = null
    let liveListener = Live.prototype.authorize;
    Live.prototype.authorize = function () {
      liveObj = this
      let val = liveListener.apply(this, arguments);
      if (arguments[0] && arguments[0].token) {
        //loadMM(arguments[0].token);
        loadMM();
      }
      return val;
    };

    //function loadMM(token) {
    function loadMM() {
      if (mmLoaded) return
      mmLoaded = true
      document.addEventListener("keyup", (evtobj) => {
        if (0 == liveObj.p.focusState) {
          if (evtobj.keyCode == Config().SCREENSHOT_KEYCODE) {
            liveObj.p.screenshot(APIHOST)
          };
        }

      }, false)
      let name = liveObj.chatName;

      addMatchesBtn(liveObj.chatName)
      let CONNECTED = false;
      let ws = new WebSocket(HOST);
      console.log(`Attempting to connect to matchmaking host: ${HOST}`);

      window.JeagueSocket = ws
      /*     let connectionListener = Live.prototype.onClose
           Live.prototype.onClose = function () {
             let val = connectionListener.apply(this, arguments)
             ws.close()
             status = UI_STATUS.offline
             updateUI()
             return val
           }*/
      let UI_STATUS = {
        idle: 0,
        queueing: 1,
        loading: 2,
        banned: 4,
        offline: 5,
      };
      let status = UI_STATUS.idle;
      let numQueue = 0;
      let numPlaying = 0;
      let numActive = 0
      let HOVERING = false;

      let timeInQueue = 0;
      let timeInc = null;
      let toMMSS = function (sec_num) {
        let minutes = Math.floor(sec_num / 60);
        let seconds = sec_num - minutes * 60;
        if (minutes < 10) {
          minutes = "0" + minutes;
        }
        if (seconds < 10) {
          seconds = "0" + seconds;
        }
        return "" + minutes + ":" + seconds;
      };
      let OFFLINED = false
      function updateUI(msg) {
        if (OFFLINED) return
        switch (status) {
          case UI_STATUS.queueing:
            if (HOVERING) {
              p.textContent = "Exit Matchmaking";
            } else {
              p.textContent = toMMSS(timeInQueue);
            }
            queueinfo.textContent = `[${numPlaying}]${numQueue} in queue\n${numActive} online`;
            break;
          case UI_STATUS.idle:
            p.textContent = "Enter Matchmaking";
            queueinfo.textContent = `[${numPlaying}]${numQueue} in queue\n${numActive} online`;
            break;
          case UI_STATUS.loading:
            p.textContent = "Loading";
            queueinfo.textContent = `[${numPlaying}]${numQueue} in queue\n${numActive} online`;
            break;
          case UI_STATUS.banned:
            queueinfo.style.minWidth = "1000px"
            queueinfo.textContent = "You Are Banned";
            p.remove();
            OFFLINED = true
            break;
          case UI_STATUS.offline:
            OFFLINED = true
            queueinfo.style.color = "#bcc8d4";
            queueinfo.className = "mmInfoContainer";
            queueinfo.textContent = "not connected to matchmaking";
            p.remove();
            break
        }
        if (msg) {
          queueinfo.textContent = msg;
        }
      }

      function ping() {
        ws.send(JSON.stringify({ type: "ping" }));
      }

      function updateClock() {
        timeInQueue += 1;
        updateUI();
      }
      p.onmouseover = function () {
        HOVERING = true;
        p.style.color = "rgba(255,255,255,0.7)";
        updateUI();
      };
      p.onmouseout = function () {
        HOVERING = false;
        p.style.color = "rgba(255,255,255,1)";
        updateUI();
      };

      ws.onmessage = event => {
        let res = JSON.parse(event.data);
        if (res.type == "room") {
          status = UI_STATUS.idle;
          liveObj.p.GS.resetAll()
          liveObj.joinRoom(res.rid)
          console.log("found match at " + res.rid)
          playSound("READY");
          if (readyKiller) {
            readyKiller()
          }
          notify('Jstris+', '🚨Match Starting!')
          document.title = "🚨Match Starting!";
          setTimeout(() => {
            document.title = "Jstris";
          }, 2000);
          updateUI();
        } else if (res.type == "readyStart") {
          cc.style.display = "none"
          readyKiller = insertChatButtons(msg => ws.send(JSON.stringify({ type: "ready", rid: liveObj.rid, cid: liveObj.cid })));

        } else if (res.type == "readyConfirm") {
          if (res.rid == liveObj.rid) {
            if (readyKiller) readyKiller()
          }
        } else if (res.type == "msg") {
          if (res.secret) {
            liveObj.showInChat("", `<em><b>${res.msg}</b></em>`)
          } else { liveObj.showInChat("[Matchmaking]", res.msg) }
        } else if (res.type == "accept") {
          timeInQueue = 0;
          clearInterval(timeInc);
          timeInc = setInterval(updateClock, 1000);
          status = UI_STATUS.queueing;
          updateUI();
        } else if (res.type == "decline") {
          if (res.shutdown) {
            alert("server preparing for an update")
          } else {
            alert("you are already in queue!")
          }
          status = UI_STATUS.idle;
          updateUI();
        } else if (res.type == "bans") {
          status = UI_STATUS.banned;
          let banmsg = "You are banned from matchmaking for:"
          console.log(res.bans)
          for (let ban of res.bans) {
            banmsg += " " + ban.reason + "; Expires: " + new Date(ban.timeout).toLocaleString();
          }
          updateUI(banmsg);
        } else if (res.type == "removed") {
          status = UI_STATUS.idle;
          updateUI();
        } else if (res.type == "ping") {
          if (res.queue > 0) {
            queueinfo.style.color = "#1b998b";
          } else {
            queueinfo.style.color = "#bcc8d4";
          }
          numQueue = res.queue;
          numPlaying = res.playing;
          numActive = res.active
          updateUI();
        } else if (res.type == "init") {
          CONNECTED = true;
          cc.prepend(p);
          status = UI_STATUS.idle;
          updateUI();
          console.log("JEAGUE LEAGUE CONNECTED");
        }
      };
      function powertipCallback(records) {
        records.forEach(function (record) {
          var list = record.addedNodes;
          var i = list.length - 1;

          for (; i > -1; i--) {
            if (list[i].className == "t-ftr" && list[i].firstChild) {
              let name = (list[i].children[0].dataset.name)
              if (name) {
                let powerTipStat = list[i].parentNode
                fetch(APIHOST + "stats/" + name).then((response) => {
                  if (response.status != 200) return
                  response.json().then(res => {
                    let mmHeader = document.createElement("div")
                    mmHeader.className = "t-titles"
                    let span = document.createElement("span")
                    span.textContent = "Matchmaking Stats"
                    mmHeader.appendChild(span)
                    powerTipStat.appendChild(mmHeader)
                    powerTipStat.appendChild(createStatBlock(res))
                  })
                })
              }
            }
          }
        });
      }

      var observer = new MutationObserver(powertipCallback);

      var targetNode = document.body;

      observer.observe(targetNode, { childList: true, subtree: true });

      let WSOPENED = false
      ws.onopen = function (event) {
        WSOPENED = true
        setInterval(ping, 10000);
        ws.send(JSON.stringify({ type: "init", name: name, version: JEAGUE_VERSION }));
        //ws.send(JSON.stringify({ type: "init", token: token, version: JEAGUE_VERSION }));
        p.onclick = function () {
          if (!CONNECTED) {
            return;
          }
          if (status == UI_STATUS.queueing) {
            status = UI_STATUS.loading;
            ws.send(JSON.stringify({ type: "disconnect" }));
          } else if (liveObj.connected) {
            status = UI_STATUS.loading;
            ws.send(JSON.stringify({ type: "connect" }));
          } else {
            alert("you are not connected to jstris")
          }
          updateUI();
        };
      };
      ws.onclose = function () {
        if (WSOPENED) {
          status = UI_STATUS.offline
          updateUI("jstris+ server down")
        }
      }
    }
  } else if (urlParts[3] && urlParts[3] == "u" && urlParts[4]) {

    let nameHolders = document.getElementsByClassName("mainName")
    let mainName = ""
    if (nameHolders[0] && nameHolders[0].firstChild) {
      mainName = nameHolders[0].firstChild.textContent.trim()
    } else {
      return
    }
    addMatchesBtn(mainName)
    let cc = document.getElementsByClassName("col-flex-bio col-flex")[0];
    let cc1 = document.getElementsByClassName("row-flex uProfileTop")[0]
    let a = document.createElement("a")
    a.href = "https://jstris.jezevec10.com/matches/" + mainName + "?plus=true"
    a.textContent = "Matchmaking History"
    a.className = "btn btn-default btn-sm"
    let img = document.createElement("img")
    img.src = "https://s.jezevec10.com/res/list.png"
    img.className = "btnIcn"
    img.style.float = "left"
    a.style.minWidth = "180px"
    a.style.textAlign = "left"
    a.prepend(img)
    a.style.backgroundColor = "#e74c3c"
    cc1.children[0].appendChild(a)
    fetch(APIHOST + "stats/" + mainName).then((response) => {
      if (response.status != 200) return
      response.json().then(res => {

        let playerInfo = document.createElement("div");
        playerInfo.className = "aboutPlayer";
        let statHeader = document.createElement("span");
        statHeader.className = "aboutTitle";
        statHeader.textContent = "Matchmaking Stats";
        playerInfo.appendChild(statHeader);
        playerInfo.appendChild(createStatBlock(res));
        cc.appendChild(playerInfo)

      })
    })
  } else if (urlParts[3] && urlParts[3] == "matches" && urlParts[4]) {
    const urlParams = new URLSearchParams(window.location.search);
    if (!urlParams.get("plus")) return

    let cc = document.getElementsByClassName("well")[0].parentNode
    for (let child of cc.children) {
      cc.removeChild(child)
    }
    let loader = document.createElement("div")
    loader.className = "mmLoader"
    cc.prepend(loader)
    document.getElementsByClassName("well")[0].remove()
    let collapsible = document.createElement("button")
    collapsible.className = "mmCollapsible"
    collapsible.textContent = "Jstris+ Matches"
    let collapsibleCarrot = createElementFromHTML("<span class='caret'></span>")
    collapsible.appendChild(collapsibleCarrot)
    let matchView = document.createElement("div")
    matchView.className = "col-sm-12 mmMatches"
    collapsible.onclick = () => {
      if (matchView.style.display == "block") { matchView.style.display = "none" }
      else { matchView.style.display = "block" }
    }
    let name = decodeURI(urlParts[4]).split('?')[0]
    fetch(APIHOST + "matches/" + name).then((response) => {
      if (response.status != 200) {

        response.text().then(res => {
          loader.remove()
          cc.textContent = res
        })
        return
      }
      response.json().then(res => {

        let modal = document.createElement("div");
        let modalContent = document.createElement("div");
        let modalClose = document.createElement("span");
        let modalTable = document.createElement("table");
        modalClose.className = "mmClose";
        modalClose.textContent = "×";
        modalContent.appendChild(modalTable);
        modalContent.appendChild(modalClose);
        modal.className = "mmModal";
        modalContent.className = "mmModal-content";
        modal.append(modalContent);
        modalClose.onclick = function () {
          modal.style.display = "none";
        };

        modalTable.className = "table table-striped table-hover match-list";

        document.body.appendChild(modal);

        function loadGame(game, match) {
          const ALL_STATS = ["apm", "pps", "cheese", "apd", "time"];
          let table = modalContent.firstChild;
          while (table.firstChild) {
            table.removeChild(table.firstChild);
          }
          let aref = document.createElement("a");
          let apic = document.createElement("img");
          aref.style.position = "absolute";
          aref.style.left = "5px";
          aref.style.top = "5px";
          apic.src = "https://jstris.jezevec10.com/res/play.png";
          aref.appendChild(apic);
          aref.href = `/games/${game.gid}`;
          aref.target = "_blank";
          table.appendChild(aref);
          //   console.log(res)
          if (game.stats || game.altStats) {
            let thead = document.createElement("thead");
            let theadtr = document.createElement("tr");
            let spacer = document.createElement("th");
            spacer.colSpan = 1;
            theadtr.appendChild(spacer);
            for (let ss of ALL_STATS) {
              let stat = document.createElement("th");
              stat.className = "apm";
              stat.textContent = ss.toUpperCase();
              theadtr.appendChild(stat);
            }
            let tdate = document.createElement("th");
            thead.appendChild(theadtr);
            table.appendChild(thead);

            let body = document.createElement("tbody");
            table.appendChild(body);
            let winnerName = match.player;
            let loserName = match.player;
            if (!game.win) {
              winnerName = match.opponent
            } else {
              loserName = match.opponent
            }
            let players = [];
            if (game.stats) {
              players.push({ name: winnerName, stats: game.stats });
            }
            if (game.altStats) {
              players.push({ name: loserName, stats: game.altStats });
            }
            for (let match of players) {
              let tr = document.createElement("tr");
              let p1 = document.createElement("td");
              p1.className = "pl1";
              var ap1 = document.createElement("a");
              ap1.textContent = match.name;
              ap1.href = `/u/${match.name}`;
              p1.appendChild(ap1);
              tr.appendChild(p1);
              if (match.stats) {
                let sstats = {};
                for (let ss of ALL_STATS) {
                  sstats[ss] = "-";
                }
                for (const [key, value] of Object.entries(match.stats)) {
                  if (isNaN(parseFloat(value)))
                    continue;
                  if (parseFloat(value) < 0)
                    continue;
                  if (sstats[key]) {
                    sstats[key] = value;
                  }
                }
                for (let ss of ALL_STATS) {
                  let stat = document.createElement("td");
                  stat.className = "apm";
                  stat.textContent = sstats[ss];
                  tr.appendChild(stat);
                }
              } else {
                for (let i = 0; i < ALL_STATS.length; i++) {
                  let stat = document.createElement("td");
                  stat.className = "apm";
                  stat.textContent = "-";
                  tr.appendChild(stat);
                }
              }

              body.appendChild(tr);
            }
          }

          modal.style.display = "block";
        }
        console.log(res)
        const ALL_STATS = ["apm", "pps", "cheese", "apd", "time"];
        //   console.log(res)
        let table = document.createElement("table");
        table.className = "table table-striped table-hover match-list";
        let thead = document.createElement("thead");
        let theadtr = document.createElement("tr");
        let spacer = document.createElement("th");
        spacer.colSpan = 3;
        theadtr.appendChild(spacer);
        for (let ss of ALL_STATS) {
          let stat = document.createElement("th");
          stat.className = "apm";
          stat.textContent = ss.toUpperCase();
          theadtr.appendChild(stat);
        }
        let tdate = document.createElement("th");
        tdate.className = "date";
        tdate.textContent = "Date";
        let tgames = document.createElement("th");
        tgames.className = "date";
        tgames.textContent = "Games";
        theadtr.appendChild(tdate);
        theadtr.appendChild(tgames);
        thead.appendChild(theadtr);
        table.appendChild(thead);

        let body = document.createElement("tbody");
        table.appendChild(body);
        for (let match of res) {
          let tr = document.createElement("tr");
          let p1 = document.createElement("td");
          p1.className = "pl1";
          var ap1 = document.createElement("a");
          ap1.textContent = name;
          ap1.href = `/u/${name}`;
          p1.appendChild(ap1);
          tr.appendChild(p1);
          let sc = document.createElement("td");
          sc.className = "sc";
          let sM = document.createElement("span");
          sM.style.color = "#04AA6D";
          sM.className = "scoreMiddle";
          let switched = match.opponent == name
          if (match.forced) {
            sM.textContent = "default";
            if (switched) {
              sM.textContent = "forfeit";
              sM.style.color = "#A90441";
            }
          } else {
            let wins = 0
            let losses = 0
            for (let game of match.games) {
              if (switched) {
                if (game.win) losses += 1
                else { wins += 1 }
              } else {
                if (!game.win) losses += 1
                else { wins += 1 }
              }
            }
            sM.textContent = `${wins} - ${losses}`;
            if (switched) {
              sM.style.color = "#A90441";
            }
          }
          sc.appendChild(sM);
          tr.appendChild(sc);
          let p2 = document.createElement("td");
          p2.className = "pl2";
          var ap2 = document.createElement("a");
          ap2.textContent = switched ? match.player : match.opponent;
          ap2.href = `/u/${switched ? match.player : match.opponent}`;
          p2.appendChild(ap2);
          tr.appendChild(p2);
          if (switched ? match.opponentStats : match.stats) {
            let sstats = {};
            for (let ss of ALL_STATS) {
              sstats[ss] = "-";
            }
            for (const [key, value] of Object.entries(switched ? match.opponentStats : match.stats)) {
              if (isNaN(parseFloat(value)))
                continue;
              if (parseFloat(value) < 0)
                continue;
              if (sstats[key]) {
                sstats[key] = value;
              }
            }
            for (let ss of ALL_STATS) {
              let stat = document.createElement("td");
              stat.className = "apm";
              stat.textContent = sstats[ss];
              tr.appendChild(stat);
            }
          } else {
            for (let i = 0; i < ALL_STATS.length; i++) {
              let stat = document.createElement("td");
              stat.className = "apm";
              stat.textContent = "-";
              tr.appendChild(stat);
            }
          }
          let date = document.createElement("td");
          date.className = "date";
          date.textContent = new Date(match.date).toLocaleDateString();
          tr.appendChild(date);
          let btns = document.createElement("td");
          if (match.games && match.games.length > 0) {
            btns.style.display = "flex";
            btns.style.justifyContent = "flex-end";
            if (match.games) {
              for (let m of match.games) {
                let btn = document.createElement("button");
                btn.className = "mm-button";
                btns.appendChild(btn);
                if (m.win == switched) {
                  btn.style.backgroundColor = "#A90441";
                }
                btn.onclick = function () {
                  loadGame(m, match);
                };
              }
            }
          } else {
            btns.className = "apm";
          }
          tr.appendChild(btns);
          body.appendChild(tr);
        }
        matchView.appendChild(table);
        let opponentFilter = document.createElement("input")
        opponentFilter.type = "text"
        opponentFilter.name = "opponent"
        opponentFilter.className = "form-control"
        opponentFilter.placeholder = "Username"
        opponentFilter.autocomplete = "off"
        opponentFilter.style.padding = "10px"
        opponentFilter.multiple = true
        opponentFilter.onchange = (event) => {
          let raw_names = opponentFilter.value.split(" ")
          let names = []
          for (let name of raw_names) {
            names.push(name.toLowerCase())
          }
          for (let i = 0; i < body.children.length; i++) {
            let child = body.children[i]
            for (let j = 0; j < child.children.length; j++) {
              let child2 = child.children[j]
              if (child2.className == "pl2") {
                if (child2.firstChild && names.includes(child2.firstChild.textContent.toLowerCase())) child.style.display = ""
                else { child.style.display = "none" }
                break
              }
            }
          }
        }
        matchView.prepend(opponentFilter)
        cc.prepend(matchView)
        loader.remove()
        //              cc.prepend(collapsible)


      })
    })
  }
};
;// CONCATENATED MODULE: ./src/toggleChatKeyInput.js


const createKeyInputElement = (varName, desc) => {
  const TOGGLE_CHAT_KEY_INPUT_ELEMENT = document.createElement("div");
  TOGGLE_CHAT_KEY_INPUT_ELEMENT.className = "settings-inputRow";
  TOGGLE_CHAT_KEY_INPUT_ELEMENT.innerHTML += `<b>${desc}</b>`

  const inputDiv = document.createElement("div");
  const input = document.createElement("input");
  input.value = displayKeyCode(Config().TOGGLE_CHAT_KEYCODE);
  input.id = `${varName}_INPUT_ELEMENT`;

  input.addEventListener("keydown", e => {
    var charCode = (e.which) ? e.which : e.keyCode
    Config().set(varName, charCode);
    input.value = displayKeyCode(charCode);
    e.stopPropagation();
    e.preventDefault();
    return false;
  });
  input.addEventListener("keypress", () => false);
  const clearBtn = document.createElement("button");
  clearBtn.addEventListener("click", e => {
    Config().set(varName, null);
    input.value = displayKeyCode(null);
  })
  clearBtn.innerHTML = "Clear";

  input.style.marginRight = "5px";
  inputDiv.style.display = "flex";
  inputDiv.appendChild(input);
  inputDiv.appendChild(clearBtn);
  TOGGLE_CHAT_KEY_INPUT_ELEMENT.appendChild(inputDiv);

  return TOGGLE_CHAT_KEY_INPUT_ELEMENT;

}


// stolen from https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
function displayKeyCode(charCode) {
  
  if (charCode == null) {
    return "<enter a key>";
  }

  let a = String.fromCharCode(charCode);
  if (charCode == 8) a = "backspace"; //  backspace
  if (charCode == 9) a = "tab"; //  tab
  if (charCode == 13) a = "enter"; //  enter
  if (charCode == 16) a = "shift"; //  shift
  if (charCode == 17) a = "ctrl"; //  ctrl
  if (charCode == 18) a = "alt"; //  alt
  if (charCode == 19) a = "pause/break"; //  pause/break
  if (charCode == 20) a = "caps lock"; //  caps lock
  if (charCode == 27) a = "escape"; //  escape
  if (charCode == 32) a = "space"; // space
  if (charCode == 33) a = "page up"; // page up, to avoid displaying alternate character and confusing people	         
  if (charCode == 34) a = "page down"; // page down
  if (charCode == 35) a = "end"; // end
  if (charCode == 36) a = "home"; // home
  if (charCode == 37) a = "left arrow"; // left arrow
  if (charCode == 38) a = "up arrow"; // up arrow
  if (charCode == 39) a = "right arrow"; // right arrow
  if (charCode == 40) a = "down arrow"; // down arrow
  if (charCode == 45) a = "insert"; // insert
  if (charCode == 46) a = "delete"; // delete
  if (charCode == 91) a = "left window"; // left window
  if (charCode == 92) a = "right window"; // right window
  if (charCode == 93) a = "select key"; // select key
  if (charCode == 96) a = "numpad 0"; // numpad 0
  if (charCode == 97) a = "numpad 1"; // numpad 1
  if (charCode == 98) a = "numpad 2"; // numpad 2
  if (charCode == 99) a = "numpad 3"; // numpad 3
  if (charCode == 100) a = "numpad 4"; // numpad 4
  if (charCode == 101) a = "numpad 5"; // numpad 5
  if (charCode == 102) a = "numpad 6"; // numpad 6
  if (charCode == 103) a = "numpad 7"; // numpad 7
  if (charCode == 104) a = "numpad 8"; // numpad 8
  if (charCode == 105) a = "numpad 9"; // numpad 9
  if (charCode == 106) a = "multiply"; // multiply
  if (charCode == 107) a = "add"; // add
  if (charCode == 109) a = "subtract"; // subtract
  if (charCode == 110) a = "decimal point"; // decimal point
  if (charCode == 111) a = "divide"; // divide
  if (charCode == 112) a = "F1"; // F1
  if (charCode == 113) a = "F2"; // F2
  if (charCode == 114) a = "F3"; // F3
  if (charCode == 115) a = "F4"; // F4
  if (charCode == 116) a = "F5"; // F5
  if (charCode == 117) a = "F6"; // F6
  if (charCode == 118) a = "F7"; // F7
  if (charCode == 119) a = "F8"; // F8
  if (charCode == 120) a = "F9"; // F9
  if (charCode == 121) a = "F10"; // F10
  if (charCode == 122) a = "F11"; // F11
  if (charCode == 123) a = "F12"; // F12
  if (charCode == 144) a = "num lock"; // num lock
  if (charCode == 145) a = "scroll lock"; // scroll lock
  if (charCode == 186) a = ";"; // semi-colon
  if (charCode == 187) a = "="; // equal-sign
  if (charCode == 188) a = ","; // comma
  if (charCode == 189) a = "-"; // dash
  if (charCode == 190) a = "."; // period
  if (charCode == 191) a = "/"; // forward slash
  if (charCode == 192) a = "`"; // grave accent
  if (charCode == 219) a = "["; // open bracket
  if (charCode == 220) a = "\\"; // back slash
  if (charCode == 221) a = "]"; // close bracket
  if (charCode == 222) a = "'"; // single quote
  return a;
}

;// CONCATENATED MODULE: ./src/chat.js





let game = null;

const initChat = () => {
  'use strict';

  // === show or hide chat timestamps code ===
  // showing timestamp logic is in css
  if (Config().ENABLE_CHAT_TIMESTAMPS)
    document.body.classList.add("show-chat-timestamps");
  Config().onChange("ENABLE_CHAT_TIMESTAMPS", val => {
    if (val) {
      document.body.classList.add("show-chat-timestamps");
    } else {
      document.body.classList.remove("show-chat-timestamps");
    }
  })

  const oldReadyGo = Game.prototype.readyGo;
  Game.prototype.readyGo = function () {
    game = this;
    return oldReadyGo.apply(this, arguments);
  }

  // === toggle chat button code ===

  document.getElementById("TOGGLE_CHAT_KEYCODE_INPUT_ELEMENT").value = displayKeyCode(Config().TOGGLE_CHAT_KEYCODE);
  document.getElementById("CLOSE_CHAT_KEYCODE_INPUT_ELEMENT").value = displayKeyCode(Config().CLOSE_CHAT_KEYCODE);

  // thanks justin https://greasyfork.org/en/scripts/423192-change-chat-key
  document.addEventListener("keydown", e => {
    var charCode = (e.which) ? e.which : e.keyCode
    if (charCode == Config().TOGGLE_CHAT_KEYCODE) {
      if (game && game.focusState !== 1) { // game already focused, unfocus
        game.setFocusState(1);
        setTimeout(function () { game.Live.chatInput.focus() }, 0) // setTimeout to prevent the key from being typed

        // if keys are same, should close chat in this case
      } else if (Config().CLOSE_CHAT_KEYCODE == Config().TOGGLE_CHAT_KEYCODE) {
        document.getElementsByClassName("layer mainLayer gfxLayer")[0].click();
        document.getElementsByClassName("layer mainLayer gfxLayer")[0].focus();

      }
    } else if (charCode == Config().CLOSE_CHAT_KEYCODE) { // focus game
      document.getElementsByClassName("layer mainLayer gfxLayer")[0].click();
      document.getElementsByClassName("layer mainLayer gfxLayer")[0].focus();
    }
  });

  // === emote code ===

  let CUSTOM_EMOTES = [
    {
      u: "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/Cheese.png",
      t: "qep",
      g: "Jstris+",
      n: "MrCheese"
    }, {
      u: "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/Cat.png",
      t: "jermy",
      g: "Jstris+",
      n: "CatUp"
    }, {
      u: "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/Freg.png",
      t: "frog",
      g: "Jstris+",
      n: "FrogSad"
    }, {
      u: "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/freycat.webp",
      t: "frey",
      g: "Jstris+",
      n: "freycat"
    }, {
      u: "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/Blahaj.png",
      t: "jermy",
      g: "Jstris+",
      n: "StarHaj"
    }
    , {
      u: "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/ThisIsFine.png",
      t: "jermy",
      g: "Jstris+",
      n: "fine"
    }
  ]
  let chatListener = Live.prototype.showInChat
  Live.prototype.showInChat = function () {
    let zandria = arguments[1]

    if (typeof zandria == "string") {
      zandria = zandria.replace(/:(.*?):/g, function (match) {
        let cEmote = null
        for (let emote of CUSTOM_EMOTES) {
          if (emote.n == match.split(':')[1]) {
            cEmote = emote
            break
          }
        }
        if (cEmote) {
          return `<img src='${cEmote.u}' class='emojiPlus' alt=':${cEmote.n}:'>`
        }
        return match
      });
    }
    arguments[1] = zandria
    let val = chatListener.apply(this, arguments)
    // Add Timestamps
    var s = document.createElement("span");
    s.className = 'chat-timestamp';
    s.innerHTML = "[" + new Date().toTimeString().slice(0, 8) + "] ";
    var c = document.getElementsByClassName("chl");
    c[c.length - 1].prepend(s);

    return val
  }
  ChatAutocomplete.prototype.loadEmotesIndex = function (_0xd06fx4) {
    if (!this.moreEmotesAdded) {
      var brentson = new XMLHttpRequest,
        terrilynne = "/code/emotes?";
      brentson.timeout = 8e3,
        brentson.open("GET", terrilynne, true);
      try {
        brentson.send();
      } catch (bleu) { };
      var areeg = this;
      brentson.ontimeout = function () { },
        brentson.onerror = brentson.onabort = function () { },
        brentson.onload = function () {
          if (200 === brentson.status) {
            let zakeriah = JSON.parse(brentson.responseText);
            for (let emote of CUSTOM_EMOTES) {
              zakeriah.unshift(emote)
            }
            null !== areeg.preProcessEmotes && (zakeriah = areeg.preProcessEmotes(zakeriah)),
              areeg.addEmotes(zakeriah),
              null !== areeg.onEmoteObjectReady && areeg.onEmoteObjectReady(zakeriah);
          }
        };
    }
  }
  EmoteSelect.prototype.initializeContainers = function () {
    console.log(this.groupEmotes["Jstris+"] = "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/emotes/freycat.webp")
    this.searchElem = document.createElement("form"), this.searchElem.classList.add("form-inline", "emoteForm"), this.emoteElem.appendChild(this.searchElem), this.searchBar = document.createElement("input"), this.searchBar.setAttribute("autocomplete", "off"), this.searchBar.classList.add("form-control"), this.searchBar.id = "emoteSearch", this.searchBar.addEventListener("input", () => {
      this.searchFunction(this.emoteList);
    }), this.searchElem.addEventListener("submit", kesean => {
      kesean.preventDefault();
    }), this.searchBar.setAttribute("type", "text"), this.searchBar.setAttribute("placeholder", "Search Emotes"), this.searchElem.appendChild(this.searchBar), this.optionsContainer = document.createElement("div"), this.optionsContainer.classList.add("optionsContainer"), this.emoteElem.appendChild(this.optionsContainer), this.emotesWrapper = document.createElement("div"), this.emotesWrapper.classList.add("emotesWrapper"), this.optionsContainer.appendChild(this.emotesWrapper);
  }
  ChatAutocomplete.prototype.processHint = function (ylario) {

    var maizah = ylario[0].toLowerCase(),
      cahlin = ylario[1];
    if ("" !== this.prfx && (null === maizah || maizah.length < this.minimalLengthForHint || maizah[0] !== this.prfx)) {
      hideElem(this.hintsElem);
    } else {
      bertile = bertile;
      var maiesha = maizah.substring(this.prfx.length),
        bertile = this.prefixInSearch
          ? maizah
          : maiesha,
        cinque = 0,
        dyllan = "function" == typeof this.hints
          ? this.hints()
          : this.hints;
      this.hintsElem.innerHTML = "";
      var roey = [],
        tishie = [];
      for (var cedrik in dyllan) {
        var catenia = (shawnteria = dyllan[cedrik]).toLowerCase();
        catenia.startsWith(bertile)
          ? roey.push(shawnteria)
          : maiesha.length >= 2 && catenia.includes(maiesha) && tishie.push(shawnteria);
      };
      if (roey.sort(), roey.length < this.maxPerHint) {
        tishie.sort();
        for (const ajitesh of tishie) {
          if (-1 === roey.indexOf(ajitesh) && (roey.push(ajitesh), roey.length >= this.maxPerHint)) {
            break;
          }
        }
      };
      for (var shawnteria of roey) {
        var vidhu = document.createElement("div");
        if (this.hintsImg && this.hintsImg[shawnteria]) {
          vidhu.className = "emHint";
          var cebria = document.createElement("img");
          let cEmote = null
          for (let emote of CUSTOM_EMOTES) {
            if (emote.n == shawnteria.split(':')[1]) {
              cEmote = emote
              break
            }
          }
          if (cEmote) {
            cebria.src = cEmote.u
          } else {
            cebria.src = CDN_URL("/" + this.hintsImg[shawnteria])
          }
          vidhu.appendChild(cebria);
          var wael = document.createElement("div");
          wael.textContent = shawnteria,
            vidhu.appendChild(wael);
        } else {
          vidhu.innerHTML = shawnteria;
        };
        vidhu.dataset.pos = cahlin,
          vidhu.dataset.str = shawnteria;
        var yolandi = this;
        if (vidhu.addEventListener("click", function (dennies) {
          for (var ajane = yolandi.inp.value, delanei = parseInt(this.dataset.pos), xila = ajane.substring(0, delanei), neng = xila.indexOf(" "), marshelia = neng + 1; -1 !== neng;) {
            -1 !== (neng = xila.indexOf(" ", neng + 1)) && (marshelia = neng + 1);
          };
          yolandi.prefixInSearch || ++marshelia,
            yolandi.inp.value = ajane.substring(0, marshelia) + this.dataset.str + " " + ajane.substring(delanei),
            yolandi.inp.focus(),
            yolandi.setCaretPosition(delanei + this.dataset.str.length + 1 - (delanei - marshelia)),
            hideElem(yolandi.hintsElem),
            yolandi.wipePrevious && (yolandi.inp.value = this.dataset.str, yolandi.onWiped && yolandi.onWiped(this.dataset.str));
        }, false), this.hintsElem.appendChild(vidhu), ++cinque >= this.maxPerHint) {
          break;
        }
      };
      this.setSelected(0),
        cinque
          ? showElem(this.hintsElem)
          : hideElem(this.hintsElem);
    }
  }
  console.log("JSTRIS+ EMOTES LOADED")
}

;// CONCATENATED MODULE: ./src/style.css
/* harmony default export */ const style = ("@import url('https://fonts.googleapis.com/css2?family=Gugi&display=swap');\r\n\r\n/* =========== settings modal css ============= */\r\n\r\n.settings-modal {\r\n  display: none;\r\n  /* Hidden by default */\r\n  position: fixed;\r\n  /* Stay in place */\r\n  z-index: 99999;\r\n  /* Sit on top */\r\n  left: 0;\r\n  top: 0;\r\n  width: 100%;\r\n  /* Full width */\r\n  height: 100%;\r\n  /* Full height */\r\n  overflow: auto;\r\n  /* Enable scroll if needed */\r\n  background-color: rgb(0, 0, 0);\r\n  /* Fallback color */\r\n  background-color: rgba(0, 0, 0, 0.4);\r\n  /* Black w/ opacity */\r\n  -webkit-animation-name: fadeIn;\r\n  /* Fade in the background */\r\n  -webkit-animation-duration: 0.4s;\r\n  animation-name: fadeIn;\r\n  animation-duration: 0.4s;\r\n}\r\n\r\n.settings-modalCheckbox {\r\n  width: 30px;\r\n  height: 30px;\r\n}\r\n\r\n.settings-text {\r\n  text-align: center;\r\n}\r\n\r\n.settings-modalTextbox {\r\n  height: 30px;\r\n  font-size: 25px;\r\n  border: solid 1px black;\r\n\r\n}\r\n\r\n.settings-modalTextarea {\r\n  height: 60px;\r\n  border: solid 1px black;\r\n  resize: none;\r\n}\r\n\r\n.settings-modalContentTitle {\r\n  text-align: left;\r\n  width: 60%;\r\n  min-width: 300px;\r\n  margin: auto;\r\n  padding: 20px;\r\n}\r\n\r\n.settings-inputRow {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  width: 60%;\r\n  min-width: 300px;\r\n  margin: auto;\r\n  padding: 10px;\r\n  border-bottom: solid 1px #2c2c2c;\r\n  position: relative;\r\n}\r\n\r\n.settings-inputRow select {\r\n  color: black;\r\n}\r\n\r\n.settings-modalOpenButton {\r\n  width: 40px;\r\n  height: 40px;\r\n  cursor: pointer;\r\n  border-radius: 10px;\r\n  position: fixed;\r\n  left: 30px;\r\n  bottom: 30px;\r\n\r\n  transition: 0.5s;\r\n\r\n}\r\n\r\n.settings-modalCloseButton {\r\n  width: 30px;\r\n  height: 30px;\r\n  cursor: pointer;\r\n  transition: 0.5s;\r\n  position: absolute;\r\n  right: 12px;\r\n  top: 12px;\r\n}\r\n\r\n.settings-modalOpenButton:hover {\r\n  transform: rotate(-360deg);\r\n  opacity: 0.3;\r\n}\r\n\r\n.settings-modalClosebutton:hover {\r\n  opacity: 0.3;\r\n}\r\n\r\n/* Modal Content */\r\n.settings-modal-content {\r\n  position: fixed;\r\n  bottom: 0;\r\n  background-color: #fefefe;\r\n  width: 100%;\r\n  height: 75vh;\r\n  -webkit-animation-name: slideIn;\r\n  -webkit-animation-duration: 0.4s;\r\n  animation-name: slideIn;\r\n  display: flex;\r\n  flex-direction: column;\r\n  animation-duration: 0.4s;\r\n}\r\n\r\n.settings-modal-header {\r\n  padding: 16px;\r\n  background-color: #5cb85c;\r\n  color: white;\r\n  text-align: center;\r\n  position: relative;\r\n}\r\n\r\n.settings-modal-header h2 {\r\n  line-height: 16px;\r\n  margin-top: 3px;\r\n  margin-bottom: 3px;\r\n}\r\n\r\n.settings-modal-body {\r\n  padding: 2px 16px;\r\n  color: black;\r\n  flex: 1;\r\n  overflow-y: scroll;\r\n  background-color: #1c1c1c;\r\n  color: white;\r\n}\r\n\r\n.settings-modal-footer {\r\n  padding: 2px 16px;\r\n  background-color: #5cb85c;\r\n  color: white;\r\n}\r\n\r\n.settings-sliderValue {\r\n  position: absolute;\r\n  font-size: 18px;\r\n  right: 330px;\r\n}\r\n\r\n.settings-slider {\r\n  -webkit-appearance: none;\r\n  max-width: 300px;\r\n  height: 15px;\r\n  border-radius: 5px;\r\n  background: #d3d3d3;\r\n  outline: none;\r\n  opacity: 0.7;\r\n  -webkit-transition: .2s;\r\n  transition: opacity .2s;\r\n}\r\n\r\n.settings-slider:hover {\r\n  opacity: 1;\r\n}\r\n\r\n.settings-slider::-webkit-slider-thumb {\r\n  -webkit-appearance: none;\r\n  appearance: none;\r\n  width: 25px;\r\n  height: 25px;\r\n  border-radius: 50%;\r\n  background: #04AA6D;\r\n  cursor: pointer;\r\n}\r\n\r\n.settings-slider::-moz-range-thumb {\r\n  width: 25px;\r\n  height: 25px;\r\n  border-radius: 50%;\r\n  background: #04AA6D;\r\n  cursor: pointer;\r\n}\r\n\r\n/* Add Animation */\r\n@-webkit-keyframes slideIn {\r\n  from {\r\n    bottom: -300px;\r\n    opacity: 0\r\n  }\r\n\r\n  to {\r\n    bottom: 0;\r\n    opacity: 1\r\n  }\r\n}\r\n\r\n@keyframes slideIn {\r\n  from {\r\n    bottom: -300px;\r\n    opacity: 0\r\n  }\r\n\r\n  to {\r\n    bottom: 0;\r\n    opacity: 1\r\n  }\r\n}\r\n\r\n@-webkit-keyframes fadeIn {\r\n  from {\r\n    opacity: 0\r\n  }\r\n\r\n  to {\r\n    opacity: 1\r\n  }\r\n}\r\n\r\n@keyframes fadeIn {\r\n  from {\r\n    opacity: 0\r\n  }\r\n\r\n  to {\r\n    opacity: 1\r\n  }\r\n}\r\n\r\n/* =========== matchmaking css ============= */\r\n.mmMatches {\r\n  padding: 0 18px;\r\n  display: block;\r\n  overflow: hidden;\r\n}\r\n\r\n.mmContainer {\r\n  display: flex;\r\n  flex-direction: row;\r\n  z-index: 50;\r\n  color: white;\r\n  position: absolute;\r\n  left: 100px;\r\n  bottom: 30px;\r\n  color: #999;\r\n  width: 200px;\r\n  position: fixed;\r\n}\r\n\r\n.mmLoader {\r\n  border: 16px solid white;\r\n  border-top: 16px solid #04AA6D;\r\n  border-radius: 50%;\r\n  width: 120px;\r\n  height: 120px;\r\n  animation: mmSpin 2s linear infinite;\r\n  position: absolute;\r\n  top: 0;\r\n  bottom: 0;\r\n  left: 0;\r\n  right: 0;\r\n\r\n  margin: auto;\r\n}\r\n\r\n@keyframes mmSpin {\r\n  0% {\r\n    transform: rotate(0deg);\r\n  }\r\n\r\n  100% {\r\n    transform: rotate(360deg);\r\n  }\r\n}\r\n\r\n.mmInfoContainer {\r\n  height: 40px;\r\n  flex-direction: column;\r\n  justify-content: center;\r\n  min-width: 150px;\r\n  align-items: center;\r\n  white-space: pre;\r\n  display: none;\r\n  /* hide unless show-queue-info */\r\n}\r\n\r\n.show-queue-info .mmInfoContainer {\r\n  display: flex !important;\r\n}\r\n\r\n.mmButton {\r\n  color: white;\r\n  height: 40px;\r\n  border: 2px solid white;\r\n  border-radius: 10px;\r\n  background-color: transparent;\r\n  min-width: 200px;\r\n  display: none;\r\n}\r\n\r\n.show-mm-button .mmButton {\r\n  display: block !important;\r\n}\r\n\r\n.mmModal {\r\n  display: none;\r\n  /* Hidden by default */\r\n  position: fixed;\r\n  /* Stay in place */\r\n  z-index: 1;\r\n  /* Sit on top */\r\n  padding-top: 100px;\r\n  /* Location of the box */\r\n  left: 0;\r\n  top: 0;\r\n  width: 100%;\r\n  /* Full width */\r\n  height: 100%;\r\n  /* Full height */\r\n  overflow: auto;\r\n  /* Enable scroll if needed */\r\n  background-color: rgb(0, 0, 0);\r\n  /* Fallback color */\r\n  background-color: rgba(0, 0, 0, 0.4);\r\n  /* Black w/ opacity */\r\n}\r\n\r\n/* Modal Content */\r\n.mmModal-content {\r\n  background-color: #fefefe;\r\n  margin: auto;\r\n  padding: 20px;\r\n  border: 1px solid #888;\r\n  width: 40%;\r\n  height: 40%;\r\n  background-color: #343837;\r\n  position: relative;\r\n}\r\n\r\n/* The Close Button */\r\n.mmClose {\r\n  position: absolute;\r\n  top: 0px;\r\n  right: 5px;\r\n  color: #aaaaaa;\r\n  font-size: 30px;\r\n  font-weight: bold;\r\n}\r\n\r\n.mmClose:hover,\r\n.mmClose:focus {\r\n  color: #000;\r\n  text-decoration: none;\r\n  cursor: pointer;\r\n}\r\n\r\n.mm-button {\r\n  border: none;\r\n  color: white;\r\n  padding: 10px;\r\n  text-align: center;\r\n  text-decoration: none;\r\n  display: inline-block;\r\n  font-size: 0px;\r\n  margin: 2px 2px;\r\n  border-radius: 100%;\r\n  border: 2px solid #222222;\r\n  background-color: #04AA6D;\r\n}\r\n\r\n.mm-button:hover {\r\n  border: 2px solid white;\r\n}\r\n\r\n.mm-chat-buttons-container {\r\n  position: sticky;\r\n  height: 45px;\r\n}\r\n\r\n.mm-ready-button {\r\n  border: none;\r\n  color: white;\r\n  padding: 10px;\r\n  text-align: center;\r\n  text-decoration: none;\r\n  margin: 2px 2px;\r\n  border: 2px solid #222222;\r\n  background-color: #04AA6D;\r\n}\r\n\r\n.mm-ready-button:hover {\r\n  border: 2px solid white;\r\n}\r\n\r\n/* =========== action text css ============= */\r\n\r\n.action-text {\r\n  transition: 1s;\r\n  /*font-family: 'Gugi', sans-serif;*/\r\n  -webkit-animation-name: bounce;\r\n  /* Fade in the background */\r\n  -webkit-animation-duration: 0.4s;\r\n  animation-name: action-text;\r\n  animation-duration: 0.4s;\r\n  animation-fill-mode: forwards;\r\n}\r\n\r\n@keyframes action-text {\r\n  0% {\r\n    transform: translateY(0);\r\n  }\r\n\r\n  30% {\r\n    transform: translateY(-3px);\r\n  }\r\n\r\n  100% {\r\n    transform: translateY(0);\r\n  }\r\n}\r\n\r\n/* Chat timestamp showing logic */\r\n\r\n.chat-timestamp {\r\n  display: none;\r\n  color: grey;\r\n}\r\n\r\n.show-chat-timestamps .chat-timestamp {\r\n  display: inline !important;\r\n}\r\n\r\n\r\n/* ===== stats css ===== */\r\n\r\n.stats-table {\r\n  z-index: 10;\r\n  color: white;\r\n  position: absolute;\r\n  left: -210px;\r\n  bottom: 40px;\r\n  color: #999;\r\n  width: 200px;\r\n}\r\n\r\n/* ===== kbd display css ===== */\r\n\r\n#keyboardHolder {\r\n  position: absolute;\r\n  left: -350px;\r\n  top: 100px;\r\n  transform-origin: top right;\r\n}\r\n\r\n@media screen and (max-width: 1425px) {\r\n  #keyboardHolder {\r\n    transform: scale(75%);\r\n    left: -262px;\r\n  }\r\n\r\n  #kps {\r\n    font-size: 27px !important;\r\n  }\r\n}\r\n\r\n@media screen and (max-width: 1260px) {\r\n  #keyboardHolder {\r\n    transform: scale(50%);\r\n    left: -200px;\r\n  }\r\n\r\n  #kps {\r\n    font-size: 40px !important;\r\n  }\r\n}\r\n\r\n@media screen and (max-width: 900px) {\r\n  #keyboardHolder {\r\n    transform: scale(50%);\r\n    left: 250px;\r\n    top: 500px;\r\n  }\r\n\r\n  #kps {\r\n    font-size: 40px !important;\r\n  }\r\n}\r\n\r\n#kbo {\r\n  text-align: center;\r\n  position: absolute;\r\n  font-size: 15px;\r\n}\r\n\r\n#kps {\r\n  margin-bottom: 10px;\r\n  font-size: 20px;\r\n}\r\n\r\n#kbo .tg {\r\n  border-collapse: collapse;\r\n  border-spacing: 0;\r\n  color: rgba(255, 60, 109);\r\n}\r\n\r\n#kbo .tg td {\r\n  padding: 10px 5px;\r\n  border-style: solid;\r\n  border-width: 2px;\r\n  transition: 0.1s;\r\n}\r\n\r\n#kbo .tg th {\r\n  padding: 10px 5px;\r\n  border-style: solid;\r\n  border-width: 2px;\r\n}\r\n\r\n#kbo .tg .kbnone {\r\n  border-color: #000000;\r\n  border: inherit;\r\n}\r\n\r\n#kbo .tg .kbkey {\r\n  border-color: rgba(130, 220, 94, 1);\r\n  background-color: black;\r\n}\r\n\r\n.hide-kbd-display {\r\n  display: none;\r\n}\r\n\r\n.really-hide-kbd-display {\r\n  /* for when keyboard display really should not be shown, like 1v1 replays (for now) */\r\n  display: none !important;\r\n}\r\n\r\n/* custom emoji */\r\n\r\n.emojiPlus {\r\n  height: 3em;\r\n  pointer-events: none;\r\n}\r\n\r\n\r\n/* practice mode settings */\r\n.show-practice-mode-settings {\r\n  display: block !important;\r\n}\r\n\r\n#customPracticeSettings {\r\n  z-index: 10;\r\n  color: white;\r\n  position: absolute;\r\n  left: -210px;\r\n  bottom: -80px;\r\n  color: #999;\r\n  width: 200px;\r\n  display: none;\r\n}\r\n\r\n#customPracticeSettings div {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  width: 100%;\r\n}\r\n\r\n#customPracticeSettings #customApmSlider {\r\n  width: 100px;\r\n}\r\n\r\n#customPracticeSettings #customApmInput {\r\n  width: 50px;\r\n}\r\n\r\n/* replay addons */\r\n\r\n.replay-btn {\r\n  padding: .25em .5em;\r\n  border: solid 1px white;\r\n  border-radius: 4px;\r\n  display: inline-block;\r\n  text-align: center;\r\n  color: #fff;\r\n  background-color: transparent;\r\n}\r\n\r\n.replay-btn:hover,\r\n.replay-btn:focus {\r\n  cursor: pointer;\r\n  color: #04AA6D;\r\n}\r\n\r\n.replay-btn-group {\r\n  display: inline-flex;\r\n  border: 1px solid white;\r\n  overflow: hidden;\r\n  border-radius: 4px;\r\n}\r\n\r\n.replay-btn-group>.c-btn {\r\n  border-radius: 0;\r\n  border: none;\r\n  border-right: 1px solid white;\r\n}\r\n\r\n.replay-btn-group>.c-btn:last-child {\r\n  border-right: none;\r\n}");
;// CONCATENATED MODULE: ./src/customSkinPresets.js



const FETCH_URL = "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/presets/skinPresets.json";
let CUSTOM_SKIN_PRESETS = [];
const fetchSkinPresets = () => {
  fetch(FETCH_URL, { cache: "reload" })
    .then(e => e.json())
    .then(j => {
      CUSTOM_SKIN_PRESETS = j;
      for (let i of CUSTOM_SKIN_PRESETS) {
        let option = document.createElement("option");
        option.value = JSON.stringify(i);
        option.innerHTML = i.name;
        dropdown.appendChild(option);
      }
    })
}


const CUSTOM_SKIN_PRESET_ELEMENT = document.createElement("div");
CUSTOM_SKIN_PRESET_ELEMENT.className = "settings-inputRow";
CUSTOM_SKIN_PRESET_ELEMENT.innerHTML += "<b>Custom skin presets</b>"

const dropdown = document.createElement("select");
dropdown.innerHTML += "<option>Select...</option>";

dropdown.addEventListener("change", () => {
  var { url, ghostUrl } = JSON.parse(dropdown.value);

  document.getElementById("CUSTOM_SKIN_URL").value = url || "";
  Config().set("CUSTOM_SKIN_URL", url || "");
  document.getElementById("CUSTOM_GHOST_SKIN_URL").value = ghostUrl || "";
  Config().set("CUSTOM_GHOST_SKIN_URL", ghostUrl || "");
  dropdown.selectedIndex = 0;
})

CUSTOM_SKIN_PRESET_ELEMENT.appendChild(dropdown);
;// CONCATENATED MODULE: ./src/customSoundPresets.js


const customSoundPresets_FETCH_URL = "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/presets/soundPresets.json"

let CUSTOM_SOUND_PRESETS = [];
const fetchSoundPresets = () => {
  fetch(customSoundPresets_FETCH_URL, { cache: "reload" })
    .then(e => e.json())
    .then(j => {
      CUSTOM_SOUND_PRESETS = j;
      for (let i of CUSTOM_SOUND_PRESETS) {
        let option = document.createElement("option");
        option.value = JSON.stringify(i);
        option.innerHTML = i.name;
        customSoundPresets_dropdown.appendChild(option);
      }
    })
}

const CUSTOM_SOUND_PRESET_ELEMENT = document.createElement("div");
CUSTOM_SOUND_PRESET_ELEMENT.className = "settings-inputRow";
CUSTOM_SOUND_PRESET_ELEMENT.innerHTML += "<b>Custom sound presets</b>"


const customSoundPresets_dropdown = document.createElement("select");
customSoundPresets_dropdown.innerHTML += "<option>Select...</option>";

customSoundPresets_dropdown.addEventListener("change", () => {
  document.getElementById("CUSTOM_SFX_JSON").value = customSoundPresets_dropdown.value;
  Config().set("CUSTOM_SFX_JSON", customSoundPresets_dropdown.value);

  customSoundPresets_dropdown.selectedIndex = 0;
})

CUSTOM_SOUND_PRESET_ELEMENT.appendChild(customSoundPresets_dropdown);


;// CONCATENATED MODULE: ./src/plusSoundPresets.js



const plusSoundPresets_FETCH_URL = "https://raw.githubusercontent.com/JstrisPlus/jstris-plus-assets/main/presets/plusSoundPresets.json"

let CUSTOM_PLUS_SOUND_PRESETS = [];
const fetchPlusSoundPresets = () => {
    fetch(plusSoundPresets_FETCH_URL, { cache: "reload" })
        .then(e => e.json())
        .then(j => {
            CUSTOM_PLUS_SOUND_PRESETS = j;
            for (let i of CUSTOM_PLUS_SOUND_PRESETS) {
                let option = document.createElement("option");
                option.value = JSON.stringify(i);
                option.innerHTML = i.name;
                plusSoundPresets_dropdown.appendChild(option);
            }
        })
}

const CUSTOM_PLUS_SOUND_PRESET_ELEMENT = document.createElement("div");
CUSTOM_PLUS_SOUND_PRESET_ELEMENT.className = "settings-inputRow";
CUSTOM_PLUS_SOUND_PRESET_ELEMENT.innerHTML += "<b>Custom Jstris+ sound presets</b>"


const plusSoundPresets_dropdown = document.createElement("select");
plusSoundPresets_dropdown.innerHTML += "<option>Select...</option>";

plusSoundPresets_dropdown.addEventListener("change", () => {
    document.getElementById("CUSTOM_PLUS_SFX_JSON").value = plusSoundPresets_dropdown.value;
    Config().set("CUSTOM_PLUS_SFX_JSON", plusSoundPresets_dropdown.value);
    setPlusSfx(plusSoundPresets_dropdown.value)

    plusSoundPresets_dropdown.selectedIndex = 0;
})

CUSTOM_PLUS_SOUND_PRESET_ELEMENT.appendChild(plusSoundPresets_dropdown);


;// CONCATENATED MODULE: ./src/settingsModal.js






const GEAR_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M7.07.65a.85.85 0 0 0-.828.662l-.238 1.05q-.57.167-1.08.448l-.91-.574a.85.85 0 0 0-1.055.118l-.606.606a.85.85 0 0 0-.118 1.054l.574.912q-.28.509-.447 1.079l-1.05.238a.85.85 0 0 0-.662.829v.857a.85.85 0 0 0 .662.829l1.05.238q.166.57.448 1.08l-.575.91a.85.85 0 0 0 .118 1.055l.607.606a.85.85 0 0 0 1.054.118l.911-.574q.51.28 1.079.447l.238 1.05a.85.85 0 0 0 .829.662h.857a.85.85 0 0 0 .829-.662l.238-1.05q.57-.166 1.08-.447l.911.574a.85.85 0 0 0 1.054-.118l.606-.606a.85.85 0 0 0 .118-1.054l-.574-.911q.282-.51.448-1.08l1.05-.238a.85.85 0 0 0 .662-.829v-.857a.85.85 0 0 0-.662-.83l-1.05-.237q-.166-.57-.447-1.08l.574-.91a.85.85 0 0 0-.118-1.055l-.606-.606a.85.85 0 0 0-1.055-.118l-.91.574a5.3 5.3 0 0 0-1.08-.448l-.239-1.05A.85.85 0 0 0 7.928.65zM4.92 3.813a4.5 4.5 0 0 1 1.795-.745L7.071 1.5h.857l.356 1.568a4.5 4.5 0 0 1 1.795.744l1.36-.857l.607.606l-.858 1.36c.37.527.628 1.136.744 1.795l1.568.356v.857l-1.568.355a4.5 4.5 0 0 1-.744 1.796l.857 1.36l-.606.606l-1.36-.857a4.5 4.5 0 0 1-1.795.743L7.928 13.5h-.857l-.356-1.568a4.5 4.5 0 0 1-1.794-.744l-1.36.858l-.607-.606l.858-1.36a4.5 4.5 0 0 1-.744-1.796L1.5 7.93v-.857l1.568-.356a4.5 4.5 0 0 1 .744-1.794L2.954 3.56l.606-.606zM9.026 7.5a1.525 1.525 0 1 1-3.05 0a1.525 1.525 0 0 1 3.05 0m.9 0a2.425 2.425 0 1 1-4.85 0a2.425 2.425 0 0 1 4.85 0" clip-rule="evenodd"/></svg>
`

const X_SVG = `
<svg fill="#000000" width="30" height="30" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
    <path d="M0 14.545L1.455 16 8 9.455 14.545 16 16 14.545 9.455 8 16 1.455 14.545 0 8 6.545 1.455 0 0 1.455 6.545 8z" fill="#333" fill-rule="evenodd"/>
</svg>
`

const createTitle = (text, style) => {
  var modalBody = document.getElementById("settingsBody");
  var p = document.createElement("h3");
  p.className = "settings-modalContentTitle";
  p.textContent = text;
  if (style)
    for (var i in style)
      p.style[i] = style[i];
  modalBody.appendChild(p);
}

const createCheckbox = (varName, displayName) => {
  var modalBody = document.getElementById("settingsBody");
  var box = document.createElement("input")
  box.type = "checkbox"
  box.id = varName;
  box.checked = Config()[varName];
  box.className = "settings-modalCheckbox";
  box.addEventListener("change", () => {
    Config().set(varName, box.checked);
  });
  var label = document.createElement("label");
  label.htmlFor = varName;
  label.innerHTML = displayName;

  var div = document.createElement("div");
  div.className = "settings-inputRow";
  div.appendChild(label);
  div.appendChild(box);

  modalBody.appendChild(div);

}

const createTextInput = (varName, displayName) => {
  var modalBody = document.getElementById("settingsBody");
  var box = document.createElement("input")
  box.type = "text"
  box.id = varName;
  box.value = Config()[varName];
  box.className = "settings-modalTextbox";
  box.addEventListener("change", () => {
    Config().set(varName, box.value);
  });
  var label = document.createElement("label");
  label.htmlFor = varName;
  label.innerHTML = displayName;

  var div = document.createElement("div");
  div.className = "settings-inputRow";
  div.appendChild(label);
  div.appendChild(box);

  modalBody.appendChild(div);

}

const createResetButton = (toReset, displayName) => {
  let vars = toReset;
  if (!Array.isArray(toReset))
    vars = [toReset];
  var modalBody = document.getElementById("settingsBody");
  var button = document.createElement("button")
  button.addEventListener("click", () => {
    vars.forEach((varName) => {
      Config().reset(varName);
      let el = document.getElementById(varName);
      if (el.type == "checkbox") {
        el.checked = Config()[varName];
      } else {
        el.value = Config()[varName];
      }
      el.dispatchEvent(new Event('change', { value: el.value }));
    });
  });
  button.textContent = displayName;

  var div = document.createElement("div");
  div.className = "settings-inputRow";
  div.appendChild(button);

  modalBody.appendChild(div);

}

const createTextArea = (varName, displayName) => {
  var modalBody = document.getElementById("settingsBody");
  var box = document.createElement("textarea")
  box.id = varName;
  box.value = Config()[varName];
  box.className = "settings-modalTextarea";
  box.addEventListener("change", () => {
    Config().set(varName, box.value);
  });
  var label = document.createElement("label");
  label.htmlFor = varName;
  label.innerHTML = displayName;

  var div = document.createElement("div");
  div.className = "settings-inputRow";
  div.appendChild(label);
  div.appendChild(box);

  modalBody.appendChild(div);

}


const createHTML = (ele) => {
  var modalBody = document.getElementById("settingsBody");
  var p = document.createElement("div");
  if (typeof ele == "string")
    p.innerHTML = ele;
  else
    p.appendChild(ele);
  modalBody.appendChild(p);
}

const createSliderInput = (varName, displayName, min = 0, max = 1, step = 0.05) => {
  var modalBody = document.getElementById("settingsBody");
  var slider = document.createElement("input")
  slider.type = "range"
  slider.min = min;
  slider.max = max;
  slider.step = step;
  slider.id = varName;
  slider.value = Config()[varName];
  slider.className = "settings-slider";
  var valueLabel = document.createElement("span");
  valueLabel.className = "settings-sliderValue"
  slider.addEventListener("change", () => {
    Config().set(varName, slider.value);
    valueLabel.innerHTML = Number.parseFloat(slider.value).toFixed(2);
  });
  valueLabel.innerHTML = Number.parseFloat(Config()[varName]).toFixed(2);

  var label = document.createElement("label");
  label.htmlFor = varName;
  label.innerHTML = displayName;

  var div = document.createElement("div");
  div.className = "settings-inputRow";
  div.appendChild(label);
  div.appendChild(slider);
  div.appendChild(valueLabel);

  modalBody.appendChild(div);

}

const generateBody = () => {
  createHTML(`<p class='settings-text'><a href="http://jeague.tali.software" class='settings-text'>About Jstris+</a></p>`)
  createTitle("Visual settings");
  createCheckbox("ENABLE_PLACE_BLOCK_ANIMATION", "Enable place block animation");
  createSliderInput("PIECE_FLASH_LENGTH", "Length of place block animation");
  createSliderInput("PIECE_FLASH_OPACITY", "Initial opacity of place block animation");
  createCheckbox("ENABLE_LINECLEAR_ANIMATION", "Enable line clear animations");
  createSliderInput("LINE_CLEAR_LENGTH", "Length of line clear animation", 0, 2);
  createCheckbox("ENABLE_LINECLEAR_SHAKE", "Enable shake on line clear");
  createSliderInput("LINE_CLEAR_SHAKE_STRENGTH", "Strength of line clear shake", 0, 5);
  createSliderInput("LINE_CLEAR_SHAKE_LENGTH", "Length of line clear shake", 0, 3);
  createCheckbox("ENABLE_ACTION_TEXT", "Enable action text");
  createResetButton([
    "ENABLE_PLACE_BLOCK_ANIMATION", "PIECE_FLASH_LENGTH", "PIECE_FLASH_OPACITY", "ENABLE_LINECLEAR_ANIMATION",
    "LINE_CLEAR_LENGTH", "ENABLE_LINECLEAR_SHAKE", "LINE_CLEAR_SHAKE_STRENGTH", "LINE_CLEAR_SHAKE_LENGTH",
    "ENABLE_ACTION_TEXT"
  ], "Reset Visual Settings to Default")

  createTitle("Customization Settings");

  createHTML(`<p class='settings-text'>Checkout the 
  <a target='_blank' href='https://docs.google.com/spreadsheets/d/1xO8DTORacMmSJAQicpJscob7WUkOVuaNH0wzkR_X194/htmlview#'>Jstris Customization Database</a>
  for a list of skins and backgrounds to use.</p>`)

  createTextInput("BACKGROUND_IMAGE_URL", "Background image url (blank for none)");

  fetchSkinPresets();
  createHTML(CUSTOM_SKIN_PRESET_ELEMENT);
  createTextInput("CUSTOM_SKIN_URL", "Custom block skin url (blank for regular skin)");
  createTextInput("CUSTOM_GHOST_SKIN_URL", "Custom ghost block skin url (blank for default)");
  createHTML(`<p class='settings-text'>(Turning off custom skin may require a refresh)</p>`);
  createCheckbox("ENABLE_REPLAY_SKIN", "Enable custom skins in replays (requires refresh)");
  createCheckbox("ENABLE_KEYBOARD_DISPLAY", "Enable keyboard overlay");

  createTitle("Audio settings");

  fetchPlusSoundPresets();
  createHTML(CUSTOM_PLUS_SOUND_PRESET_ELEMENT)
  createTextArea("CUSTOM_PLUS_SFX_JSON", "Data for custom plus SFX");
  createHTML(`<p class='settings-text' id='custom_plus_sfx_json_err'></p>`);

  createCheckbox("ENABLE_OPPONENT_SFX", "Enable opponent SFX");
  createSliderInput("OPPONENT_SFX_VOLUME_MULTPLIER", "Opponent SFX volume");
  createCheckbox("ENABLE_CUSTOM_SFX", "Enable custom SFX (turning off requires refresh)");
  createHTML(`<p class='settings-text'>(Turning off custom sounds may require a refresh)</p>`)
  createCheckbox("ENABLE_CUSTOM_VFX", "Enable custom spawn SFX (voice annotations)");
  createHTML(`<p class='settings-text'>(Custom SFX must be enabled for spawn SFX)</p>`);

  fetchSoundPresets();
  createHTML(CUSTOM_SOUND_PRESET_ELEMENT);
  createTextArea("CUSTOM_SFX_JSON", "Data for custom SFX");
  createHTML(`<p class='settings-text' id='custom_sfx_json_err'></p>`);

  createHTML(`<p class='settings-text'>Refer to the <a target="_blank" href="https://docs.google.com/document/d/1FaijL-LlBRnSZBbnQ2FUWxF9ktgoAQy0NnoHpjkXadE/edit#">guide</a> and the 
  <a target='_blank' href='https://docs.google.com/spreadsheets/d/1xO8DTORacMmSJAQicpJscob7WUkOVuaNH0wzkR_X194/htmlview#'>Jstris Customization Database</a>
  for custom SFX resources.`)

  createTitle("Custom stats settings");
  createCheckbox("ENABLE_STAT_APP", "Enable attack per piece stat (for all modes)");
  createCheckbox("ENABLE_STAT_PPD", "Enable pieces per downstack stat (100L cheese pace / 100) (for all modes)");
  createCheckbox("ENABLE_STAT_CHEESE_BLOCK_PACE", "Enable block pace stat for cheese race");
  createCheckbox("ENABLE_STAT_CHEESE_TIME_PACE", "Enable time pace stat for cheese race");
  createCheckbox("ENABLE_STAT_PPB", "Enable points per block stat for ultra");
  createCheckbox("ENABLE_STAT_SCORE_PACE", "Enable score pace for ultra");
  createCheckbox("ENABLE_STAT_PC_NUMBER", "Enable pc number indicator for pc mode");

  createTitle("Misc settings");
  createCheckbox("ENABLE_AUTOMATIC_REPLAY_CODES", "Enable automatic replay code saving on reset");
  createHTML(createKeyInputElement("UNDO_KEYCODE", "keybind to undo moves in practice mode"));
  createCheckbox("ENABLE_CHAT_TIMESTAMPS", "Enable chat timestamps");
  createCheckbox("SHOW_MM_BUTTON", "Show matchmaking button");
  createCheckbox("SHOW_QUEUE_INFO", "Show matchmaking queue info");
  createHTML(createKeyInputElement("TOGGLE_CHAT_KEYCODE", "Open chat with this button"));
  createHTML(createKeyInputElement("CLOSE_CHAT_KEYCODE", "Close chat with this button"));
  createHTML(createKeyInputElement("SCREENSHOT_KEYCODE", "Take a screenshot with this button"));
}

const initModal = () => {


  // modal UI inject
  var modalButton = document.createElement("div");
  modalButton.innerHTML = GEAR_SVG;
  modalButton.className = "settings-modalOpenButton";

  var modalCloseButton = document.createElement("div");
  modalCloseButton.innerHTML = X_SVG;
  modalCloseButton.className = "settings-modalCloseButton"

  modalButton.addEventListener("click", () => {
    if (typeof ($) == "function")
      $(window).trigger('modal-opened');
    modal.style.display = "flex";
  });
  modalCloseButton.addEventListener("click", () => {
    modal.style.display = "none";
  });

  var modal = document.createElement("div");
  modal.className = "settings-modal";

  var modalContent = document.createElement("div");
  modalContent.className = "settings-modal-content";


  var modalHeader = document.createElement("div");
  modalHeader.className = "settings-modal-header";

  var header = document.createElement("h2");
  header.innerHTML = "Jstris+ Settings";

  modalHeader.appendChild(header);
  modalHeader.appendChild(modalCloseButton)

  var modalBody = document.createElement("div");
  modalBody.id = "settingsBody";
  modalBody.className = "settings-modal-body";

  modal.appendChild(modalContent);
  modalContent.appendChild(modalHeader);
  modalContent.appendChild(modalBody);

  document.body.appendChild(modal);
  document.body.appendChild(modalButton);


  generateBody();

}
;// CONCATENATED MODULE: ./src/layout.js


const createBGElement = () => {
  const ele = document.createElement("div");
  ele.id = "JS_PLUS_BG_ELEMENT";
  ele.style = "z-index: -100; position: absolute; height: max(100%, 100vh); width: 100%; top: 0px; left: 0px; background-size: cover;";
  document.body.appendChild(ele)
  return ele;
}

const changeBG = link => {
  console.log("Changing BG to " + link);
  document.body.style.position = "relative"
  var app = document.getElementById("JS_PLUS_BG_ELEMENT") ?? createBGElement();
  app.style.backgroundImage = `url(${link})`;
}

const initLayout = () => {
  changeBG(Config().BACKGROUND_IMAGE_URL)
  Config().onChange("BACKGROUND_IMAGE_URL", val => {
    changeBG(val);
  });
  console.log("Layout loaded.");
}
;// CONCATENATED MODULE: ./src/stats.js



const replaceBadValues = (n, defaultValue) => {
  // NaN check
  if (Number.isNaN(n) || !Number.isFinite(n))
    return defaultValue || 0;
  return n;

}

let stats = [];
const updateStats = function () {

  const index = this.ISGAME? 0 : parseInt(this.v.canvas.parentElement.getAttribute("data-index"));
  stats[index].forEach((stat) => {
    if (stat.enabled && stat.row) {
      if (stat.enabledMode && stat.enabledMode != this.pmode) {
        stat.row.style.display = "none";
        return;
      }
      stat.row.style.display = "table-row";
      var val = stat.calc(this);
      stat.row.children[1].innerHTML = val;
    } else {
      stat.row.style.display = "none";
    }
  });
}
const initStat = (index, name, configVar, calc, options = {}) => {
  stats[index].push({
    name,
    calc,
    val: 0,
    enabled: Config()[configVar],
    initialValue: options.initialValue || 0,
    enabledMode: options.enabledMode || 0, // 0 = enabled for all modes 
  });
  Config().onChange(configVar, val => {
    for (var individualStats of stats)
      var stat = individualStats.find(e => e.name == name);
      stat.enabled = val;
  })
}

const initStats = () => {
  const stages = document.querySelectorAll("#stage");
  stages.forEach((stageEle, i) => {
    stageEle.setAttribute("data-index", i); 
    stats.push([]);
    initGameStats(stageEle, i);
  })

}
const initGameStats = (stageEle, index) => {
  // these must be non-arrow functions so they can be bound
  initStat(index,"APP", "ENABLE_STAT_APP", game => replaceBadValues(game.gamedata.attack / game.placedBlocks).toFixed(3));
  initStat(index,"PPD", "ENABLE_STAT_PPD", game => replaceBadValues(game.placedBlocks / game.gamedata.garbageCleared).toFixed(3));

  initStat(index,"Block pace", "ENABLE_STAT_CHEESE_BLOCK_PACE", game => {
    let totalLines = game.ISGAME ? game["cheeseModes"][game["sprintMode"]] : game.initialLines;
    let linesLeft = game.linesRemaining
    let linesCleared = totalLines - linesLeft
    var piecePace = replaceBadValues((linesLeft / linesCleared) * game["placedBlocks"] + game["placedBlocks"])
    return (piecePace * 0 + 1) ? Math.floor(piecePace) : '0'
  }, { enabledMode: 3 });

  initStat(index,"Time pace", "ENABLE_STAT_CHEESE_TIME_PACE", game => {
    let totalLines = game.ISGAME ? game["cheeseModes"][game["sprintMode"]] : game.initialLines;
    let linesLeft = game.linesRemaining
    let linesCleared = totalLines - linesLeft;
    let time = game.ISGAME? game.clock : game.clock / 1000;
    var seconds = replaceBadValues((totalLines / linesCleared) * time);
    let m = Math.floor(seconds / 60)
    let s = Math.floor(seconds % 60)
    let ms = Math.floor((seconds % 1) * 100)
    return (m ? (m + ":") : '') + ("0" + s).slice(-2) + "." + ("0" + ms).slice(-2)
  }, { enabledMode: 3 });

  initStat(index,"PPB", "ENABLE_STAT_PPB", game => {
    var score = game["gamedata"]["score"];
    var placedBlocks = game["placedBlocks"];
    return replaceBadValues(score / placedBlocks).toFixed(2);
  }, { enabledMode: 5 });

  initStat(index,"Score pace", "ENABLE_STAT_SCORE_PACE", game => {
    var score = game["gamedata"]["score"];
    let time = game.ISGAME? game.clock : game.clock / 1000;
    return replaceBadValues(score + score / time * (120 - time)).toFixed(0);
  }, { enabledMode: 5 });

  initStat(index,"PC #", "ENABLE_STAT_PC_NUMBER", game => {
    let suffixes = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th'];
    let pcs = game.gamedata.PCs;
    
    if (!game.PCdata)
      return "1st";
    var blocks = game.placedBlocks - game.PCdata.blocks;
    let pcNumber = ((pcs + 1 + 3 * ((10 * pcs - blocks) / 5)) % 7) || 7;
    pcNumber = replaceBadValues(pcNumber, 1);
    if (!Number.isInteger(pcNumber))
      return "";
    return pcNumber + suffixes[pcNumber];
  }, { enabledMode: 8 });

  const statsTable = document.createElement("TABLE");
  statsTable.className = 'stats-table'
  //document.getElementById("stage").appendChild(statsTable);
  stageEle.appendChild(statsTable);

  stats[index].forEach(stat => {
    const row = document.createElement('tr');
    row.style.display = "none";
    const name = document.createElement('td');
    name.innerHTML = stat.name;
    const val = document.createElement('td');
    val.className = 'val';
    val.id = `${stat.name}-val`;
    val.innerHTML = stat.val;
    row.appendChild(name);
    row.appendChild(val);
    statsTable.appendChild(row);
    stat.row = row;
  })
  if (typeof Game == "function") {
    let oldQueueBoxFunc = Game.prototype.updateQueueBox;
    Game.prototype.updateQueueBox = function () {
      updateStats.call(this)
      return oldQueueBoxFunc.apply(this, arguments);
    }
  }
  if (typeof Replayer == "function" && typeof Game != "function") {
    let oldQueueBoxFunc = Replayer.prototype.updateQueueBox;
    Replayer.prototype.updateQueueBox = function () {
      updateStats.call(this)      
      return oldQueueBoxFunc.apply(this, arguments);
    }

    let oldCheckLineClears = Replayer.prototype.checkLineClears;
    Replayer.prototype.checkLineClears = function() {
      let val = oldCheckLineClears.apply(this, arguments);
      if (this.PCdata) {
        // empty matrix check
        if (this.matrix.every(row => row.every(cell => cell == 0))) {
          this.PCdata.blocks = 0;
        } else {
          this.PCdata.blocks++;
        }

      }
      return val;
    }

    var oldInitReplay = Replayer.prototype.initReplay;
    Replayer.prototype.initReplay = function () {
      let val = oldInitReplay.apply(this, arguments);
      this.initialLines = this.linesRemaining;
      if (this.pmode == 8)
        this.PCdata = { blocks : 0 }

      return val;
    }
  }

}
;// CONCATENATED MODULE: ./src/sfxLoader.js


const attemptLoadSFX = function () {
    if (typeof loadSFX == "function") {
        loadSFX(...arguments);
    } else {
        setTimeout(() => attemptLoadSFX(...arguments), 200);
    }
}
const loadSound = (name, url) => {
    if (!name || !url) {
        return;
    }
    let ishta = url.url
    if (ishta) {

        let enslee = createjs.Sound.registerSound(ishta, name);
        if (!enslee || !createjs.Sound._idHash[name]) {
            return void console.error("loadSounds error: src parse / cannot init plugins, id=" + name + (false === enslee ? ", rs=false" : ", no _idHash"));
        }
        createjs.Sound._idHash[name].sndObj = url;
    }
};

/*
// functionality is now addressed in replayer-sfx.js
const loadReplayerSFX = function (sfx) {
    let SOUNDS = ["hold", "linefall", "lock", "harddrop", "rotate", "success", "garbage", "b2b", "land", "move", "died", "ready", "go", "golive", "ding", "msg", "fault", "item", "pickup"];
    let SErot = localStorage.getItem("SErot")
    if (!SErot) {
        sfx.rotate = { url: "null.wav" }
    }
    if (sfx.scoring) {
        for (var i = 0; i < sfx.scoring.length; ++i) {
            sfx.scoring[i] && loadSound("s" + i, sfx.scoring[i]);
        }
    }
    if (sfx.b2bScoring && Array.isArray(sfx.b2bScoring)) {
        for (i = 0; i < sfx.b2bScoring.length; ++i) {
            sfx.b2bScoring[i] && loadSound("bs" + i, sfx.b2bScoring[i]);
        }
    }
    if (sfx.spawns) {
        for (var talitha in sfx.spawns) {
            loadSound("b_" + talitha, sfx.spawns[talitha]);
        }
    }
    for (i = 0; i < SOUNDS.length; ++i) {
        let kayley = SOUNDS[i];
        loadSound(kayley, sfx[kayley]);
    }
    if (sfx.comboTones && Array.isArray(sfx.comboTones)) {
        for (i = 0; i < sfx.comboTones.length; ++i) {
            var zohet = sfx.comboTones[i];
            zohet && createjs.Sound.registerSound(sfx.getSoundUrlFromObj(zohet), "c" + i);
        }
        sfx.maxCombo = sfx.comboTones.length - 1;
    } else {
        if (sfx.comboTones) {
            var kisa = [];
            for (i = 0; i < sfx.comboTones.cnt; ++i) {
                kisa.push({ id: "c" + i, startTime: i * (sfx.comboTones.duration + sfx.comboTones.spacing), duration: sfx.comboTones.duration });
            }
            sfx.maxCombo = sfx.comboTones.cnt - 1;
            var kaley = [{ src: sfx.getSoundUrl("comboTones"), data: { audioSprite: kisa } }];
            createjs.Sound.registerSounds(kaley, "");
        }
    }
}
*/

const loadDefaultSFX = () => {
    console.log("loading default sfx")
    try {
        loadSFX(new window.SFXsets[localStorage["SFXset"]].data());
    } catch (e) { // just in case
        console.log("failed loading default sfx: " + e);
    }
    return;
}

const changeSFX = () => {
    var json = Config().CUSTOM_SFX_JSON;
    let sfx = null;

    if (json) {
        try {
            sfx = JSON.parse(json);
            document.getElementById("custom_sfx_json_err").innerHTML = "Loaded " + (sfx.name || "custom sounds");
        } catch (e) {
            console.log("SFX json was invalid.");
            document.getElementById("custom_sfx_json_err").innerHTML = "SFX json is invalid.";
        }
    } else {
        document.getElementById("custom_sfx_json_err").innerHTML = "";
    }
    if (typeof Game == "function") {
        if (!Config().ENABLE_CUSTOM_SFX || !sfx) {
            loadDefaultSFX();
        } else {
            console.log("Changing SFX...");
            console.log(sfx);

            let csfx = loadCustomSFX(sfx);
            attemptLoadSFX(csfx)

        }
    }

    /*
    // functionality here now addressed in replayer-sfx.js
    if (typeof window.View == "function" && typeof window.Live != "function") { //force sfx on replayers
        let onready = View.prototype.onReady
        View.prototype.onReady = function () {
            let val = onready.apply(this, arguments);
            let csfx = loadCustomSFX(sfx)
            this.SFXset = csfx
            //   loadReplayerSFX(csfx)
            //   console.log(this.SFXset)
            return val
        }
    }
    */
}

const initCustomSFX = () => {
    if (!createjs) return


    if (typeof Game == "function") {
        let onnextblock = Game.prototype.getNextBlock
        Game.prototype.getNextBlock = function () {

            if (Config().ENABLE_CUSTOM_VFX) {
                this.playCurrentPieceSound()
            }
            let val = onnextblock.apply(this, arguments)
            return val
        }
        let onholdblock = Game.prototype.holdBlock
        Game.prototype.holdBlock = function () {
            if (Config().ENABLE_CUSTOM_VFX) {
                this.playCurrentPieceSound()
            }
            let val = onholdblock.apply(this, arguments)
            return val
        }
    }

    /*   let onPlay = createjs.Sound.play
       createjs.Sound.play = function () {
           console.log(arguments[0])
           let val = onPlay.apply(this, arguments)
           return val
       }*/
    changeSFX(Config().CUSTOM_SFX_JSON)
    Config().onChange("CUSTOM_SFX_JSON", changeSFX);
    Config().onChange("ENABLE_CUSTOM_SFX", changeSFX);
    Config().onChange("ENABLE_CUSTOM_VFX", changeSFX);
    return true
}

const loadCustomSFX = (sfx = {}) => {
    const SOUNDS = ["hold", "linefall", "lock", "harddrop", "rotate", "success", "garbage", "b2b", "land", "move", "died", "ready", "go", "golive", "ding", "msg", "fault", "item", "pickup"]
    let SCORES = [
        "SOFT_DROP",
        "HARD_DROP",
        "CLEAR1",
        "CLEAR2",
        "CLEAR3",
        "CLEAR4",
        "TSPIN_MINI",
        "TSPIN",
        "TSPIN_MINI_SINGLE",
        "TSPIN_SINGLE",
        "TSPIN_DOUBLE",
        "TSPIN_TRIPLE",
        "PERFECT_CLEAR",
        "COMBO",
        "CLEAR5"
    ]
    function CustomSFXset() {
        this.volume = 1
    }
    CustomSFXset.prototype = new BaseSFXset;
    CustomSFXset.prototype.getSoundUrlFromObj = function (obj) {
        return obj.url
    }

    CustomSFXset.prototype.getClearSFX = function (altClearType, clearType, b2b, combo) {
        let sounds = [],
            prefix = '';
        let specialSound = null
        let override = false
        if (this.specialScoring) {
            let scorings = [this.specialScoring[SCORES[clearType]]]
            if ((clearType > 4 && clearType <= 11) || clearType == 14) {
                if (this.specialScoring.TSPINORTETRIS) {
                    scorings.push(this.specialScoring.TSPINORTETRIS)
                }
            } else if (clearType == 127) {
                if (this.specialScoring.ALLSPIN) {
                    scorings.push(this.specialScoring.ALLSPIN)
                }
            }
            for (let scoring of scorings) {
                if (Array.isArray(scoring)) {

                    let bestFit = { score: 0.5, sound: null, combo: -1 }
                    for (let sfx of scoring) {
                        let score = 0
                        if (sfx.hasOwnProperty("b2b") && sfx.b2b == b2b) {
                            score += 1
                        }
                        if (sfx.hasOwnProperty("combo") && sfx.combo <= combo) {
                            score += 1
                        }
                        if (bestFit.score < score) {
                            override = sfx.override
                            bestFit = { score: score, sound: sfx.name, combo: combo }
                        } else if (bestFit.score == score) {
                            if (sfx.combo && combo > bestFit.combo) {
                                override = sfx.override
                                bestFit = { score: score, sound: sfx.name, combo: combo }
                            }
                        }
                    }
                    if (bestFit.sound != null) {
                        specialSound = bestFit.sound
                        sounds.push(specialSound)
                    }
                }
            }

            if (this.specialScoring.ANY) {
                let bestFit = { score: 0, sound: null, combo: -1 }

                for (let sfx of this.specialScoring.ANY) {
                    let score = 0
                    if (sfx.hasOwnProperty("b2b")) {
                        if (sfx.b2b == b2b) score += 1
                        else continue
                    }

                    if (sfx.hasOwnProperty("combo")) {
                        if (sfx.combo <= combo) score += 1
                        else continue
                    }
                    if (bestFit.score < score) {
                        override = sfx.override
                        bestFit = { score: score, sound: sfx.name, combo: combo }
                    } else if (bestFit.score == score) {
                        if (sfx.combo && combo > bestFit.combo) {
                            override = sfx.override
                            bestFit = { score: score, sound: sfx.name, combo: combo }
                        }
                    }
                }
                if (bestFit.sound != null) {
                    specialSound = bestFit.sound
                    sounds.push(specialSound)
                }
            }

        }
        if (sfx.hasOwnProperty("b2b") && b2b) {
            sounds.push('b2b')
        }
        if (combo >= 0) {
            sounds.push(this.getComboSFX(combo))
        }
        if (this.scoring && (!specialSound || override == false)) {
            sounds.push(prefix + this.getScoreSFX(clearType))
        }
        if (altClearType == Score.A.PERFECT_CLEAR) {
            sounds.push(prefix + this.getScoreSFX(altClearType))
        }
        //   console.log(sounds)
        return sounds
    }
    let customSFX = new CustomSFXset

    /*    function CustomVFXset() {
            this.volume = 1
        }
        CustomVFXset.prototype = new NullSFXset
        CustomVFXset.prototype.getSoundUrlFromObj = function (obj) {
            return obj.url
        }
        let customVFX = new CustomVFXset*/

    for (let name of SOUNDS) {
        if (sfx.hasOwnProperty(name)) {
            customSFX[name] = {
                url: sfx[name],
            }
        } else {
            customSFX[name] = {
                url: "null.wav",
            }
        }
    }
    if (sfx.comboTones) {
        if (Array.isArray(sfx.comboTones)) {
            customSFX.comboTones = []
            for (let tone of sfx.comboTones) {
                if (typeof tone === 'string') {
                    customSFX.comboTones.push({ url: tone })
                } else {
                    customSFX.comboTones.push({ url: "null.wav" })
                }
            }
        } else if (typeof sfx.comboTones == "object") {
            if (sfx.comboTones.duration && sfx.comboTones.spacing && sfx.comboTones.cnt) {
                customSFX.comboTones = {
                    url: sfx.comboTones.url,
                    duration: sfx.comboTones.duration,
                    spacing: sfx.comboTones.spacing,
                    cnt: sfx.comboTones.cnt,
                }
            }
        }
    }
    if (sfx.specialScoring && typeof sfx.specialScoring == "object") {
        for (let key in sfx.specialScoring) {
            if (!Array.isArray(sfx.specialScoring[key])) continue
            for (let i in sfx.specialScoring[key]) {
                let sound = sfx.specialScoring[key][i]
                sound.name = "CUSTOMSFX" + key + i
                loadSound(sound.name, sound)
            }
        }
        customSFX.specialScoring = sfx.specialScoring
    }
    if (sfx.scoring && typeof sfx.scoring == "object") {
        customSFX.scoring = Array(15)

        for (let key in sfx.scoring) {
            let i = SCORES.indexOf(key)
            if (i < 0) continue
            customSFX.scoring[i] = { url: sfx.scoring[key] }
        }
    }
    if (sfx.spawns && typeof sfx.spawns == "object") {
        let scores = [
            "I", "O", "T", "L", "J", "S", "Z"
        ]
        for (let key in sfx.spawns) {
            let i = scores.indexOf(key)
            if (i > 0) {
                loadSound("b_" + key, { url: sfx.spawns[key] })
            }

        }
    } else {
        let scores = [
            "I", "O", "T", "L", "J", "S", "Z"
        ]
        for (var key of scores) {
            loadSound("b_" + key, { url: "null.wav" })
        }
    }
    return customSFX
    //    attemptLoadSFX(customSFX);

}

;// CONCATENATED MODULE: ./src/replayer-sfx.js




const initReplayerSFX = () => {
  
  if (typeof View == "function" && typeof window.Live != "function" && !location.href.includes('export'))
    initCustomReplaySFX();
  if (typeof SlotView == "function")
    initOpponentSFX();
}

const initCustomReplaySFX = () => {
  console.log("init replayer sfx")
  var json = Config().CUSTOM_SFX_JSON;
  let sfx = null;
  if (json) {
      try {
          sfx = JSON.parse(json);
          document.getElementById("custom_sfx_json_err").innerHTML = "Loaded " + (sfx.name || "custom sounds");
      } catch (e) {
          console.log("SFX json was invalid.");
          document.getElementById("custom_sfx_json_err").innerHTML = "SFX json is invalid.";
      }
  } else {
      document.getElementById("custom_sfx_json_err").innerHTML = "";
  }
  
  if (!Config().ENABLE_CUSTOM_SFX || !Config().CUSTOM_SFX_JSON) {
    return;
  }

  let customSFXSet = loadCustomSFX(sfx);
  console.log(customSFXSet);
  const oldOnReady = View.prototype.onReady
  View.prototype.onReady = function() {
    this.changeSFX(customSFXSet);
    return oldOnReady.apply(this, arguments);
  }

  // spectator replayer sfx

  View.prototype.onLinesCleared = function(attack, comboAttack, { type, b2b, cmb }) {

    let suhrit = [type, type, b2b && this.g.isBack2Back, cmb];
    var sounds = this.SFXset.getClearSFX(...suhrit);

    if (Array.isArray(sounds))
      sounds.forEach(sound => this.SEenabled && createjs.Sound.play(sound));
    else
      this.playReplayerSound(sounds);

    // --- old onLinesCleared code ---
    
    // don't need this line anymore
    // this.SEenabled && createjs.Sound.play(this.SFXset.getComboSFX(this.g.comboCounter));
    this.g.pmode && (7 === this.g.pmode ? this.lrem.textContent = this.g.gamedata.TSD : 8 === this.g.pmode ? this.lrem.textContent = this.g.gamedata.PCs : 5 !== this.g.pmode && (this.lrem.textContent = this.g.linesRemaining));
  
  }
}
const initOpponentSFX = () => {
  // spectator replayer sfx

  console.log("init opponent sfx");
  SlotView.prototype.playReplayerSound = function(sound) {
    let volume = Config().OPPONENT_SFX_VOLUME_MULTPLIER || 0;

    if (!shouldRenderEffectsOnView(this)) {
      volume /= 4;
    }
    let enabled = !!localStorage.getItem("SE") && Config().ENABLE_OPPONENT_SFX;
    if (enabled) {
      if (Array.isArray(sound)) {
        sound.forEach(e => {
          let instance = createjs.Sound.play(e);
          instance.volume = volume;
        });
      } else {
        var instance = createjs.Sound.play(sound);
        instance.volume = volume;
      }
    }
      
  }
  const onBlockHold = SlotView.prototype.onBlockHold;
  SlotView.prototype.onBlockHold = function() {
    this.playReplayerSound("hold");
    onBlockHold.apply(this, arguments);
  }

  const onBlockMove = SlotView.prototype.onBlockMove;
  SlotView.prototype.onBlockMove = function() {
    this.playReplayerSound("move");
    onBlockMove.apply(this, arguments);
  }
  const onGameOver = SlotView.prototype.onGameOver;
  SlotView.prototype.onGameOver = function() {
    if (this.g.queue.length !== 0) // ignore bugged top outs from static queues ending in map vs. change this when jez fixes that
      this.playReplayerSound("died");
    onGameOver.apply(this, arguments);
  }
  const onBlockLocked = SlotView.prototype.onBlockLocked;
  SlotView.prototype.onBlockLocked = function() {
    this.playReplayerSound("lock");
    onBlockLocked.apply(this, arguments);
  }
  const onLinesCleared = SlotView.prototype.onLinesCleared;
  SlotView.prototype.onLinesCleared = function(attack, comboAttack, { type, b2b, cmb }) {

    let game = this.slot.gs.p;
    let suhrit = [type, type, b2b && this.g.isBack2Back, cmb];
    var sounds = game.SFXset.getClearSFX(...suhrit);

    if (Array.isArray(sounds))
      sounds.forEach(sound => this.playReplayerSound(sound));
    else
      this.playReplayerSound(sounds);

    onLinesCleared.apply(this, arguments);
  }
  if (typeof Game == "function") {
    const oldReadyGo = Game.prototype.readyGo;
    
    // bot sfx
    Game.prototype.readyGo = function() {
      let val = oldReadyGo.apply(this, arguments)
      console.log("injected bot sfx")
      if (this.Bots && this.Bots.bots) {
        this.Bots.bots.forEach(e => {
          if (e.g) {
            e.g.SFXset = this.SFXset;
            e.g.playSound = (a) => {
              if (a) {
                SlotView.prototype.playReplayerSound(a)
              }
            }
            let oldOnBotMove = e.__proto__.onBotMove;
            e.__proto__.onBotMove = function() {
              let val = oldOnBotMove.apply(this, arguments);
              SlotView.prototype.playReplayerSound("harddrop");
              return val;
            }
            let oldOnBotGameOver = e.__proto__.onGameOver;
            e.__proto__.onGameOver = function() {
              let val = oldOnBotGameOver.apply(this, arguments);
              // when you restart the game, all the bots get gameovered
              if (!e.p.p.gameEnded)
                SlotView.prototype.playReplayerSound("died");
              return val;
            }
          }
        });
      }

      return val;
    }

  }

  // replay replayer sfx
  
}
;// CONCATENATED MODULE: ./src/keyboardDisplay.js

const initKeyboardDisplay = () => {
  const isGame = typeof Game != "undefined";
  const isReplayer = typeof Replayer != "undefined";

  if (!isGame && !isReplayer) return;

  const keyConfig = [
    [
      'new',
      null,
      {k: 'reset', l: 'F4'},
    ],
    [
      null
    ],
    [
      '180',
      'ccw',
      'cw',
      null,
      null,
      'hd',
    ],
    [
      null,
      null,
      {k: 'hold', l: 'HLD'},
      null,
      {k: 'left', l: 'L'},
      'sd',
      {k: 'right', l: 'R'}
    ]
  ];

  var kbhold = document.createElement("div");
  kbhold.id = "keyboardHolder";

  if (!Config().ENABLE_KEYBOARD_DISPLAY)
    kbhold.classList.add('hide-kbd-display')
  Config().onChange("ENABLE_KEYBOARD_DISPLAY", val => {
    if (val) {
      kbhold.classList.remove('hide-kbd-display')
    } else {
      kbhold.classList.add('hide-kbd-display')
    }
  })
  document.getElementById("stage").appendChild(kbhold);
  
  

  let keyTable = `
    <div id="kbo">
      <div id="kps"></div>
      <table class="tg">
  `;

  for (const row of keyConfig) {
    keyTable += `<tr>`;

    for (const key of row) {
      let isKey = key != null;
      let label = "";
      let cssClass = "kbnone";

      if (isKey) {
        label = typeof key == 'string'? key.toUpperCase() : key.l;
        cssClass = "kbkey kbd-" + (typeof key == 'string'? key.toLowerCase() : key.k);
      }

      keyTable += `<td class="${cssClass}">${label}</td>`;
    }

    keyTable += `</tr>`;
  }

  keyTable += `
      </table>
    </div>
  `;

  keyboardHolder.innerHTML = keyTable;
  let setKey = function(key, type) {
    for (const td of document.getElementsByClassName(`kbd-${key}`)) {
      td.style.backgroundColor = ["", "lightgoldenrodyellow"][type];
    }
  }

  if (isGame) {
    let oldReadyGo = Game.prototype.readyGo;
    Game.prototype.readyGo = function() {
      Game['set2ings'] = this.Settings.controls;
      return oldReadyGo.apply(this, arguments);
    }

    let oldUpdateTextBar = Game.prototype.updateTextBar;
    Game.prototype.updateTextBar = function() {
      let val = oldUpdateTextBar.apply(this, arguments);
      kps.innerHTML = 'KPS: ' + (this.getKPP() * this.placedBlocks / this.clock).toFixed(2);
      return val;
    }

    let press = function (e) {
      if (typeof Game.set2ings == 'undefined') return;

      let i = Game.set2ings.indexOf(e.keyCode);
      if (i == -1) return;

      let key = ['left', 'right', 'sd', 'hd', 'ccw', 'cw', 'hold', '180', 'reset', 'new'][i];
      setKey(key, +(e.type == "keydown"))
    }

    document.addEventListener('keydown', press);
    document.addEventListener('keyup', press);

  } else if (isReplayer) {
    var url = window.location.href.split("/")

    if (!url[2].endsWith("jstris.jezevec10.com")) return;
    if (url[3] != "replay") return;
    if (url[4] == "1v1") {
      kbhold.classList.add("really-hide-kbd-display");
      return;
    }

    let L;

    let fetchURL = "https://"+url[2]+"/replay/data?id="+url[(L=url[4]=="live")+4]+"&type="+(L?1:0);
    /*
    if(url[4] == "live"){
      fetchURL = "https://"+url[2]+"/replay/data?id=" + url[5] + "&type=1"
    } else {
      fetchURL = "https://"+url[2]+"/replay/data?id=" + url[4] + "&type=0"
    }
    */

    //fetch(`https://${url[2]}/replay/data?id=${url.length == 6? (url[5] + "&live=1") : url[4]}&type=0`)
    fetch(fetchURL)
      .then(res => res.json())
      .then(json => {
        if (!json.c)
          return;
        let das = json.c.das;

        Replayer.setKey = setKey;

        let oldPlayUntilTime = Replayer.prototype.playUntilTime
        Replayer.prototype.playUntilTime = function() {
          
          kps.innerHTML = 'KPS: ' + (this.getKPP() * this.placedBlocks / this.clock * 1000).toFixed(2);

          if (this.ptr == 0) Replayer.lastPtr = -1;

          this.kbdActions = [];

          for (let i = 0; i < this.actions.length; i++) {
            let o = {a: this.actions[i].a, t: this.actions[i].t};

            if (o.a == 2 || o.a == 3) {
              o.a -= 2;
              for (let j = i - 1; j >= 0; j--) {
                if (this.kbdActions[j].a < 2) {
                  this.kbdActions[j].a += 2;
                  break;
                }
              }
            }

            this.kbdActions.push(o);
          }

          let pressKey = function(key, type) {
            Replayer.setKey(key, Math.min(type, 1));

            if (type == 2) {
              setTimeout(x => Replayer.setKey(key, 0), das * 3 / 5)
            }
          };
          
          let val = oldPlayUntilTime.apply(this, arguments);
          
          if (this.ptr != Replayer.lastPtr && this.ptr - 1 < this.kbdActions.length) {
            var highlight = [
              ["left", 2],
              ["right", 2],
              ["left", 1],
              ["right", 1],
              ["ccw", 2],
              ["cw", 2],
              ["180", 2],
              ["hd", 2],
              ["sd", 2],
              null,
              ["hold", 2]
            ][this.kbdActions[this.ptr - 1].a];

            if (highlight) {
              pressKey(...highlight)
            }
          }

          Replayer.lastPtr = this.ptr;

          return val;
        };
      });
  }
}
;// CONCATENATED MODULE: ./src/skin.js

let offscreenCanvas = document.createElement('canvas');
let offscreenContext = offscreenCanvas.getContext("2d");
offscreenCanvas.height = 32;
offscreenCanvas.width = 32;
let customSkinSize = 32
let customGhostSkinSize = 32
let usingConnected = false
let usingGhostConnected = false
function loadCustomSkin(url, ghost = false) {

  // if not allowing force replay skin, don't load custom skin
  if (location.href.includes('replay') && !Config().ENABLE_REPLAY_SKIN) {
    return;
  }

  let img = new Image();
  console.log(url, ghost)
  img.onload = function () {
    var height = img.height;
    var width = img.width;
    if (width / height == 9 && !ghost) {
      customSkinSize = height
      usingConnected = false
      if (window.loadSkin) loadSkin(url, customSkinSize)
    } else if (width / height == 9 / 20 && !ghost) {
      usingConnected = true
      customSkinSize = width / 9
      if (window.loadSkin) loadSkin(url, customSkinSize)
    } else if (width / height == 7 && ghost) {
      usingGhostConnected = false
      customGhostSkinSize = height
      if (window.loadGhostSkin) loadGhostSkin(url, height)
    } else if (width / height == 7 / 20 && ghost) {
      offscreenCanvas.height = width / 7;
      offscreenCanvas.width = width / 7;
      usingGhostConnected = true
      customGhostSkinSize = width / 7
      if (window.loadSkin) loadGhostSkin(url, width / 7)
    }
  }
  img.src = url;

}
window.loadCustomSkin = loadCustomSkin

const initCustomSkin = () => {
  initConnectedSkins()
  let skinLoaded = false
  let game = null
  if (Config().CUSTOM_SKIN_URL)
    loadCustomSkin(Config().CUSTOM_SKIN_URL);

  if (Config().CUSTOM_GHOST_SKIN_URL)
    loadCustomSkin(Config().CUSTOM_GHOST_SKIN_URL, true)
  if (typeof window.Live == "function") {
    Config().onChange("CUSTOM_SKIN_URL", val => {
      if (val)
        loadCustomSkin(val);
      else {
        loadSkin("resetRegular")
      }
    });
    Config().onChange("CUSTOM_GHOST_SKIN_URL", val => {
      if (val) loadCustomSkin(val, true)
      else if (game) {
        game.ghostSkinId = 0
        usingGhostConnected = false
      }
    });
    let onload = Live.prototype.onCIDassigned

    Live.prototype.onCIDassigned = function () {
      let v = onload.apply(this, arguments)

      if (!skinLoaded) {
        game = this.p
        skinLoaded = true
        if (Config().CUSTOM_SKIN_URL)
          loadCustomSkin(Config().CUSTOM_SKIN_URL);

        if (Config().CUSTOM_GHOST_SKIN_URL)
          loadCustomSkin(Config().CUSTOM_GHOST_SKIN_URL, true)
      }

      return v
    }
  }
  if (typeof window.View == "function" && typeof window.Live != "function") { //force skin on replayers
    let onready = View.prototype.onReady
    View.prototype.onReady = function () {
      let val = onready.apply(this, arguments);
      if (Config().ENABLE_REPLAY_SKIN && Config().CUSTOM_SKIN_URL) {
        this.tex.crossOrigin = "anonymous"
        this.skinId = 1
        this.g.skins[1].data = Config().CUSTOM_SKIN_URL
        this.g.skins[1].w = customSkinSize
        this.tex.src = this.g.skins[1].data
      }
      return val
    }
  }
  if (typeof window.Game == "function") {
    let ls = Game.prototype.changeSkin
    Game.prototype.changeSkin = function () {
      let val = ls.apply(this, arguments)
      let url = this.skins[arguments[0]].data
      if (url == "resetRegular") {
        usingConnected = false
        ls.apply(this, [0])
        return val
      }
      if (this.v && this.v.NAME == "webGL") {
        this.v.ai_setBlend()
      }
      return val
    }
  }
  console.log("Custom skin loaded.");

}

const initConnectedSkins = () => {
  const removeDimple = true
  const ghostAlpha = 0.5
  //  const blockConnections = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  const blockConnections = [-1, 1, 1, 1, 1, 1, 1, 1, 2, 3]


  let colors = blockConnections
  function solveConnected(blocks, x, y) {
    let connect_value = 0
    let checks = { N: false, S: false, E: false, W: false }
    let row = y
    let col = x
    if (row != 0 && blocks[row - 1][col] > 0) { connect_value += 1; checks.N = true }
    if (row != blocks.length - 1 && blocks[row + 1][col] > 0) { connect_value += 2; checks.S = true }
    if (blocks[row][col - 1] > 0) { connect_value += 4; checks.W = true; }
    if (blocks[row][col + 1] > 0) { connect_value += 8; checks.E = true; }
    let corners = { a: false, b: false, c: false, d: false }

    if (checks.N && checks.E && row != 0 && blocks[row - 1][col + 1] > 0) corners.a = true
    if (checks.S && checks.E && blocks[row + 1][col + 1] > 0) corners.b = true
    if (checks.S && checks.W && blocks[row + 1][col - 1] > 0) corners.c = true
    if (checks.N && checks.W && row != 0 && blocks[row - 1][col - 1] > 0) corners.d = true
    let overlay = 0
    if (corners.a) overlay = 16
    if (corners.b) overlay = 17
    if (corners.c) overlay = 18
    if (corners.d) overlay = 19
    return { connect_value: connect_value, overlay: overlay }
  }

  let drawCanvas = false

  if (window.WebGLView != undefined) {
    let onRedrawMatrix = WebGLView['prototype']['redrawMatrix']
    WebGLView['prototype']['redrawMatrix'] = function () {
      if (usingConnected) {
        this['clearMainCanvas']();
        if (this['g']['isInvisibleSkin']) {
          return
        };
        this.g.ai_drawMatrix()
        return
      }
      let val = onRedrawMatrix.apply(this, arguments)
      return val
    }
    let onWebglLoad = WebGLView.prototype.initRenderer
    WebGLView.prototype.initRenderer = function () {
      let val = onWebglLoad.apply(this, arguments)
      this.ai_setBlend()
      return val
    }
    WebGLView.prototype.ai_setBlend = function () {
      for (let ctx of this.ctxs) {
        let gl = ctx.gl
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
      }
    }
    WebGLView['prototype']['ai_drawBlock'] = function (pos_x, pos_y, block_value, connect_value, main) {
      if (block_value) {
        let skin = this.g.skins[this.g.skinId]
        let scale = this['g']['drawScale'] * this['g']['block_size'];
        let cmain = this['ctxs'][main],
          texture = cmain['textureInfos'][0];

        this['drawImage'](cmain, texture['texture'], texture['width'], texture['height'], this['g']['coffset'][block_value] * skin.w, connect_value * skin.w, skin.w, skin.w, pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], scale, scale)
      }
    };
    WebGLView['prototype']['ai_drawGhostBlock'] = function (pos_x, pos_y, block_value, connect_value) {
      let skinSize = this.g.skins[this.g.skinId].w
      var cmain = this['ctxs'][0];
      if (this['g']['ghostSkinId'] === 0) {
        cmain['gl']['uniform1f'](cmain['globalAlpha'], 0.5);
        this['ai_drawBlock'](pos_x, pos_y, block_value, connect_value, 0);
        cmain['gl']['uniform1f'](cmain['globalAlpha'], 1)
      } else {
        var scale = this['g']['drawScale'] * this['g']['block_size'];
        var texture = cmain['textureInfos'][1];
        this['drawImage'](cmain, texture['texture'], texture['width'], texture['height'], (this['g']['coffset'][block_value] - 2) * skinSize, connect_value * skinSize, skinSize, skinSize, pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], scale, scale)
      }

    };
    WebGLView['prototype']['ai_drawBlockOnCanvas'] = function (a, b, c, d, e) {
      this['ai_drawBlock'](a, b, c, d, e)
    };
  }
  if (window.Ctx2DView != undefined) {
    let onRedrawMatrix = Ctx2DView['prototype']['redrawMatrix']
    Ctx2DView['prototype']['redrawMatrix'] = function () {
      if (usingConnected) {
        this['clearMainCanvas']();
        if (this['g']['isInvisibleSkin']) {
          return
        };
        this.g.ai_drawMatrix()
        return
      }
      let val = onRedrawMatrix.apply(this, arguments)
      return val
    }
    Ctx2DView['prototype']['ai_drawBlock'] = function (pos_x, pos_y, block_value, connect_value) {
      if (block_value && pos_x >= 0 && pos_y >= 0 && pos_x < 10 && pos_y < 20) {
        var scale = this['g']['drawScale'] * this['g']['block_size'];
        if (this['g']['skinId']) {
          this['ctx']['drawImage'](this['g']['tex'], this['g']['coffset'][block_value] * this['g']['skins'][this['g']['skinId']]['w'], connect_value * this['g']['skins'][this['g']['skinId']]['w'], this['g']['skins'][this['g']['skinId']]['w'], this['g']['skins'][this['g']['skinId']]['w'], pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], scale, scale)
        } else {
          var mono = (this['g']['monochromeSkin'] && block_value <= 7) ? this['g']['monochromeSkin'] : this['g']['colors'][block_value];
          this['drawRectangle'](this['ctx'], pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], scale, scale, mono)
        }
      }
    };
    Ctx2DView['prototype']['ai_drawGhostBlock'] = function (pos_x, pos_y, block_value, connect_value) {
      let scale = this['g']['drawScale'] * this['g']['block_size'];
      let skin = this.g.ghostSkins[this.g.ghostSkinId]
      let tex = this.g.ghostTex
      let coffset = this.g.coffset[block_value] - 2
      if (this.g.ghostSkinId === 0) {
        this['ctx']['globalAlpha'] = ghostAlpha;
        skin = this.g.skins[this.g.skinId]
        tex = this.g.tex
        coffset += 2
      }
      offscreenContext.drawImage(tex, coffset * skin.w, connect_value * skin.w, skin.w, skin.w, 0, 0, skin.w, skin.w)

      if (drawCanvas) {
        this.ctx.drawImage(offscreenCanvas, 0, 0, skin.w, skin.w, pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], scale, scale)
      }
      this['ctx']['globalAlpha'] = 1
    }
    Ctx2DView['prototype']['ai_drawBlockOnCanvas'] = function (pos_x, pos_y, block_value, connect_value, render) {
      var renderer = (render === this['HOLD']) ? this['hctx'] : this['qctx'];
      if (this['g']['skinId'] === 0) {
        var mono = (this['g']['monochromeSkin'] && block_value <= 7) ? this['g']['monochromeSkin'] : this['g']['colors'][block_value];
        this['drawRectangle'](renderer, pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], this['g']['block_size'], this['g']['block_size'], mono)
      } else {
        renderer['drawImage'](this['g']['tex'], this['g']['coffset'][block_value] * this['g']['skins'][this['g']['skinId']]['w'], connect_value * this['g']['skins'][this['g']['skinId']]['w'], this['g']['skins'][this['g']['skinId']]['w'], this['g']['skins'][this['g']['skinId']]['w'], pos_x * this['g']['block_size'], pos_y * this['g']['block_size'], this['g']['block_size'], this['g']['block_size'])
      }
    };

  };
  let template1 = function () {
    let blockset = this['blockSets'][this['activeBlock']['set']],
      blocks = (blockset['scale'] === 1) ? blockset['blocks'][this['activeBlock']['id']]['blocks'][this['activeBlock']['rot']] : blockset['previewAs']['blocks'][this['activeBlock']['id']]['blocks'][this['activeBlock']['rot']],
      blocks_length = blocks['length'];
    this['drawScale'] = blockset['scale'];
    if (this['ghostEnabled'] && !this['gameEnded']) {
      for (let y = 0; y < blocks_length; y++) {
        for (let x = 0; x < blocks_length; x++) {
          if (blocks[y][x] > 0) {
            if (!usingGhostConnected && this.ghostSkinId != 0) {
              this.v.drawGhostBlock(this.ghostPiece.pos.x + x * this.drawScale, this.ghostPiece.pos.y + y * this.drawScale, blockset.blocks[this.activeBlock.id].color)
              if (this.activeBlock.item && blocks[y][x] === this.activeBlock.item) {
                this.v.drawBrickOverlay(this.ghostPiece.pos.x + x * this.drawScale, this.ghostPiece.pos.y + y * this.drawScale, true)
              }
              continue
            }
            let solve = solveConnected(blocks, x, y)
            offscreenContext.clearRect(0, 0, this.skins[this.skinId].w, this.skins[this.skinId].w)
            if (solve.overlay > 0 && removeDimple) {
              drawCanvas = false
              this['v']['ai_drawGhostBlock'](this['ghostPiece']['pos']['x'] + x * this['drawScale'], this['ghostPiece']['pos']['y'] + y * this['drawScale'], blockset['blocks'][this['activeBlock']['id']]['color'], solve.connect_value, 0);
              if (this['activeBlock']['item'] && blocks[y][x] === this['activeBlock']['item']) {
                this['v']['drawBrickOverlay'](this['ghostPiece']['pos']['x'] + x * this['drawScale'], this['ghostPiece']['pos']['y'] + y * this['drawScale'], true)
              }
              drawCanvas = true
              this['v']['ai_drawGhostBlock'](this['ghostPiece']['pos']['x'] + x * this['drawScale'], this['ghostPiece']['pos']['y'] + y * this['drawScale'], blockset['blocks'][this['activeBlock']['id']]['color'], solve.overlay, 0)
            }
            else {
              drawCanvas = true
              this['v']['ai_drawGhostBlock'](this['ghostPiece']['pos']['x'] + x * this['drawScale'], this['ghostPiece']['pos']['y'] + y * this['drawScale'], blockset['blocks'][this['activeBlock']['id']]['color'], solve.connect_value, 0);

              if (this['activeBlock']['item'] && blocks[y][x] === this['activeBlock']['item']) {
                this['v']['drawBrickOverlay'](this['ghostPiece']['pos']['x'] + x * this['drawScale'], this['ghostPiece']['pos']['y'] + y * this['drawScale'], true)
              }
            }
          }
        }
      }
    };
    if (!this['gameEnded']) {
      for (let y = 0; y < blocks_length; y++) {
        for (let x = 0; x < blocks_length; x++) {
          if (blocks[y][x] > 0) {

            if (!usingConnected) {
              this.v.drawBlock(this.activeBlock.pos.x + x * this.drawScale, this.activeBlock.pos.y + y * this.drawScale, blockset.blocks[this.activeBlock.id].color, 0)
              if (this['activeBlock']['item'] && blocks[y][x] === this['activeBlock']['item']) {
                this['v']['drawBrickOverlay'](this['activeBlock']['pos']['x'] + x * this['drawScale'], this['activeBlock']['pos']['y'] + y * this['drawScale'], false)
              }
              continue
            }
            let solve = solveConnected(blocks, x, y)
            this['v']['ai_drawBlock'](this['activeBlock']['pos']['x'] + x * this['drawScale'], this['activeBlock']['pos']['y'] + y * this['drawScale'], blockset['blocks'][this['activeBlock']['id']]['color'], solve.connect_value, 0);
            if (this['activeBlock']['item'] && blocks[y][x] === this['activeBlock']['item']) {
              this['v']['drawBrickOverlay'](this['activeBlock']['pos']['x'] + x * this['drawScale'], this['activeBlock']['pos']['y'] + y * this['drawScale'], false)
            }
            if (solve.overlay > 0 && removeDimple) this['v']['ai_drawBlock'](this['activeBlock']['pos']['x'] + x * this['drawScale'], this['activeBlock']['pos']['y'] + y * this['drawScale'], blockset['blocks'][this['activeBlock']['id']]['color'], solve.overlay, 0);
          }
        }
      }
    };
    this['drawScale'] = 1
  };
  let template2 = function () {
    if (this['ISGAME'] && this['redrawBlocked']) {
      return
    };
    if (!this['ISGAME'] && (this['v']['redrawBlocked'] || !this['v']['QueueHoldEnabled'])) {
      return
    };
    this['v']['clearHoldCanvas']();
    if (this['blockInHold'] !== null) {
      var currSet = this['blockSets'][this['blockInHold']['set']]['previewAs'],
        blocks = currSet['blocks'][this['blockInHold']['id']]['blocks'][0],
        currColor = currSet['blocks'][this['blockInHold']['id']]['color'],
        currWeird = (!currSet['equidist']) ? currSet['blocks'][this['blockInHold']['id']]['yp'] : [0, 3],
        blocks_length = blocks['length'],
        something = (currSet['blocks'][this['blockInHold']['id']]['xp']) ? currSet['blocks'][this['blockInHold']['id']]['xp'] : [0, blocks_length - 1];
      for (var y = currWeird[0]; y <= currWeird[1]; y++) {
        for (var x = something[0]; x <= something[1]; x++) {
          if (blocks[y][x] > 0) {
            let solve = solveConnected(blocks, x, y)
            this['v']['ai_drawBlockOnCanvas'](x - something[0], y - currWeird[0], currColor, solve.connect_value, this['v'].HOLD);
            if (this['blockInHold']['item'] && blocks[y][x] === this['blockInHold']['item']) {
              this['v']['drawBrickOverlayOnCanvas'](x - something[0], y - currWeird[0], this['v'].HOLD)
            }
            if (solve.overlay > 0 && removeDimple) this['v']['ai_drawBlockOnCanvas'](x - something[0], y - currWeird[0], currColor, solve.overlay, this['v'].HOLD);
          }
        }
      }
    }
  };
  let template3 = function () {
    for (var row = 0; row < 20; row++) {
      for (var col = 0; col < 10; col++) {
        let block_value = this['matrix'][row][col]
        if (!block_value) continue
        let block_color = block_value

        block_value = colors[block_value]

        let connect_value = 0
        let checks = { N: false, S: false, E: false, W: false }

        if (row == 0) { if (colors[this.deadline[col]] == block_value) { connect_value += 1; checks.N = true } }
        else if (colors[this.matrix[row - 1][col]] == block_value) { connect_value += 1; checks.N = true }
        if (row != 19 && colors[this.matrix[row + 1][col]] == block_value) { connect_value += 2; checks.S = true }
        if (colors[this.matrix[row][col - 1]] == block_value) { connect_value += 4; checks.W = true; }
        if (colors[this.matrix[row][col + 1]] == block_value) { connect_value += 8; checks.E = true; }
        let corners = { a: false, b: false, c: false, d: false }

        if (checks.N && checks.E) { if (row == 0) { if (colors[this.deadline[col + 1]] == block_value) corners.a = true } else if (colors[this.matrix[row - 1][col + 1]] == block_value) corners.a = true }
        if (checks.S && checks.E && colors[this.matrix[row + 1][col + 1]] == block_value) corners.b = true
        if (checks.S && checks.W && colors[this.matrix[row + 1][col - 1]] == block_value) corners.c = true
        if (checks.N && checks.W) { if (row == 0) { if (colors[this.deadline[col - 1]] == block_value) corners.d = true } else if (colors[this.matrix[row - 1][col - 1]] == block_value) corners.d = true }

        this['v']['ai_drawBlock'](col, row, block_color, connect_value, this.v.MAIN)

        if (!removeDimple) continue
        if (corners.a) this['v']['ai_drawBlock'](col, row, block_color, 16, this.v.MAIN)
        if (corners.b) this['v']['ai_drawBlock'](col, row, block_color, 17, this.v.MAIN)
        if (corners.c) this['v']['ai_drawBlock'](col, row, block_color, 18, this.v.MAIN)
        if (corners.d) this['v']['ai_drawBlock'](col, row, block_color, 19, this.v.MAIN)
      }
    }
  }
  let template4 = function () {
    if (this['ISGAME'] && this['redrawBlocked']) {
      return
    } else {
      if (!this['ISGAME'] && (this['v']['redrawBlocked'] || !this['v']['QueueHoldEnabled'])) {
        return
      }
    };
    this['v']['clearQueueCanvas']();
    let plug = 0;
    for (var count = 0; count < this['R']['showPreviews']; count++) {
      if (count >= this['queue']['length']) {
        if (this['pmode'] != 9) {
          break
        };
        if (this['ModeManager']['repeatQueue']) {
          this['ModeManager']['addStaticQueueToQueue']()
        } else {
          break
        }
      };
      var currPiece = this['queue'][count];
      var currSet = this['blockSets'][currPiece['set']]['previewAs'],
        blocks = currSet['blocks'][currPiece['id']]['blocks'][0],
        currColor = currSet['blocks'][currPiece['id']]['color'],
        currWeird = (!currSet['equidist']) ? currSet['blocks'][currPiece['id']]['yp'] : [0, 3],
        blocks_length = blocks['length'],
        something = (currSet['blocks'][currPiece['id']]['xp']) ? currSet['blocks'][currPiece['id']]['xp'] : [0, blocks_length - 1];
      for (var y = currWeird[0]; y <= currWeird[1]; y++) {
        for (var x = something[0]; x <= something[1]; x++) {
          if (blocks[y][x] > 0) {
            let solve = solveConnected(blocks, x, y)
            this['v']['ai_drawBlockOnCanvas'](x - something[0], y - currWeird[0] + plug, currColor, solve.connect_value, this['v'].QUEUE);
            if (currPiece['item'] && blocks[y][x] === currPiece['item']) {
              this['v']['drawBrickOverlayOnCanvas'](x - something[0], y - currWeird[0] + plug, this['v'].QUEUE)
            }
            if (solve.overlay > 0 && removeDimple) this['v']['ai_drawBlockOnCanvas'](x - something[0], y - currWeird[0] + plug, currColor, solve.overlay, this['v'].QUEUE);
          }
        }
      };
      if (currSet['equidist']) {
        plug += 3
      } else {
        plug += currWeird[1] - currWeird[0] + 2
      }
    }
  };
  if (window.Game != undefined) {
    let onG = Game['prototype']['drawGhostAndCurrent']
    Game['prototype']['drawGhostAndCurrent'] = function () {
      if (usingConnected || usingGhostConnected) {
        return template1.call(this)
      }
      let val = onG.apply(this, arguments)
      return val
    }
    let onH = Game['prototype']['redrawHoldBox']
    Game['prototype']['redrawHoldBox'] = function () {
      if (usingConnected) {
        return template2.call(this)
      }
      let val = onH.apply(this, arguments)
      return val
    }
    let onQ = Game['prototype']['updateQueueBox']
    Game['prototype']['updateQueueBox'] = function () {
      if (usingConnected) {
        return template4.call(this)
      }
      let val = onQ.apply(this, arguments)
      return val
    }
    Game.prototype.ai_drawMatrix = template3
  }
  if (window.Replayer != undefined && location.href.includes('replay')) {
    Replayer.prototype.ai_drawMatrix = template3

    let onG = Replayer['prototype']['drawGhostAndCurrent']
    Replayer['prototype']['drawGhostAndCurrent'] = function () {
      if (usingConnected || (usingGhostConnected && this.g.ghostSkinId === 0)) {
        return template1.call(this)
      }
      let val = onG.apply(this, arguments)
      return val
    }
    let onH = Replayer['prototype']['redrawHoldBox']
    Replayer['prototype']['redrawHoldBox'] = function () {
      if (usingConnected) {
        return template2.call(this)
      }
      let val = onH.apply(this, arguments)
      return val
    }
    let onQ = Replayer['prototype']['updateQueueBox']
    Replayer['prototype']['updateQueueBox'] = function () {
      if (usingConnected) {
        return template4.call(this)
      }
      let val = onQ.apply(this, arguments)
      return val
    }
  }
  if (window.View != undefined) {
    if (!location.href.includes('export')) {
      View.prototype.ai_drawBlockOnCanvas = function (t, e, i, c, s) {
        let o = s === this.HOLD ? this.hctx : this.qctx;
        if (0 === this.skinId) {
          var n = this.g.monochromeSkin && i <= 7 ? this.g.monochromeSkin : this.g.colors[i];
          this.drawRectangle(o, t * this.block_size, e * this.block_size, this.block_size, this.block_size, n)
        } else {
          o.drawImage(this.tex, this.g.coffset[i] * this.g.skins[this.skinId].w, c * this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, t * this.block_size, e * this.block_size, this.block_size, this.block_size)
        }
      }
      let redraw = View.prototype.redraw
      View.prototype.redraw = function () {
        if (usingConnected) {
          if (!this.redrawBlocked) {
            if (this.clearMainCanvas(), !this.g.isInvisibleSkin) this.g.ai_drawMatrix()
            this.drawGhostAndCurrent(), this.g.redBar && this.drawRectangle(this.ctx, 240, (20 - this.g.redBar) * this.block_size, 8, this.g.redBar * this.block_size, "#FF270F")
          }
          return
        }

        return redraw.apply(this, arguments)
      }
      View.prototype.ai_drawBlock = function (t, e, i, c) {
        if (i && t >= 0 && e >= 0 && t < 10 && e < 20) {
          var s = this.drawScale * this.block_size;
          this.ctx.drawImage(this.tex, this.g.coffset[i] * this.g.skins[this.skinId].w, c * this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, t * this.block_size, e * this.block_size, s, s);
        }
      }
      View.prototype.ai_drawGhostBlock = function (t, e, i, c) {
        if (t >= 0 && e >= 0 && t < 10 && e < 20) {
          var s = this.drawScale * this.block_size;
          this.ctx.globalAlpha = ghostAlpha
          offscreenContext.drawImage(this.tex, this.g.coffset[i] * this.g.skins[this.skinId].w, c * this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, 0, 0, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w)
          if (drawCanvas) this.ctx.drawImage(offscreenCanvas, 0, 0, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, t * this.block_size, e * this.block_size, s, s)
          this.ctx.globalAlpha = 1;
        }
      }

      var oldDrawGhostAndCurrent = View.prototype.drawGhostAndCurrent;
      View.prototype.drawGhostAndCurrent = function () {

        if (!usingConnected)
          return oldDrawGhostAndCurrent.apply(this, arguments);

        var t = this.g.blockSets[this.g.activeBlock.set],
          e = 1 === t.scale ? t.blocks[this.g.activeBlock.id].blocks[this.g.activeBlock.rot] : t.previewAs.blocks[this.g.activeBlock.id].blocks[this.g.activeBlock.rot],
          i = e.length;
        if (this.drawScale = t.scale, this.ghostEnabled) {
          for (var s = 0; s < i; s++) {
            for (var o = 0; o < i; o++) {
              if (e[s][o] > 0) {
                let solve = solveConnected(e, o, s)
                offscreenContext.clearRect(0, 0, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w)
                drawCanvas = false
                if (solve.overlay > 0 && removeDimple) {
                  this.ai_drawGhostBlock(this.g.ghostPiece.pos.x + o * this.drawScale, this.g.ghostPiece.pos.y + s * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.connect_value);
                  drawCanvas = true
                  this.ai_drawGhostBlock(this.g.ghostPiece.pos.x + o * this.drawScale, this.g.ghostPiece.pos.y + s * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.overlay);
                }
                else {
                  drawCanvas = true
                  this.ai_drawGhostBlock(this.g.ghostPiece.pos.x + o * this.drawScale, this.g.ghostPiece.pos.y + s * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.connect_value);
                }
              }
            }
          }
        }
        for (s = 0; s < i; s++) {
          for (o = 0; o < i; o++) {
            if (e[s][o] > 0) {
              let solve = solveConnected(e, o, s)
              this.ai_drawBlock(this.g.activeBlock.pos.x + o * this.drawScale, this.g.activeBlock.pos.y + s * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.connect_value);
              if (solve.overlay > 0 && removeDimple) this.ai_drawBlock(this.g.activeBlock.pos.x + o * this.drawScale, this.g.activeBlock.pos.y + s * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.overlay);
            }
          }
        }
        this.drawScale = 1
      }
    }
    else {
      View.prototype.ai_drawBlockOnCanvas = function (t, i, s, c, e) {
        let h = this.block_size,
          o = this.ctx;
        if (e === this.HOLD ? (this.drawOffsetTop = this.AP.HLD.T, this.drawOffsetLeft = this.AP.HLD.L, this.block_size = this.AP.HLD.BS) : (this.drawOffsetTop = this.AP.QUE.T, this.drawOffsetLeft = this.AP.QUE.L, this.block_size = this.AP.QUE.BS), 0 === this.skinId) {
          var r = this.g.monochromeSkin && s <= 7 ? this.g.monochromeSkin : this.g.colors[s];
          this.drawRectangle(o, t * this.block_size, i * this.block_size, this.block_size, this.block_size, r)
        } else this.drawImage(o, this.tex, this.g.coffset[s] * this.g.skins[this.skinId].w, c * this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, t * this.block_size, i * this.block_size, this.block_size, this.block_size);
        this.block_size = h
      }
      let redraw = View.prototype.drawMainStage
      View.prototype.drawMainStage = function () {
        if (!usingConnected) {
          return redraw.apply(this, arguments)
        }
        if (this.drawOffsetTop = this.AP.STG.T, this.drawOffsetLeft = this.AP.STG.L, !this.g.isInvisibleSkin) this.g.ai_drawMatrix()
        this.drawGhostAndCurrent()
        if (this.g.redBar) this.drawRectangle(this.ctx, this.AP.STG.W, (20 - this.g.redBar) * this.BS, 8, this.g.redBar * this.BS, "#FF270F")
      }
      View.prototype.ai_drawBlock = function (t, i, s, c) {
        if (s && t >= 0 && i >= 0 && t < 10 && i < 20) {
          var e = this.drawScale * this.BS;
          this.drawImage(this.ctx, this.tex, this.g.coffset[s] * this.g.skins[this.skinId].w, c * this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, t * this.BS, i * this.BS, e, e);
        }
      }
      View.prototype.ai_drawGhostBlock = function (t, i, s, c) {
        if (t >= 0 && i >= 0 && t < 10 && i < 20) {
          var e = this.drawScale * this.BS;
          this.ctx.globalAlpha = ghostAlpha
          offscreenContext.drawImage(this.tex, this.g.coffset[s] * this.g.skins[this.skinId].w, c * this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, 0, 0, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w)
          if (drawCanvas) this.drawImage(this.ctx, offscreenCanvas, 0, 0, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w, t * this.BS, i * this.BS, e, e)
          this.ctx.globalAlpha = 1;
        }
      }
      var oldDrawGhostAndCurrent = View.prototype.drawGhostAndCurrent;
      View.prototype.drawGhostAndCurrent = function () {
        if (!usingConnected)
          return oldDrawGhostAndCurrent.apply(this, arguments);
        var t = this.g.blockSets[this.g.activeBlock.set],
          i = 1 === t.scale ? t.blocks[this.g.activeBlock.id].blocks[this.g.activeBlock.rot] : t.previewAs.blocks[this.g.activeBlock.id].blocks[this.g.activeBlock.rot],
          s = i.length;
        if (this.drawScale = t.scale, this.ghostEnabled) {
          for (var e = 0; e < s; e++) {
            for (var h = 0; h < s; h++) {
              if (i[e][h] > 0) {
                let solve = solveConnected(i, h, e)
                offscreenContext.clearRect(0, 0, this.g.skins[this.skinId].w, this.g.skins[this.skinId].w)
                drawCanvas = false
                if (solve.overlay > 0 && removeDimple) {
                  this.ai_drawGhostBlock(this.g.ghostPiece.pos.x + h * this.drawScale, this.g.ghostPiece.pos.y + e * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.connect_value);
                  drawCanvas = true
                  this.ai_drawGhostBlock(this.g.ghostPiece.pos.x + h * this.drawScale, this.g.ghostPiece.pos.y + e * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.overlay);
                }
                else {
                  drawCanvas = true
                  this.ai_drawGhostBlock(this.g.ghostPiece.pos.x + h * this.drawScale, this.g.ghostPiece.pos.y + e * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.connect_value);
                }
              }
            }
          }
        }
        for (e = 0; e < s; e++) {
          for (h = 0; h < s; h++) {
            if (i[e][h] > 0) {
              let solve = solveConnected(i, h, e)
              this.ai_drawBlock(this.g.activeBlock.pos.x + h * this.drawScale, this.g.activeBlock.pos.y + e * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.connect_value);
              if (solve.overlay > 0 && removeDimple) this.ai_drawBlock(this.g.activeBlock.pos.x + h * this.drawScale, this.g.activeBlock.pos.y + e * this.drawScale, t.blocks[this.g.activeBlock.id].color, solve.overlay);

            }
          }
        }
        this.drawScale = 1
      }
    }

  }
}
;// CONCATENATED MODULE: ./src/practiceUndo.js

const clone = function (x) { return JSON.parse(JSON.stringify(x)); }
class SaveState {
    /**
     * Creates a SaveState out of the given Game (all attributes are deep copies)
     */
    constructor(game) {
        this.matrix = clone(game.matrix);
        this.deadline = clone(game.deadline);
        this.activeBlock = clone(game.activeBlock);
        this.blockInHold = clone(game.blockInHold);

        this.b2b = game.b2b;
        this.combo = game.comboCounter;

        // save stat-related fields. might need to add a few more?
        this.placedBlocks = game.placedBlocks;
        this.totalFinesse = game.totalFinesse;
        this.totalKeyPresses = game.totalKeyPresses;
        this.incomingGarbage = clone(game.incomingGarbage);
        this.redBar = game.redBar;

        this.gamedata = {};
        for (const [key, value] of Object.entries(game.gamedata)) {
            this.gamedata[key] = value;
        }
    }
}
const initPracticeUndo = () => {
    const MaxSaveStates = 100;
    /**
     * Creates a save state from the current game state and adds it to the stack.
     * If this pushes the stack above MaxSaveStates, delete the least recent save.
     * To be run before each hard drop.
     */
    Game.prototype.addSaveState = function () {
        if (this.pmode !== 2) return;

        this.saveStates.push(new SaveState(this));
        if (this.saveStates.length > MaxSaveStates) this.saveStates.shift();
    }

    /**
     * Rewinds to the last save state and removes it from the stack. If no states available, prints a message to the in-game chat.
     */
    Game.prototype.undoToSaveState = function () {
        if (this.pmode !== 2) return;
        if (this.saveStates.length === 0) {
            this.Live.showInChat("Jstris+", "Can't undo any further!")
            return;
        }
        if (this.fumenPages) {
            this.fumenPages.pop();
        }
        this.Replay.invalidFromUndo = true;
        let lastState = this.saveStates.pop();
        this.loadSaveState(lastState);
    }
    Game.prototype.loadSaveState = function (lastState) {
        this.matrix = lastState.matrix;
        this.deadline = lastState.deadline;
        this.isBack2Back = lastState.b2b;
        this.comboCounter = lastState.combo;

        this.loadSeedAndPieces(
            this.Replay.config.seed,
            this.conf[0].rnd,
            lastState.placedBlocks,
            lastState.activeBlock,
            lastState.blockInHold
        );


        this.placedBlocks = lastState.placedBlocks;
        this.totalFinesse = lastState.totalFinesse;
        this.totalKeyPresses = lastState.totalKeyPresses;
        this.gamedata = lastState.gamedata;
        this.incomingGarbage = lastState.incomingGarbage;
        this.redBar = lastState.redBar;

        this.holdUsedAlready = false;
        this.setCurrentPieceToDefaultPos();
        this.updateGhostPiece(true);
        this.redrawAll();

        // update all stats' text to the new values
        this.GameStats.get("RECV").set(this.gamedata.linesReceived);
        this.GameStats.get("SCORE").set(this.gamedata.score);
        this.GameStats.get("HOLD").set(this.gamedata.holds);
        this.GameStats.get("LINES").set(this.gamedata.lines);
        this.GameStats.get("ATTACK").set(this.gamedata.linesSent)
        this.GameStats.get("BLOCKS").set(this.placedBlocks);
        this.GameStats.get("KPP").set(this.getKPP());
        this.GameStats.get("WASTE").set(this.getWasted());
        this.GameStats.get("FINESSE").set(this.totalFinesse);
        this.updateTextBar(); // updates stats for clock, pps, apm, and vs, and renders the new stats
    }

    /**
     * Sets the seed, queue, active block, and held block based on the parameters
     * @param {int} seed
     * @param {int} rngType
     * @param {int} placedBlockCount
     * @param {Block} activeBlock
     * @param {Block} heldBlock
     */
    Game.prototype.loadSeedAndPieces = function(seed, rngType, placedBlockCount, activeBlock, heldBlock) {
        // recreate rng's state at game start (from seed stored in replay)
        this.Replay.config.seed = seed;
        this.blockRNG = alea(seed);
        this.RNG = alea(seed);
        this.initRandomizer(rngType);

        // to get the rng to the right state, roll for each previously generated block
        // +1 for current piece and +1 for hold, because those are saved separately
        let rollCount = placedBlockCount + 1;
        if (heldBlock != null) rollCount += 1;
        for (let i = 0; i < rollCount; i++) {
            this.getRandomizerBlock(); // result is ignored but rng is adjusted
        }

        // generate queue from new rng, and set active and held block from save state
        this.queue = [];
        this.generateQueue();
        this.activeBlock = activeBlock;
        this.blockInHold = heldBlock;
    }

    /**
     * initializes the save state stack. To be run before a practice mode is a started
     */
    Game.prototype.initSaveStates = function () {
        if (this.pmode !== 2) return;
        this.saveStates = [];
    }

    // call `addSaveState` before each hard drop
    const oldBeforeHardDrop = Game.prototype.beforeHardDrop;
    Game.prototype.beforeHardDrop = function () {
        if (this.pmode === 2) this.addSaveState();

        return oldBeforeHardDrop.apply(this, arguments);
    }

    // add `initSaveStates` to generatePracticeQueue
    let keyListenerInjected = false
    const oldGeneratePracticeQueue = Game.prototype.generatePracticeQueue;
    Game.prototype.generatePracticeQueue = function () {
        if (this.pmode === 2) {
            this.initSaveStates();
            if (!keyListenerInjected) {
                document.addEventListener("keydown", (keyEvent) => {
                    if (this.focusState === 0) {
                        if (keyEvent.keyCode === Config().UNDO_KEYCODE) {
                            this.undoToSaveState()
                        }
                    }

                }, false)
            }
            keyListenerInjected = true
        }


        return oldGeneratePracticeQueue.apply(this, arguments);
    }

    // neatly tell the user that replays don't work with undos or fumen/snapshot imports
    const oldUploadError = Replay.prototype.uploadError;
    Replay.prototype.uploadError = function (LivePtr, err) {
        if (this.invalidFromSnapshot) {
            LivePtr.showInChat("Jstris+", "Can't generate replay for game with fumen or snapshot import!");
            return;
        }
        if (this.invalidFromUndo) {
            LivePtr.showInChat("Jstris+", "Can't generate replay for game with undos!");
            return;
        }
        return oldUploadError.apply(this, arguments);
    }
}
;// CONCATENATED MODULE: ./src/practiceSurvivalMode.js


const initPracticeSurvivalMode = () => {

  // 60 apm cycle from rivi's usermode
  const baseCycle = [
    {time: 4, attack: 4},
    {time: 4, attack: 5},
    {time: 4, attack: 2},
    {time: 3, attack: 1},
    {time: 4, attack: 4},
    {time: 4, attack: 4},
    {time: 3, attack: 5},
    {time: 3, attack: 5}
  ]

  let isCycling = false;
  let shouldStartCycle = false;
  let shouldCancel = true;
  let timeFactor = 1;
  let hangingTimeout = 0;
  
  const INIT_MESS = 20;
  let setMess = m => null;
  const changeAPM = (apm) => timeFactor = 60 / apm;

  let hasInit = false;

  const doCycle = (game, i) => {
    const cycleStep = baseCycle[i];
    if (!isCycling) return;
    if (game.pmode != 2) return stopCycle();
    console.log(game.pmode);
    hangingTimeout = setTimeout(() => {
      if (!isCycling) return;
      if (game.pmode != 2) return stopCycle();
      game.addIntoGarbageQueue(cycleStep.attack);
      doCycle(game, (i+1)%baseCycle.length)
    }, cycleStep.time * timeFactor * 1000);
  }
  const startCycle = (game) => {
    
    if (!isCycling) {
      isCycling = true;
      doCycle(game, 0);
    }
      
  }
  const stopCycle = () => {
    clearTimeout(hangingTimeout);
    isCycling = false;
  }
  if (typeof Game == "function") {

    const oldQueueBoxFunc = Game.prototype.updateQueueBox;
    Game.prototype.updateQueueBox = function () {
      if (this.pmode != 2)
        return oldQueueBoxFunc.apply(this, arguments);
      return oldQueueBoxFunc.apply(this, arguments);
    }
    const oldLineClears = GameCore.prototype.checkLineClears;
    GameCore.prototype.checkLineClears = function (x) {
      let oldAttack = this.gamedata.attack;
      let val = oldLineClears.apply(this, arguments);
      let curAttack = this.gamedata.attack - oldAttack;
      if (this.pmode == 2 && curAttack > 0) {
        this.gamedata.attack -= curAttack; // block or send attack also adds to the attack, so just subtracting to make stat accurate
        if (shouldCancel) {
          this.blockOrSendAttack(curAttack, x);
        }
        
      }
      return val;
    }
      

    const oldReadyGo = Game.prototype.readyGo
    Game.prototype.readyGo = function () {

      if (this.pmode == 2) {
        settingsDiv.classList.add("show-practice-mode-settings");
      } else {
        settingsDiv.classList.remove("show-practice-mode-settings");
      }

      if (shouldStartCycle)
        startCycle(this);

      if (!hasInit) {
        let oldOnGameEnd = Settings.prototype.onGameEnd;
        if (this.pmode == 2) {
          this.R.mess = INIT_MESS;
        }
        window.game = this;
        setMess = m => {
          if (this.pmode == 2) {
            this.R.mess = m;
          }
        }
        this.Settings.onGameEnd = function() {
          if (this.p.pmode == 2) {
            stopCycle();
          }
          return oldOnGameEnd.apply(this, arguments)
        }
        startStopButton.addEventListener("click", () => {
          shouldStartCycle = !shouldStartCycle;
      
          if (shouldStartCycle) {
            startCycle(this);
            startStopButton.innerHTML = "Stop APM Cycle";
          } else {
            stopCycle(this);
            startStopButton.innerHTML = "Start APM Cycle";
          }
    
        })
        startStopButton.disabled = false;
        hasInit = true;
      }
      return oldReadyGo.apply(this, arguments)
    }

  }


  const stage = document.getElementById("stage");
  const settingsDiv = document.createElement("DIV");
  settingsDiv.id = "customPracticeSettings";

  var slider = document.createElement("input")
  slider.type = "range"
  slider.min = 5;
  slider.max = 200;
  slider.step = 5;
  slider.id = "customApmSlider";
  slider.value = 60;
  var valueLabel = document.createElement("input");
  valueLabel.type = "number";
  valueLabel.min = 5;
  valueLabel.max = 200;
  valueLabel.id = "customApmInput";
  slider.addEventListener("mousemove", () => {
    valueLabel.value = Number.parseFloat(slider.value).toFixed(0);
    changeAPM(Number.parseFloat(slider.value));
  });
  valueLabel.value = Number.parseFloat(slider.value).toFixed(0);

  valueLabel.addEventListener("change", () => {
    var num = Number.parseFloat(valueLabel.value);
    num = Math.max(5,Math.min(num, 200));
    slider.value = num.toFixed(0);
    valueLabel.value = num;
    changeAPM(num);
  });

  valueLabel.addEventListener("click", () => {
    $(window).trigger('modal-opened');
  })

  var label = document.createElement("label");
  label.htmlFor = "customApmSlider";
  label.innerHTML = "APM";

  var sliderDiv = document.createElement("div");
  sliderDiv.appendChild(label);
  sliderDiv.appendChild(slider);
  sliderDiv.appendChild(valueLabel);

  var messSlider = document.createElement("input")
  messSlider.type = "range"
  messSlider.min = 0;
  messSlider.max = 100;
  messSlider.step = 1;
  messSlider.id = "customApmSlider";
  messSlider.value = INIT_MESS;
  var messValueLabel = document.createElement("input");
  messValueLabel.type = "number";
  messValueLabel.min = 0;
  messValueLabel.max = 100;
  messValueLabel.id = "customApmInput";
  messSlider.addEventListener("mousemove", () => {
    messValueLabel.value = Number.parseFloat(messSlider.value).toFixed(0);
    setMess(Number.parseFloat(messSlider.value));
  });
  messValueLabel.value = Number.parseFloat(messSlider.value).toFixed(0);

  messValueLabel.addEventListener("change", () => {
    var num = Number.parseFloat(messValueLabel.value);
    num = Math.max(0,Math.min(num, 100));
    messSlider.value = num.toFixed(0);
    messValueLabel.value = num;
    setMess(num);
  });

  messValueLabel.addEventListener("click", () => {
    $(window).trigger('modal-opened');
  })

  var messLabel = document.createElement("label");
  messLabel.htmlFor = "customApmSlider";
  messLabel.innerHTML = "🧀%";

  var messSliderDiv = document.createElement("div");
  messSliderDiv.appendChild(messLabel);
  messSliderDiv.appendChild(messSlider);
  messSliderDiv.appendChild(messValueLabel);

  var cancelLabel = document.createElement("label");
  cancelLabel.htmlFor = "cancelCheckbox";
  cancelLabel.innerHTML = "Allow cancel";

  var cancelCheckbox = document.createElement("input");
  cancelCheckbox.type = "checkbox";
  cancelCheckbox.id = "cancelCheckbox";
  cancelCheckbox.checked = true;

  cancelCheckbox.addEventListener("change", () => {
    shouldCancel = cancelCheckbox.checked
  })

  var cancelDiv = document.createElement("div");
  cancelDiv.appendChild(cancelLabel);
  cancelDiv.appendChild(cancelCheckbox);

  var startStopButton = document.createElement("button");
  startStopButton.innerHTML = "Start APM Cycle";
  startStopButton.disabled = true;
  settingsDiv.innerHTML+="<b>Downstack Practice</b><br/>"
  settingsDiv.appendChild(sliderDiv);
  settingsDiv.appendChild(messSliderDiv);
  settingsDiv.appendChild(cancelDiv);
  settingsDiv.appendChild(startStopButton);
  stage.appendChild(settingsDiv);

}

;// CONCATENATED MODULE: ./src/teamsMode.js
const fixTeamsMode = () => {
    let oldDecode = Live.prototype.decodeActionsAndPlay
    Live.prototype.decodeActionsAndPlay = function () {
        let temp = this.p.GS.extendedAvailable
        if (this.p.GS.teamData) {
            this.p.GS.extendedAvailable = true
            var cid = this.rcS[arguments[0][1]];
            if (cid in this.p.GS.cidSlots && this.clients[cid].rep) {
                this.clients[cid].rep.v.cancelLiveMatrix = true
            }
        }
        let v = oldDecode.apply(this, arguments)
        this.p.GS.extendedAvailable = temp
        return v
    }
    let oldRep = Game.prototype.sendRepFragment
    Game.prototype.sendRepFragment = function () {
        let temp = this.transmitMode
        if (this.GS.teamData) {
            this.transmitMode = 1
        }
        let v = oldRep.apply(this, arguments)
        this.transmitMode = temp
        return v
    }
    let oldUpdate = Game.prototype.update
    Game.prototype.update = function () {
        let temp = this.transmitMode
        if (this.GS.teamData) {
            this.transmitMode = 1
        }
        let v = oldUpdate.apply(this, arguments)
        this.transmitMode = temp
        return v
    }
    let oldFlash = SlotView.prototype.updateLiveMatrix
    SlotView.prototype.updateLiveMatrix = function () {
        if (this.cancelLiveMatrix) {
            this.queueCanvas.style.display = "block"
            this.holdCanvas.style.display = "block"
            return
        }
        this.queueCanvas.style.display = "none"
        this.holdCanvas.style.display = "none"
        return oldFlash.apply(this, arguments)
    }
    let oldHold = Replayer.prototype.redrawHoldBox
    Replayer.prototype.redrawHoldBox = function () {
        this.v.QueueHoldEnabled = true;
        this.v.holdCanvas.style.display = 'block';
        return oldHold.apply(this, arguments)
    }
    let oldQueue = Replayer.prototype.updateQueueBox
    Replayer.prototype.updateQueueBox = function () {
        this.v.QueueHoldEnabled = true;
        this.v.queueCanvas.style.display = 'block';
        return oldQueue.apply(this, arguments)
    }
    let oldSlotInit = Slot.prototype.init
    Slot.prototype.init = function () {
        let life = this.gs.p.Live
        if (life?.roomConfig?.mode != 2) {
            return oldSlotInit.apply(this, arguments)
        }
        this.v.queueCanvas.style.display = "none"
        this.v.holdCanvas.style.display = "none"
        this.gs.holdQueueBlockSize = this.gs.matrixHeight / 20
        //    console.log("hi2", this.gs.holdQueueBlockSize)
        this.v.QueueHoldEnabled = true
        this.v.cancelLiveMatrix = false
        this.slotDiv.className = "slot"
        this.slotDiv.style.left = this.x + "px"
        this.slotDiv.style.top = this.y + "px"
        this.stageDiv.style.position = "relative"
        this.name.style.width = this.gs.matrixWidth + 2 + "px"
        this.name.style.height = this.gs.nameHeight + "px"
        this.name.style.fontSize = this.gs.nameFontSize + "px"
        this.pCan.width = this.bgCan.width = this.gs.matrixWidth
        this.pCan.height = this.bgCan.height = this.gs.matrixHeight
        this.queueCan.width = this.holdCan.width = 4 * this.gs.holdQueueBlockSize
        this.holdCan.height = 4 * this.gs.holdQueueBlockSize
        this.queueCan.height = 15 * this.gs.holdQueueBlockSize
        this.pCan.style.top = this.bgCan.style.top = this.holdCan.style.top = this.queueCan.style.top = this.gs.nameHeight + "px", this.holdCan.style.left = "0px";
        var widad = .8 * this.gs.holdQueueBlockSize
        let keior = 4 * this.gs.holdQueueBlockSize + widad;
        if (this.name.style.left = keior + "px", this.pCan.style.left = this.bgCan.style.left = keior + "px", this.queueCan.style.left = keior + this.pCan.width + widad + "px", this.gs.slotStats && this.gs.matrixWidth >= 50) {
            this.stats.init(), this.stats.statsDiv.style.left = keior + "px", this.slotDiv.appendChild(this.stats.statsDiv);
            let leonilla = 1.1 * this.stats.statsDiv.childNodes[0].clientWidth, thorson = 2 * leonilla < .85 * this.gs.matrixWidth || leonilla > .6 * this.gs.matrixWidth;
            this.stats.winCounter.style.display = thorson ? null : "none";
        } else {
            this.stats.disable();
        }
        ;
        this.slotDiv.appendChild(this.name), this.slotDiv.appendChild(this.stageDiv), this.stageDiv.appendChild(this.bgCan), this.stageDiv.appendChild(this.pCan), this.stageDiv.appendChild(this.holdCan), this.stageDiv.appendChild(this.queueCan), this.slotDiv.style.display = "block", this.gs.gsDiv.appendChild(this.slotDiv), this.v.onResized();

        this.stats.statsDiv.style.width = "250px"
    }
    GameSlots.prototype.tsetup = function (teamLengths) {
        var maxTeamLength = Math.max.apply(null, teamLengths),
            edweina = this.h / 2,
            slotIndex = 0;
        this.isExtended = false, this.nameFontSize = 15, this.nameHeight = 18;
        var shonte = edweina,
            coline = 1 === (curTeamLength = maxTeamLength) ? 0 : (2 === curTeamLength ? 30 : 60) / (curTeamLength - 1),
            cinnamin = this.tagHeight + 2;

        this.slotHeight = this.nmob(shonte - this.nameHeight - 15)

        this.redBarWidth = Math.ceil(this.slotHeight / 55) + 1
        this.slotWidth = this.slotHeight / 2 + this.redBarWidth;

        var janishia = this.slotWidth * curTeamLength + (curTeamLength - 1) * coline;
        janishia > this.w && (this.slotWidth = Math.floor(this.w / curTeamLength) - coline, this.slotHeight = this.nmob(2 * (this.slotWidth - this.redBarWidth)), this.redBarWidth = Math.ceil(this.slotHeight / 55) + 1, this.slotWidth = this.slotHeight / 2 + this.redBarWidth, janishia = this.slotWidth * curTeamLength + (curTeamLength - 1) * coline), this.liveBlockSize = this.slotHeight / 20;

        // OLD
        //var estarlin = this.slotHeight + this.nameHeight + 15 + cinnamin;
        // INJECTED
        var estarlin = this.slotHeight + this.nameHeight * (this.slotStats ? 3 : 1) + 15 + cinnamin;

        this.matrixHeight = this.slotHeight
        this.matrixWidth = this.slotWidth;

        // inject slot width here instead of in Slot.init because tsetup is called first.
        this.slotWidth = this.matrixWidth * 1.7413

        for (var teamIndex = 0; teamIndex < teamLengths.length; teamIndex++) {
            var curTeamLength = teamLengths[teamIndex];

            // begin injected code
            let queueHoldBoxPadding = .8 * this.holdQueueBlockSize
            let queueHoldBoxWidthPlusPadding = 4 * this.holdQueueBlockSize + queueHoldBoxPadding;

            // OLD LINE:
            //janishia = this.slotWidth * letrina + (letrina - 1) * coline;
            // INJECTED LINE:
            janishia = this.slotWidth * curTeamLength + (curTeamLength - 1) * coline + queueHoldBoxWidthPlusPadding;

            // OLD LINE:
            //var baseSlotXCoord = Math.floor((this.w - janishia) / 2);
            // INJECTED LINE (TO PREVENT OVERLAP WITH BOARD)
            var baseSlotXCoord = Math.max(0, Math.floor((this.w - janishia) / 2));

            // end injected code

            curTeamLength > 0 && this.initTeamTag(teamIndex, baseSlotXCoord, estarlin * teamIndex, janishia);
            for (var teamSlot = 0; teamSlot < curTeamLength; teamSlot++) {
                var slotX = baseSlotXCoord + teamSlot * (this.slotWidth + coline),
                    slotY = estarlin * teamIndex + cinnamin;
                slotIndex >= this.slots.length ? this.slots[slotIndex] = new Slot(slotIndex, slotX, slotY, this) : (this.slots[slotIndex].x = slotX, this.slots[slotIndex].y = slotY, this.slots[slotIndex].init()), slotIndex++;
            }
        };
        for (this.shownSlots = slotIndex; slotIndex < this.slots.length;) {
            this.slots[slotIndex].hide(), slotIndex++;
        };
        this.realHeight = estarlin * teamLengths.length - 15, this.resizeElements();
    }
}
// EXTERNAL MODULE: ./node_modules/tetris-fumen/index.js
var tetris_fumen = __webpack_require__(451);
;// CONCATENATED MODULE: ./src/practiceFumen.js


const practiceFumen_clone = function (x) { return JSON.parse(JSON.stringify(x)); }

const reverseMatrix = ['_', 'Z', 'L', 'O', 'S', 'I', 'J', 'T', 'X', 'X', 'I', 'O', 'T', 'L', 'J', 'S', 'Z', 'I', 'O', 'T', 'L', 'J', 'S', 'Z']
const jstrisToCenterX = [[1, 2, 2, 1], [1, 1, 2, 2], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
const jstrisToCenterY = [[1, 1, 2, 2], [2, 1, 1, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]
const pIndex = ['I', 'O', 'T', 'L', 'J', 'S', 'Z']
const rIndex = ["spawn", "right", "reverse", "left"]
const quizFilter = new RegExp('[^' + 'IOTLJSZ' + ']', 'g');

function downloadText(filename, text) {
    var element = document.createElement('a');
    element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
    element.setAttribute('download', filename);

    element.style.display = 'none';
    document.body.appendChild(element);

    element.click();

    document.body.removeChild(element);
}

const generateFumenQueue = function (lim = null) {
    if (!lim) lim = this.queue.length
    let bs = this.blockSets[this.activeBlock.set]
    lim = Math.min(lim, this.queue.length)
    let r1 = ""
    if (this.activeBlock) {
        r1 = bs.blocks[this.activeBlock.id].name
    }
    let r2 = ""
    if (this.blockInHold) {
        r2 = bs.blocks[this.blockInHold.id].name
    }
    let qq = `#Q=[${r2}](${r1})`
    for (let i = 0; i < lim; i++) {
        qq += bs.blocks[this.queue[i].id].name
    }
    return qq
}
const generateFumenMatrix = function () {
    let fieldStr = ''
    for (let i in this.deadline) {
        fieldStr += reverseMatrix[this.deadline[i]]
    }
    for (let row in this.matrix) {
        for (let col in this.matrix[row]) {
            fieldStr += reverseMatrix[this.matrix[row][col]]
        }
    }
    return fieldStr
}
const initPracticeFumen = () => {
    const oldRestart = Game.prototype.restart;
    Game.prototype.restart = function () {
        const urlParams = new URLSearchParams(window.location.search);
        const snapshot = urlParams.get("snapshotPlus")

        if (this.pmode === 2 && snapshot != null) {
            let val = oldRestart.apply(this, arguments)
            let game = LZString.decompressFromEncodedURIComponent(snapshot)
            game = JSON.parse(game)
            console.log(game)

            let heldBlock = game.holdID == null ? null : new Block(game.holdID);
            this.loadSeedAndPieces(
                game.seed,
                game.rnd,
                game.placedBlocks,
                new Block(game.activeBlockID),
                heldBlock
            )

            this.matrix = practiceFumen_clone(game.matrix)
            this.deadline = practiceFumen_clone(game.deadline)
            this.setCurrentPieceToDefaultPos();
            this.updateGhostPiece(true);
            this.redrawAll();
            this.invalidFromSnapshot = true
            return val
        } else {
            this.fumenPages = null
            if (this.pmode === 2) this.fumenPages = []
            return oldRestart.apply(this, arguments);

        }

    }
    Game.prototype.generateFumenQueue = generateFumenQueue
    Game.prototype.generateFumenMatrix = generateFumenMatrix
    const onGarbageAdded = Game.prototype.addGarbage
    Game.prototype.addGarbage = function () {
        this.fumenMatrixRoll = true //matrix modulated, need to update fumen matrix
        return onGarbageAdded.apply(this, arguments)
    }
    const onHardDrop = Game.prototype.beforeHardDrop

    Game.prototype.beforeHardDrop = function () {
        let val = onHardDrop.apply(this, arguments)
        if (!this.fumenPages) return val
        if (this.altBlocks) {
            this.pages.push({ field: tetris_fumen/* Field.create */.gN.create(this.generateFumenMatrix()) })
            return
        }
        let ss = this.activeBlock

        let x = jstrisToCenterX[ss.id][ss.rot] + this.activeBlock.pos.x
        let y = 19 - (jstrisToCenterY[ss.id][ss.rot] + this.ghostPiece.pos.y)
        let msg = {
            operation: { type: this.blockSets[ss.set].blocks[ss.id].name, rotation: rIndex[ss.rot], x: x, y: y }
        }
        if (this.fumenMatrixRoll) {
            msg.field = tetris_fumen/* Field.create */.gN.create(this.generateFumenMatrix())
            this.fumenMatrixRoll = false
        }
        msg.comment = this.generateFumenQueue()
        msg.flags = { quiz: true }
        this.fumenPages.push(msg)
        //   console.log(encoder.encode(this.fumenPages))
        return val
    }

    const chatListener = Live.prototype.sendChat
    Live.prototype.sendChat = function (rawmsg) {
        var msg = "string" != typeof rawmsg ? this.chatInput.value.replace(/"/g, '\\"') : rawmsg;
        if (msg == "/fumen") {
            if (this.p.pmode != 2) {
                this.showInChat("Jstris+", "Live fumen export only supported in practice mode")
                this.chatInput.value = "";
                return
            }
            if (!this.p.fumenPages) {
                this.showInChat("Jstris+", "No fumen data available")
                this.chatInput.value = "";
                return
            }
            let fumen = tetris_fumen/* encoder.encode */.g7.encode(this.p.fumenPages)
            var coderro = "<span class='wFirstLine'><span class='wTitle'>!" + i18n.warning2 + "!</span> <b>" + i18n.repFail + "</b> (<em>" + "Jstris+ Fumen Export" + "</em>)</span>";

            coderro += "<p>" + "Fumen code dumped into the chat." + "</p>"
            coderro += `<a href="https://harddrop.com/fumen/?${fumen}" target="_blank">Link</a>`
            coderro += '<textarea readonly cols="30" onclick="this.focus();this.select()">'
            coderro += fumen + "</textarea>"
            this.chatMajorWarning(coderro);
            this.chatInput.value = "";
            return
        } else if ("/fumen" === msg.substring(0, 6)) {
            if (this.p.pmode != 2) {
                this.showInChat("Jstris+", "Fumen import only supported in practce mode")
                this.chatInput.value = "";
                return
            }
            let pages = null
            try {
                pages = tetris_fumen/* decoder.decode */.xv.decode(msg.substring(5))
            } catch (error) {
                console.log(error)
                this.showInChat("Jstris+", error.message)
                this.chatInput.value = "";
                return
            }
            let gamestates = loadFumen(pages)
            this.p.loadSaveState(gamestates)
            for (let i = this.p.queue.length; i < 7; i++) {
                this.p.refillQueue()
            }
            this.p.redrawAll();
            this.p.saveStates = []
            this.p.addSaveState()
            this.p.fumenPages = []
            this.chatInput.value = "";
            this.p.invalidFromSnapshot = true
            return
        }
        const val = chatListener.apply(this, [rawmsg])
        return val
    }
}
const initReplayerSnapshot = () => {

    let repControls = document.getElementById("repControls")
    let skipButton = document.createElement("button")
    skipButton.className = "replay-btn"
    skipButton.textContent = "snapshot"
    let fumenButton = document.createElement("button")
    fumenButton.className = "replay-btn"
    fumenButton.textContent = "fumen"
    let pcButton = document.createElement("button")
    pcButton.className = "replay-btn"
    pcButton.textContent = "pc solver"
    let wellRow1 = document.createElement("div")
    wellRow1.className = "replay-btn-group"
    let injected = false
    const lR = ReplayController.prototype.loadReplay
    ReplayController.prototype.loadReplay = function () {
        if (!injected && this.g.length == 1) {
            //  let well = document.createElement("div")
            //  well.className = 'well'


            //    well.appendChild(wellRow1)
            Replayer.prototype.generateFumenQueue = generateFumenQueue.bind(this.g[0])
            Replayer.prototype.generateFumenMatrix = generateFumenMatrix.bind(this.g[0])
            repControls.appendChild(wellRow1)
            wellRow1.appendChild(skipButton)
            wellRow1.appendChild(fumenButton)

            skipButton.onclick = () => {
                let code = this.g[0].snapshotPlus()
                window.open(`https://jstris.jezevec10.com/?play=2&snapshotPlus=${code}`, '_blank')
            }
            pcButton.onclick = () => {
                let code = this.g[0].snapshotFumen()
                window.open(`https://wirelyre.github.io/tetra-tools/pc-solver.html?fumen=${encodeURIComponent(code)}`, '_blank')
            }
            fumenButton.onclick = () => {
                let rep = document.getElementById('rep0').value
                fumenButton.disabled = true
                fumenButton.textContent = "loading"
                fetch(`https://fumen.tstman.net/jstris`, {
                    method: 'POST',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json'
                    },
                    body: `replay=${rep}`
                }).then((response) => response.json())
                    .then((data) => {
                        navigator.clipboard.writeText(data.fumen).then(() => {
                            fumenButton.textContent = "copied"
                        }).catch((err) => {
                            fumenButton.textContent = `err ${err}`
                        }).finally(() => {
                            if (data.fumen.length < 8168) {
                                let newWin = window.open(`https://harddrop.com/fumen/?${data.fumen}`, '_blank')
                            }
                            let textArea = document.createElement('textarea')
                            textArea.className = "repArea"
                            textArea.rows = 1
                            textArea.textContent = data.fumen
                            let dlButton = document.createElement("button")
                            dlButton.textContent = "download"
                            dlButton.className = "replay-btn"
                            dlButton.onclick = () => {
                                downloadText('jstrisFumen.txt', data.fumen)
                            }
                            let openButton = document.createElement("button")
                            openButton.textContent = "open"
                            let fumenLink = `https://harddrop.com/fumen/?${data.fumen}`
                            if (data.fumen.length >= 8168) {
                                alert("fumen code too long for url, you'll need to paste the code in manually")
                                fumenLink = `https://harddrop.com/fumen/?`
                            }

                            openButton.className = "replay-btn"
                            openButton.onclick = () => {
                                window.open(fumenLink, '_blank')
                            }
                            repControls.appendChild(textArea)
                            repControls.appendChild(dlButton)
                            repControls.appendChild(openButton)
                        });

                    });
            }
            injected = true
        }
        let val = lR.apply(this, arguments)
        if (this.g[0].pmode == 8) {
            wellRow1.appendChild(pcButton)
        }
        return val
    }
    Replayer.prototype.snapshotFumen = function () {


        /*
        let ss = this.activeBlock
        let x = jstrisToCenterX[ss.id][ss.rot] + this.activeBlock.pos.x
        let y = 19 - (jstrisToCenterY[ss.id][ss.rot] + this.ghostPiece.pos.y)
           let msg = {
               operation: { type: this.blockSets[ss.set].blocks[ss.id].name, rotation: rIndex[ss.rot], x: x, y: y }
           }*/
        let msg = {}
        let fieldStr = this.generateFumenMatrix().substring(170)
        let airCount = fieldStr.split('_').length - 1
        msg.field = tetris_fumen/* Field.create */.gN.create(fieldStr)
        msg.comment = this.generateFumenQueue().replace(quizFilter, '')
        msg.comment = msg.comment.substring(0, Math.floor(airCount / 4) + 1)
        console.log(msg)
        let code = tetris_fumen/* encoder.encode */.g7.encode([msg])
        console.log(code)
        return code
    }
    Replayer.prototype.snapshotPlus = function () {
        let matrix = practiceFumen_clone(this.matrix)
        let deadline = practiceFumen_clone(this.deadline)
        let placedBlocks = this.placedBlocks
        let seed = this.r.c.seed
        let activeBlockID = this.activeBlock.id;
        let holdID = null
        if (this.blockInHold) {
            holdID = this.blockInHold.id
        }
        let rnd = this.R.rnd
        return LZString.compressToEncodedURIComponent(JSON.stringify({
            matrix, deadline, placedBlocks, seed, activeBlockID, holdID, rnd
        }))
    }
}
const loadFumen = (pages) => {

    const page = pages[pages.length - 1]
    const field = page.field
    let matrix = Array(20).fill().map(() => Array(10).fill(0))
    let deadline = Array(10).fill(0)
    let activeBlock = new Block(0)
    let hold = null, queue = []
    if (page.flags.quiz) {
        let match = /^#Q=\[([LOJSTZI]?)\]?\(([LOJSTZI]?)\)([LOJSTZI]*)$/.exec(page.comment);
        console.log(match)
        if (match[1]) {
            hold = new Block(pIndex.indexOf(match[1]))
        }
        if (match[2]) {
            activeBlock = new Block(pIndex.indexOf(match[2]))
        }
        if (match[3]) {
            for (let char of match[3]) {
                queue.push(new Block(pIndex.indexOf(char)))
            }
        }
    }


    for (let x = 0; x < 10; x++) {
        for (let y = 0; y < 20; y++) {
            let v = reverseMatrix.indexOf(field.at(x, y))
            if (v > 0) matrix[19 - y][x] = v
        }
    }
    for (let x = 0; x < 10; x++) {
        let v = reverseMatrix.indexOf(field.at(x, 20))
        if (v > 0) deadline[x] = v
    }
    let game = {
        matrix: matrix,
        deadline: deadline,
        activeBlock: activeBlock,
        blockInHold: hold,
        queue: queue,
        b2b: 0,
        combo: 0,
        placedBlocks: 0,
        totalFinesse: 0,
        totalKeyPresses: 0,
        incomingGarbage: [],
        redBar: 0,
        gamedata: {
            "lines": 0,
            "singles": 0,
            "doubles": 0,
            "triples": 0,
            "tetrises": 0,
            "maxCombo": 0,
            "linesSent": 0,
            "linesReceived": 9,
            "PCs": 0,
            "lastPC": 0,
            "TSD": 0,
            "TSD20": 0,
            "B2B": 0,
            "attack": 0,
            "score": 0,
            "holds": 0,
            "garbageCleared": 0,
            "wasted": 1,
            "tpieces": 1,
            "tspins": 0
        }
    }
    return game
}
;// CONCATENATED MODULE: ./src/screenshot.js
const overlayCanvases = (canvases) => {
    let tempCanvas = document.createElement("canvas")
    let ctx = tempCanvas.getContext("2d");
    tempCanvas.width = canvases[0].width
    tempCanvas.height = canvases[0].height
    for (let canvas of canvases) {
        ctx.drawImage(canvas, 0, 0);
    };
    return tempCanvas
}
const combineCanvases = (canvases) => {
    let maxHeight = 0
    let width = 0
    for (let canvas of canvases) {
        if (canvas.height > maxHeight) maxHeight = canvas.height
        width += canvas.width
    }
    let tempCanvas = document.createElement("canvas")
    let ctx = tempCanvas.getContext("2d")

    tempCanvas.width = width //+ 200
    tempCanvas.height = maxHeight
    let dx = 0
    for (let i = 0; i < canvases.length; i++) {
        ctx.drawImage(canvases[i], dx, 0)
        dx += canvases[i].width
    }
    ctx.globalCompositeOperation = 'destination-over'
    ctx.fillStyle = "black"
    ctx.fillRect(0, 0, tempCanvas.width + 30, tempCanvas.height);
    ctx.font = "15px serif"
    ctx.fillStyle = "white"
    ctx.globalCompositeOperation = 'source-over'
    /* let dy = 20
     for (let key in gamedata) {
         ctx.fillText(`${key}: ${gamedata[key]}`, width, dy)
         dy += 20
     }*/

    return tempCanvas
}
const downloadUri = (uri) => {
    let link = document.createElement("a");
    link.download = "screenshot";
    link.href = uri;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}
const initScreenshot = () => {
    let screenShotting = false
    Game.prototype.screenshot = function (apiLink) {
        if (screenShotting) return
        let oldTitle = document.title
        document.title = "Screenshotting..."
        screenShotting = true
        this.redrawAll()
        let main = overlayCanvases([document.getElementById("bgLayer"), document.getElementById("myCanvas")])
        let queue = overlayCanvases([document.getElementById("queueCanvas")])
        let hold = overlayCanvases([document.getElementById("holdCanvas")])
        let combined = combineCanvases([hold, main, queue])
        this.Replay.getData()
        let rep = this.Replay.string
        const formData = new FormData();
        combined.toBlob((blob) => {
            formData.append('screenshot', blob);
            formData.append('replay', rep)
            const options = {
                method: 'POST',
                body: formData,
                // If you add this, upload won't work
                // headers: {
                //   'Content-Type': 'multipart/form-data',
                // }
            };
            //    console.log(`${apiLink}uploadScreenshot`)
            fetch(`${apiLink}uploadScreenshot`, options).then(response => {
                response.text().then(val => {
                    if (response.status != 200) {
                        return alert(`err: ${val}`)
                    }
                    let dom = new URL(apiLink)
                    window.open(`https://${dom.hostname}/s/${val}.png`, '_blank')
                })
            }).finally(() => {
                screenShotting = false
                document.title = oldTitle
            });
        })

        //  console.log(combined.toDataURL())
        //     downloadUri(combined.toDataURL())
    }
}


;// CONCATENATED MODULE: ./src/automatic_replay_codes.js


const initAutomaticReplayCodes = () => {
    window.copyReplayText = function (number) {
        var copyText = document.getElementById("replay" + number);
        copyText.select();
        document.execCommand("copy");
        document.getElementById("replayButton" + number).innerHTML = "Copied!"
        setTimeout(() => {
            document.getElementById("replayButton" + number).innerHTML = "Copy"
        }, 1000);

    }

    const oldStartPractice = Game.prototype.startPractice;
    
    Game.prototype.startPractice = function() {
      
      //how many pieces should the replay at least have
      let piecesPlacedCutoff = 1

      if (typeof this['replayCounter'] == "undefined") {
          this['replayCounter'] = 1
      }

      this['Replay']['getData']();

      if (this.GameStats.stats.BLOCKS.value > piecesPlacedCutoff && Config().ENABLE_AUTOMATIC_REPLAY_CODES) {
          let replayHTML = "<div style='font-size:14px;'>Userscript Generated Replay <b>#" + this["replayCounter"] + "</b> </div>";
          replayHTML += '<div style="font-size:16px;">Time:  <b>' + this.GameStats.stats.CLOCK.value + '</b> Blocks:  <b>' + this.GameStats.stats.BLOCKS.value + '</b> Waste:  <b>' + this.GameStats.stats.WASTE.value + '</b> </div>'
          replayHTML += '<textarea id=replay' + this["replayCounter"] + ' readonly style="width:75%;" onclick="this.focus();this.select()">' + this['Replay']['string'] + '</textarea>';
          replayHTML += '<button id=replayButton' + this["replayCounter"] + ' onclick=window.copyReplayText(' + this["replayCounter"] + ')>Copy</button>'
          this["Live"]['chatMajorWarning'](replayHTML);
          this["replayCounter"]++;
      }

      let val = oldStartPractice.apply(this, arguments);

      return val;
    }
}
;// CONCATENATED MODULE: ./src/index.js
















//import { initConnectedSkins } from './connectedSkins';








// inject style
var styleSheet = document.createElement("style");
styleSheet.innerText = style;
document.body.appendChild(styleSheet);

initConfig();
initModal();

if (Config().FIRST_OPEN) {
    alert("Hi! Thank you for installing Jstris+! Remember to turn off all other userscripts and refresh the page before trying to play. Enjoy!")
    Config().set("FIRST_OPEN", false);
}

authNotification()

if (typeof ReplayController == "function") {
    initReplayManager()
    initReplayerSnapshot()
}

if (typeof GameCore == "function") {
    initCustomSkin();
    if (!location.href.includes('export')) {
        initActionText();
        initFX();
        initKeyboardDisplay();
    }
    initStats();
    initCustomSFX();

    initPracticeSurvivalMode();
}
if (typeof Game == "function") {
    initLayout();
    initPracticeUndo();
    initPracticeFumen();
    setPlusSfx(Config().CUSTOM_PLUS_SFX_JSON);
    let pbListener = GameCaption.prototype.newPB;
    GameCaption.prototype.newPB = function () {
        playSound("PB");
        let val = pbListener.apply(this, arguments);
        return val;
    }
    let b4Reset = Live.prototype.beforeReset
    Live.prototype.beforeReset = function () {
        if (!this.p.isTabFocused) {
            notify("Jstris", "⚠ New game starting! ⚠");
        }
        return b4Reset.apply(this, arguments);
    }
    initScreenshot();
    fixTeamsMode();
    initAutomaticReplayCodes();
}
if (typeof Live == "function") initChat();
initReplayerSFX();
initMM();
})();

/******/ })()
;