jstris+

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

// ==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();
})();

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