您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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 + "‑Spin " + clearText; // ‑ is non-breaking hyphen, is non-brekaing space else if (this.spinMiniPossible) clearText = blockName + "‑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(); })(); /******/ })() ;