您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Revamps the deck editor search feature.
// ==UserScript== // @name DuelingNexus Deck Editor Revamp // @namespace https://duelingnexus.com/ // @version 0.9.1 // @description Revamps the deck editor search feature. // @author Sock#3222 // @grant none // @include https://duelingnexus.com/editor/* // ==/UserScript== // TODO: separate search by name/eff // TODO: rehash sort function const EXT = { RESULTS_PER_PAGE: 30, MIN_INPUT_LENGTH: 1, SEARCH_BY_TEXT: true, MAX_UNDO_RECORD: 30, DECK_SIZE_LIMIT: null, Search: { cache: [], current_page: 1, max_page: null, per_page: null, messages: [], }, // contents defined later EDIT_API: {} }; window.EXT = EXT; const FN = { compose: function (f, g) { return function (...inputs) { return f(g(...inputs)); } }, hook: function (f, h, g) { return function (...inputs) { let fv = f(...inputs); let gv = g(...inputs); return h(fv, gv); } }, not: function (x) { return !x; }, or: function (x, y) { return x || y; }, and: function (x, y) { return x && y; }, add: function (x, y) { return x + y; }, fold: function (f, xs, seed = 0) { return xs.reduce(f, seed); }, sum: function (xs) { return FN.fold(FN.add, xs); }, // I heard you like obfuscation xor: function (x, y) { return x ? y ? 0 : 1 : y; }, }; window.FN = FN; const TokenTypes = { OPERATOR: Symbol("TokenTypes.OPERATOR"), OPEN_PAREN: Symbol("TokenTypes.OPEN_PAREN"), CLOSE_PAREN: Symbol("TokenTypes.CLOSE_PAREN"), WHITESPACE: Symbol("TokenTypes.WHITESPACE"), EXPRESSION: Symbol("TokenTypes.EXPRESSION"), UNKNOWN: Symbol("TokenTypes.UNKNOWN"), }; class SearchInputToken { constructor(raw, type, position) { this.raw = raw; this.type = type; this.position = position; } isWhiteSpace() { return this.type === TokenTypes.WHITESPACE; } isData() { return this.type === TokenTypes.EXPRESSION; } isOperator() { return this.type === TokenTypes.OPERATOR; } isOpenParenthesis() { return this.type === TokenTypes.OPEN_PAREN; } isCloseParenthesis() { return this.type === TokenTypes.CLOSE_PAREN; } toString() { // let repr = "Token[ "; // repr += this.position.toString().padStart(2); // repr += ":"; // repr += this.type.toString().padEnd(32); // repr += JSON.stringify(this.raw); // repr += " ]"; // return repr; return "SearchInputToken(" + JSON.stringify(this.raw) + ", " + this.type.toString() + ", " + this.position + ")"; } inspect() { return this.toString(); } } class SearchInputParser { constructor(input) { this.input = input; this.tokens = []; this.index = 0; } get running() { return this.index < this.input.length; } step() { let slice = this.input.slice(this.index); for(let [regex, type] of SearchInputParser.RULES) { let match = slice.match(regex); if(match && match.length) { match = match[0]; if(type === TokenTypes.UNKNOWN) { throw new Error("Unknown token(" + this.index + "): \"" + match + '"'); } this.tokens.push(new SearchInputToken(match, type, this.index)); this.index += match.length; break; } } } parse() { while(this.running) { this.step(); } } static parse(text) { let inst = new SearchInputParser(text); inst.parse(); return inst.tokens; } static parseSignificant(text) { return SearchInputParser.parse(text).filter(token => !token.isWhiteSpace()); } } SearchInputParser.RULE_EXPRESSION = /^((?:[&=]|[!><]=?)\s*)?("(?:[^"]|"")*"|\S+?\b)/; SearchInputParser.RULES = [ [/^\s+/, TokenTypes.WHITESPACE], [/^\(/, TokenTypes.OPEN_PAREN], [/^\)/, TokenTypes.CLOSE_PAREN], [/^\b(?:OR|AND)\b|,/, TokenTypes.OPERATOR], [SearchInputParser.RULE_EXPRESSION, TokenTypes.EXPRESSION], [/^./, TokenTypes.UNKNOWN], ]; // higher = tighter const OPERATOR_PRECEDENCE = { "AND": 100, "OR": 50, }; class SearchInputShunter { constructor(input) { this.tokens = SearchInputParser.parseSignificant(input); this.index = 0; this.operatorStack = []; this.outputQueue = []; } static parseValue(raw) { let wholeMatch = raw.match(SearchInputParser.RULE_EXPRESSION); if(wholeMatch !== null) { let [ match, comp, value ] = wholeMatch; // default to = for comparison raw = (comp || "=") + value; } raw = raw.replace(/"((?:[^"]|"")*)"/g, function (match, inner) { // undouble escapes return inner.replace(/""/g, '"'); }); return raw; } get operatorStackTop() { return this.operatorStack[this.operatorStack.length - 1]; } get running() { return this.index < this.tokens.length; } parseToken(token) { if(token.isData()) { // idc if this is impure shunting procedures, it works! let modifiedToken = new SearchInputToken(SearchInputShunter.parseValue(token.raw), token.type, token.position); this.outputQueue.push(modifiedToken); } else if(token.isOpenParenthesis()) { this.operatorStack.push(token); } else if(token.isCloseParenthesis()) { while(this.operatorStackTop && !this.operatorStackTop.isOpenParenthesis()) { this.outputQueue.push(this.operatorStack.pop()); } if(this.operatorStackTop && this.operatorStackTop.isOpenParenthesis()) { this.operatorStack.pop(); } else { throw new Error("Unbalanced parentheses"); } } else if(token.isOperator()) { let currentPrecedence = OPERATOR_PRECEDENCE[token.raw]; while(this.operatorStackTop && OPERATOR_PRECEDENCE[this.operatorStackTop.raw] > currentPrecedence) { this.outputQueue.push(this.operatorStack.pop()); } this.operatorStack.push(token); } } shunt() { for(let token of this.tokens) { this.parseToken(token); } while(this.operatorStackTop) { this.outputQueue.push(this.operatorStack.pop()); } } static parseSections(text) { let shunter = new SearchInputShunter(text); shunter.shunt(); return shunter.outputQueue; } } let onStart = function () { // minified with https://kangax.github.io/html-minifier/ const ADVANCED_SETTINGS_HTML_STRING = ` <div id=rs-ext-advanced-search-bar> <button id=rs-ext-monster-toggle class="engine-button engine-button-default">monster</button> <button id=rs-ext-spell-toggle class="engine-button engine-button-default">spell</button> <button id=rs-ext-trap-toggle class="engine-button engine-button-default">trap</button> <button id=rs-ext-sort-toggle class="engine-button engine-button-default rs-ext-right-float">sort</button> <div id=rs-ext-advanced-pop-outs> <div id=rs-ext-sort class="rs-ext-shrinkable rs-ext-shrunk"> <table id=rs-ext-sort-table class=rs-ext-table> <tr> <th>Sort By</th> <td> <select class=rs-ext-input id=rs-ext-sort-by> <option>Name</option> <option>Level</option> <option>ATK</option> <option>DEF</option> </select> </td> </tr> <tr> <th>Sort Order</th> <td> <select class=rs-ext-input id=rs-ext-sort-order> <option>Ascending</option> <option>Descending</option> </select> </td> </tr> <tr> <th>Stratify?</th> <td><input type=checkbox id=rs-ext-sort-stratify checked></td> </tr> </table> </div> <div id=rs-ext-spell class="rs-ext-shrinkable rs-ext-shrunk"> <table> <tr> <th>Spell Card Type</th> <td> <select id=rs-ext-spell-type> <option></option> <option>Normal</option> <option>Quick-play</option> <option>Field</option> <option>Continuous</option> <option>Ritual</option> <option>Equip</option> </select> </td> </tr> <tr> <th>Limit</th> <td><input class=rs-ext-input id=rs-ext-spell-limit></td> </tr> </table> </div> <div id=rs-ext-trap class="rs-ext-shrinkable rs-ext-shrunk"> <table> <tr> <th>Trap Card Type</th> <td> <select id=rs-ext-trap-type> <option></option> <option>Normal</option> <option>Continuous</option> <option>Counter</option> </select> </td> </tr> <tr> <th>Limit</th> <td><input class=rs-ext-input id=rs-ext-trap-limit></td> </tr> </table> </div> <div id=rs-ext-monster class="rs-ext-shrinkable rs-ext-shrunk"> <table class="rs-ext-left-float rs-ext-table"id=rs-ext-link-arrows> <tr> <th colspan=3>Link Arrows</th> </tr> <tr> <td><button class=rs-ext-toggle-button>↖</button></td> <td><button class=rs-ext-toggle-button>↑</button></td> <td><button class=rs-ext-toggle-button>↗</button></td> </tr> <tr> <td><button class=rs-ext-toggle-button>←</button></td> <td><button class=rs-ext-toggle-button id=rs-ext-equals>=</button></td> <td><button class=rs-ext-toggle-button>→</button></td> </tr> <tr> <td><button class=rs-ext-toggle-button>↙</button></td> <td><button class=rs-ext-toggle-button>↓</button></td> <td><button class=rs-ext-toggle-button>↘</button></td> </tr> </table> <div id=rs-ext-monster-table class="rs-ext-left-float rs-ext-table"> <table> <tr> <th>Category</th> <td> <select class=rs-ext-input id=rs-ext-monster-category> <option></option> <option>Normal</option> <option>Effect</option> <option>Ritual</option> <option>Fusion</option> <option>Synchro</option> <option>Xyz</option> <option>Pendulum</option> <option>Link</option> <option>Leveled</option> <option>Extra Deck</option> <option>Non-Effect</option> <option>Gemini</option> <option>Flip</option> <option>Spirit</option> <option>Toon</option> </select> </td> </tr> <tr> <th>Ability</th> <td> <select id=rs-ext-monster-ability class=rs-exit-input> <option></option> <option>Tuner</option> <option>Toon</option> <option>Spirit</option> <option>Union</option> <option>Gemini</option> <option>Flip</option> <option>Pendulum</option> </select> </td> </tr> <tr> <th>Type</th> <td> <select id=rs-ext-monster-type class=rs-ext-input> <option></option> <option>Aqua</option> <option>Beast</option> <option>Beast-Warrior</option> <option>Cyberse</option> <option>Dinosaur</option> <option>Dragon</option> <option>Fairy</option> <option>Fiend</option> <option>Fish</option> <option>Insect</option> <option>Machine</option> <option>Plant</option> <option>Psychic</option> <option>Pyro</option> <option>Reptile</option> <option>Rock</option> <option>Sea Serpent</option> <option>Spellcaster</option> <option>Thunder</option> <option>Warrior</option> <option>Winged Beast</option> <option>Wyrm</option> <option>Zombie</option> <option>Creator God</option> <option>Divine-Beast</option> </select> </td> </tr> <tr> <th>Attribute</th> <td> <select id=rs-ext-monster-attribute class=rs-ext-input> <option></option> <option>DARK</option> <option>EARTH</option> <option>FIRE</option> <option>LIGHT</option> <option>WATER</option> <option>WIND</option> <option>DIVINE</option> </select> </td> </tr> <tr> <th>Limit</th> <td><input class=rs-ext-input id=rs-ext-monster-limit></td> </tr> <tr> <th>Level/Rank/Link Rating</th> <td><input class=rs-ext-input id=rs-ext-level></td> </tr> <tr> <th>Pendulum Scale</th> <td><input class=rs-ext-input id=rs-ext-scale></td> </tr> <tr> <th>ATK</th> <td><input class=rs-ext-input id=rs-ext-atk></td> </tr> <tr> <th>DEF</th> <td><input class=rs-ext-input id=rs-ext-def></td> </tr> </table> </div> </div> </div> <div id=rs-ext-spacer></div> </div> `; const ADVANCED_SETTINGS_HTML_ELS = jQuery.parseHTML(ADVANCED_SETTINGS_HTML_STRING); ADVANCED_SETTINGS_HTML_ELS.reverse(); // minified with cssminifier.com const ADVANCED_SETTINGS_CSS_STRING = "#rs-ext-advanced-search-bar{width:100%}.rs-ext-toggle-button{width:3em;height:3em;background:#ddd;border:1px solid #000}.rs-ext-toggle-button:hover{background:#fff}button.rs-ext-selected{background:#00008b;color:#fff}button.rs-ext-selected:hover{background:#55d}.rs-ext-left-float{float:left}.rs-ext-right-float{float:right}.rs-ext-shrinkable{transition-property:transform;transition-duration:.3s;transition-timing-function:ease-out;height:auto;background:#ccc;width:100%;transform:scaleY(1);transform-origin:top;overflow:hidden;z-index:10000}.rs-ext-shrinkable>*{margin:10px}#rs-ext-monster,#rs-ext-spell,#rs-ext-trap,#rs-ext-sort{background:rgba(0,0,0,.7)}.rs-ext-shrunk{transform:scaleY(0);z-index:100}#rs-ext-advanced-pop-outs{position:relative}#rs-ext-advanced-pop-outs>.rs-ext-shrinkable{position:absolute;top:0;left:0}#rs-ext-monster-table th,#rs-ext-sort-table th{text-align:right}.rs-ext-table{padding-right:5px}#rs-ext-spacer{height:0;transition:height .3s ease-out}#rs-ext-sort{transition-property:top,transform}.engine-button[disabled],.engine-button:disabled{cursor:not-allowed;background:rgb(50,0,0);color:#a0a0a0;font-style:italic;}.rs-ext-card-entry-table button,.rs-ext-fullwidth-wrapper{width:100%;height:100%;}.rs-ext-card-entry-table{border-collapse:collapse;}.rs-ext-card-entry-table tr,.rs-ext-card-entry-table td{height:100%;}.editor-search-description{white-space:normal;}#editor-menu-spacer{width:15%;}.engine-button{cursor:pointer;}@media (min-width:1600px){.editor-search-result{font-size:1em}.editor-search-card{width:12.5%}.editor-search-banlist-icon{width:5%}.editor-search-result{width:100%}}.rs-ext-table-button{padding:8px;text-align:center;border:1px solid #AAAAAA;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}.rs-ext-table-button:active{padding:8px 7px 8px 9px;}.rs-ext-flex{display:flex;width:100%;justify-content:space-between}.rs-ext-flex input{width:48%;}input::placeholder{font-style:italic;text-align:center;}"; // disable default listener (Z.Pb) $("#editor-search-text").off("input"); // VOLATILE FUNCTIONS, MAY CHANGE AFTER A MAIN UPDATE /* reload cards until pendulum hotfix */ // const CARD_LIST = X; const CARD_LIST = {}; const CardObject = function (a) { this.id = a.id; this.A = a.als || 0; this.za = a.sc || []; this.type = a.typ || 0; this.attack = a.atk || 0; this.i = a.def || 0; var b = a.lvl || 0; this.race = a.rac || 0; this.H = a.att || 0; this.level = b & 0xFF; this.lscale = (b >> 24) & 0xFF; this.rscale = (b >> 16) & 0xFF; }; const CARD_LIST_VERSION = 84; const readCards = function (a) { jQuery.ajaxSetup({ beforeSend: function(a) { a.overrideMimeType && a.overrideMimeType("application/json") } }); jQuery.getJSON(h(`data/cards.json?v=${CARD_LIST_VERSION}`), function(b) { for (let card of b.cards) { CARD_LIST[card.id] = new CardObject(card); } // NOTE: different request from the above jQuery.getJSON(h(`data/cards_en.json?v=${CARD_LIST_VERSION}`), function(b) { for (let card of b.texts) { let other = CARD_LIST[card.id]; if (other) { other.name = card.n; other.description = card.d; other.ra = card.s || []; other.Z = qa(other.name); } } if(a) { a(); } }) }) }; readCards(); const TYPE_HASH = bg; const TYPE_LIST = Object.values(TYPE_HASH); const ATTRIBUTE_MASK_HASH = Yf; const ATTRIBUTE_HASH = {}; for(obfs in ATTRIBUTE_MASK_HASH) { let attr = ag[obfs].toUpperCase(); ATTRIBUTE_HASH[attr] = ATTRIBUTE_MASK_HASH[obfs]; } const attributeOf = function (cardObject) { return cardObject.H; } const makeSearchable = function (name) { return name.replace(/ /g, "") .toUpperCase(); } const searchableCardName = function (id_or_card) { let card; if(typeof id_or_card === "number") { card = CARD_LIST[id_or_card]; } else { card = id_or_card; } let searchable = makeSearchable(card.name); // cache name if(!card.searchName) { card.searchName = searchable; } return searchable; } const monsterType = function (card) { return Ef[card.race]; } // U provides a map // (a.type & U[c]) => (Vf[U[c]]) const monsterTypeMap = {}; for(let key in Wf) { let value = Wf[key]; monsterTypeMap[value] = parseInt(key, 10); } window.monsterTypeMap = monsterTypeMap; const allowedCount = function (card) { // card.A = the source id (e.g. for alt arts) // card.id = the actual id return Vf(card.A || card.id); } const clearVisualSearchOptions = function () { return Z.Db(); } const isPlayableCard = function (card) { // internally checks U.U - having that bit means its not a playable card // (reserved for tokens, it seems) return Z.ua(card); } const sanitizeText = function (text) { // qa - converts to uppercase and removes: // newlines, hyphens, spaces, colons, and periods return qa(text); } const cardCompare = function (cardA, cardB) { return Z.fa(cardA, cardB); } const shuffle = function (list) { return Z.Nb(list); } // clears the visual state, allowing editing to take place const clearVisualState = function () { Z.pa(); } // restores the visual state const restoreVisualState = function () { Z.ma(); } const lastElement = function (arr) { return arr[arr.length - 1]; } // modified from https://stackoverflow.com/a/6969486/4119004 const escapeRegex = function (string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const previewCard = Cc; // corresponds to EDIT_LOCATION where save was clicked // EXT.EDIT_API.SAVED_INDEX = 0; // const saveDeck = function () { // Z.Ob(); // $("#editor-save-button").addClass("engine-button-disabled"); // $.post("api/update-deck.php", { // id: window.Deck.id, // deck: JSON.stringify({ // main: window.Deck.main, // extra: window.Deck.extra, // side: window.Deck.side // }) // }, function(a) { // a.success || "no_changes" === a.error ? $("#editor-save-button").text("Saved!").delay(2E3).queue(function() { // $(this).removeClass("engine-button-disabled").text("Save").dequeue() // }) : // $("#editor-save-button").text("Error while saving: " + a.error).delay(5E3).queue(function() { // $(this).removeClass("engine-button-disabled").text("Save").dequeue() // }) // }, "json"); // } // EXT.EDIT_API.save = saveDeck; // re-attach listener // let saveButton = document.getElementById("editor-save-button"); // $(saveButton).unbind(); // saveButton.addEventListener("click", saveDeck); // capture page beforeunload // window.addEventListener("beforeunload", function (ev) { // if(EXT.EDIT_API.SAVED_INDEX !== EXT.EDIT_API.EDIT_LOCATION) { // ev.preventDefault(); // PREFERED METHOD (not supported universally) // return event.returnValue = ""; // deprecated // } // return null; // }); const engineButton = function (content, id = null) { let button = makeElement("button", id, content); button.classList.add("engine-button", "engine-button-default"); return button; } // Z.Ba = function() { // if(Z.selection) { // console.log("tick"); // Z.selection.css("left", Z.Ib + 3).css("top", Z.Jb + 3); // } // }; // reimplements Z.la const banlistIcons = { 3: null, 2: "banlist-semilimited.png", 1: "banlist-limited.png", 0: "banlist-banned.png", }; const addCardToSearchWithButtons = function (cardId) { let card = CARD_LIST[cardId]; let template = $(Z.Lb); template.find(".template-name").text(card.name); l(template.find(".template-picture"), card.id); if (card.type & U.L) { template.find(".template-if-spell").remove(); template.find(".template-level").text(card.level); template.find(".template-atk").text(card.attack); template.find(".template-def").text(card.i); // Df - object indexed by powers of 2 containg *type* information // card.race = type template.find(".template-race").text(Ef[card.race]); // Ef - object indexed by powers of 2 containing *attribute* information // card.H = attribute template.find(".template-attribute").text(Ff[card.H]); } else { template.find(".template-if-monster").remove(); var types = []; for (let n of Object.values(U)) { if(card.type & n) { types.push(Wf[n]); } } template.find(".template-types").text(types.join("|")); } template.data("id", card.id); template.mouseover(function () { previewCard($(this).data("id")); }); template.mousedown(function (a) { // left mouse - move card to tooltip if (1 == a.which) { let id = $(this).data("id"); Z.ya(id); return false; } // right mouse - do nothing (allow contextmenu to trigger) if (3 == a.which) { return false; } }); let addThisCard = function (el, destination = null) { let id = $(el).data("id"); let card = CARD_LIST[id]; // deduce destination if(!destination) { destination = "main"; if (card.type & U.S || card.type & U.T || card.type & U.G || card.type & U.C) { destination = "extra"; } } addCard(id, destination, -1); return false; } template.on("contextmenu", function () { return addThisCard(this); }); /* BANLIST TOKEN GENERATION */ var banlistIcon = template.find(".editor-search-banlist-icon"); let limitStatus = allowedCount(card); if(limitStatus !== 3) { banlistIcon.attr("src", "assets/images/" + banlistIcons[limitStatus]); } else { banlistIcon.remove(); } let container = $("<table width=100% class=rs-ext-card-entry-table><tr><td width=74%></td><td width=13% class=rs-ext-table-button>Add to Main</td><td width=13% class=rs-ext-table-button>Add to Side</td></tr></table>"); let [ cardTd, mainTd, sideTd ] = container.find("td"); cardTd.append(...template); // let mainDeckAdd = engineButton("Add to Main"); mainTd.addEventListener("click", function () { addThisCard(template); }); // mainTd.append(mainDeckAdd); // let sideDeckAdd = engineButton("Add to Side"); sideTd.addEventListener("click", function () { addThisCard(template, "side"); }); // sideTd.append(sideDeckAdd); Z.xa.append(container); } const addCardToSearch = function (card) { // return Z.la(card); return addCardToSearchWithButtons(card); } // interaction stuff const pluralize = function (noun, count, suffix = "s", base = "") { return noun + (count == 1 ? base : suffix); } // card identification stuff const isToken = (card) => card.type & monsterTypeMap["Token"]; const isTrapCard = (card) => card.type & monsterTypeMap["Trap"]; const isSpellCard = (card) => card.type & monsterTypeMap["Spell"]; const isRitualSpell = (card) => isSpellCard(card) && (card.type & monsterTypeMap["Ritual"]); const isContinuous = (card) => card.type & monsterTypeMap["Continuous"]; const isCounter = (card) => card.type & monsterTypeMap["Counter"]; const isField = (card) => card.type & monsterTypeMap["Field"]; const isEquip = (card) => card.type & monsterTypeMap["Equip"]; const isQuickPlay = (card) => card.type & monsterTypeMap["Quick-Play"]; const isSpellOrTrap = (card) => isSpellCard(card) || isTrapCard(card); const nonNormalSpellTraps = [isContinuous, isQuickPlay, isField, isCounter, isEquip, isRitualSpell]; const isNormalSpellOrTrap = (card) => isSpellOrTrap(card) && !nonNormalSpellTraps.some(cond => cond(card)); const isNormalMonster = (card) => card.type & monsterTypeMap["Normal"]; const isEffectMonster = (card) => card.type & monsterTypeMap["Effect"]; const isMonster = (card) => !isTrapCard(card) && !isSpellCard(card); const isNonEffectMonster = (card) => !isEffectMonster(card); const isFusionMonster = (card) => card.type & monsterTypeMap["Fusion"]; const isRitualMonster = (card) => isMonster(card) && (card.type & monsterTypeMap["Ritual"]); const isSynchroMonster = (card) => card.type & monsterTypeMap["Synchro"]; const isTunerMonster = (card) => card.type & monsterTypeMap["Tuner"]; const isLinkMonster = (card) => card.type & monsterTypeMap["Link"]; const isGeminiMonster = (card) => card.type & monsterTypeMap["Dual"]; const isToonMonster = (card) => card.type & monsterTypeMap["Toon"]; const isFlipMonster = (card) => card.type & monsterTypeMap["Flip"]; const isSpiritMonster = (card) => card.type & monsterTypeMap["Spirit"]; const isXyzMonster = (card) => card.type & monsterTypeMap["Xyz"]; const isPendulumMonster = (card) => card.type & monsterTypeMap["Pendulum"]; const isExtraDeckMonster = (card) => [ isFusionMonster, isSynchroMonster, isXyzMonster, isLinkMonster ].some(fn => fn(card)); const isLevelMonster = (card) => isMonster(card) && !isLinkMonster(card) && !isXyzMonster(card); // non-ritual main deck monster const isBasicMainDeckMonster = (card) => isMonster(card) && !isRitualMonster(card) && !isExtraDeckMonster(card); let kindMap = { "TRAP": isTrapCard, "SPELL": isSpellCard, "MONSTER": isMonster, "CONT": isContinuous, "CONTINUOUS": isContinuous, "COUNTER": isCounter, "FIELD": isField, "EQUIP": isEquip, "QUICK": isQuickPlay, "QUICKPLAY": isQuickPlay, "NORMAL": isNormalMonster, "NORMALST": isNormalSpellOrTrap, "EFFECT": isEffectMonster, "NONEFF": isNonEffectMonster, "NONEFFECT": isNonEffectMonster, "FUSION": isFusionMonster, "RITUAL": isRitualMonster, "RITUALST": isRitualSpell, "TUNER": isTunerMonster, "LINK": isLinkMonster, "SYNC": isSynchroMonster, "SYNCHRO": isSynchroMonster, "DUAL": isGeminiMonster, "GEMINI": isGeminiMonster, "TOON": isToonMonster, "FLIP": isFlipMonster, "SPIRIT": isSpiritMonster, "XYZ": isXyzMonster, "PENDULUM": isPendulumMonster, "PEND": isPendulumMonster, "LEVELED": isLevelMonster, "EXTRA": isExtraDeckMonster, }; const allSatisfies = function (tags, card) { return tags.every(tag => tag(card)); } const defaultSearchOptionState = function () { clearVisualSearchOptions(); }; const displayResults = function () { defaultSearchOptionState(); if(EXT.Search.cache.length !== 0) { replaceTextNode(currentPageIndicator, EXT.Search.current_page); let startPosition = (EXT.Search.current_page - 1) * EXT.Search.per_page; let endPosition = Math.min( EXT.Search.cache.length, startPosition + EXT.Search.per_page ); for(let i = startPosition; i < endPosition; i++) { addCardToSearch(EXT.Search.cache[i].id); } } clearChildren(infoBox); for(let container of EXT.Search.messages) { let [kind, message] = container; let color, symbol; switch(kind) { case STATUS.ERROR: color = GUI_COLORS.HEADER.FAILURE; symbol = SYMBOLS.ERROR; break; case STATUS.SUCCESS: color = GUI_COLORS.HEADER.SUCCESS; symbol = SYMBOLS.SUCCESS; break; case STATUS.NEUTRAL: default: color = GUI_COLORS.HEADER.NEUTRAL; symbol = SYMBOLS.INFO; break; } let symbolElement = document.createElement("span"); let messageElement = document.createElement("span"); appendTextNode(symbolElement, symbol); symbolElement.style.padding = "3px"; appendTextNode(messageElement, message); let alignTable = document.createElement("table"); alignTable.style.backgroundColor = color; alignTable.style.padding = "2px"; alignTable.appendChild(makeElement("tr")); alignTable.children[0].appendChild(makeElement("td")); alignTable.children[0].children[0].appendChild(symbolElement); alignTable.children[0].appendChild(makeElement("td")); alignTable.children[0].children[1].appendChild(messageElement); infoBox.appendChild(alignTable); } } const STATUS = { ERROR: 0, NEUTRAL: -1, SUCCESS: 1 }; const addMessage = function (kind, message) { EXT.Search.messages.push([kind, message]); } const initializeMessageContainer = function () { EXT.Search.messages = []; } // gui/dom manipulation stuff const clearChildren = function (el) { while(el.firstChild) { el.removeChild(el.firstChild); } return true; } const appendTextNode = function (el, text) { let textNode = document.createTextNode(text); el.appendChild(textNode); return true; } const makeElement = function (name, id = null, content = null, opts = {}) { let el = document.createElement(name); if(id !== null) { el.id = id; } if(content !== null) { appendTextNode(el, content); } for(let [key, val] of Object.entries(opts)) { el[key] = val; } return el; } const HTMLTag = function (tag, id, classes, ...children) { let el = makeElement(tag, id); if(classes) { el.classList.add(...classes); } for(let child of children) { el.appendChild(child); } return el; } const replaceTextNode = function (el, newText) { clearChildren(el); appendTextNode(el, newText); } const NO_SELECT_PROPERTIES = [ "-webkit-touch-callout", "-webkit-user-select", "-khtml-user-select", "-moz-user-select", "-ms-user-select", "user-select", ]; // TODO: select based on browser? const noSelect = function (el) { NO_SELECT_PROPERTIES.forEach(property => { el.style[property] = "none"; }); } const GUI_COLORS = { HEADER: { NEUTRAL: "rgba(0, 0, 0, 0.7)", SUCCESS: "rgba(0, 150, 0, 0.7)", FAILURE: "rgba(150, 0, 0, 0.7)" } }; const SYMBOLS = { SUCCESS: "✔", INFO: "🛈", ERROR: "⚠", }; let styleHeaderNeutral = function (el) { el.style.background = GUI_COLORS.HEADER.NEUTRAL; el.style.fontSize = "16px"; el.style.padding = "3px"; el.style.marginBottom = "10px"; } // https://stackoverflow.com/a/30832210/4119004 const download = function promptSaveFile (data, filename, type) { var file = new Blob([data], {type: type}); if (window.navigator.msSaveOrOpenBlob) // IE10+ window.navigator.msSaveOrOpenBlob(file, filename); else { // Others var a = document.createElement("a"), url = URL.createObjectURL(file); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(function() { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 0); } } /* TODO: allow larger deck sizes */ /* let deckSizes = { "main": [ 40, 60, 100], "extra": [ 10, 15, 25], "side": [ 10, 15, 25], }; // if length > corresponding cell in deckSizes, pad by this much let cardMargins = { "main": [ -3.6, -4.05, -6.00], "extra": [ -3.6, -4.05, -6.00], "side": [ -3.6, -4.05, -6.00], }; Z.Aa = function (a) { var padding = "0"; // let if(Z[a].length > ("main" == a ? 80 : 25)) { // padding = "main" == a ? "-4.05%" : "-4.72%"; padding = "-6.0%"; } else if(Z[a].length > ("main" == a ? 60 : 15)) { padding = "main" == a ? "-4.05%" : "-4.72%"; } else if(Z[a].length > ("main" == a ? 40 : 10)) { padding = "-3.6%"; } if (Z.ha[a] !== padding) { Z.ha[a] = padding; for (var c = 0; c < Z[a].length; ++c) Z[a][c].css("margin-right", padding); } }, */ /* UNDO / REDO */ // const addCardSilent = Z.O; const addCardSilent = function(a, destination, c, d) { let card = X[a]; if (card && !isToken(card)) { let toMainDeck = destination === "main"; let toExtraDeck = destination === "extra"; let isExtra = isExtraDeckMonster(card); let sizeLimit = EXT.DECK_SIZE_LIMIT || toMainDeck ? 60 : 15; let isInvalidLocation = ( isExtra && toMainDeck || !isExtra && toExtraDeck || Z[destination].length >= sizeLimit // cannot have more than 3 copies! || Z.Eb(card.A ? card.A : card.id) >= 3 ); if (!isInvalidLocation) { g = Z[destination]; var k = $("#editor-" + destination + "-deck"); d = $("<img>").css("margin-right", Z.ha[destination]).addClass("editor-card-small"); l(d, a); d.mouseover(function() { previewCard($(this).data("id")); }); d.mousedown(function(a) { if (1 == a.which) { a = $(this).data("id"); var b = $(this).data("location"), c = $(this).parent().children().index($(this)); Z.P(b, c); Z.ya(a); return false } if (3 == a.which) return false }); d.mouseup(function(a) { if (1 == a.which && Z.selection) { a = $(this).data("location"); var b = $(this).parent().children().index($(this)); addCard(Z.selection.data("id"), a, b); Z.aa(); return false } }); d.on("contextmenu", function() { var a = $(this).data("location"), b = $(this).parent().children().index($(this)); Z.P(a, b); return false }); d.data("id", a); d.data("alias", card.A); d.data("location", destination); if(c === -1) { k.append(d); g.push(d); } else { if(0 === c && 0 == k.children().length) { k.append(d) } else { k.children().eq(c).before(d); g.splice(c, 0, d); }; } g = allowedCount(card); 3 !== g ? (a = 2 === g ? "banlist-semilimited.png" : 1 === g ? "banlist-limited.png" : "banlist-banned.png", a = $("<img>").attr("src", "assets/images/" + a), d.data("banlist", a), $("#editor-banlist-icons").append(a)) : d.data("banlist", null); Z.Aa(destination); Z.N(destination) } } }; const removeCardSilent = Z.P; const addCard = function (...args) { addCardSilent(...args); addEditPoint(); }; Z.O = addCard; EXT.EDIT_API.addCard = addCard; EXT.EDIT_API.addCardSilent = addCardSilent; const removeCard = function (...args) { removeCardSilent(...args); addEditPoint(); }; Z.P = removeCard; EXT.EDIT_API.removeCard = removeCard; EXT.EDIT_API.removeCardSilent = removeCardSilent; const deepCloneArray = function (arr) { return arr.map ? arr.map(deepCloneArray) : arr; }; const isRawObject = (obj) => typeof obj === "object"; const deepEquals = function (left, right) { if(left.map && right.map) { if(left.length !== right.length) { return false; } return left.every((e, i) => deepEquals(e, right[i])); } else { if(isRawObject(left) && isRawObject(right)) { let leftEntries = Object.entries(left).sort(); let rightEntries = Object.entries(right).sort(); return deepEquals(leftEntries, rightEntries); } return left === right; } }; const mapIDs = function (arr) { return arr.map(e => e.data("id")); }; const currentDeckState = function () { return { main: mapIDs(Z.main), extra: mapIDs(Z.extra), side: mapIDs(Z.side) }; } const clearLocation = function (location) { while(Z[location].length) { removeCardSilent(location, 0); } }; const clearDeck = function () { for (let location of ["main", "extra", "side"]) { clearLocation(location); } }; // for use in undo/redo - replaces the current state with the new state /* TypeError: Z[c][e].appendTo is not a function engine.min.js:144:115 ma https://duelingnexus.com/script/engine.min.js?v=187:144 restoreVisualState debugger eval code:387 updateDeckState debugger eval code:933 undo debugger eval code:987 */ const updateDeckState = function (newState) { let state = currentDeckState(); clearVisualState(); overMainExtraSide((contents, location) => { if(deepEquals(contents, newState[location])) { // console.warn("EXT.EDIT_API.updateDeckState - new and old sections for " + location + " are equal, not updating"); return; } clearLocation(location); for (let id of newState[location]) { addCardSilent(id, location, -1); } }); restoreVisualState(); }; EXT.EDIT_API.updateDeckState = updateDeckState; EXT.EDIT_API.currentDeckState = currentDeckState; EXT.EDIT_API.EDIT_HISTORY = [ currentDeckState() ]; EXT.EDIT_API.EDIT_LOCATION = 0; const addEditPoint = function () { let state = currentDeckState(); let previousState = lastElement(EXT.EDIT_API.EDIT_HISTORY); // do not add a duplicate entry if(previousState && deepEquals(state, previousState)) { return false; } // remove all entries past the current edit location if(EXT.EDIT_API.EDIT_LOCATION >= EXT.EDIT_API.EDIT_HISTORY.length) { EXT.EDIT_API.EDIT_HISTORY.splice(EXT.EDIT_API.EDIT_LOCATION); } // add the state EXT.EDIT_API.EDIT_HISTORY.push(state); // remove a state from the front if we have too many if(EXT.EDIT_API.EDIT_HISTORY.length > EXT.MAX_UNDO_RECORD) { EXT.EDIT_API.EDIT_HISTORY.shift(); } // update the edit location EXT.EDIT_API.EDIT_LOCATION = EXT.EDIT_API.EDIT_HISTORY.length - 1; // we can no longer redo, but now we can undo undoButton.disabled = false; redoButton.disabled = true; return true; }; EXT.EDIT_API.addEditPoint = addEditPoint; const undo = function () { // we can't undo if there's nothing behind us if(EXT.EDIT_API.EDIT_LOCATION === 0) { console.warn("EXT.EDIT_API.undo function failed - nothing to undo!"); return false; } // we moved back one EXT.EDIT_API.EDIT_LOCATION--; // refresh the deck state updateDeckState(EXT.EDIT_API.EDIT_HISTORY[EXT.EDIT_API.EDIT_LOCATION]); // we can now redo redoButton.disabled = false; // disable the undo button if there is nothing left to undo if(EXT.EDIT_API.EDIT_LOCATION === 0) { undoButton.disabled = true; } return true; } EXT.EDIT_API.undo = undo; const redo = function () { // if we're at the front, we can't redo if(EXT.EDIT_API.EDIT_LOCATION === EXT.EDIT_API.EDIT_HISTORY.length - 1) { console.warn("EXT.EDIT_API.redo function failed - nothing to redo!"); return false; } // move forward once EXT.EDIT_API.EDIT_LOCATION++; // refresh the deck state updateDeckState(EXT.EDIT_API.EDIT_HISTORY[EXT.EDIT_API.EDIT_LOCATION]); // we can now undo undoButton.disabled = false; // disable the redo button if there is nothing left to redo if(EXT.EDIT_API.EDIT_LOCATION === EXT.EDIT_API.EDIT_HISTORY.length - 1) { redoButton.disabled = true; } return true; } EXT.EDIT_API.redo = redo; /* reimplementing nexus buttons with edit history enabled */ const clear = function () { clearVisualState(); overMainExtraSide((contents, location) => { Z[location] = []; }); restoreVisualState(); addEditPoint(); } EXT.EDIT_API.clear = clear; // reset listener for editor's clear button let clearButton = document.getElementById("editor-clear-button"); $(clearButton).unbind(); clearButton.addEventListener("click", clear); const overMainExtraSide = function (it) { for(let name of ["main", "extra", "side"]) { it(Z[name], name); } }; // sorts everything const sort = function () { clearVisualState(); // for (var a = ["main", "extra", "side"], b = 0; b < a.length; ++b) { // var c = a[b]; // Z[c].sort(function(a, b) { // return Z.fa(X[a.data("id")], X[b.data("id")]) // }); // Z.N(c); // } overMainExtraSide((contents, location) => { contents.sort((c1, c2) => Z.fa(X[c1.data("id")], X[c2.data("id")]) ); Z.N(location); }); restoreVisualState(); addEditPoint(); }; // reset listener for editor's sort button let sortButton = document.getElementById("editor-sort-button"); $(sortButton).unbind(); sortButton.addEventListener("click", sort); const shuffleAll = function () { clearVisualState(); overMainExtraSide((contents, location) => { Z.Nb(contents); Z.N(location); }); restoreVisualState(); addEditPoint(); } let shuffleButton = document.getElementById("editor-shuffle-button"); $(shuffleButton).unbind(); shuffleButton.addEventListener("click", shuffleAll); // code for Export readable const countIn = function (arr, el) { return arr.filter(e => e === el).length; } const namesOf = function (el) { let names = [...el.children].map(e => CARD_LIST[$(e).data("id")].name); let uniq = [...new Set(names)]; return uniq.map(name => `${countIn(names, name)}x ${name}` ); } const outputDeck = function () { let decks = { Main: "editor-main-deck", Extra: "editor-extra-deck", Side: "editor-side-deck", }; return Object.entries(decks).map(([key, value]) => [key + " Deck:", ...namesOf(document.getElementById(value))] .join("\n") ).join("\n--------------\n"); } /* UPDATE PAGE STRUCTURE */ // add new buttons let editorMenuContent = document.getElementById("editor-menu-content"); let undoButton = makeElement("button", "rs-ext-editor-export-button", "Undo"); undoButton.classList.add("engine-button", "engine-button", "engine-button-default"); appendTextNode(editorMenuContent, " "); editorMenuContent.appendChild(undoButton); undoButton.disabled = true; undoButton.addEventListener("click", undo); let redoButton = makeElement("button", "rs-ext-editor-export-button", "Redo"); redoButton.classList.add("engine-button", "engine-button", "engine-button-default"); appendTextNode(editorMenuContent, " "); editorMenuContent.appendChild(redoButton); redoButton.disabled = true; redoButton.addEventListener("click", redo); let exportButton = makeElement("button", "rs-ext-editor-export-button", "Export .ydk"); exportButton.classList.add("engine-button", "engine-button", "engine-button-default"); exportButton.title = "Export Saved Version of Deck"; appendTextNode(editorMenuContent, " "); editorMenuContent.appendChild(exportButton); exportButton.addEventListener("click", function () { let lines = [ "#created by RefinedSearch plugin" ]; for(let kind of ["main", "extra", "side"]) { let header = (kind === "side" ? "!" : "#") + kind; lines.push(header); // TODO: add option to use card.A rather than card.id lines.push(...Deck[kind]); } let message = lines.join("\n"); download(message, Deck.name + ".ydk", "text"); }); let exportRawButton = makeElement("button", "rs-ext-editor-export-button", "Export Readable"); exportRawButton.classList.add("engine-button", "engine-button", "engine-button-default"); exportRawButton.title = "Export Human Readable Version of Deck"; appendTextNode(editorMenuContent, " "); editorMenuContent.appendChild(exportRawButton); exportRawButton.addEventListener("click", function () { let message = outputDeck(); download(message, Deck.name + ".txt", "text"); }); // add options tile let optionsArea = makeElement("div", "options-area"); /* USES NEXUS DEFAULT CSS/ID */ let options = makeElement("button", "rs-ext-options"); options.classList.add("engine-button", "engine-button", "engine-button-default"); let cog = makeElement("i"); cog.classList.add("fa", "fa-cog"); options.appendChild(cog); appendTextNode(options, " Options"); optionsArea.appendChild(options); // TODO: implement option toggle area // document.body.appendChild(optionsArea); let optionsWindow; /* USES NEXUS DEFAULT CSS/ID */ let overflowDeckSizeElement; //; // makeElement("div", "options-window") optionsWindow = HTMLTag("div", "options-window", null, HTMLTag("p", null, null, overflowDeckSizeElement = makeElement("input", "rs-ext-decksize-overflow", { type: "checkbox" }), makeElement("label", null, "Enable deck overflow", { for: overflowDeckSizeElement.id }) ) ); // TODO: add this // add css let rsExtCustomCss = makeElement("style", null, ADVANCED_SETTINGS_CSS_STRING); document.head.appendChild(rsExtCustomCss); let searchText = document.getElementById("editor-search-text"); // info box let infoBox = makeElement("div"); // let infoMessage = makeElement("span", "rs-ext-info-message"); // infoBox.appendChild(document.createTextNode("🛈 ")); // infoBox.appendChild(infoMessage); styleHeaderNeutral(infoBox); searchText.parentNode.insertBefore(infoBox, searchText); // advanced search settings for(let el of ADVANCED_SETTINGS_HTML_ELS) { searchText.parentNode.insertBefore(el, searchText); } // page navigation bar let navigationHolder = makeElement("div", "rs-ext-navigation"); let leftButton = makeElement("button", "rs-ext-navigate-left", "<"); let rightButton = makeElement("button", "rs-ext-navigate-right", ">"); for(let button of [leftButton, rightButton]) { button.classList.add("engine-button"); button.classList.add("engine-button-default"); } leftButton.style.margin = rightButton.style.margin = "5px"; let pageInfo = makeElement("span", null, "Page "); let currentPageIndicator = makeElement("span","rs-ext-current-page", "X"); let maxPageIndicator = makeElement("span", "rs-ext-max-page", "X"); pageInfo.appendChild(currentPageIndicator); appendTextNode(pageInfo, " of "); pageInfo.appendChild(maxPageIndicator); navigationHolder.appendChild(leftButton); navigationHolder.appendChild(pageInfo); navigationHolder.appendChild(rightButton); styleHeaderNeutral(navigationHolder); navigationHolder.style.textAlign = "center"; noSelect(navigationHolder); searchText.parentNode.insertBefore(navigationHolder, searchText); // wire event listeners for advanced search settings let toggleButtonState = function () { let isSelected = this.classList.contains("rs-ext-selected"); if(isSelected) { this.classList.remove("rs-ext-selected"); } else { this.classList.add("rs-ext-selected"); } updateSearchContents(); }; [...document.querySelectorAll(".rs-ext-toggle-button")].forEach(el => { el.addEventListener("click", toggleButtonState.bind(el)); }); let monsterTab = document.getElementById("rs-ext-monster"); let spellTab = document.getElementById("rs-ext-spell"); let trapTab = document.getElementById("rs-ext-trap"); let sortTab = document.getElementById("rs-ext-sort"); let spacer = document.getElementById("rs-ext-spacer"); // returns `true` if the object is visible, `false` otherwise let toggleShrinkable = function (target, state = null) { let isShrunk = state; if(isShrunk === null) { isShrunk = target.classList.contains("rs-ext-shrunk"); } if(isShrunk) { spacer.classList.add("rs-ext-activated"); target.classList.remove("rs-ext-shrunk"); return true; } else { spacer.classList.remove("rs-ext-activated"); target.classList.add("rs-ext-shrunk"); return false; } // equiv. `return isShrunk;`, changed for clarity } let createToggleOtherListener = function (target, ...others) { return function () { let wasShrunk = toggleShrinkable(target); if(wasShrunk) { others.forEach(other => other.classList.add("rs-ext-shrunk")); } updateSearchContents(); } }; document.getElementById("rs-ext-monster-toggle") .addEventListener("click", createToggleOtherListener(monsterTab, spellTab, trapTab)); document.getElementById("rs-ext-spell-toggle") .addEventListener("click", createToggleOtherListener(spellTab, monsterTab, trapTab)); document.getElementById("rs-ext-trap-toggle") .addEventListener("click", createToggleOtherListener(trapTab, monsterTab, spellTab)); document.getElementById("rs-ext-sort-toggle").addEventListener("click", function () { toggleShrinkable(sortTab); }); const currentSections = function () { return [monsterTab, spellTab, trapTab, sortTab].filter(el => !el.classList.contains("rs-ext-shrunk")) || null; } const updatePaddingHeight = function () { let sections = currentSections(); let height = FN.sum(sections.map(section => section.clientHeight)); spacer.style.height = height + "px"; // update top position of sort, if necessary if(!sortTab.classList.contains("rs-ext-shrunk")) { sortTab.style.top = (height - sortTab.clientHeight) + "px"; } } let interval = setInterval(updatePaddingHeight, 1); console.info("Interval started. ", interval); const LINK_ARROW_MEANING = { "Bottom-Left": 0b000000001, "Bottom-Middle": 0b000000010, "Bottom-Right": 0b000000100, "Center-Left": 0b000001000, // "Center-Middle": 0b000010000, "Center-Right": 0b000100000, "Top-Left": 0b001000000, "Top-Middle": 0b010000000, "Top-Right": 0b100000000, }; const UNICODE_TO_LINK_NUMBER = { "\u2196": LINK_ARROW_MEANING["Top-Left"], "\u2191": LINK_ARROW_MEANING["Top-Middle"], "\u2197": LINK_ARROW_MEANING["Top-Right"], "\u2190": LINK_ARROW_MEANING["Center-Left"], // no center middle "\u2192": LINK_ARROW_MEANING["Center-Right"], "\u2199": LINK_ARROW_MEANING["Bottom-Left"], "\u2193": LINK_ARROW_MEANING["Bottom-Middle"], "\u2198": LINK_ARROW_MEANING["Bottom-Right"], // meaningless "=": 0b0, }; const convertUnicodeToNumber = function (chr) { return UNICODE_TO_LINK_NUMBER[chr] || 0; } const tagStringOf = function (tag, value = null, comp = "") { if(value !== null) { return "{" + tag + " " + comp + value + "}"; } else { return "{" + tag + "}"; } } // various elements const INPUTS_USE_QUOTES = { "TYPE": true, }; const MONSTER_INPUTS = { ARROWS: [...document.querySelectorAll(".rs-ext-toggle-button")], TYPE: document.getElementById("rs-ext-monster-type"), ATTRIBUTE: document.getElementById("rs-ext-monster-attribute"), LEVEL: document.getElementById("rs-ext-level"), SCALE: document.getElementById("rs-ext-scale"), LIMIT: document.getElementById("rs-ext-monster-limit"), ATK: document.getElementById("rs-ext-atk"), DEF: document.getElementById("rs-ext-def"), CATEGORY: document.getElementById("rs-ext-monster-category"), ABILITY: document.getElementById("rs-ext-monster-ability"), }; const INPUT_TO_KEYWORD = { // ARROWS: "ARROWS", TYPE: "TYPE", ATTRIBUTE: "ATTR", LEVEL: "LEVIND", ATK: "ATK", DEF: "DEF", SCALE: "SCALE", LIMIT: "LIMIT", }; const CATEGORY_TO_KEYWORD = { // primary "Normal": "NORMAL", "Effect": "EFFECT", "Ritual": "RITUAL", "Fusion": "FUSION", "Synchro": "SYNC", "Xyz": "XYZ", "Pendulum": "PEND", "Link": "LINK", // derived "Leveled": "LEVELED", "Extra Deck": "EXTRA", "Non-Effect": "NONEFF", // secondary "Flip": "FLIP", "Spirit": "SPIRIT", "Gemini": "GEMINI", "Toon": "TOON", "Tuner": "TUNER", }; const CATEGORY_SOURCES = [ "CATEGORY", "ABILITY" ]; const monsterSectionTags = function () { let tagString = "{MONSTER}"; // links let selectedArrows = MONSTER_INPUTS.ARROWS.filter(arrow => arrow.classList.contains("rs-ext-selected")); let selectedSymbols = selectedArrows.map(arrow => arrow.textContent); let bitmaps = selectedSymbols.map(convertUnicodeToNumber); let mask = bitmaps.reduce((a, c) => a | c, 0b0); if(mask) { let comp = (selectedSymbols.indexOf("=") !== -1) ? "=" : "&"; tagString += tagStringOf("ARROWS", mask, comp); } // category for(let category of CATEGORY_SOURCES) { let value = MONSTER_INPUTS[category].value; if(value) { let keyword = CATEGORY_TO_KEYWORD[value]; tagString += tagStringOf(keyword); } } for(let [inputName, tagName] of Object.entries(INPUT_TO_KEYWORD)) { let inputElement = MONSTER_INPUTS[inputName]; let value = inputElement.value; if(!value) continue; if(INPUTS_USE_QUOTES[inputName]) { value = '"' + value + '"'; } switch(inputElement.tagName) { case "INPUT": tagString += tagStringOf(tagName, value); break; case "SELECT": tagString += tagStringOf(tagName, value); break; default: console.error("Fatal error: unknown"); break; } } return tagString; } const SPELL_TRAP_INPUTS = { SPELL: document.getElementById("rs-ext-spell-type"), SPELL_LIMIT: document.getElementById("rs-ext-spell-limit"), TRAP: document.getElementById("rs-ext-trap-type"), TRAP_LIMIT: document.getElementById("rs-ext-trap-limit"), }; const SPELL_TO_KEYWORD = { "Normal": "NORMALST", "Quick-play": "QUICK", "Field": "FIELD", "Continuous": "CONT", "Ritual": "RITUALST", "Equip": "EQUIP", }; const spellSectionTags = function () { let tagString = "{SPELL}"; let value = SPELL_TRAP_INPUTS.SPELL.value; if(value) { let keyword = SPELL_TO_KEYWORD[value]; tagString += tagStringOf(keyword); } let limit = SPELL_TRAP_INPUTS.SPELL_LIMIT.value; if(limit) { tagString += tagStringOf("LIM", limit); } return tagString; }; const TRAP_TO_KEYWORD = { "Normal": "NORMALST", "Continuous": "CONT", "Counter": "COUNTER", }; const trapSectionTags = function () { let tagString = "{TRAP}"; let value = SPELL_TRAP_INPUTS.TRAP.value; if(value) { let keyword = TRAP_TO_KEYWORD[value]; tagString += tagStringOf(keyword); } let limit = SPELL_TRAP_INPUTS.TRAP_LIMIT.value; if(limit) { tagString += tagStringOf("LIM", limit); } return tagString; } const generateSearchFilters = function () { let tags = ""; for(let section of currentSections()) { switch(section) { case monsterTab: tags += monsterSectionTags(); break; case spellTab: tags += spellSectionTags(); break; case trapTab: tags += trapSectionTags(); break; // case null: default: break; } } return tags; } /* main code */ const COMPARATORS = { ">": function (x, y) { return x > y; }, "<": function (x, y) { return x < y; }, "=": function (x, y) { return x === y; }, // used exclusively for masks "&": function (x, y) { return (x & y) == y; }, // "!": function (x, y) { return x !== y; }, "!=": function (x, y) { return x !== y; }, "<=": function (x, y) { return x <= y; }, ">=": function (x, y) { return x >= y; }, }; const generateComparator = function(compIdentifier) { let comp = COMPARATORS[compIdentifier]; if(comp) { return comp; } else { return null; } } const createKindValidator = function (tagName) { tagName = tagName.toUpperCase(); if(kindMap[tagName]) { return kindMap[tagName]; } else { addMessage(STATUS.ERROR, "No such kind tag: " + tagName); return null; } } const VALIDATOR_ONTO_MAP = { "ATK": "attack", "DEF": "i", "ARROWS": "i", "SCALE": "lscale", }; const VALIDATOR_LEVEL_MAP = { "LEVEL": isLevelMonster, "LV": isLevelMonster, "RANK": isXyzMonster, "RK": isXyzMonster, "LINK": isLinkMonster, "LR": isLinkMonster, "LI": () => true, "LEVIND": () => true, }; const initialCapitalize = function (str) { return str.replace(/\w+/g, function (word) { return word[0].toUpperCase() + word.slice(1).toLowerCase(); }); } // returns a validation function /* * ALLOWED PARAMETERS: * ATK, num - search for atk * DEF, num - search for atk * LIM, num - search by limit status (0 = banned, 1 = limited, 2 = semi-limited, 3 = unlimited) * LV, num - search by level indicator * LEVEL,RANK,RK,LINK,LR - same for respective type * * * tag { * value: compare to * param: tag name * comp: functional comparator * } */ const createValidator = function (tag) { if(VALIDATOR_ONTO_MAP[tag.param]) { let value = parseInt(tag.value, 10); let prop = VALIDATOR_ONTO_MAP[tag.param]; return function (cardObject) { let objectValue = cardObject[prop]; if(tag.param === "DEF" && isLinkMonster(cardObject)) { return false; } if(tag.param === "ARROWS" && !isLinkMonster(cardObject)) { return false; } if(tag.param === "SCALE" && !isPendulumMonster(cardObject)) { return false; } return isMonster(cardObject) && tag.comp(objectValue, value); }; } else if(VALIDATOR_LEVEL_MAP[tag.param]) { let check = VALIDATOR_LEVEL_MAP[tag.param]; let level = parseInt(tag.value, 10); return function (cardObject) { return check(cardObject) && tag.comp(cardObject.level, level); } } else if(tag.param === "LIM" || tag.param === "LIMIT") { if(/^\d+$/.test(tag.value)) { let value = parseInt(tag.value, 10); return function (cardObject) { let count = allowedCount(cardObject); return tag.comp(count, value); }; } else { addMessage(STATUS.ERROR, "Invalid numeral " + tag.value + ", ignoring tag"); return function () { return true; }; } } else if(tag.param === "NAME") { let sub = sanitizeText(tag.value); return function (cardObject) { return sanitizeText(cardObject.Z).indexOf(sub) !== -1; }; } else if(tag.param === "TYPE") { let searchType = initialCapitalize(tag.value); if(TYPE_LIST.indexOf(searchType) !== -1) { return function (cardObject) { return monsterType(cardObject) == searchType; }; } else { addMessage(STATUS.ERROR, "Invalid type " + tag.value + ", ignoring tag"); return function () { return true; }; } } else if(tag.param === "ATTR" || tag.param === "ATTRIBUTE") { let searchAttribute = tag.value.toUpperCase(); let attributeMask = ATTRIBUTE_HASH[searchAttribute]; if(attributeMask !== undefined) { attributeMask = parseInt(attributeMask, 10); return function (cardObject) { return attributeOf(cardObject) === attributeMask; }; } else { addMessage(STATUS.ERROR, "Invalid attribute " + tag.value + ", ignoring tag"); return function () { return true; }; } } else { addMessage(STATUS.ERROR, "No such parameter supported: " + tag.param); return null; } } const ISOLATE_COMPARATOR_REGEX = /^([&=]|[!><]=?)?(.+)/; // returns 1 or 2 elements const isolateComparator = function (str) { let [_, comp, rest] = str.match(ISOLATE_COMPARATOR_REGEX); return [rest, comp].filter(e => e); } const expressionToPredicate = function (expression, param, defaultComp = "=") { let [rest, comp] = isolateComparator(expression); comp = comp || defaultComp; let fn = generateComparator(comp); if(!fn) { throw new Error("Invalid comparator: " + comp); } let tag = { value: rest, param: param.toUpperCase(), comp: fn, }; return createValidator(tag); } const operatorFunctions = { "OR": FN.or, "AND": FN.and, }; const operatorNameToFunction = function (opName) { let fn = operatorFunctions[opName]; return fn; } const textToPredicate = function (text, param) { let tokens = SearchInputShunter.parseSections(text); let stack = []; for(let token of tokens) { if(token.type === TokenTypes.EXPRESSION) { stack.push(expressionToPredicate(token.raw, param)); } else if(token.type === TokenTypes.OPERATOR) { let right = stack.pop(); let left = stack.pop(); if(!left || !right) { throw new Error("Insufficient arguments (incomplete input)"); } let fn = operatorNameToFunction(token.raw); stack.push(FN.hook(left, fn, right)); } } if(stack.length === 0) { addMessage(STATUS.ERROR, "Malformed input, not enough tokens."); return null; } return stack.pop(); } const compare = (a, b) => (a > b) - (a < b); const compareBy = (f, rev = false) => (a, b) => compare(f(a), f(b)) * (rev ? -1 : 1); // TODO: put traps always at end const SORT_BY_COMPARATORS = { "Name": compareBy(x => x.name), "Level": compareBy(x => x.level), "ATK": compareBy(x => x.attack), "DEF": compareBy(x => x.i), // TODO: sort each sub-strata? e.g. all level 1s by name // "Level": compareByAlterantives("level", "name"), // "ATK": compareByAlterantives("attack", "name"), // "DEF": compareByAlterantives("i", "name"), }; let STRATA_KINDS = { LEVELED: [ isLevelMonster, isXyzMonster, isLinkMonster, isSpellCard, isTrapCard ], DEFAULT: [ isNormalMonster, isBasicMainDeckMonster, isRitualMonster, isFusionMonster, isSynchroMonster, isXyzMonster, isLinkMonster, isLevelMonster, isSpellCard, isTrapCard ], }; let formStrata = function (list, by = "DEFAULT") { let fns = STRATA_KINDS[by]; let strata = fns.map(e => []); for(let card of list) { let index = fns.findIndex(fn => fn(card)); if(index !== -1) { strata[index].push(card); } else { console.error("Index not found in strata array", card); } } return strata; }; let sortCardList = function (list, options) { let { methods, reverse, stratify, strataKind } = options; if(!Array.isArray(methods)) { methods = [ methods, "Name", "Level", "ATK", "DEF" ]; } if(stratify) { let strata = formStrata(list, strataKind).map(stratum => sortCardList(stratum, { methods: methods, reverse: reverse, stratify: false, }) ); return [].concat(...strata); } let cmps = methods.map(method => SORT_BY_COMPARATORS[method]); let sorted = list.sort((a, b) => { for(let cmp of cmps) { let diff = cmp(a, b); if(diff !== 0) { return diff; } } return 0; }); if(reverse) { sorted.reverse(); } return sorted; } let ensureCompareText = function (card) { card.compareText = card.compareText || card.description.toUpperCase(); return card.compareText; }; let parseInputQuery = function (text) { let prepend = ""; let append = ""; let inner = text.toString().trim(); if(inner[0] === "^") { prepend = inner[0]; inner = inner.slice(1); } if(lastElement(inner) === "$") { append = lastElement(inner); inner = inner.slice(0, -1); } inner = escapeRegex(inner); inner = inner.replace(/\\\*/g, ".*") .replace(/_/g, "."); let compiled = prepend + inner + append; return new RegExp(compiled); }; const ISOLATE_TAG_REGEX = /\{(!?)(\w+)([^\{\}]*?)\}/g; let updateSearchContents = function () { clearVisualSearchOptions(); initializeMessageContainer(); // TODO: move sanitation process later in the procedure let input = $("#editor-search-text").val(); let effect = $("#editor-search-effect").val() || ""; // append tags generated by search options let extraTags = generateSearchFilters(); if(extraTags) { input += extraTags; } // isolate the tags in the input let tags = []; input = input.replace(ISOLATE_TAG_REGEX, function (match, isNegation, param, info) { // over each tag: let validator; try { if(info) { validator = textToPredicate(info, param); } else { validator = createKindValidator(param); } } catch(error) { addMessage(STATUS.ERROR, "Problem creating validator: " + error.message); return ""; } if(validator) { if(isNegation) { validator = FN.compose(FN.not, validator); } tags.push(validator); } else if(validator !== null) { addMessage(STATUS.ERROR, "Invalid validator (bug, please report): " + match); } // remove the tag, replace with nothing return ""; }); // remove any improper tags input = input.replace(/\{[^\}]*$/, function (tag) { addMessage(STATUS.NEUTRAL, "Removed incomplete tag: " + tag); return ""; }); // needs non-empty input if (input.length !== 0 || effect.length !== 0 || tags.length !== 0) { let exactMatches = []; let fuzzyMatches = []; let cardId = parseInt(input, 10); // if the user is searching by ID if(!isNaN(cardId)) { cardId = CARD_LIST[cardId]; if(cardId && isPlayableCard(cardId)) { fuzzyMatches.push(cardId); } } // only if the user is not searching by a valid ID let hasTags = tags.length !== 0; let isLongEnough = input.length >= EXT.MIN_INPUT_LENGTH || effect.length >= EXT.MIN_INPUT_LENGTH; let isFuzzySearch = hasTags || isLongEnough; let uppercaseInput = input.toUpperCase(); let uppercaseText = parseInputQuery(effect.toUpperCase()); let searchableInput = parseInputQuery(uppercaseInput.replace(/ /g, "")); if (0 === fuzzyMatches.length) { // for each card ID for (var e in CARD_LIST) { let card = CARD_LIST[e]; if(!isPlayableCard(card) || !allSatisfies(tags, card)) { continue; } let compareName = searchableCardName(card); if(compareName === searchableInput) { // if the search name is the input, push it exactMatches.push(card); } else if(isFuzzySearch) { ensureCompareText(card); let cardMatchesName = compareName.search(searchableInput) !== -1; let cardMatchesEffect = card.compareText.search(uppercaseText) !== -1; if(cardMatchesName && cardMatchesEffect) { fuzzyMatches.push(card); } } } } // sort the results let method = $("#rs-ext-sort-by").val(); let reverseResults = $("#rs-ext-sort-order").val() === "Descending"; let stratify = document.getElementById("rs-ext-sort-stratify").checked; let strataKind; if(method === "Level") { strataKind = "LEVELED"; stratify = true; } let params = { methods: method, reverse: reverseResults, stratify: stratify, strataKind: strataKind, }; exactMatches = sortCardList(exactMatches, params); fuzzyMatches = sortCardList(fuzzyMatches, params); // exactMatches.sort(cardCompare); // fuzzyMatches.sort(cardCompare); // display let totalEntryCount = exactMatches.length + fuzzyMatches.length; let displayAmount = Math.min(totalEntryCount, EXT.RESULTS_PER_PAGE); if(displayAmount === 0) { EXT.Search.cache = []; if(isFuzzySearch) { let suggestion; if(hasTags) { suggestion = "Try changing or removing some tags."; } else { suggestion = "Check your spelling and try again."; } if(input.replace(/\W/g, "").toLowerCase() == "sock") { addMessage(STATUS.ERROR, "No results found. If you're looking for me, try Sock#3222 on discord :D"); } else { addMessage(STATUS.ERROR, "No results found. " + suggestion); } } else { addMessage(STATUS.ERROR, "Your input was too short. Try typing in some text or adding some tags."); } } else { let cache = exactMatches.concat(fuzzyMatches); EXT.Search.cache = cache; // RESULTS_PER_PAGE can change between calculations EXT.Search.per_page = EXT.RESULTS_PER_PAGE; EXT.Search.current_page = 1; EXT.Search.max_page = Math.ceil(EXT.Search.cache.length / EXT.Search.per_page); replaceTextNode(currentPageIndicator, EXT.Search.current_page); replaceTextNode(maxPageIndicator, EXT.Search.max_page); let message = ""; let anyErrors = EXT.Search.messages.some( message => message[0] == STATUS.ERROR ); message += "Search successful"; if(anyErrors) { message += ", but there were some errors"; } message += ". Found " + totalEntryCount + " "; message += pluralize("entr", totalEntryCount, "ies", "y"); message += " (" + EXT.Search.max_page + " " + pluralize("page", EXT.Search.max_page) + ")"; addMessage(anyErrors ? STATUS.NEUTRAL : STATUS.SUCCESS, message); } } else { EXT.Search.cache = []; replaceTextNode(currentPageIndicator, "X"); replaceTextNode(maxPageIndicator, "X"); addMessage(STATUS.NEUTRAL, "Please enter a search term to begin."); } displayResults(); }; // add new input function $("#editor-search-text").on("input", updateSearchContents) updateSearchContents(); // add search by effect $("#editor-search-text").wrap($("<div class=rs-ext-flex></div>")); let searchWrapper = $("#editor-search-text").parent(); let editorSearchByEffect = $("<input type=text class=engine-text-box id=editor-search-effect></input>"); searchWrapper.append(editorSearchByEffect); editorSearchByEffect.on("input", updateSearchContents); // update attributes $("#editor-search-text").attr("placeholder", "Search by name/query"); editorSearchByEffect.attr("placeholder", "Search by effect"); // add relevant listeners let allInputs = [ document.querySelectorAll("#rs-ext-monster-table input, #rs-ext-monster-table select"), Object.values(SPELL_TRAP_INPUTS), document.querySelectorAll("#rs-ext-sort-table input, #rs-ext-sort-table select"), ].flat(); for(let input of allInputs) { $(input).on("input", updateSearchContents); } let previousPage = function () { EXT.Search.current_page = Math.max(1, EXT.Search.current_page - 1); displayResults(); } let nextPage = function () { EXT.Search.current_page = Math.min(EXT.Search.max_page, EXT.Search.current_page + 1); displayResults(); } $(leftButton).on("click", previousPage); $(rightButton).on("click", nextPage); }; let checkStartUp = function () { if(Z.xa) { onStart(); // destroy reference; pseudo-closure onStart = null; } else { setTimeout(checkStartUp, 100); } } checkStartUp();